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; a model set to Runge-Kutta in Stella gives other results - 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 with a number is a constant, one with an equation 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 it needs its flows as lists, so that it knows which to cut.
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 what has been on the belt for a given time.
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 no conveyors at all.
Graphical functions
In XMILE a graphical function is a variable with points; in the SD DSL the points are a list under a name in model.points, which sd.lookup reads.
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 neither ranges nor transposition - see What the transpiler does not compile.