Model Structure
system dynamics, xmile, stella, sd dsl, bptk, bptk-py, python, stock, flow, biflow, conveyor, arrays
Model Structure
What an XMILE file declares in XML, the SD DSL writes as Python. Each section shows the XML, shortened to what matters, and then the SD DSL. The code of all sections together is one model.
Run specs
XMILE
<sim_specs method="Euler">
<start>0</start>
<stop>10</stop>
<dt>0.5</dt>
</sim_specs>SD DSL
from BPTK_Py import Model
from BPTK_Py import sd_functions as sd
model = Model(starttime=0.0, stoptime=10.0, dt=0.5,
name="structure")The SD DSL integrates with Euler’s method only; the transpiler refuses a model set to Runge-Kutta in Stella - see Limitations.
Stocks, flows and converters
XMILE lists a stock’s flows in the stock; the SD DSL does the same with inflows and outflows, which write the stock’s equation as the inflows minus the outflows. An aux whose equation is a plain number becomes a constant; any other aux becomes a converter.
XMILE
<stock name="inventory">
<eqn>100</eqn>
<inflow>production</inflow>
<outflow>shipments</outflow>
</stock>
<flow name="production">
<eqn>20</eqn>
<non_negative/>
</flow>
<flow name="shipments">
<eqn>inventory / cover</eqn>
<non_negative/>
</flow>
<aux name="cover">
<eqn>5</eqn>
</aux>SD DSL
inventory = model.stock("inventory")
inventory.initial_value = 100.0
production = model.flow("production")
production.equation = 20.0
shipments = model.flow("shipments")
cover = model.constant("cover")
cover.equation = 5.0
shipments.equation = inventory / cover
inventory.inflows = [production]
inventory.outflows = [shipments]A flow in XMILE runs both ways unless it is non_negative. The SD DSL has the two kinds as two elements: model.flow never runs backwards, model.biflow does.
XMILE
<flow name="adjustment">
<eqn>(150 - inventory) / 4</eqn>
</flow>SD DSL
adjustment = model.biflow("adjustment")
adjustment.equation = (150.0 - inventory) / 4.0Non-negative stocks
A stock that must not fall below zero cuts back its outflows, in the order they are listed. In the SD DSL its inflows and outflows have to be given as lists, so that the stock knows which flows to cut back, and in which order.
XMILE
<stock name="cash">
<eqn>50</eqn>
<inflow>income</inflow>
<outflow>spending</outflow>
<non_negative/>
</stock>SD DSL
income = model.flow("income")
income.equation = 10.0
spending = model.flow("spending")
spending.equation = 25.0
cash = model.stock("cash")
cash.initial_value = 50.0
cash.inflows = [income]
cash.outflows = [spending]
cash.non_negative = TrueConveyors
A conveyor carries what flows in for a transit time; a leak flow takes a fraction of it on the way. The outflow at the end of the belt and the leak have no equation of their own in the SD DSL - the belt computes them. AGECOUNT counts the material that has been on the belt for between a minimum and a maximum time - here between 0 and 2.
XMILE
<stock name="cohort">
<eqn>1500</eqn>
<inflow>arriving</inflow>
<outflow>leaving</outflow>
<outflow>dropping_out</outflow>
<conveyor>
<len>4</len>
</conveyor>
</stock>
<flow name="arriving">
<eqn>100</eqn>
</flow>
<flow name="leaving"/>
<flow name="dropping_out">
<eqn>0.02</eqn>
<leak/>
</flow>
<aux name="young">
<eqn>AGECOUNT(cohort, 0, 2)</eqn>
</aux>SD DSL
arriving = model.flow("arriving")
arriving.equation = 100.0
leaving = model.flow("leaving")
dropping_out = model.flow("dropping_out")
cohort = model.conveyor("cohort")
cohort.transit_time = 4.0
cohort.initial_value = 1500.0
cohort.inflows = [arriving]
cohort.outflows = [leaving]
cohort.leaks = {dropping_out: 0.02}
young = model.converter("young")
young.equation = sd.agecount(cohort, 0.0, 2.0)The SD DSL has linear and exponential leakage and FIFO conveyors - see Conveyor - but not leak zones, capacity, inflow limits, arrest or discrete conveyors. The transpiler compiles an XMILE conveyor into an SD DSL conveyor, and refuses one that uses a feature the SD DSL does not have.
Graphical functions
In XMILE, a graphical function is a variable with its own points. In the SD DSL the points are a list stored under a name in model.points, and sd.lookup reads them by that name.
XMILE
<aux name="effect">
<eqn>cover</eqn>
<gf>
<xscale min="0" max="10"/>
<ypts>0,50,80</ypts>
</gf>
</aux>SD DSL
model.points["effect"] = [[0.0, 0.0], [5.0, 50.0], [10.0, 80.0]]
effect = model.converter("effect")
effect.equation = sd.lookup(cover, "effect")Arrays
A dimension in XMILE is a list of names; in the SD DSL the names are the keys of an arrayed element. An equation over arrays works on every element alike, as XMILE’s apply-to-all does.
XMILE
<dimensions>
<dim name="region">
<elem name="north"/>
<elem name="south"/>
<elem name="west"/>
</dim>
</dimensions>
<aux name="sales">
<dimensions><dim name="region"/></dimensions>
<element subscript="north"><eqn>10</eqn></element>
<element subscript="south"><eqn>30</eqn></element>
<element subscript="west"><eqn>20</eqn></element>
</aux>
<aux name="doubled">
<dimensions><dim name="region"/></dimensions>
<eqn>sales * 2</eqn>
</aux>SD DSL
sales = model.converter("sales")
sales.setup_named_vector({"north": 10.0, "south": 30.0, "west": 20.0})
doubled = model.converter("doubled")
doubled.setup_named_vector({"north": 0.0, "south": 0.0, "west": 0.0})
doubled.equation = sales * 2.0A part of an array and a transposed matrix:
XMILE
SUM(sales[north:south])
routes'
SD DSL
first_two = model.converter("first_two")
first_two.equation = sales["north":"south"].arr_sum()
routes = model.converter("routes")
routes.setup_named_matrix({"north": {"a": 1.0, "b": 2.0},
"south": {"a": 3.0, "b": 4.0}})
turned = model.converter("turned")
turned.setup_named_matrix({"a": {"north": 0.0, "south": 0.0},
"b": {"north": 0.0, "south": 0.0}})
turned.equation = routes.transpose()A range includes its end, as in Stella. The SD DSL has no @: its arrays have labels, not named dimensions. The transpiler compiles ranges and transposition, and refuses @ - see What the transpiler does not compile.