Using the XMILE Compiler Standalone
system dynamics, systemdynamics, xmile, bptk, bptk-py, python, business simulation, stella
Using the XMILE Compiler Standalone
The BPTK-Py package contains a compiler which compiles System Dynamics models that use the XMILE format into Python code. This means that you can compile models created with Stella, iThink or Vensim (via the XMILE export) into Python code which will then run standalone.
The result of the compilation is a Python class that only depends on NumPy and SciPy. This class contains the entire simulation model and it can run “standalone”, i.e. you don’t need the BPTK framework to actually run the simulation and extract simulation results.
Running models standalone can be useful if you want to run your model many times with different initial settings, e.g. for machine learning experiments. In such cases you don’t need the extra functionality provided by the framework’s bptk class and it is more efficient to work with the raw simulation model.
The compiler needs the [xmile] extra (pip install bptk_py[xmile]) and it writes a file, so the code on this page is shown rather than run - the outputs below are from a real session with the model described next.
Compiling a model
Assume a small XMILE model called test_compiler.stmx in a simulation_models folder. It contains one converter called pulse_converter with the equation 1+PULSE(0.5, 3.25). Compiling it into test_compiler.py takes three lines:
from BPTK_Py.sdcompiler.compile import compile_xmile
compile_xmile("./simulation_models/test_compiler.stmx",
"./simulation_models/test_compiler.py",
"py")The generated file contains everything needed to run the model. The model itself is in a class simulation_model, alongside a number of generic methods that system dynamics simulations need - simulation_model.delay or simulation_model.random_with_seed among them. These are always generated, whether or not your model uses them, so that every generated simulation_model class has the same methods.
Running it
The equations themselves are stored in the class as a dictionary of lambda functions. Since the example model has a single converter, so does the dictionary - and because each equation is a lambda taking the time t, it can be called directly:
from simulation_models.test_compiler import simulation_model
model = simulation_model()
model.equations
# {'pulseConverter':
# <function simulation_model.__init__.<locals>.<lambda>(t)>}
model.equations["pulseConverter"](5.25)
# 3.0Plotting the results
The simulation_model has no convenience functions for plotting - those live in the framework’s bptk class. But the equations are ordinary callables, so collecting them into a Pandas dataframe and using its plot() method takes just as few lines:
import pandas as pd
from numpy import arange
data = [[t, model.equations["pulseConverter"](t)]
for t in arange(0, 20, 0.25)]
df = pd.DataFrame(data, columns=["time", "pulse_converter"])
df = df.set_index("time")
df.loc[2:5] # the interesting stretch: the pulse arrives at t = 3.25| time | pulse_converter |
|---|---|
| 2.00 | 1.0 |
| 2.25 | 1.0 |
| 2.50 | 1.0 |
| 2.75 | 1.0 |
| 3.00 | 1.0 |
| 3.25 | 3.0 |
| 3.50 | 3.0 |
| 3.75 | 3.0 |
| 4.00 | 3.0 |
| 4.25 | 3.0 |
| 4.50 | 3.0 |
| 4.75 | 3.0 |
| 5.00 | 3.0 |
The dataframe plots itself:
df.plot()