XMILE in the SD DSL

How XMILE builtins and model structure are written in the SD DSL, where the SD DSL departs from XMILE and Stella, and what the transpiler does not compile.
Keywords

system dynamics, xmile, smile, stella, sd dsl, bptk, bptk-py, python, builtins, transpiler

XMILE in the SD DSL

A model written in XMILE - in Stella, say - reaches BPTK-Py in one of two ways. The transpiler turns the file into an SD DSL model, as XMILE Step by Step shows. Or you write the model again in the SD DSL, by hand. This chapter puts the two side by side, says where the SD DSL departs from XMILE and Stella, and lists what the transpiler does not compile. Everything the transpiler compiles runs on both engines.

  • Builtins Side by Side: every builtin as an XMILE equation and the SD DSL call that computes the same
  • Built from Other Functions: SELF, CLOCKTIME, ENDVAL, RUNCOUNT and SENSIRUNCOUNT, which the SD DSL writes with what it has
  • Model Structure: stocks, flows, conveyors, graphical functions, arrays and run specs in XML and in Python

XMILE, Stella and the SD DSL

Stella sometimes departs from the XMILE standard. The SD DSL follows these rules:

  • It follows the XMILE standard.
  • Where the standard is not specific, it follows Stella.
  • It departs from the standard or from Stella only for a good reason - where their answer would hide a mistake in the model, or where a different answer serves models better: an argument a function cannot take gives NaN and an [ERROR] rather than a quiet substitute, and a delay time is read as given rather than stretched to one step. Every such case is a row in the table below.

The order of the operators

An equation without parentheses is read by the precedence of its operators. XMILE, Stella and the SD DSL rank them as follows, from the highest level to the lowest; operators on one level are read from left to right, except ^, which is read from right to left:

Level XMILE Stella SD DSL
1 ^ ^ ^
2 unary +, - and NOT listed together with ^; the order between them is not stated unary +, - and NOT: -2^2 is -4
3 *, / and MOD * and / *, / and MOD
4 + and - +, - and MOD + and -
5 <, <=, >, >=, then = and <> one level lower all comparisons on one level, not associative all comparisons on one level, as Stella
6 AND AND AND
7 OR OR OR

The rows on comparisons and on MOD below show what the differences mean for a model.

The cases known so far

The rows are in alphabetical order of the builtin or operator they are about.

Case XMILE Stella SD DSL
AGECOUNT(belt, 0, 1) on a conveyor whose transit time changes no AGECOUNT the true age if the material is time-stamped. Otherwise Stella approximates the age while the change works through the belt, and gives NaN on a belt with a leak the true age: how long the material has been on the belt
ARCSIN(2), GEOMETRIC(0), LN(-1), SQRT(-4), …: an argument outside the domain gives the domain of ARCSIN, LN and SQRT, not what happens outside it; no GEOMETRIC stops the run: “overflow or divide by 0” NaN and an [ERROR] - and the run goes on
COMBINATIONS(5, -1) and PERMUTATIONS(5, -1): a negative number of items to choose no COMBINATIONS or PERMUTATIONS 1 for both, as for choosing 0 items NaN and an [ERROR]
a = b < c: comparisons chained without parentheses <, <=, > and >= rank above = and <>: a = (b < c) all comparisons on one level, left to right: (a = b) < c as Stella: (a = b) < c - the transpiler reads Stella’s models, and a chain of comparisons without parentheses is rare
DELAY(input, 0), DELAY(input, 0.3) and DELAY(input, -3) with dt 1 a fixed delay time; nothing on zero, fractions or a minimum at least one step: all three delay by 1 the delay time rounded to whole steps, with no minimum: 0 and 0.3 give the input of the same step, -3 gives NaN and an [ERROR]
DELAY(input, T) with T changing during the run; the same for DELAY1, DELAY3, DELAYN a fixed delay time for all four DELAY keeps T’s initial value; DELAY1, DELAY3 and DELAYN follow T all four follow T, read at every step; the transpiler says so at [WARN] for DELAY
Division by zero: 1 / X with X reaching 0 not defined stops the run: “overflow or divide by 0” the usual values up to that time, then NaN where it divides by zero, and one [ERROR] - the run goes on
FACTORIAL(168) to FACTORIAL(171) no FACTORIAL infinity from 168, NaN from 171 the value up to 170, infinity from 171
FACTORIAL(2.4), COMBINATIONS(5.6, 2), PERMUTATIONS(5, 2.6): an argument that is no whole number no FACTORIAL, COMBINATIONS or PERMUTATIONS 2, 15 and 60: each argument is rounded to the nearest whole number, as in FACTORIAL(2), COMBINATIONS(6, 2) and PERMUTATIONS(5, 3) NaN and an [ERROR]: the arguments have to be whole numbers
GAMMA(2, 3, 5, 4, 8) no GAMMA values outside 4 to 8 are drawn too: the bounds are not applied, although Stella’s documentation lists min and max for GAMMA only values from 4 to 8 are drawn, as for every other distribution
GEOMETRIC(0.25) no GEOMETRIC 0.75 - that is 1 − 0.25 - is the success probability, although Stella’s documentation names the argument, 0.25, the success probability 0.25 is the success probability, as Stella’s documentation says
A graphical function of type extrapolate, which continues the first and last segment beyond the ends, or discrete, which holds each value up to the next point the type is stored with the table: <gf type="discrete">. Without a type, continuous: interpolated between the points, the end values held as the standard the same three types, chosen where the table is read: sd.lookup(x, "table", kind="discrete")
LOOKUPMEAN(table, 0.6, 0.4) on a table with the points [0, 1], [0.25, 1], [0.5, 1], [0.75, 1] and [1, 1]. The table is flat, so every x is equally likely. Stella takes the upper bound first: the range is 0.4 to 0.6, and both bounds lie between two points no LOOKUPMEAN 0.35, which lies outside the range. With both bounds on points, as in LOOKUPMEAN(table, 0.75, 0.25), Stella gives 0.5 0.5, the middle of the range: on a flat table, the mean of a range is its middle
MAX(1, TIME, 3) MAX takes one or two arguments refused the largest of the three - sd.max takes any number of arguments
-7 MOD 3 and 7 MOD -3: MOD with a negative number 2 and -2: the floored modulus - the result takes the sign of the divisor -1 and 1: the result takes the sign of the dividend, as C’s fmod 2 and -2: the floored modulus, as XMILE - Python’s %
7 MOD 3 * 2 and 3 + 5 MOD 4: MOD beside other operators 2 and 4: MOD on the level of * and / - (7 MOD 3) * 2, 3 + (5 MOD 4) 1 and 0: MOD on the level of + and -, as Stella’s documentation says - 7 MOD (3 * 2), (3 + 5) MOD 4 2 and 4, as XMILE; the transpiler says so at [WARN]
NORMAL(0, 1, 5, 1, -1): a minimum above the maximum min and max are no XMILE arguments NaN NaN and an [ERROR]
NORMAL(0, 1, 5, 10, 11): bounds the distribution almost never reaches min and max are no XMILE arguments after 100 draws outside the bounds, a uniform number between them is chosen after 100 draws outside the bounds, NaN and an [ERROR]
NORMALPINK(10, 2, 0): a correlation time of 0 no NORMALPINK stops the run: “overflow or divide by 0” NaN and an [ERROR] - and the run goes on
NORMALPINK(10, 2, 5, 7, 10): a step that would leave the bounds no NORMALPINK draws again, starting from the rejected value rather than the previous one: the noise is pulled towards the bound, a mean of 11.55 over 5,000 steps draws again, starting from the previous value: a mean of 11.8 over 5,000 steps
PARETO(1, 100) no PARETO values just above 1: the first argument is read as the scale and the second as the shape, although Stella’s documentation names the first shape and the second scale shape 1 and scale 100, as Stella’s documentation says: values of at least 100
PERCENTILE(values, 0, 0, weights): the 0th percentile with weights no PERCENTILE 0. Without weights, it gives the smallest value the smallest value, with and without weights
PREVIOUS(input * 2, 0) the first argument is a variable: PREVIOUS(price, 0) refused: the first argument must be a variable sd.previous takes an expression
PULSE(20), no first pulse PULSE takes 2 or 3 arguments; read as PULSE(20, STARTTIME), it is one pulse, at the start time a pulse at every DT from the start time one pulse, at the start time
PULSE(20, 2), no interval one pulse, at 2 a pulse at every DT from 2 one pulse, at 2; the transpiler says so at [WARN]
PULSE(20, 2, 1.5) with dt 1: the second pulse, at 3.5, falls between two steps not defined fires at 4 fires at 4
ROUND(3.14159, 2) no ROUND refused: ROUND takes one argument 3.14 - sd.round(x, digits)

Where a Stella model computes differently once transpiled - MOD next to *, /, + or - without parentheses, PULSE without an interval, DELAY with a delay time that can change - the transpiler says so at [WARN], naming the element.

What the transpiler does not compile

The transpiler refuses a model it cannot express, with a message naming the element and the reason, and writes no file:

  • ENDVAL, RUNCOUNT and SENSIRUNCOUNT, which read earlier runs: a BPTK-Py model is one run - Built from Other Functions shows how Python carries values between runs.
  • Integration other than Euler: RK2 and RK4.
  • Data imported from a file, and a module kept in a file of its own.
  • Arrays of more than two dimensions, and @, the position in a dimension.
  • A graphical function per element of an arrayed flow; one graphical function for the whole flow compiles.
  • SUM, PROD and SIZE with more than one argument, and an array beside other arguments of MEAN, MAX and MIN - as in Stella, * and ranges select cells only in an aggregation of one argument.
  • The options of a conveyor - capacity, inflow limit, arrest - and leak zones; ovens and queues.

Loop and pathway scores - variables with LOOPSCORE or PATHSCORE, which Stella creates for loop dominance analysis - are no reason to refuse a model: they are left out with a [WARN], and the model compiles without them.