SD DSL - Under The Hood
system dynamics, systemdynamics, sd dsl, bptk, bptk-py, python, metaprogramming, business simulation
SD DSL: Under The Hood
Creating A Domain Specific Language For System Dynamics Simulations
Part of the BPTK Framework is the System Dynamics Domain Specific Language (SD DSL). Using the SD DSL, you can easily build System Dynamics Mmodels directly in Python using an intuitive Syntax.
This notebook takes a look under the hood of the SD DSL and explains how it creates the underlying equations that are needed to run a simulation.
This should give you a better understanding of System Dynamics, of the SD DSL and of Python metaprogramming techniques.
The following diagram contains the three key System Dynamics language elements:
- Stocks
- Flows
- Converters
At any given time \(t\), a stock is equal to the value of the stock at time \(t-1\) plus the sum of all inflows, minus the sum of all outflow at time \(t-1\) are simply the sum of the flows that flow into and out of them, over time.
Flows and converters are simply functions of their inputs.
The general mathematical equations for the stocks above are:
\[\begin{equation*} stock(t)=stock(t-1)+\sum_{inflows}inflow(t-1)-\sum_{outflows}outflow(t-1) \end{equation*}\]
\[\begin{equation*} flow(t) = function(input_1,...,input_n) \end{equation*}\]
\[\begin{equation*} converter(t) = function(input_1,...,input_n) \end{equation*}\]
The concrete equations for the model in the diagram above are:
\[\begin{equation*} stock(t)=stock(t-1)+\sum_{inflows}inflow(t-1) \end{equation*}\]
\[\begin{equation*} flow(t) = stock(t)*rate(t) \end{equation*}\]
\[\begin{equation*} rate(t) = 0.1 \end{equation*}\]
\[\begin{equation*} stock(0) = 1 \end{equation*}\]
The SD DSL allows us to write SD models in Python using a simple syntax:
stock = model.stock("stock")
flow = model.flow("flow")
rate = model.converter("rate")
stock.initial_value = 1
rate.equation=0.1
flow.equation=stock*rate
stock.equation=flowBut under the hood, the SD DSL needs to build the equations listed above to ensure that we can simulate the model.
Let’s take a look at how this is done, step by step.
Step 1: Write Python Code For The Equations Above
The SD DSL uses lambda functions lambda functions to implement the equation if the System Dynamics model.
2.5937424601
Step 2: Plot The Resulting Equation
Plot the stock equation for 20 timesteps, e.g. using a pandas dataframe.
Step 3: Keep Equations in A Dictionary
Assume we want to keep equations in a “model” and add new ones dynamically: create a dictioanry of equations that are accessed by equation names as strings, e.g. equation["stock"](5)
2.5937424601
Step 4: Use Memoization To Improve Performance
In order to calculate the value of a stock at a given time \(t\), you need to know all preceding values. The way the model is encoded at the moment, this means that we first calculate the value at time \(t = 1\), then to calculate the value at time \(t = 2\) we need to calcualte the values at time \(t=1\) and then at time \(t =2\) and so on … this means that even models with only few timesteps can take a very long time to evaluate.
The way around this of course is to remember the value of each equation at each time step, so that you don’t have to recurse through all timesteps at every timestep.
We do this using a technique called memoization.
The memoization function stores the value of each equation at each timestep. When asking for the value of an equation at a given time, it first checks if the value has already been calculated. If yes, it returns that value. If no, it calculates the value, stores it and then returns it.
1.61051
117.39085287969533
Step 5: Compile Equations From Strings
Now that we know what equations we want, we need a find a way of generating them directly. We do this by first generating the code in string form and then compiling that string into code using the evalfunction.
1.61051
Step 6: Encapsulate The Basic Functionality In Classes
Now that we have all the building blocks for our SD DSL, we would like to encapsulate them in classes that allow us to build a model at runtime.
We want to be able to build a model using objects at runtime, the underlying equations should be generated for us.
- Create classes for the model (that will hold the model elements and equations) and for the elements themselves.
- New elements are created using the model as a factory (e.g.
another_stock = model.stock("name of another stock")) - Elements know which model they belong to
- The equations for each element are defined using an equation method on the elements (e.g.
stock.equation=flow) - The memoization function should be part of the model class
- Each element should have a term function
element.termthat generate a string representation of itself (e.gmodel.memoize("stock",t)) - Remember that stocks have an initial value
"model.memoize('stock',t)"
"model.memoize('rate',t)"
"model.memoize('flow',t)"
<__main__.Flow object at 0x10ac2b230>
Step 7: Auto-Generate The Equations
Extend the framework to auto-generate the underlying lambda-equations and add them to the model. For the first version assume we just have a stock and a constant flow.
Override the call operator to evalue the element at a particular timestep (e.g. stock(10))
Create a plot function that evaluates an element in the range 0 to 100.
"model.memoize('stock',t)"
"model.memoize('flow',t)"
<__main__.Flow_1 object at 0x10ac2b620>
1
11
Step 8: Extend the Equation Syntax Using Operators
In order to re-create our initial model, we need to formulate an equation such as flow.equation = stock*rate.
Override the __mul__ methods of the Element and create a multiplication operator to deal with this.
1.3310000000000002