XMILE in the SD DSL
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,RUNCOUNTandSENSIRUNCOUNT, 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,RUNCOUNTandSENSIRUNCOUNT, 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,PRODandSIZEwith more than one argument, and an array beside other arguments ofMEAN,MAXandMIN- 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.