Python bindings for the Simlin system dynamics simulation engine
Project description
pysimlin - Python bindings for Simlin
Python bindings for the Simlin system dynamics simulation engine.
Features
- Load models from XMILE, Vensim MDL, and binary protobuf formats
- Run system dynamics simulations with full control
- Get simulation results as pandas DataFrames
- Analyze model structure and feedback loops
- Edit existing models or build new ones programmatically via Python context managers
- Full type hints for IDE support
- Loops That Matter (LTM) analysis for feedback loop importance
Installation
pip install pysimlin
Note: Install with pip install pysimlin but import with import simlin.
Requirements
- Python 3.11 or higher
- numpy >= 1.22.0
- pandas >= 1.5.0
- cffi >= 1.15.0
Quick Start
import simlin
from pathlib import Path
# Load a model from file (auto-detects format)
project = simlin.Project.from_file("model.stmx")
# Get the default model
model = project.get_model()
# Create and run a simulation
sim = model.new_sim()
sim.run_to_end()
# Get results as a pandas DataFrame
results = sim.get_results()
print(results.head())
# Access individual variables
population = sim.get_series("population")
Examples
Editing a flow in an existing model
"""Example showing how to edit an existing model's flow equation with pysimlin."""
from __future__ import annotations
import simlin
EXAMPLE_XMILE = b"""<?xml version='1.0' encoding='utf-8'?>
<xmile version=\"1.0\" xmlns=\"http://docs.oasis-open.org/xmile/ns/XMILE/v1.0\" xmlns:isee=\"http://iseesystems.com/XMILE\" xmlns:simlin=\"https://simlin.com/XMILE/v1.0\">
<header>
<name>pysimlin-edit-example</name>
<vendor>Simlin</vendor>
<product version=\"0.1.0\" lang=\"en\">Simlin</product>
</header>
<sim_specs method=\"Euler\" time_units=\"Year\">
<start>0</start>
<stop>80</stop>
<dt>0.25</dt>
</sim_specs>
<model name=\"main\">
<variables>
<stock name=\"population\">
<eqn>25</eqn>
<inflow>net_birth_rate</inflow>
</stock>
<flow name=\"net_birth_rate\">
<eqn>fractional_growth_rate * population</eqn>
</flow>
<aux name=\"fractional_growth_rate\">
<eqn>maximum_growth_rate * (1 - population / carrying_capacity)</eqn>
</aux>
<aux name=\"maximum_growth_rate\">
<eqn>0.10</eqn>
</aux>
<aux name=\"carrying_capacity\">
<eqn>1000</eqn>
</aux>
</variables>
</model>
</xmile>
"""
def run_simulation(model: simlin.Model) -> float:
"""Run the model to the configured stop time and return the ending population."""
with model.new_sim() as sim:
sim.run_to_end()
return float(sim.get_value("population"))
def main() -> None:
"""Demonstrate editing a flow equation and verify the change takes effect."""
project = simlin.Project.from_xmile(EXAMPLE_XMILE)
model = project.get_model()
baseline_final = run_simulation(model)
with model.edit() as (current, patch):
flow_var = current["net_birth_rate"]
flow_var.flow.equation.scalar.equation = (
"fractional_growth_rate * population * 1.5"
)
patch.upsert_flow(flow_var.flow)
accelerated_final = run_simulation(model)
if not accelerated_final > baseline_final + 10:
raise RuntimeError(
"Edited model did not accelerate growth as expected: "
f"baseline={baseline_final:.2f} accelerated={accelerated_final:.2f}"
)
print(
"Updated growth equation increased the final population from "
f"{baseline_final:.1f} to {accelerated_final:.1f}."
)
if __name__ == "__main__":
main()
Building a logistic population model programmatically
"""Create a new Simlin project and build a simple population model using pysimlin's edit API."""
from __future__ import annotations
import simlin
import simlin.pb as pb
def build_population_project() -> simlin.Project:
"""Return a project containing a logistic population model created via model.edit()."""
project = simlin.Project.new(
name="pysimlin-population-example",
sim_start=0.0,
sim_stop=80.0,
dt=0.25,
time_units="years",
)
project.set_sim_specs(
start=0.0,
stop=80.0,
dt={"value": 0.25},
time_units="years",
)
model = project.get_model()
with model.edit() as (_, patch):
population = pb.Variable.Stock()
population.ident = "population"
population.equation.scalar.equation = "50"
population.inflows.extend(["births"])
population.outflows.extend(["deaths"])
patch.upsert_stock(population)
births = pb.Variable.Flow()
births.ident = "births"
births.equation.scalar.equation = "population * birth_rate"
patch.upsert_flow(births)
deaths = pb.Variable.Flow()
deaths.ident = "deaths"
deaths.equation.scalar.equation = "population * birth_rate * (population / 1000)"
patch.upsert_flow(deaths)
birth_rate = pb.Variable.Aux()
birth_rate.ident = "birth_rate"
birth_rate.equation.scalar.equation = "0.08"
patch.upsert_aux(birth_rate)
return project
def validate_population_curve(values: list[float]) -> None:
"""Ensure the generated population series shows logistic (S-shaped) growth."""
if len(values) < 3:
raise RuntimeError("Population series is unexpectedly short")
if any(b < a for a, b in zip(values, values[1:])):
raise RuntimeError("Population should not decline in this model")
initial = values[0]
mid = values[len(values) // 2]
last = values[-1]
growth_first_half = mid - initial
growth_second_half = last - mid
if not growth_first_half > 0:
raise RuntimeError("Population failed to grow early in the simulation")
if not growth_second_half > 0:
raise RuntimeError("Population failed to grow late in the simulation")
if not growth_second_half < growth_first_half:
raise RuntimeError("Logistic growth should slow over time")
if not 950 <= last <= 1025:
raise RuntimeError(
"Population should approach the carrying capacity (~1000), "
f"but ended at {last:.2f}"
)
def main() -> None:
"""Build, simulate, and validate the population model."""
project = build_population_project()
errors = project.get_errors()
if errors:
raise RuntimeError(f"Generated project contains validation errors: {errors}")
model = project.get_model()
with model.new_sim() as sim:
sim.run_to_end()
population_series = [float(value) for value in sim.get_series("population")]
validate_population_curve(population_series)
print(
"Population grows from "
f"{population_series[0]:.1f} to {population_series[-1]:.1f}, forming an S-shaped trajectory."
)
if __name__ == "__main__":
main()
Both examples live under src/pysimlin/examples/ and are executed by scripts/pysimlin-tests.sh.
API Reference
Creating Projects
# Create a new project with a blank model
project = simlin.Project.new(name="my-project")
model = project.get_model() # defaults to the root model "main"
The engine currently expects the root model to be named main.
Loading Models
# Load from different formats
project = simlin.Project.from_xmile(xmile_bytes) # XMILE/STMX format
project = simlin.Project.from_mdl(mdl_bytes) # Vensim MDL format
project = simlin.Project.from_protobin(pb_bytes) # Binary protobuf format
# Auto-detect format from file extension
project = simlin.Project.from_file("model.stmx") # .stmx, .mdl, .pb, etc.
# Context manager for automatic cleanup
with simlin.Project.from_file("model.stmx") as project:
model = project.get_model()
# Project is automatically cleaned up when exiting the context
Working with Projects
# Get model information
model_names = project.get_model_names()
# Access models
model = project.get_model() # Get default/main model
model = project.get_model("submodel") # Get specific model by name
# Check for compilation errors
errors = project.get_errors()
if errors:
for error in errors:
print(f"{error.code.name}: {error.message}")
Model Analysis
# Get model structure
var_names = model.get_var_names()
var_count = model.get_var_count()
# Analyze variable dependencies
incoming_deps = model.get_incoming_links("population") # Variables that affect "population"
# Get causal links
links = model.get_links()
for link in links:
print(f"{link.from_var} --{link.polarity}--> {link.to_var}")
Model Editing
import simlin.pb as pb
# Edit existing model variables using context manager
with model.edit() as (current, patch):
# Access current variables by name
stock_var = current["population"]
# Modify the variable's properties
stock_var.stock.equation.scalar.equation = "100" # Change initial value
# Apply the change
patch.upsert_stock(stock_var.stock)
# Create new variables programmatically
with model.edit() as (current, patch):
# Create a new auxiliary variable
new_aux = pb.Variable.Aux()
new_aux.ident = "growth_rate"
new_aux.equation.scalar.equation = "0.05"
patch.upsert_aux(new_aux)
# Create a new flow variable
new_flow = pb.Variable.Flow()
new_flow.ident = "births"
new_flow.equation.scalar.equation = "population * growth_rate"
patch.upsert_flow(new_flow)
Running Simulations
# Create simulation
sim = model.new_sim() # Standard simulation
sim = model.new_sim(enable_ltm=True) # Enable Loops That Matter
# Run simulation
sim.run_to_end() # Run to final time
sim.run_to(50.0) # Run to specific time
# Reset to run again
sim.reset()
sim.run_to_end()
# Context manager for automatic cleanup
with model.new_sim() as sim:
sim.run_to_end()
results = sim.get_results()
Accessing Results
# Get results as pandas DataFrame
df = sim.get_results() # All variables
df = sim.get_results(variables=["x", "y"]) # Specific variables
# Get individual time series as numpy arrays
values = sim.get_series("population")
# Get current value (at current simulation time)
current_val = sim.get_value("population")
# Get metadata
step_count = sim.get_step_count()
Protobuf Types
The protobuf message types are directly accessible via the pb module:
import simlin.pb as pb
# Create protobuf messages for model editing
stock = pb.Variable.Stock()
flow = pb.Variable.Flow()
aux = pb.Variable.Aux()
# Access protobuf enums
sim_method = pb.SimMethod.EULER # or RUNGE_KUTTA_4
# Create complex structures
project = pb.Project()
model = pb.Model()
Model Interventions
# Set initial values before running
sim.set_value("initial_population", 1000)
sim.run_to_end()
# Mid-simulation interventions
sim.run_to(10)
sim.set_value("growth_rate", 0.05)
sim.run_to_end()
Feedback Loop Analysis
# Get all feedback loops
loops = project.get_loops()
for loop in loops:
print(f"Loop {loop.id} ({loop.polarity}): {' -> '.join(loop.variables)}")
# Check if variable is in a loop
for loop in loops:
if loop.contains_variable("population"):
print(f"Population is in loop {loop.id}")
Loops That Matter (LTM)
# Run simulation with LTM enabled
sim = model.new_sim(enable_ltm=True)
sim.run_to_end()
# Get links with importance scores over time
links = sim.get_links()
for link in links:
if link.has_score():
print(f"{link.from_var} -> {link.to_var}")
print(f" Average score: {link.average_score():.4f}")
print(f" Max score: {link.max_score():.4f}")
# Get relative loop scores
loops = project.get_loops()
if loops:
loop_scores = sim.get_relative_loop_score(loops[0].id)
Model Export
# Export to different formats
xmile_bytes = project.to_xmile() # Export as XMILE
pb_bytes = project.serialize() # Export as protobuf
# Save to file
Path("exported.stmx").write_bytes(xmile_bytes)
Path("model.pb").write_bytes(pb_bytes)
Error Handling
from simlin import (
SimlinError,
SimlinImportError,
SimlinRuntimeError,
SimlinCompilationError,
ErrorCode
)
try:
project = simlin.Project.from_file("model.stmx")
except SimlinImportError as e:
print(f"Import failed: {e}")
if e.code == ErrorCode.XML_DESERIALIZATION:
print("Invalid XML format")
# Check for compilation errors
errors = project.get_errors()
for error in errors:
print(f"{error.code.name} in {error.model_name}/{error.variable_name}")
print(f" {error.message}")
Complete Example
import simlin
import pandas as pd
import matplotlib.pyplot as plt
# Load and run a population model
with simlin.Project.from_file("population_model.stmx") as project:
model = project.get_model()
# Run baseline simulation
with model.new_sim() as sim:
sim.run_to_end()
baseline = sim.get_results()
# Run intervention scenario
with model.new_sim() as sim:
sim.set_value("birth_rate", 0.03)
sim.run_to_end()
intervention = sim.get_results()
# Compare results
fig, ax = plt.subplots()
ax.plot(baseline.index, baseline["population"], label="Baseline")
ax.plot(intervention.index, intervention["population"], label="Intervention")
ax.set_xlabel("Time")
ax.set_ylabel("Population")
ax.legend()
plt.show()
Supported Platforms
- macOS (ARM64)
- Linux (ARM64, x86_64)
License
Apache License 2.0
Development
For development setup and contribution guidelines, see the main Simlin repository.
Running Tests
cd src/pysimlin
pip install -e ".[dev]"
pytest
Building from Source
cd src/pysimlin
python -m build
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distributions
Built Distributions
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pysimlin-0.2.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.
File metadata
- Download URL: pysimlin-0.2.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
- Upload date:
- Size: 12.1 MB
- Tags: CPython 3.13, manylinux: glibc 2.17+ x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5a81a59a3dcfe01277764c57ad204806b945014e0ab1bbdb2a7c83575baadb13
|
|
| MD5 |
9b4398f45394dc33f5267cb61f294f87
|
|
| BLAKE2b-256 |
aa8ba7bf97465134b57420faab79d137534efd67f0d47f8ed185274e27d25683
|
File details
Details for the file pysimlin-0.2.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.
File metadata
- Download URL: pysimlin-0.2.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
- Upload date:
- Size: 11.8 MB
- Tags: CPython 3.13, manylinux: glibc 2.17+ ARM64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
13c23e231cfad03701bc3b58684d61fb8b8362a7bdbd1d740b8841b25bfa17df
|
|
| MD5 |
28e9ed5033dfa917b8cee58d1fd3a464
|
|
| BLAKE2b-256 |
503f9cf6d0434d4aaa585694956188826cc9157bbf8552fcc85c3e11efedcf54
|
File details
Details for the file pysimlin-0.2.2-cp313-cp313-macosx_11_0_arm64.whl.
File metadata
- Download URL: pysimlin-0.2.2-cp313-cp313-macosx_11_0_arm64.whl
- Upload date:
- Size: 2.8 MB
- Tags: CPython 3.13, macOS 11.0+ ARM64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a3c55f2b07982909681317beb4f5ece45f34e523e7eccee72831c31162e7a7fd
|
|
| MD5 |
e4ca5de04cbde23429db2bb0e8cd89af
|
|
| BLAKE2b-256 |
4bb5b0bdb7bb13ee910ffff0d9631f50fda65e2b8a92a0b8858b782e9e138398
|
File details
Details for the file pysimlin-0.2.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.
File metadata
- Download URL: pysimlin-0.2.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
- Upload date:
- Size: 12.1 MB
- Tags: CPython 3.12, manylinux: glibc 2.17+ x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3638dbfde9a91f05f54cc0023b5b5d683128ca3f69074418a676a8724dbd2b58
|
|
| MD5 |
44686b51802247d3eb241fda4f13db3b
|
|
| BLAKE2b-256 |
056a33d494ffa03337d1c4ba8f3827537dfa9fd53364df4a7ed2a8f4f829f978
|
File details
Details for the file pysimlin-0.2.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.
File metadata
- Download URL: pysimlin-0.2.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
- Upload date:
- Size: 11.8 MB
- Tags: CPython 3.12, manylinux: glibc 2.17+ ARM64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0f1f76afd72cdae881a100e304f35192672d95ae1fb91116ae7423ad2f845a5b
|
|
| MD5 |
30c92887484ad625d7d78bd4cbee08c2
|
|
| BLAKE2b-256 |
314e5cc39287e91ba82d44360fcf207e3f9dbc108913770dd51e1cbabd9fe941
|
File details
Details for the file pysimlin-0.2.2-cp312-cp312-macosx_11_0_arm64.whl.
File metadata
- Download URL: pysimlin-0.2.2-cp312-cp312-macosx_11_0_arm64.whl
- Upload date:
- Size: 2.8 MB
- Tags: CPython 3.12, macOS 11.0+ ARM64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3b50c411ebde55309230368220f53d6e41d80301b9f91dea1f2422c705ac17a4
|
|
| MD5 |
4f858dd575c00e3f4d982f3547214ff2
|
|
| BLAKE2b-256 |
50b51d4b9031fd4894a49ef4f7023fa993ab877218b2ab614bd99273724830c9
|
File details
Details for the file pysimlin-0.2.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.
File metadata
- Download URL: pysimlin-0.2.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
- Upload date:
- Size: 12.1 MB
- Tags: CPython 3.11, manylinux: glibc 2.17+ x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d963fcb3307dcedfdc24dcfaa6a9b48f45b94b5225608ba22606c7fbd700a457
|
|
| MD5 |
c324a0e23babc6967e8accc1c33e94dd
|
|
| BLAKE2b-256 |
2c446a89164533a56e5131bce02e57010154034c8f4602868f44b0f84ba0122b
|
File details
Details for the file pysimlin-0.2.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.
File metadata
- Download URL: pysimlin-0.2.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
- Upload date:
- Size: 11.8 MB
- Tags: CPython 3.11, manylinux: glibc 2.17+ ARM64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9c5ce9ff643e9bd8c4e4049946bca8f8ec458b140cc9926c0edb41e4d4e84cab
|
|
| MD5 |
f6a04284c28b2417fb22e32fc24f3c5a
|
|
| BLAKE2b-256 |
48b5ee1b0bc5bb72114f2ac4cc0dd20f90d46098638d2aea9afb50fae89e2cba
|
File details
Details for the file pysimlin-0.2.2-cp311-cp311-macosx_11_0_arm64.whl.
File metadata
- Download URL: pysimlin-0.2.2-cp311-cp311-macosx_11_0_arm64.whl
- Upload date:
- Size: 2.8 MB
- Tags: CPython 3.11, macOS 11.0+ ARM64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
623a291a4901dc4dd042ff729efecef97b0b528a8d80fd56e1af63632c8cde4e
|
|
| MD5 |
19508cbdca1dee5dca5096ad03c4765a
|
|
| BLAKE2b-256 |
cc45313ff6261c15e17882c089bc375b222007228325b18d9b005f9cce30cd6a
|