Builtins Side by Side
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) |