Operations on Arrayed Components
system dynamics, systemdynamics, sd dsl, arrays, bptk, bptk-py, python, business simulation
Operations on Arrayed Components
The examples on this page share one model, set up here:
Arrays are not a special case in the SD DSL: every operator and function that takes an operand works on an arrayed element, element by element. That covers the four arithmetic operations, powers and modulo, the math functions, comparisons and conditionals, max and min, the table and time functions, and the stateful smooth, trend and delay.
On top of that there are the array-specific operations, which are the ones that do something an element-wise operator cannot: they aggregate a whole array into a single value, or multiply arrays in the linear-algebra sense.
Standard Operations
The arithmetic operations \(+\), \(-\), \(*\), \(/\) as well as \(**\) (power) and \(\%\) (modulo) accept any of these operand pairings:
| Operand 1 | Operand 2 |
|---|---|
| Arrayed Element | Arrayed Element |
| Arrayed Element | Scalar Element |
| Scalar Element | Arrayed Element |
| Arrayed Element | Float/Integer |
| Float/Integer | Arrayed Element |
Scalar Element and Float/Integer are not the same thing. A scalar element is a model element without a shape - a constant, converter, stock or flow - so it has an equation of its own and its value can change over the course of a simulation, and changing it changes every index that reads it. A Float or Integer is a plain Python number written into the equation, fixed for the whole run. Both are allowed on either side of the operator; v * 2.0 and v * some_constant differ only in whether the factor can still move.
⚠️ If both operands are arrayed elements, they must have the same numerical or string-valued indices.
That means it is not possible to have operand 1 = vector with numerical indices and operand 2 = vector with string-valued indices, even if they have the same size.
It is also not possible to have operand 1 = vector and operand 2 = matrix or vice versa - there is no broadcasting. A mismatch raises an exception when the equation is assigned rather than guessing what was meant: mixing a vector and a matrix gives Attempted invalid array addition, two vectors of different length Cannot perform binary operation on arrays with different sizes, and a numerical against a named vector Cannot perform binary operation on arrays with different indices.
Every one of these operations is performed element-wise: index by index, never across indices. Let’s have a look at some examples:
Addition (\(+\))
\[\begin{equation*} \begin{pmatrix} 1.1 \\ 2.2 \end{pmatrix} + \begin{pmatrix} 3.1 \\ 4.2 \end{pmatrix} = \begin{pmatrix} 4.2 \\ 6.4 \end{pmatrix} \end{equation*}\]
[ 4.2 , 6.4 ]
\[\begin{equation*} \begin{pmatrix} 3.1 \\ 4.2 \end{pmatrix} +1.0 = \begin{pmatrix} 4.1 \\ 5.2 \end{pmatrix} \end{equation*}\]
[ 4.1 , 5.2 ]
Subtraction (\(-\))
\[\begin{equation*} \begin{pmatrix} 1.1 & 2.2 \\ 3.3 & 4.4 \end{pmatrix} - \begin{pmatrix} 5.5 & 7.7 \\ 3.3 & 14.4 \end{pmatrix} = \begin{pmatrix} -4.4 & -5.5\\ 0.0 & -10.0 \end{pmatrix} \end{equation*}\]
[ [-4.4 , -5.5] [0.0 , -10.0] ]
\[\begin{equation*} \begin{pmatrix} 5.5 & 7.7 \\ 3.3 & 14.4 \end{pmatrix} -1.0 = \begin{pmatrix} 4.5 & 6.7\\ 2.3 & 13.4 \end{pmatrix} \end{equation*}\]
[ [4.5 , 6.7] [2.3 , 13.4] ]
Multiplication (\(*\))
\[\begin{equation*} \begin{pmatrix} \text{'value1'}: & 4.0 \\ \text{'value2'}: & 5.0 \end{pmatrix} \odot \begin{pmatrix} \text{'value1'}: & 6.0 \\ \text{'value2'}: & 7.0 \end{pmatrix} = \begin{pmatrix} \text{'value1'}: & 24.0 \\ \text{'value2'}: & 35.0 \end{pmatrix} \end{equation*}\]
[ 24.0 , 35.0 ]
\[\begin{equation*} \begin{pmatrix} \text{'value1'}: & 6.0 \\ \text{'value2'}: & 7.0 \end{pmatrix} \cdot 3.0 = \begin{pmatrix} \text{'value1'}: & 18.0 \\ \text{'value2'}: & 21.0 \end{pmatrix} \end{equation*}\]
[ 18.0 , 21.0 ]
The case “- arrayed element” is a special case since it is interpreted as “(-1) \(\cdot\) element”:
\[\begin{equation*} - \begin{pmatrix} \text{'value1'}: & 6.0 \\ \text{'value2'}: & 7.0 \end{pmatrix} = \begin{pmatrix} \text{'value1'}: & -6.0 \\ \text{'value2'}: & -7.0 \end{pmatrix} \end{equation*}\]
[ -6.0 , -7.0 ]
Division (\(/\))
\[\begin{equation*} \begin{pmatrix} 2.0 & 4.0\\ 8.0 & 16.0 \end{pmatrix} \oslash \begin{pmatrix} 2.0 & 1.0\\ 0.5 & 0.25 \end{pmatrix} = \begin{pmatrix} 1.0 & 4.0\\ 16.0 & 64.0 \end{pmatrix} \end{equation*}\]
[ [1.0 , 4.0] [16.0 , 64.0] ]
\[\begin{equation*} \begin{pmatrix} 2.0 & 1.0\\ 0.5 & 0.25 \end{pmatrix} / \text{ } 5.0 = \begin{pmatrix} 0.4 & 0.2\\ 0.1 & 0.05 \end{pmatrix} \end{equation*}\]
[ [0.4 , 0.2] [0.1 , 0.05] ]
Power (\(**\)) and Modulo (\(\%\))
This section and the ones that follow all use the same little named vector, so the results are easy to compare.
\[\begin{equation*} \begin{pmatrix} \text{'small'}: & 4.0 \\ \text{'large'}: & 9.0 \end{pmatrix} ^{2} = \begin{pmatrix} \text{'small'}: & 16.0 \\ \text{'large'}: & 81.0 \end{pmatrix} \end{equation*}\]
\[\begin{equation*} \begin{pmatrix} \text{'small'}: & 4.0 \\ \text{'large'}: & 9.0 \end{pmatrix} \bmod 5 = \begin{pmatrix} \text{'small'}: & 4.0 \\ \text{'large'}: & 4.0 \end{pmatrix} \end{equation*}\]
v ** 2: [ 16.0 , 81.0 ] v % 5: [ 4.0 , 4.0 ]
Math Functions
Every function in sd_functions that takes a value works on an array and returns an array of the same shape: sqrt, exp, ln, log10, sin, cos, tan, arcsin, arccos, arctan, floor, ceil, round, abs, sinwave, coswave.
\[\begin{equation*} \sqrt{ \begin{pmatrix} \text{'small'}: & 4.0 \\ \text{'large'}: & 9.0 \end{pmatrix} } = \begin{pmatrix} \text{'small'}: & 2.0 \\ \text{'large'}: & 3.0 \end{pmatrix} \end{equation*}\]
\[\begin{equation*} 2 \cdot \ln \begin{pmatrix} \text{'small'}: & 4.0 \\ \text{'large'}: & 9.0 \end{pmatrix} = \begin{pmatrix} \text{'small'}: & 2.7726 \\ \text{'large'}: & 4.3944 \end{pmatrix} \end{equation*}\]
sqrt(v): [ 2.0 , 3.0 ] 2 * ln(v): [ 2.7726 , 4.3944 ]
Comparisons, If, And, Or, Not
A comparison over an array gives one boolean per index, and If then chooses per index as well. The condition, the then branch and the else branch may each be arrayed or scalar in any combination, as long as the arrayed ones agree on shape.
\[\begin{equation*} \mathrm{If}\left( \begin{pmatrix} \text{'small'}: & 4.0 \\ \text{'large'}: & 9.0 \end{pmatrix} > 6, \begin{pmatrix} \text{'small'}: & 4.0 \\ \text{'large'}: & 9.0 \end{pmatrix} , 0 \right) = \begin{pmatrix} \text{'small'}: & 0.0 \\ \text{'large'}: & 9.0 \end{pmatrix} \end{equation*}\]
If(v > 6, v, 0): [ 0.0 , 9.0 ] And(v > 3, v < 6): [ True , False ]
max and min
sd.max and sd.min compare two operands, element by element. Not to be confused with arr_max and arr_min further down, which take the largest or the smallest value within one array.
\[\begin{equation*} \max\left( \begin{pmatrix} \text{'small'}: & 4.0 \\ \text{'large'}: & 9.0 \end{pmatrix} , 6 \right) = \begin{pmatrix} \text{'small'}: & 6.0 \\ \text{'large'}: & 9.0 \end{pmatrix} \end{equation*}\]
max(v, 6): [ 6.0 , 9.0 ]
smooth, trend and delay
The stateful functions work over arrays as well, and each index carries its own history: smoothing a vector is not one smoothed value copied across the indices, it is one smoothing chain per index. Below, both indices start at the initial value 1.0 and each converges towards its own input - the recurrence \(s_{t+1} = s_t + \frac{dt}{\tau}(x - s_t)\) runs once per index:
\[\begin{equation*} \mathrm{smooth}\left( \begin{pmatrix} \text{'small'}: & 4.0 \\ \text{'large'}: & 9.0 \end{pmatrix} , \tau = 3, s_0 = 1 \right) \Bigr|_{t=4} = \begin{pmatrix} \text{'small'}: & 3.4074 \\ \text{'large'}: & 7.4198 \end{pmatrix} \end{equation*}\]
t= 0: [ 1.0 , 1.0 ] t= 1: [ 2.0 , 3.6667 ] t= 4: [ 3.4074 , 7.4198 ] t=10: [ 3.948 , 8.8613 ]