Creating User-defined Functions in SD Models
system dynamics, systemdynamics, sd dsl, bptk, bptk-py, python, business simulation
Creating User-defined Functions in SD Models
One of the benefits of creating System Dynamics models in Python is that we can use the full power of Python to create our own functions, which we can then use in our models.
This how-to illustrates how to do this.
First of all, let’s set up our model:
Now let’s define a function we would like to use in our model. A user defined function can have as many arguments as you like, but it must accept at least a model and time parameter (you don’t need to use the parameters if you don’t want to).
How you define your function is up to you - you can use any of the methods available in Python, such as class methods, using def, or lambda functions.
The example below uses a lambda function which simply multiplies the current time t with 5.
As you can see, much like with stocks and converters, we associate our function with the model by calling the model’s function method.
Next we set up a converter whose equation calls the function, and test it at t = 5:
25.0
Let’s plot the function over time:
We can also create a stock that has the converter as an inflow:
You can do all the usual arithmetic in an equation. Here the same converter is divided by time before it flows in - a second stock rather than a second equation on the first one, because an element whose equation is set in two cells has no defined value outside the cell that just set it:
The function we created above was just dependent on time and not on other model variables. Let’s create a function that takes more arguments, e.g. one that multiplies a model variable with time.
You can add as many arguments as you like, but they must come after the model and t arguments.
The function needs two inputs, so we define a converter for each of them and then a third converter that applies another_model_function to both:
Of course functions defined in this way can also be used within scenarios. The quickest way to set up a scenario manager for a given model is register_model, which creates a scenario manager named after the model with the prefix “sm” - the name is normalized to start with a capital letter, so TestModel becomes smTestmodel - together with a “base” scenario that runs the model as-is.
Three more scenarios are added below, each with a different multiplier, and list_scenarios shows what the manager now holds:
*** smTestmodel ***
base
multiplier5
multiplier10
multiplier15
And plotting the four scenarios against each other shows what the multiplier does:
Where a user-defined function may stand
A function computes the value of an element, so it goes where an equation goes: a converter, a flow, a biflow, and the equation of a stock. It can also be a stock’s initial value, since an initial value is an expression like any other:
starting_point = model.function("starting_point", lambda model, t: 42.0)
stock = model.stock("stock")
stock.initial_value = starting_point()An initial value is evaluated before the run rather than during it, so a function standing there is called once, with t at the start time.
Which functions run on the Rust engine
A model with user-defined functions runs on the Rust engine. The engine evaluates the model and calls back into Python at those nodes, once per node and timestep, so the function you wrote is the function that runs.
The run says so at [WARN], and Execution Backends explains what that means and what it costs.
A function registered with elementwise=False, and one in a hybrid model that reads what the agents produced, cannot run on the engine; Limitations says why.
On the server
/execute refuses a model that carries user-defined functions, with HTTP 400 naming them. The model arrived over HTTP, and running the Python that goes with it is a different decision from running the model. Serve such a model from a process that holds the functions - a bptk instance with the scenario manager registered - rather than by posting the model itself.
Functions and arrays
How a function handles an arrayed argument - per index, or the whole array with elementwise=False - is explained under User-defined Functions and Arrayed Models.