Model Structure

Stocks, flows, converters, non-negative stocks, conveyors, graphical functions, arrays and run specs in XMILE and in the SD DSL.
Keywords

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.0

Non-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 = True

Conveyors

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.0

A 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.