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 is an XMILE equation and the SD DSL expression that computes the same - the same arguments, in the same places. The rows use a small model, 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)
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 things recur in every group:

  • A function that needs the run specs or keeps state takes the model first - sd.dt(model), sd.delay(model, ...), sd.smooth(model, ...). Its result is a structure of its own inside the model, which it needs the model to build.
  • An optional XMILE argument is a keyword argument with the same default, so it can be left out in both.
  • 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 %. INT rounds down, so it is sd.floor; ROUND without digits is sd.round(..., 0), which rounds half away from zero, as Stella does.

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()

Conditions

Python’s and, or and not cannot be given a meaning for model elements, so the SD DSL has functions for them. XMILE’s = and <> are == 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

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

Without an initial value each of them starts at its input.

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

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)

Finance

FV, PV and PMT are signed as in a spreadsheet: money paid out is negative.

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; in the SD DSL it is a list of points under a name in model.points, or the list itself. With one x, LOOKUPAREA is the area from the table’s start up to it - in the SD DSL that bound is to_x.

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)

Random numbers

Each takes an optional seed as its last argument, as in XMILE. The numbers are not Stella’s: its generator is not open source.

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)

Two of the statistical functions are not random, and compute the same in both:

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

An aggregation over a whole array is a method of the arrayed element. With two arguments MAX and MIN compare numbers, as above; with one they look along an array. RANK gives a position, which the SD DSL counts from 0 and XMILE from 1.

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) sd.rank(sales, 1)
INTERPOLATE(sales, 0.25) sd.interpolate(sales, 0.25)