System Dynamics modeling library for Python that integrates with data science tools
—
PySD's functions module provides a comprehensive set of mathematical and utility functions equivalent to Vensim/XMILE built-ins, enabling System Dynamics models to perform complex calculations and logic operations.
Core mathematical operations with special handling for System Dynamics modeling requirements.
def if_then_else(condition, val_if_true, val_if_false):
"""
Conditional logic implementation.
Parameters:
- condition: bool or array - Conditional expression
- val_if_true: scalar or array - Value when condition is True
- val_if_false: scalar or array - Value when condition is False
Returns:
scalar or array: Selected value based on condition
"""
def xidz(numerator, denominator, x):
"""
Division with specified result for 0/0.
Parameters:
- numerator: scalar or array - Numerator value
- denominator: scalar or array - Denominator value
- x: scalar or array - Value returned when both numerator and denominator are 0
Returns:
scalar or array: Division result or x for 0/0 case
"""
def zidz(numerator, denominator):
"""
Division returning 0 for 0/0.
Parameters:
- numerator: scalar or array - Numerator value
- denominator: scalar or array - Denominator value
Returns:
scalar or array: Division result or 0 for 0/0 case
"""
def integer(x):
"""
Integer truncation.
Parameters:
- x: scalar or array - Input value
Returns:
int or array: Truncated integer value
"""
def quantum(a, b):
"""
Quantum function - rounds a to nearest multiple of b.
Parameters:
- a: scalar or array - Value to round
- b: scalar or array - Multiple to round to
Returns:
scalar or array: Rounded value
"""
def modulo(x, m):
"""
Modulo operation.
Parameters:
- x: scalar or array - Input value
- m: scalar or array - Modulo divisor
Returns:
scalar or array: Remainder after division
"""from pysd import functions
# Conditional logic
temperature = 25
comfort_level = functions.if_then_else(
temperature > 20,
"comfortable",
"cold"
)
# Safe division avoiding division by zero
growth_rate = functions.xidz(
new_population - old_population,
old_population,
0 # Return 0 if both are 0
)
# Integer operations
whole_units = functions.integer(3.7) # Returns 3Functions for time-based calculations and temporal patterns.
def ramp(time, slope, start, finish=None):
"""
Ramp function over time.
Parameters:
- time: scalar or array - Current time value
- slope: scalar - Rate of increase per time unit
- start: scalar - Time when ramp begins
- finish: scalar or None - Time when ramp ends (None for no end)
Returns:
scalar or array: Ramp value at given time
"""
def step(time, value, tstep):
"""
Step function at specified time.
Parameters:
- time: scalar or array - Current time value
- value: scalar - Step height
- tstep: scalar - Time when step occurs
Returns:
scalar or array: 0 before tstep, value at and after tstep
"""
def pulse(time, start, repeat_time=0, width=None, magnitude=None, end=None):
"""
Pulse and pulse train functions.
Parameters:
- time: scalar or array - Current time value
- start: scalar - Time when first pulse begins
- repeat_time: scalar - Time between pulses (0 for single pulse)
- width: scalar or None - Pulse duration (default: time step)
- magnitude: scalar or None - Pulse height (default: 1/time_step)
- end: scalar or None - Final time of the pulse (None for no end)
Returns:
scalar or array: Pulse value at given time
"""
def get_time_value(time, relativeto, offset, measure):
"""
Extract time values with offsets.
Parameters:
- time: scalar - Current time
- relativeto: str - Reference point ("start", "end")
- offset: scalar - Time offset from reference
- measure: str - Time unit ("years", "months", "days", "hours", "minutes", "seconds")
Returns:
scalar: Calculated time value
"""# Create time-based patterns
current_time = 15
# Gradual increase starting at time 10
production_ramp = functions.ramp(current_time, slope=2, start=10)
# Policy change at time 20
policy_effect = functions.step(current_time, value=100, tstep=20)
# Periodic maintenance every 12 time units
maintenance = functions.pulse(current_time, start=0, repeat_time=12, width=1)Operations on multi-dimensional arrays and subscripted variables.
def sum(x, dim=None):
"""
Sum along specified dimensions.
Parameters:
- x: array - Input array
- dim: str or list or None - Dimensions to sum over (None for all)
Returns:
scalar or array: Sum result
"""
def prod(x, dim=None):
"""
Product along dimensions.
Parameters:
- x: array - Input array
- dim: str or list or None - Dimensions to multiply over
Returns:
scalar or array: Product result
"""
def vmin(x, dim=None):
"""
Minimum along dimensions.
Parameters:
- x: array - Input array
- dim: str or list or None - Dimensions to find minimum over
Returns:
scalar or array: Minimum value
"""
def vmax(x, dim=None):
"""
Maximum along dimensions.
Parameters:
- x: array - Input array
- dim: str or list or None - Dimensions to find maximum over
Returns:
scalar or array: Maximum value
"""
def invert_matrix(mat):
"""
Matrix inversion.
Parameters:
- mat: 2D array - Input matrix
Returns:
2D array: Inverted matrix
Raises:
LinAlgError: If matrix is singular
"""import numpy as np
# Array operations on subscripted variables
population_by_region = np.array([1000, 1500, 800, 1200])
# Total population across all regions
total_pop = functions.sum(population_by_region)
# Find region with maximum population
max_pop = functions.vmax(population_by_region)
# Matrix operations for economic modeling
input_output_matrix = np.array([[0.2, 0.3], [0.4, 0.1]])
inverse_matrix = functions.invert_matrix(input_output_matrix)Specialized functions for vector manipulation and analysis.
def vector_select(selection_array, expression_array, dim, missing_vals, numerical_action, error_action):
"""
Vector selection operations implementing Vensim's VECTOR SELECT function.
Parameters:
- selection_array: xarray.DataArray - Selection array with zeros and non-zero values
- expression_array: xarray.DataArray - Expression that elements are selected from
- dim: list - Dimensions to apply the function over
- missing_vals: float - Value to use when there are only zeroes in selection array
- numerical_action: int - Action to take (0=sum, 1=product, 2=min, 3=max)
- error_action: int - Error handling action
Returns:
xarray.DataArray: Selected vector elements based on selection criteria
"""
def vector_sort_order(vector, direction=1):
"""
Vector sorting indices.
Parameters:
- vector: array - Input vector
- direction: int - Sort direction (1 for ascending, -1 for descending)
Returns:
array: Indices that would sort the vector
"""
def vector_reorder(vector, svector):
"""
Vector reordering based on sort indices.
Parameters:
- vector: array - Vector to reorder
- svector: array - Sort indices
Returns:
array: Reordered vector
"""
def vector_rank(vector, direction=1):
"""
Vector ranking.
Parameters:
- vector: array - Input vector
- direction: int - Ranking direction (1 for ascending, -1 for descending)
Returns:
array: Rank of each element (1-based)
"""# Vector analysis
sales_data = np.array([150, 200, 120, 180, 220])
# Get sorting order
sort_indices = functions.vector_sort_order(sales_data, direction=-1) # Descending
# Reorder vector
sorted_sales = functions.vector_reorder(sales_data, sort_indices)
# Get rankings
sales_ranks = functions.vector_rank(sales_data, direction=-1)
print(f"Sales rankings: {sales_ranks}")Helper functions for model development and debugging.
def active_initial(stage, expr, init_val):
"""
Active initial value handling.
Parameters:
- stage: str - Initialization stage
- expr: callable - Expression to evaluate
- init_val: scalar - Initial value
Returns:
scalar: Appropriate value based on stage
"""
def incomplete(*args):
"""
Placeholder for incomplete functions.
Parameters:
- *args: Any arguments
Returns:
float: Returns 0.0
Note:
Used as placeholder during model development
"""
def not_implemented_function(*args):
"""
Placeholder for unimplemented functions.
Parameters:
- *args: Any arguments
Raises:
NotImplementedError: Always raises this error
Note:
Used to mark functions that need implementation
"""These functions are automatically available within translated System Dynamics models and can also be used directly:
import pysd
from pysd import functions
# Load model that uses these functions internally
model = pysd.read_vensim('complex_model.mdl')
# Functions are used automatically during simulation
results = model.run()
# Can also use functions directly for custom calculations
custom_result = functions.if_then_else(
results['Population'].iloc[-1] > 10000,
"High population",
"Low population"
)Functions include appropriate error handling for System Dynamics contexts:
# Safe operations that won't crash simulations
safe_ratio = functions.zidz(numerator, 0) # Returns 0 instead of error
conditional_value = functions.if_then_else(
denominator != 0,
numerator / denominator,
0
)Install with Tessl CLI
npx tessl i tessl/pypi-pysd