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; 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.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 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 = 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 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.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 ranges and transposition, and refuses @ - see What the transpiler does not compile.