Builtins Side by Side

Every XMILE builtin next to the SD DSL call that computes the same, with the arguments in their places.
Keywords

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

Builtins Side by Side

Each row below shows an XMILE equation next to the SD DSL expression that computes the same, with the arguments in the same order. This page shows where the two notations differ; what the arguments mean is documented on the SD DSL’s function pages, which each group links to. The rows use a small model, shown here in both notations:

XMILE

demand = 100 + 10 * TIME
price  = 2 + 0.5 * TIME
stream = IF TIME < 2 THEN -100 ELSE 30
table  = graphical function through
         (0, 0), (5, 50), (10, 80)
line   = the same, of type extrapolate
steps  = the same, of type discrete
sales[region] = north: 10, south: 30, west: 20

Run specs: from 0 to 10, DT 0.5

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="side_by_side")
demand = model.converter("demand")
demand.equation = 100.0 + 10.0 * sd.time()
price = model.converter("price")
price.equation = 2.0 + 0.5 * sd.time()
stream = model.converter("stream")
stream.equation = sd.If(sd.time() < 2.0, -100.0, 30.0)
model.points["table"] = [[0.0, 0.0], [5.0, 50.0], [10.0, 80.0]]
sales = model.converter("sales")
sales.setup_named_vector({"north": 10.0, "south": 30.0, "west": 20.0})

Three rules apply to every group:

  • A function that needs the run specs or keeps state takes the model as its first argument - sd.dt(model), sd.delay(model, ...), sd.smooth(model, ...). Such a function builds a structure of its own inside the model, and it needs the model to do so.
  • An optional XMILE argument is a keyword argument in the SD DSL, with the same default, so it can be left out in both notations.
  • Every argument can be a number, an element or an expression, as in XMILE.

Simulation time

XMILE SD DSL
TIME sd.time()
DT sd.dt(model)
STARTTIME sd.starttime(model)
STOPTIME sd.stoptime(model)

Mathematics

MOD is Python’s %. XMILE’s INT rounds down, so it is sd.floor. ROUND without a number of digits is sd.round(..., 0), which rounds half away from zero, as Stella does. What each function computes is documented on Deterministic Functions.

XMILE SD DSL
ABS(price - 5) sd.abs(price - 5.0)
SQRT(demand) sd.sqrt(demand)
ROOTN(demand, 3) sd.rootn(demand, 3.0)
EXP(price) sd.exp(price)
LN(price) sd.ln(price)
LOG10(demand) sd.log10(demand)
SIN(price) sd.sin(price)
COS(price) sd.cos(price)
TAN(price) sd.tan(price)
ARCSIN(0.5) sd.arcsin(0.5)
ARCCOS(0.5) sd.arccos(0.5)
ARCTAN(price) sd.arctan(price)
INT(price) sd.floor(price)
ROUND(price) sd.round(price, 0)
ROUND(price / 3, 2) sd.round(price / 3.0, 2)
demand MOD 7 demand % 7.0
MAX(demand, 150) sd.max(demand, 150.0)
MIN(demand, 150, price * 60) sd.min(demand, 150.0, price * 60.0)
PERCENT(price) sd.percent(price)
SAFEDIV(demand, price - 3) sd.safediv(demand, price - 3.0)
SAFEDIV(demand, price - 3, -1) sd.safediv(demand, price - 3.0, -1.0)
CGROWTH(10) sd.cgrowth(model, 10.0)
COUNTER(0, 4) sd.counter(model, 0.0, 4.0)
COMBINATIONS(7, 3) sd.combinations(7.0, 3.0)
PERMUTATIONS(7, 3) sd.permutations(7.0, 3.0)
FACTORIAL(5) sd.factorial(5.0)
GAMMALN(4.5) sd.gammaln(4.5)
PI sd.pi()
INF sd.Inf()
NAN sd.nan()
ISNAN(SQRT(price - 5)) sd.isnan(sd.sqrt(price - 5.0))
REWORK(10) sd.rework(10.0)

Bounded and soft curves

These functions take Stella’s arguments in Stella’s order. What each argument does is documented on Deterministic Functions.

XMILE SD DSL
EXPBOUND(10, 80, TIME, 2, 2, 8) sd.expbound(10.0, 80.0, sd.time(), 2.0, 2.0, 8.0)
EXPBOUND(10, 80, price, 0, 3, 6) sd.expbound(10.0, 80.0, price, 0.0, 3.0, 6.0)
LOGISTICBOUND(10, 80, TIME, 5, 1) sd.logisticbound(10.0, 80.0, sd.time(), 5.0, 1.0)
LOGISTICBOUND(10, 80, TIME, 5, 1, 1, 9) sd.logisticbound(10.0, 80.0, sd.time(), 5.0, 1.0, 1.0, 9.0)
MAXSOFT(demand, 150, 20) sd.maxsoft(demand, 150.0, 20.0)
MINSOFT(demand, 150, 20) sd.minsoft(demand, 150.0, 20.0)

Conditions

Python’s and, or and not cannot be applied to model elements, so the SD DSL has the functions sd.And, sd.Or and sd.Not instead. XMILE’s comparisons = and <> are written == and !=.

XMILE SD DSL
IF price > 4 THEN demand ELSE 0 sd.If(price > 4.0, demand, 0.0)
IF price = 4 THEN 1 ELSE 0 sd.If(price == 4.0, 1.0, 0.0)
IF price <> 4 THEN 1 ELSE 0 sd.If(price != 4.0, 1.0, 0.0)
IF price > 4 AND demand < 180 THEN 1 ELSE 0 sd.If(sd.And(price > 4.0, demand < 180.0), 1.0, 0.0)
IF price < 3 OR price > 6 THEN 1 ELSE 0 sd.If(sd.Or(price < 3.0, price > 6.0), 1.0, 0.0)
IF NOT (price > 4) THEN 1 ELSE 0 sd.If(sd.Not(price > 4.0), 1.0, 0.0)

Test inputs

What the arguments mean is documented on Deterministic Functions.

XMILE SD DSL
STEP(50, 3) sd.step(50.0, 3.0)
RAMP(10, 2) sd.ramp(10.0, 2.0)
RAMP(10, 2, 6) sd.ramp(10.0, 2.0, 6.0)
PULSE(20) sd.pulse(model, 20.0)
PULSE(20, 2, 3) sd.pulse(model, 20.0, 2.0, 3.0)
SINWAVE(10, 4) sd.sinwave(10.0, 4.0)
COSWAVE(10, 4) sd.coswave(10.0, 4.0)

Delays and smoothing

What the arguments mean is documented on Deterministic Functions, including the value each delay starts from when no initial value is given.

XMILE SD DSL
DELAY(demand, 2) sd.delay(model, demand, 2.0)
DELAY(demand, 2, 0) sd.delay(model, demand, 2.0, 0.0)
DELAY1(demand, 3) sd.delay1(model, demand, 3.0)
DELAY1(demand, 3, 80) sd.delay1(model, demand, 3.0, 80.0)
DELAY3(demand, 3) sd.delay3(model, demand, 3.0)
DELAYN(demand, 3, 4) sd.delayn(model, demand, 3.0, 4)
DELAYN(demand, 3, 4, 80) sd.delayn(model, demand, 3.0, 4, 80.0)
SMTH1(demand, 3) sd.smooth(model, demand, 3.0)
SMTH1(demand, 3, 80) sd.smooth(model, demand, 3.0, 80.0)
SMTH3(demand, 3) sd.smooth3(model, demand, 3.0)
SMTHN(demand, 3, 4) sd.smoothn(model, demand, 3.0, 4)
TREND(demand, 3) sd.trend(model, demand, 3.0)
TREND(demand, 3, 0.1) sd.trend(model, demand, 3.0, 0.1)
FORCST(demand, 3, 2) sd.forecast(model, demand, 3.0, 2.0)

The past of a variable

The RUN functions take the time window, from and to, after the variable, as Stella does. Stella’s further arguments read another run, which has no counterpart in the SD DSL. What the other functions compute is documented on Deterministic Functions.

XMILE SD DSL
INIT(demand) sd.init(model, demand)
PREVIOUS(demand, 0) sd.previous(model, demand, 0.0)
HISTORY(demand, TIME - 2) sd.history(model, demand, sd.time() - 2.0)
DERIVN(demand * price, 1) sd.derivn(model, demand * price, 1)
RUNCUM(demand) sd.runcum(model, demand)
RUNCUM(demand, TIME - 2, TIME) sd.runcum(model, demand, sd.time() - 2.0, sd.time())
RUNMAX(price) sd.runmax(model, price)
RUNMIN(price, 2, 6) sd.runmin(model, price, 2.0, 6.0)
RUNMEAN(demand * price) sd.runmean(model, demand * price)
RUNCUMTARGET(stream, 100) sd.runcumtarget(model, stream, 100.0)

Finance

What the arguments mean is documented on Deterministic Functions, including the sign convention of FV, PV and PMT.

XMILE SD DSL
NPV(stream, 0.1) sd.npv(model, stream, 0.1)
NPV(stream, 0.1, 0) sd.npv(model, stream, 0.1, 0.0)
IRR(stream) sd.irr(model, stream)
FV(0.05, 10, -100) sd.fv(0.05, 10.0, -100.0)
FV(0.05, 10, -100, -1000) sd.fv(0.05, 10.0, -100.0, -1000.0)
PV(0.05, 10, -100) sd.pv(0.05, 10.0, -100.0)
PMT(0.05, 10, 1000) sd.pmt(0.05, 10.0, 1000.0)

Graphical functions

In XMILE, a graphical function is a variable of its own. In the SD DSL it is a list of points, stored under a name in model.points - the functions take the name or the list itself.

XMILE stores the type of a graphical function - continuous, extrapolate or discrete - with the function. The SD DSL gives the type where the table is read, as the argument kind. That is why line and steps in the example model are the same table as table in the SD DSL, read with a different kind.

With a single x, LOOKUPAREA gives the area from the start of the table up to x; in the SD DSL this x is the argument to_x.

LOOKUPMAX, LOOKUPMIN and LOOKUPMEAN take the end of their range first and its start second; the SD DSL takes the start (from_x) first, as LOOKUPAREA does. The order does not change the result.

What each function computes is documented on Deterministic Functions.

XMILE SD DSL
LOOKUP(table, price) sd.lookup(price, "table")
LOOKUPINV(table, 40) sd.lookupinv(40.0, "table")
LOOKUPAREA(table, 4) sd.lookuparea("table", to_x=4.0)
LOOKUPAREA(table, 2, 4) sd.lookuparea("table", 2.0, 4.0)
LOOKUP(line, 2 * TIME) sd.lookup(2.0 * sd.time(), "table", kind="extrapolate")
LOOKUP(steps, price) sd.lookup(price, "table", kind="discrete")
LOOKUPINV(line, 100) sd.lookupinv(100.0, "table", kind="extrapolate")
LOOKUPAREA(steps, 2, 8) sd.lookuparea("table", 2.0, 8.0, kind="discrete")
LOOKUPMAX(table) sd.lookupmax("table")
LOOKUPMIN(table, 8, 2) sd.lookupmin("table", 2.0, 8.0)
LOOKUPMEAN(table) sd.lookupmean("table")
LOOKUPMEAN(table, 5) sd.lookupmean("table", to_x=5.0)
LOOKUPRANDOM(table, 3) sd.lookuprandom("table", 3)

Random numbers

Every random function takes an optional seed as its last argument, as in XMILE. With the same seed, BPTK-Py draws different numbers than Stella, because Stella’s random number generator is not published.

Every random function except RANDOM, whose first two arguments already are its bounds, also takes Stella’s optional arguments min, max and sample after the seed. In the SD DSL they are called minimum, maximum and sample, because min and max are names of Python functions. Where Stella writes NAN to leave an argument out, the SD DSL simply leaves out the keyword.

NORMALPINK remembers its previous value, so the SD DSL passes it the model as the first argument.

What the arguments mean is documented on Random Functions.

XMILE SD DSL
RANDOM(0, 10) sd.random(0.0, 10.0)
RANDOM(0, 10, 42) sd.random(0.0, 10.0, 42)
UNIFORM(0, 10) sd.uniform(0.0, 10.0)
NORMAL(100, 10) sd.normal(100.0, 10.0)
LOGNORMAL(100, 10) sd.lognormal(100.0, 10.0)
EXPRND(5) sd.exprnd(5.0)
POISSON(5) sd.poisson(5.0)
BETA(2, 5) sd.beta(2.0, 5.0)
BINOMIAL(10, 0.3) sd.binomial(10.0, 0.3)
NEGBINOMIAL(5, 0.5) sd.negbinomial(5.0, 0.5)
GAMMA(2, 3) sd.gamma(2.0, 3.0)
GEOMETRIC(0.3) sd.geometric(0.3)
LOGISTIC(0, 1) sd.logistic(0.0, 1.0)
PARETO(3, 1) sd.pareto(3.0, 1.0)
TRIANGULAR(0, 5, 10) sd.triangular(0.0, 5.0, 10.0)
WEIBULL(2, 1) sd.weibull(2.0, 1.0)
MONTECARLO(50) sd.montecarlo(50.0)
NORMALPINK(100, 10, 3) sd.normalpink(model, 100.0, 10.0, 3.0)
NORMAL(100, 10, 42, 90, 110) sd.normal(100.0, 10.0, 42, minimum=90.0, maximum=110.0)
EXPRND(5, 42, 2) sd.exprnd(5.0, 42, minimum=2.0)
UNIFORM(0, 10, 42, NAN, NAN, 2) sd.uniform(0.0, 10.0, 42, sample=2.0)

Two statistical functions, INVNORM and NORMALCDF, are not random. They compute the same in Stella and in the SD DSL:

XMILE SD DSL
INVNORM(0.9, 100, 10) sd.invnorm(0.9, 100.0, 10.0)
NORMALCDF(-1, 1) sd.normalcdf(-1.0, 1.0)

Arrays

A function that aggregates a whole array, such as SUM(sales[*]), is a method of the arrayed element in the SD DSL: sales.arr_sum().

MAX and MIN work in two ways. With two or more arguments they return the largest or smallest of them - in an arrayed equation cell by cell, so two arrays give an array. The arrays must have the same dimensions; arrays of different sizes or with different labels are refused with an error when the equation is set. With a single array argument, such as MAX(sales[*]), they return its largest or smallest cell.

PERCENTILE(values, p, lower, weights) has two optional arguments. The third argument, lower, is a second percentile that is subtracted: PERCENTILE(sales, 75, 25) is the 75th minus the 25th percentile. The fourth argument, weights, counts each value as often as its weight says. To give weights without subtracting anything, Stella needs 0 as the third argument. The SD DSL names both arguments - lower= and weights= - so either can be given alone.

What each function computes is documented on Array Functions.

XMILE SD DSL
SUM(sales[*]) sales.arr_sum()
PROD(sales[*]) sales.arr_prod()
MEAN(sales[*]) sales.arr_mean()
STDDEV(sales[*]) sales.arr_stddev()
SIZE(sales[*]) sales.arr_size()
MAX(sales[*]) sales.arr_max()
MIN(sales[*]) sales.arr_min()
sales[south] sales["south"]
RANK(sales[*], 1) sales.arr_rank(1)
INTERPOLATE(sales, 0.25) sales.arr_interpolate(0.25)
PERCENTILE(sales, 50) sales.arr_percentile(50.0)
PERCENTILE(sales, 75, 25) sales.arr_percentile(75.0, 25.0)
PERCENTILE(sales, 40, 0, sales) sales.arr_percentile(40.0, weights=sales)

Builtins without a row

Six XMILE builtins have no row above:

  • AGECOUNT is sd.agecount. It counts what is on a conveyor, and the example model of this page has no conveyor - the example is on Model Structure
  • SELF and CLOCKTIME have no SD DSL function of their own and are written with other SD DSL functions - see Built from Other Functions
  • ENDVAL, RUNCOUNT and SENSIRUNCOUNT read earlier runs, which the transpiler refuses; Built from Other Functions shows how Python carries values between runs