Creating multidimensional SD Models
system dynamics, systemdynamics, sd dsl, bptk, bptk-py, python, business simulation
Multidimensional SD Models
These pages are the reference for arrays in the SD DSL: how to give an element a shape, which operators work on it, what each one returns, and what is deliberately not supported. Every section runs its own small example, so you can read them as a catalogue and copy from them.
- Defining Arrayed Components – vectors and matrices, with numerical or named indices, and how to plot them
- Operations on Arrayed Components – the arithmetic, math and comparison operators,
If,maxandmin, andsmooth,trendanddelay, each applied element by element - Array Functions – what turns an array into one value or multiplies arrays: sum, product, rank, mean, median, standard deviation, maximum and minimum, size and
dot - A Simple Arrayed Model – an investment depot with two accounts, from setting up the model to the plot
For arrays in a model that does something, the Model Library has two worked examples: a workforce aging chain over a vector of seniority levels, and a regional product portfolio built on a matrix of products across regions.
Things to watch out for
These all work; they just do not always work the way a first reading suggests.
- Element-wise is the default, everywhere. Every operator applies to an array index by index, and so does a user-defined function: it is called once per index, and the result is an array of the same shape. A function that wants the whole array instead is registered with
elementwise=False- and that one does not run on the Rust engine: a run that asks for it raises, and a run that does not stays in Python. The element-wise form runs on the engine like everything else here. - An aggregation is a scalar, and it needs an element of its own.
headcount.arr_sum()is one number, and assigning it to an element you gave a shape raises: an arrayed element holds no value beside its cells. - The order of a dimension is the order you declared it in.
arr_rankandarr_mediansort, and both engines sort the same list - but a named vector’s “first” index is the first key you set up, not the alphabetically first label. - A sub-element is an element of its own.
headcount["north"]has its own equation, its own name in the result frame (headcount[north]), and a scenario overrides it under that name.
What Is Not Supported
Worth knowing before you build on arrays. None of these fails silently - each one raises with a message that says what happened.
- Two dimensions is the limit.
setup_matrixtakes exactly two sizes, and there is no three-dimensional array. - A named matrix in a
dotneeds the same column labels in every row. Such a matrix is legal everywhere else - adotis the one operation that sums over an axis and therefore needs a single set of column labels to sum over. - A
dotcannot mix a named array with an unnamed one. There is nothing for the labels to line up against, so it raises. - There is no aggregation over a single dimension.
arr_sumandarr_prodtake adimensionsargument, but the only values it accepts are"*"(the default) and an integer equal to the array’s depth - both of which aggregate every cell. Aggregating along the rows of a matrix would have to return a vector, which is not supported; where you need it, address the rows yourself. A row of a named matrix is a named vector in its own right, somatrix['north'].arr_sum()is the total of that row. arr_sizecounts the first dimension, not the number of cells: 2 for a \(2 \times 3\) matrix.- There is no broadcasting. Both operands of an element-wise operation must have the same shape and the same indices; a vector and a matrix is an error.
- No transpose, and no dimension-position operator. The XMILE standard has both - a transpose, and
@to name a position within a dimension - and the SD DSL has never implemented either, because nothing in the model library or the test corpus has needed one. Where you would reach for a transpose, address the cells the other way round when you set the matrix up; a row of a named matrix is a named vector, somatrix['north']is that row and there is no need to turn the matrix around to get at it. - A flow never goes negative, arrayed or not: every flow equation is wrapped in
max(0, ...). A quantity that has to move both ways belongs in a biflow, which takes the samesetup_vectorandsetup_named_vectoras any other element.