Builtins Side by Side
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:
AGECOUNTissd.agecount. It counts what is on a conveyor, and the example model of this page has no conveyor - the example is on Model StructureSELFandCLOCKTIMEhave no SD DSL function of their own and are written with other SD DSL functions - see Built from Other FunctionsENDVAL,RUNCOUNTandSENSIRUNCOUNTread earlier runs, which the transpiler refuses; Built from Other Functions shows how Python carries values between runs