Introduction

Welcome to the se-lib User’s Guide with examples. The examples after the User’s Guide can be run separately from their cells, copied and pasted.


Installation

Run the cell below once at the beginning of each session to install the se-lib library. The from selib import * statement will import all its functions and classes to be called with no alias in the notebook.


!pip install -q se-lib

from selib import *


System Dynamics Modeling User’s Guide

Table of Contents


1. Overview

This manual provides an introduction and reference for modeling continuous systems using the se-lib Python package. se-lib supports the construction and simulation of system dynamics models using standard stock and flow structures using both procedural and object-oriented approaches. It adheres to the XMILE standard for system dynamics models and integrates with PySD for simulation execution. Models are compatible with Vensim and iThink/Stella tools using XMILE and MDL formats for importing and exporting.

The object-oriented interface is built around the SystemDynamicsModel class, enabling encapsulation of models, concurrent instantiation, and extensions of the model class. Users may also use the original procedural API for quick prototyping or compatibility with existing scripts. The procedural approach provides a more implicit modeling, but can be less flexible.

The manual includes reusable modeling templates, core system dynamics patterns such as goal-gap control and balancing loops, recommended modeling practices, detailed API documentation, and visual representations of feedback logic. It is suitable for a wide range of applications in systems thinking, behavioral simulation, engineering analysis, and education.

Detailed function references and more examples are available online at http://selib.org/function_reference.html#system-dynamics.


2. Getting Started with SE-Lib

To install the latest version of se-lib, use pip:

pip install se-lib

A system model is described by defining the standard elements for stocks (levels), flows (rates), and auxiliary constants or equations. Utility functions are available for equation formulation, data collection and displaying output.

Model construction begins by importing se-lib and initializing a model with the required time parameters. Simulations operate over a user-defined time horizon and return data as pandas DataFrames, which can be used for analysis and visualization.

from selib import *
init_sd_model(start=0, stop=30, dt=1)

Refer to Section 11 for a complete description of the modeling API.


3. Modeling Guidance and Tips

Modeling with se-lib requires attention to syntax, naming conventions, and logical structure. Names of model elements are specified as character strings and should not contain spaces; underscores (_) must be used to separate terms (e.g., Production_Rate). Variable names are case-sensitive and must exactly match those used in equations.

Equations are expressed as strings using standard arithmetic operators. Variables referenced in equations must already be defined within the model scope. The simulator does not check equations at entry time, so errors from undefined variables or misspellings will only become apparent at runtime.

Conditional statements can be expressed with logical constructs or, and, and not or the XMILE uppercase standards OR, AND, and NOT. The if_then_else(condition, value if true, value if false) or uppercase IF_THEN_ELSE(condition, value if true, value if false) are available.

Random number functions such as random() and random.uniform(a, b) are supported and internally translated into XMILE-compliant expressions. These generate new values at each timestep and are useful for modeling uncertainty or variability in external influences.

Time in se-lib is dimensionless. Modelers must maintain consistency in time unit interpretation (e.g., using all variables in units of days, months, etc.). Inconsistent use of time units can lead to misleading or invalid results.

The draw_model() function is recommended for verifying model structure prior to simulation. This visual representation helps ensure the correctness of stock-flow relationships and the presence of intended feedback loops.


4. Basic Modeling Patterns

4.1 Negative Feedback Loop

This classic balancing structure adjusts a level toward a target over time. It works by calculating the gap between a goal and the current level, then feeding a corrective flow in the direction that reduces this gap. The delay serves as a time constant, governing the speed of the system’s response. Over time, the level asymptotically approaches the goal value.

from selib import *

# negative feedback
init_sd_model(start=0, stop=5, dt=.1)
add_stock("level", 50, inflows=["rate"])
add_auxiliary("delay", .5)
add_auxiliary("goal", 100)
add_flow("rate", "(goal - level) / delay")

draw_model()
run_model()
plot_graph("level")

negative_feedback_diagram

negative_feedback_run_output

negative_feedback_level_graph

4.2 Exponential Growth

This reinforcing loop demonstrates positive feedback. The flow increases as the stock increases, which in turn further amplifies the flow. This results in exponential behavior and is frequently seen in unchecked population growth, viral spread, or compound interest.

from selib import *

# exponential growth
init_sd_model(start=0, stop=10, dt=.5)
add_stock("level", 100, inflows=["rate"])
add_auxiliary("growth_fraction", .8)
add_flow("rate", "growth_fraction * level")

draw_model()
run_model()
plot_graph('level')

exponential_growth_diagram

exponential_growth_run_output

exponential_growth_level_graph

4.3 Balancing Loop

A balancing loop with inventory control adjusts production to maintain a desired stock level. When inventory falls below target, production increases; when it’s above target, production slows. This loop exhibits goal-seeking behavior and helps stabilize system performance. External demand acts as an opposing outflow.

from selib import *

init_sd_model(0, 10, 1)
add_stock("Inventory", 50, inflows=["Production_Rate"], outflows=["Demand_Rate"])
add_auxiliary("Target_Inventory", 100)
add_auxiliary("Adj_Time", 4)
add_flow("Production_Rate", "(Target_Inventory - Inventory) / Adj_Time")
add_auxiliary("Demand", 10)
add_flow("Demand_Rate", "Demand")

draw_model()
run_model()
plot_graph(["Inventory", "Production_Rate"])

balancing_loop_model_diagram

balancing_loop_model_run_output

balancing_loop_model_output

4.4 Goal-Gap Structure

This control structure reduces the deviation between a system’s state and a set point. A feedback signal (difference between goal and current value) is amplified by a gain factor and used to drive the stock toward the goal.

from selib import *

init_sd_model(0, 30, 1)
add_stock("Temperature", 20, inflows=["Heating"])
add_auxiliary("Set_Point", 22)
add_auxiliary("Gain", 1.5)
add_flow("Heating", "(Set_Point - Temperature) * Gain")

draw_model()
run_model()
plot_graph(["Temperature", "Set_Point"])

goal_gap_structure_delay_model_diagram

goal_gap_structure_model_run_output

goal_gap_structure_model_output

4.5 First-Order Delay

First-order delays represent lagged responses where a stock adjusts toward an input with exponential smoothing. The greater the delay time, the slower the convergence.

from selib import *

init_sd_model(0, 15, 1)
add_stock("Output", 0, inflows=["Flow"])
add_auxiliary("Input", 100)
add_auxiliary("Delay_Time", 5)
add_flow("Flow", "(Input - Output) / Delay_Time")

draw_model()
run_model()
plot_graph(["Input", "Output"])

first_order_delay_model_diagram

first_order_delay_model_run_output

first_order_delay_model_output

4.6 Second-Order Delay

A second-order delay structure creates smoother responses by cascading two first-order delays. This is useful when modeling systems with inertia or buffer stages such as material transport, bureaucratic processes, or cognitive recognition lags.

from selib import *

init_sd_model(0, 30, 1)
add_stock("Stage1", 0, inflows=["Flow1"], outflows=["Flow2"])
add_stock("Stage2", 0, inflows=["Flow2"])
add_auxiliary("Input", 100)
add_auxiliary("DelayTime", 5)
add_flow("Flow1", "(Input - Stage1) / DelayTime")
add_flow("Flow2", "(Stage1 - Stage2) / DelayTime")

draw_model()
run_model()
plot_graph(["Stage1", "Stage2"])

second_order_delay_model_diagram

second_order_delay_model_run_output

second_order_delay_model_output


5. Displaying Output

Upon execution of a system dynamics model, se-lib returns simulation results as a pandas.DataFrame object. This output contains the time-indexed values of all defined stocks, flows, and auxiliaries. The DataFrame facilitates further analysis, visualization, or export to external tools. The `run_model() function will execute a simulation and display a Pandas dataframe of the output. To execute a model and capture its output:

results = run_model()

Each column in the DataFrame corresponds to a model variable, and each row represents a timestep, as defined by the start, stop, and dt parameters provided during model initialization.

To inspect specific variables or values at particular timesteps, standard pandas indexing may be used:

results['Population']               # Full time series for Population
results['Births'][10]              # Value of Births at the 10th timestep
results[['Population', 'Births']] # Multiple variables

This structured output enables direct integration with plotting functions, statistical analysis routines, or export mechanisms. All graphing and saving operations described in Section 6 operate directly on this result structure.


6. Graphing Functions

se-lib provides built-in graphing capabilities to visualize model outputs directly from the simulation results. These graphs are generated using the matplotlib library and support both single-variable and multi-variable plots.

Plotting to Screen

The plot_graph() function displays simulation results interactively. It supports multiple input formats: single variable names, separate arguments, or a list of variables.

plot_graph("Population")
plot_graph("Births", "Deaths")
plot_graph(["Population", "Births", "Deaths"])

When a list is used, variables are plotted together on a shared axis. When passed as separate arguments, they are displayed on individual subplots.

Saving to File

The save_graph() function saves output graphs to image files. The default format is PNG, and the format may be changed by modifying the filename extension.

save_graph("Population", filename="population_plot.png")
save_graph(["Births", "Deaths"], filename="flow_graph.svg")

Saved graphs are rendered as separate figures and written to disk, which is useful for reporting or presentation workflows. As with plotting, graphing functions operate on the output returned by run_model().


7. Random Number Functions

se-lib supports stochastic modeling using random number generation within auxiliary and flow equations. This allows the representation of uncertainty, noise, or nondeterministic behavior in the modeled system. In equations for auxiliaries and rates, the random number functions supported are called as if the following import has occurred: from random import random, random.uniform.

The random functions supported are:

  • random(): returns a new float uniformly distributed between 0 and 1 at each timestep.

  • random.uniform(min, max): returns a new float uniformly distributed between min and max at each timestep.

These functions are automatically translated to XMILE-compatible expressions when the model is compiled for simulation. For xmile format compatibility, the functions RANDOM_0_1 and RANDOM_UNIFORM(min, max) are equivalent and also acceptable.

Example

The following example defines an auxiliary variable using random noise:

init_sd_model(start=0, stop=3, dt=.5)
add_auxiliary("random_parameter", "20*random()")
run_model()
( INITIAL TIME FINAL TIME TIME STEP SAVEPER random_parameter
0.0 0 3 0.5 0.5 0.087466
0.5 0 3 0.5 0.5 11.174929
1.0 0 3 0.5 0.5 13.506344
1.5 0 3 0.5 0.5 4.342211
2.0 0 3 0.5 0.5 5.449781
2.5 0 3 0.5 0.5 7.323554
3.0 0 3 0.5 0.5 17.220789,

Each timestep generates a new random value for Stochastic_Input, producing a non-repeating time series. It is important to note that random values are recomputed each time the model is run, and results may vary unless reproducibility measures (e.g., seeding) are implemented outside SE-Lib.


8. Utility and Test Functions

Standard test functions are available for pulse, ramp and step inputs as shown below. The full set of functions available in PySD can be used but have not all been tested.

init_sd_model(0, 10, 1)
add_stock("Level", 0, inflows=["Pulse", "Ramp"])
add_flow("Pulse", "pulse(100, 2)")
add_flow("Ramp", "ramp(3, 5)")
run_model()
plot_graph("Pulse", "Ramp", "Level")

9. Advanced Usage

9.1 Extending a Model Instance

Create a new class that extends a population model by adding carrying capacity with the following.

class PopulationWithCapacityModel(SystemDynamicsModel):
    def __init__(self, start, stop, dt, initial_population, carrying_capacity):
        super().__init__(start, stop, dt)
        self.add_stock("Population", initial=initial_population, inflows=["Births"], outflows=["Deaths"])
        self.add_auxiliary("BirthRate", equation=0.02)
        self.add_auxiliary("CarryingCapacity", equation=carrying_capacity)
        self.add_auxiliary("CrowdingEffect", equation="Population / CarryingCapacity")
        self.add_auxiliary("EffectiveGrowthRate", equation="BirthRate * (1 - CrowdingEffect)")
        self.add_flow("Births", equation="Population * EffectiveGrowthRate")
        self.add_auxiliary("DeathRate", equation=0.01)
        self.add_flow("Deaths", equation="Population * DeathRate") # Keep a simple death rate

# Create an instance of the extended model
population_capacity_model = PopulationWithCapacityModel(start=0, stop=200, dt=1, initial_population=50, carrying_capacity=500)

9.1 PySD Integration

SE-Lib is fully compatible with PySD. Any PySD function or supported XMILE structure may be used where appropriate. See PySD Docs for advanced formulations.


10. Appendix A - Function Reference

The following functions and methods define the modeling API for se-lib. All functions can be used in either procedural or object-oriented workflows.

init_sd_model(start, stop, dt, stop_when=None)

Initializes a new system dynamics model over the specified simulation time frame.

Args: - start (float): Start time of the simulation. - stop (float): End time of the simulation. - dt (float): Time step for numerical integration. - stop_when (float): Logical condition using model variables to stop the simulation.

Returns: - SystemDynamicsModel instance

Example:

init_sd_model(start=0, stop=50, dt=1.0)

Examples with stop_when:

init_sd_model(start=0, stop=50, dt=1.0, stop_when("stock1 >= 100")
init_sd_model(start=0, stop=50, dt=1.0, stop_when("stock1 >= 100 or stock2 >=80")

add_stock(name, initial, inflows=[], outflows=[])

Adds a stock variable (level) to the system dynamics model.

Args: - name (str): Name of the stock. - initial (float): Initial value of the stock. - inflows (list, optional): Names of inflow variables affecting this stock. - outflows (list, optional): Names of outflow variables affecting this stock.

Returns: - None

Example:

add_stock("Population", initial=100, inflows=["Births"], outflows=["Deaths"])

add_flow(name, equation)

Defines a flow (rate of change) for use in stock updates.

Args: - name (str): Name of the flow. - equation (str): Equation as a string to compute the flow value.

Returns: - None

Example:

add_flow("Births", "Birth_Rate * Population")

add_auxiliary(name, equation)

Defines an auxiliary variable or constant used in the model.

Args: - name (str): Name of the auxiliary variable. - equation (str): String representation of the equation or constant value.

Returns: - None

Example:

add_auxiliary("Birth_Rate", "0.02")

add_auxiliary_lookup(name, input, xpoints, ypoints)

Creates a lookup function for a nonlinear relationship.

Args: - name (str): Name of the auxiliary variable. - input (str): Name of the input variable used in the lookup. - xpoints (list): List of input values (x-axis). - ypoints (list): List of output values (y-axis).

Returns: - None

Example:

add_auxiliary_lookup("Saturation", "Population", [0, 1000, 2000], [1, 0.5, 0.1])

plot_graph(*outputs)

Plots model results using matplotlib.

Args: - *outputs (str or list): Names of variables to plot (e.g., “Population”, [“Births”, “Deaths”]).

Returns: - None

Example:

plot_graph("Population")
plot_graph(["Births", "Deaths"])

save_graph(*outputs, filename='graph.png')

Saves a plot of the simulation

draw_sd_model(filename=None, format=’svg’)

Generates and renders a visual system dynamics model diagram using Graphviz.

get_stop_time()

Returns the logical end time based on a stop_when condition.

get_model_structure()

Returns a dictionary of model components (stocks, flows, auxiliaries).


Examples


Exponential Growth


from selib import *

# exponential_growth
init_sd_model(start=0, stop=10, dt=.5)
add_stock("level", 100, inflows=["rate"])
add_auxiliary("growth_fraction", .8)
add_flow("rate", "growth_fraction * level")

draw_model()
run_model()

save_graph('level', filename="exponential_growth.png")

--------------

SVG Diagram 1




      INITIAL TIME  FINAL TIME  TIME STEP  SAVEPER  growth_fraction  \
time
0.0              0          10        0.5      0.5              0.8
0.5              0          10        0.5      0.5              0.8
1.0              0          10        0.5      0.5              0.8
1.5              0          10        0.5      0.5              0.8
2.0              0          10        0.5      0.5              0.8
2.5              0          10        0.5      0.5              0.8
3.0              0          10        0.5      0.5              0.8
3.5              0          10        0.5      0.5              0.8
4.0              0          10        0.5      0.5              0.8
4.5              0          10        0.5      0.5              0.8
5.0              0          10        0.5      0.5              0.8
5.5              0          10        0.5      0.5              0.8
6.0              0          10        0.5      0.5              0.8
6.5              0          10        0.5      0.5              0.8
7.0              0          10        0.5      0.5              0.8
7.5              0          10        0.5      0.5              0.8
8.0              0          10        0.5      0.5              0.8
8.5              0          10        0.5      0.5              0.8
9.0              0          10        0.5      0.5              0.8
9.5              0          10        0.5      0.5              0.8
10.0             0          10        0.5      0.5              0.8

              rate         level
time
0.0      80.000000    100.000000
0.5     112.000000    140.000000
1.0     156.800000    196.000000
1.5     219.520000    274.400000
2.0     307.328000    384.160000
2.5     430.259200    537.824000
3.0     602.362880    752.953600
3.5     843.308032   1054.135040
4.0    1180.631245   1475.789056
4.5    1652.883743   2066.104678
5.0    2314.037240   2892.546550
5.5    3239.652136   4049.565170
6.0    4535.512990   5669.391238
6.5    6349.718186   7937.147733
7.0    8889.605460  11112.006826
7.5   12445.447645  15556.809556
8.0   17423.626702  21779.533378
8.5   24393.077383  30491.346729
9.0   34150.308337  42687.885421
9.5   47810.431672  59763.039589
10.0  66934.604340  83668.255425


--------------

PNG Image 2


Negative Feedback


# negative feedback
from selib import *

init_sd_model(start=0, stop=10, dt=.1)
add_stock("level", 50, inflows=["rate"])
add_auxiliary("delay", .5)
add_auxiliary("goal", 100)
add_flow("rate", "(goal - level) / delay")

draw_model()
run_model()

save_graph(['level', 'goal', 'rate'], filename="negative_feedback.svg")

--------------

SVG Diagram 3




      INITIAL TIME  FINAL TIME  TIME STEP  SAVEPER  delay  goal          rate  \
time
0.0              0          10        0.1      0.1    0.5   100  1.000000e+02
0.1              0          10        0.1      0.1    0.5   100  8.000000e+01
0.2              0          10        0.1      0.1    0.5   100  6.400000e+01
0.3              0          10        0.1      0.1    0.5   100  5.120000e+01
0.4              0          10        0.1      0.1    0.5   100  4.096000e+01
...            ...         ...        ...      ...    ...   ...           ...
9.6              0          10        0.1      0.1    0.5   100  4.973231e-08
9.7              0          10        0.1      0.1    0.5   100  3.978585e-08
9.8              0          10        0.1      0.1    0.5   100  3.182868e-08
9.9              0          10        0.1      0.1    0.5   100  2.546295e-08
10.0             0          10        0.1      0.1    0.5   100  2.037035e-08

       level
time
0.0    50.00
0.1    60.00
0.2    68.00
0.3    74.40
0.4    79.52
...      ...
9.6   100.00
9.7   100.00
9.8   100.00
9.9   100.00
10.0  100.00

[101 rows x 8 columns]


--------------

PNG Image 4


Goal Seeking Behavior


from selib import *

# goal seeking behavior with negative feedback

init_sd_model(start=0, stop=10, dt=.1)
add_stock("level", 50, inflows=["rate"])
add_auxiliary("time_constant", .5)
add_auxiliary("goal", 200)
add_flow("rate", "(goal - level) / time_constant")

draw_model()
run_model()

plot_graph(['goal', 'level'])

--------------

SVG Diagram 5




      INITIAL TIME  FINAL TIME  TIME STEP  SAVEPER  time_constant  goal  \
time
0.0              0          10        0.1      0.1            0.5   200
0.1              0          10        0.1      0.1            0.5   200
0.2              0          10        0.1      0.1            0.5   200
0.3              0          10        0.1      0.1            0.5   200
0.4              0          10        0.1      0.1            0.5   200
...            ...         ...        ...      ...            ...   ...
9.6              0          10        0.1      0.1            0.5   200
9.7              0          10        0.1      0.1            0.5   200
9.8              0          10        0.1      0.1            0.5   200
9.9              0          10        0.1      0.1            0.5   200
10.0             0          10        0.1      0.1            0.5   200

              rate   level
time
0.0   3.000000e+02   50.00
0.1   2.400000e+02   80.00
0.2   1.920000e+02  104.00
0.3   1.536000e+02  123.20
0.4   1.228800e+02  138.56
...            ...     ...
9.6   1.491970e-07  200.00
9.7   1.193576e-07  200.00
9.8   9.548609e-08  200.00
9.9   7.638886e-08  200.00
10.0  6.111111e-08  200.00

[101 rows x 8 columns]


--------------

PNG Image 6


Rayleigh Curve Staffing


# Rayleigh Curve Staffing
from selib import *

init_sd_model(start=0, stop=6, dt=.2)
add_stock("cumulative_effort", 0, inflows=["effort rate"])
add_flow("effort rate",
         "learning_function * (estimated_total_effort - cumulative_effort)")
add_auxiliary("learning_function", "manpower_buildup_parameter * time")
add_auxiliary("manpower_buildup_parameter", .5)
add_auxiliary("estimated_total_effort", 30)

draw_model()
run_model()

save_graph("effort rate", filename="rayleigh_curve_effort_rate.png")
save_graph(["effort rate", "cumulative_effort"],
           filename="rayleigh_curve_components.png")

--------------

SVG Diagram 7




      INITIAL TIME  FINAL TIME  TIME STEP  SAVEPER  learning_function  \
time
0.0              0           6        0.2      0.2                0.0
0.2              0           6        0.2      0.2                0.1
0.4              0           6        0.2      0.2                0.2
0.6              0           6        0.2      0.2                0.3
0.8              0           6        0.2      0.2                0.4
1.0              0           6        0.2      0.2                0.5
1.2              0           6        0.2      0.2                0.6
1.4              0           6        0.2      0.2                0.7
1.6              0           6        0.2      0.2                0.8
1.8              0           6        0.2      0.2                0.9
2.0              0           6        0.2      0.2                1.0
2.2              0           6        0.2      0.2                1.1
2.4              0           6        0.2      0.2                1.2
2.6              0           6        0.2      0.2                1.3
2.8              0           6        0.2      0.2                1.4
3.0              0           6        0.2      0.2                1.5
3.2              0           6        0.2      0.2                1.6
3.4              0           6        0.2      0.2                1.7
3.6              0           6        0.2      0.2                1.8
3.8              0           6        0.2      0.2                1.9
4.0              0           6        0.2      0.2                2.0
4.2              0           6        0.2      0.2                2.1
4.4              0           6        0.2      0.2                2.2
4.6              0           6        0.2      0.2                2.3
4.8              0           6        0.2      0.2                2.4
5.0              0           6        0.2      0.2                2.5
5.2              0           6        0.2      0.2                2.6
5.4              0           6        0.2      0.2                2.7
5.6              0           6        0.2      0.2                2.8
5.8              0           6        0.2      0.2                2.9
6.0              0           6        0.2      0.2                3.0

      manpower_buildup_parameter  estimated_total_effort  effort rate  \
time
0.0                          0.5                      30     0.000000
0.2                          0.5                      30     3.000000
0.4                          0.5                      30     5.880000
0.6                          0.5                      30     8.467200
0.8                          0.5                      30    10.612224
1.0                          0.5                      30    12.204058
1.2                          0.5                      30    13.180382
1.4                          0.5                      30    13.531859
1.6                          0.5                      30    13.299884
1.8                          0.5                      30    12.568391
2.0                          0.5                      30    11.451200
2.2                          0.5                      30    10.077056
2.4                          0.5                      30     8.574659
2.6                          0.5                      30     7.059802
2.8                          0.5                      30     5.626120
3.0                          0.5                      30     4.340149
3.2                          0.5                      30     3.240645
3.4                          0.5                      30     2.341366
3.6                          0.5                      30     1.636202
3.8                          0.5                      30     1.105345
4.0                          0.5                      30     0.721383
4.2                          0.5                      30     0.454471
4.4                          0.5                      30     0.276145
4.6                          0.5                      30     0.161671
4.8                          0.5                      30     0.091098
5.0                          0.5                      30     0.049345
5.2                          0.5                      30     0.025659
5.4                          0.5                      30     0.012790
5.6                          0.5                      30     0.006101
5.8                          0.5                      30     0.002780
6.0                          0.5                      30     0.001208

      cumulative_effort
time
0.0            0.000000
0.2            0.000000
0.4            0.600000
0.6            1.776000
0.8            3.469440
1.0            5.591885
1.2            8.032696
1.4           10.668773
1.6           13.375145
1.8           16.035121
2.0           18.548800
2.2           20.839040
2.4           22.854451
2.6           24.569383
2.8           25.981343
3.0           27.106567
3.2           27.974597
3.4           28.622726
3.6           29.090999
3.8           29.418239
4.0           29.639308
4.2           29.783585
4.4           29.874479
4.6           29.929708
4.8           29.962043
5.0           29.980262
5.2           29.990131
5.4           29.995263
5.6           29.997821
5.8           29.999041
6.0           29.999597


--------------

PNG Image 8


--------------

PNG Image 9


Random Functions and Measuring Stop Time

This example illustrates measuring elapsed simulation time using the stop_when initialization parameter and get_stop_time() function. It captures the project end when staffing effort rate goes to zero.


from selib import *

init_sd_model(start=0, stop=6, dt=.2, stop_when="effort_rate <= 0")
add_stock("cumulative_effort", 0, inflows=["effort rate"])
add_flow("effort_rate",
    "learning_function * (estimated_total_effort - cumulative_effort)")
add_auxiliary("learning_function", "manpower_buildup_parameter * time")
add_auxiliary("manpower_buildup_parameter", 'random.uniform(.2, .7)')
add_auxiliary("estimated_total_effort", 'random.uniform(25, 35)')

run_model()

print(f"Project end time is {get_stop_time()}")

Simulation ends at time = 3.0

      INITIAL TIME  FINAL TIME  TIME STEP  SAVEPER  learning_function  \
time
0.0              0           6        0.2      0.2           0.000000
0.2              0           6        0.2      0.2           0.122395
0.4              0           6        0.2      0.2           0.235581
0.6              0           6        0.2      0.2           0.206761
0.8              0           6        0.2      0.2           0.304143
1.0              0           6        0.2      0.2           0.207023
1.2              0           6        0.2      0.2           0.459536
1.4              0           6        0.2      0.2           0.320776
1.6              0           6        0.2      0.2           0.781662
1.8              0           6        0.2      0.2           0.851055
2.0              0           6        0.2      0.2           1.260378
2.2              0           6        0.2      0.2           1.045175
2.4              0           6        0.2      0.2           1.072665
2.6              0           6        0.2      0.2           1.511612
2.8              0           6        0.2      0.2           1.299925
3.0              0           6        0.2      0.2           1.825976

      manpower_buildup_parameter  estimated_total_effort  effort_rate  \
time
0.0                     0.682189               34.323998     0.000000
0.2                     0.611974               27.620483     3.380603
0.4                     0.588953               30.330142     6.985926
0.6                     0.344601               29.166838     5.601875
0.8                     0.380179               33.010074     9.068448
1.0                     0.207023               31.018010     5.384799
1.2                     0.382947               32.624585    12.196203
1.4                     0.229126               28.600847     6.440316
1.6                     0.488538               30.481037    16.156478
1.8                     0.472808               29.680247    14.159276
2.0                     0.630189               31.585909    19.801960
2.2                     0.475079               25.422697     5.839935
2.4                     0.446944               31.348129    11.096682
2.6                     0.581389               29.838478    10.000791
2.8                     0.464259               30.166600     6.426755
3.0                     0.608659               25.307849    -2.191463

      cumulative_effort
time
0.0            0.000000
0.2            0.000000
0.4            0.676121
0.6            2.073306
0.8            3.193681
1.0            5.007370
1.2            6.084330
1.4            8.523571
1.6            9.811634
1.8           13.042930
2.0           15.874785
2.2           19.835177
2.4           21.003164
2.6           23.222500
2.8           25.222658
3.0           26.508009


Project end time is 3.0


Predator Prey Model


# Predator Prey Model with Carrying Capacity
import selib as se

# Initialize model
init_sd_model(start=0, stop=300, dt=0.25)

# Auxiliaries
add_auxiliary_lookup("Carrying_Capacity_Effect", "Hares",
                     ["0, 500, 1000, 1500, 2000"], ["1, .8, .5, .2, .05"])

add_auxiliary("Hare_Growth_Rate", "0.162 * Carrying_Capacity_Effect")
add_auxiliary("Hare_Death_Rate", ".0008 * Lynx")
add_auxiliary("Lynx_Death_Rate", "0.12")
add_auxiliary("Lynx_Birth_Rate", "0.001 * Hares")

# Stocks
add_stock("Hares", "150", inflows=["Hare_Births"], outflows=["Hare_Deaths"])
add_stock("Lynx", "100", inflows=["Lynx_Births"], outflows=["Lynx_Deaths"])

# Flows
add_flow("Hare_Births", "Hare_Growth_Rate * Hares")
add_flow("Hare_Deaths", "Hare_Death_Rate * Hares")
add_flow("Lynx_Births", "Lynx_Birth_Rate * Lynx")
add_flow("Lynx_Deaths", "Lynx_Death_Rate * Lynx")

d = draw_model_diagram(filename="sd")
results = run_model()
save_graph(["Hares", "Lynx"])

--------------

SVG Diagram 10




        INITIAL TIME  FINAL TIME  TIME STEP  SAVEPER  \
time
0.00               0         300       0.25     0.25
0.25               0         300       0.25     0.25
0.50               0         300       0.25     0.25
0.75               0         300       0.25     0.25
1.00               0         300       0.25     0.25
...              ...         ...        ...      ...
299.00             0         300       0.25     0.25
299.25             0         300       0.25     0.25
299.50             0         300       0.25     0.25
299.75             0         300       0.25     0.25
300.00             0         300       0.25     0.25

        Carrying_Capacity_Effect  Hare_Growth_Rate  Hare_Death_Rate  \
time
0.00                    0.940000          0.152280         0.080000
0.25                    0.938916          0.152104         0.080600
0.50                    0.937824          0.151927         0.081259
0.75                    0.936725          0.151750         0.081979
1.00                    0.935622          0.151571         0.082762
...                          ...               ...              ...
299.00                  0.942403          0.152669         0.212617
299.25                  0.943266          0.152809         0.213893
299.50                  0.944132          0.152949         0.215060
299.75                  0.945000          0.153090         0.216118
300.00                  0.945866          0.153230         0.217063

        Lynx_Death_Rate  Lynx_Birth_Rate  Hare_Births  Hare_Deaths  \
time
0.00               0.12         0.150000    22.842000    12.000000
0.25               0.12         0.152710    23.227933    12.308466
0.50               0.12         0.155440    23.615661    12.630947
0.75               0.12         0.158187    24.004731    12.967987
1.00               0.12         0.160946    24.394660    13.320143
...                 ...              ...          ...          ...
299.00             0.12         0.143993    21.983347    30.615492
299.25             0.12         0.141835    21.673716    30.337545
299.50             0.12         0.139669    21.362341    30.037341
299.75             0.12         0.137501    21.049955    29.716338
300.00             0.12         0.135334    20.737271    29.376063

        Lynx_Births  Lynx_Deaths       Hares        Lynx
time
0.00      15.000000    12.000000  150.000000  100.000000
0.25      15.385583    12.090000  152.710500  100.750000
0.50      15.788684    12.188867  155.440367  101.573896
0.75      16.209984    12.296862  158.186545  102.473850
1.00      16.650179    12.414256  160.945731  103.452130
...             ...          ...         ...         ...
299.00    38.269365    31.892621  143.993301  265.771845
299.25    37.921932    32.083924  141.835264  267.366031
299.50    37.546676    32.259064  139.669307  268.825533
299.75    37.145423    32.417692  137.500557  270.147436
300.00    36.720078    32.559524  135.333961  271.329369

[1201 rows x 15 columns]


--------------

PNG Image 11


# patch to increase padding for model diagrams
import graphviz

# Store original methods
_original_digraph_init = graphviz.Digraph.__init__

def patched_digraph_init(self, *args, **kwargs):
    _original_digraph_init(self, *args, **kwargs)
    # Apply defaults
    self.attr('graph', pad='1')

# Apply patches
graphviz.Digraph.__init__ = patched_digraph_init

Brooks’s Law Model


# Brooks's Law Model
from selib import *

init_sd_model(start=0, stop=10, dt=.1)

add_stock("requirements", 500, inflows=[], outflows=["software development rate"])
add_stock("developed_software", 0, inflows=["software development rate"])
add_flow("software development rate", "nominal_productivity*(1.-communication_overhead/100.)*(.8*new_project_personnel+1.2*(experienced_personnel-experienced_personnel_needed_for_training))")

add_stock("experienced_personnel", 10.0, inflows=["assimilation rate"])
add_stock("new_project_personnel", 10.0, inflows=["people_added"], outflows=["assimilation rate"])
add_flow("assimilation rate", "new_project_personnel/20")
add_flow("people_added", "pulse(10,5)")

add_auxiliary("nominal_productivity", .1)
add_auxiliary("training_overhead_FTE_experienced", 25.)
add_auxiliary("experienced_personnel_needed_for_training", "new_project_personnel*training_overhead_FTE_experienced/100.")

add_auxiliary_lookup("communication_overhead", "experienced_personnel + new_project_personnel",
                     ["0, 5, 10, 15, 20, 25, 30"], ["0, 1.5, 6, 13.5, 24, 37.5, 54"])

draw_sd_model()
output = run_model()

plot_graph("developed_software")
plot_graph("software development rate")
plot_graph(["software development rate", "communication_overhead"])
plot_graph(["new_project_personnel", "people_added", "experienced_personnel_needed_for_training"])

--------------

SVG Diagram 12




      INITIAL TIME  FINAL TIME  TIME STEP  SAVEPER  nominal_productivity  \
time
0.0              0          10        0.1      0.1                   0.1
0.1              0          10        0.1      0.1                   0.1
0.2              0          10        0.1      0.1                   0.1
0.3              0          10        0.1      0.1                   0.1
0.4              0          10        0.1      0.1                   0.1
...            ...         ...        ...      ...                   ...
9.6              0          10        0.1      0.1                   0.1
9.7              0          10        0.1      0.1                   0.1
9.8              0          10        0.1      0.1                   0.1
9.9              0          10        0.1      0.1                   0.1
10.0             0          10        0.1      0.1                   0.1

      training_overhead_FTE_experienced  \
time
0.0                                25.0
0.1                                25.0
0.2                                25.0
0.3                                25.0
0.4                                25.0
...                                 ...
9.6                                25.0
9.7                                25.0
9.8                                25.0
9.9                                25.0
10.0                               25.0

      experienced_personnel_needed_for_training  communication_overhead  \
time
0.0                                    2.500000                    24.0
0.1                                    2.487500                    24.0
0.2                                    2.475062                    24.0
0.3                                    2.462687                    24.0
0.4                                    2.450374                    24.0
...                                         ...                     ...
9.6                                    3.540261                    54.0
9.7                                    3.522560                    54.0
9.8                                    3.504947                    54.0
9.9                                    3.487422                    54.0
10.0                                   3.469985                    54.0

      software development rate  assimilation rate  people_added  \
time
0.0                    1.292000           0.500000           0.0
0.1                    1.294660           0.497500           0.0
0.2                    1.297307           0.495012           0.0
0.3                    1.299940           0.492537           0.0
0.4                    1.302560           0.490075           0.0
...                         ...                ...           ...
9.6                    1.200014           0.708052           0.0
9.7                    1.202294           0.704512           0.0
9.8                    1.204563           0.700989           0.0
9.9                    1.206820           0.697484           0.0
10.0                   1.209066           0.693997           0.0

      requirements  developed_software  experienced_personnel  \
time
0.0     500.000000            0.000000              10.000000
0.1     499.870800            0.129200              10.050000
0.2     499.741334            0.258666              10.099750
0.3     499.611603            0.388397              10.149251
0.4     499.481609            0.518391              10.198505
...            ...                 ...                    ...
9.6     487.953319           12.046681              15.838955
9.7     487.833317           12.166683              15.909760
9.8     487.713088           12.286912              15.980211
9.9     487.592632           12.407368              16.050310
10.0    487.471950           12.528050              16.120059

      new_project_personnel
time
0.0               10.000000
0.1                9.950000
0.2                9.900250
0.3                9.850749
0.4                9.801495
...                     ...
9.6               14.161045
9.7               14.090240
9.8               14.019789
9.9               13.949690
10.0              13.879941

[101 rows x 15 columns]


--------------

PNG Image 13


--------------

PNG Image 14


--------------

PNG Image 15


--------------

PNG Image 16


--------------

Object Oriented Simulation Using Classes


# @title Capability Development Model with Stop Condition
from selib import *

# Initialize SystemDynamicsModel model instance
model = SystemDynamicsModel(start=0, stop=25, dt=0.1, stop_when="developed_capabilities >= 100")

model.add_stock(name="capabilities_to_do", initial=100, outflows=["development_rate"])
model.add_stock(name="developed_capabilities", initial=0, inflows=["development_rate"])
model.add_flow(name="development_rate", equation="IF capabilities_to_do > 0 THEN 10 ELSE 0")

# Draw the model diagram
model.draw_model()

# Run the model simulation
results = model.run_model()

# Plot the 'developed_capabilities'
model.plot_graph('developed_capabilities')

# Print the stop time
stop_time = model.get_stop_time()
print(f'stop time = {stop_time}')

--------------

SVG Diagram 17




Simulation ends at time = 10.0

      INITIAL TIME  FINAL TIME  TIME STEP  SAVEPER  development_rate  \
time
0.0              0          25        0.1      0.1                10
0.1              0          25        0.1      0.1                10
0.2              0          25        0.1      0.1                10
0.3              0          25        0.1      0.1                10
0.4              0          25        0.1      0.1                10
...            ...         ...        ...      ...               ...
9.6              0          25        0.1      0.1                10
9.7              0          25        0.1      0.1                10
9.8              0          25        0.1      0.1                10
9.9              0          25        0.1      0.1                10
10.0             0          25        0.1      0.1                 0

      capabilities_to_do  developed_capabilities
time
0.0                100.0                     0.0
0.1                 99.0                     1.0
0.2                 98.0                     2.0
0.3                 97.0                     3.0
0.4                 96.0                     4.0
...                  ...                     ...
9.6                  4.0                    96.0
9.7                  3.0                    97.0
9.8                  2.0                    98.0
9.9                  1.0                    99.0
10.0                 0.0                   100.0

[101 rows x 7 columns]


--------------

PNG Image 18


stop time = 10.0

Monte Carlo Simulation and Output Analysis


# Monte Carlo simulation for Rayleigh Curve Staffing and end time output analysis
from selib import *
import numpy as np

number_runs = 100

# vary manpower_buildup_parameter and estimated_total_effort in model definition

end_times = []  # initialize output data list

for run in range(number_runs):
    init_sd_model(start=0, stop=6, dt=.2, stop_when="effort_rate <=0")
    add_stock("cumulative_effort", 0, inflows=["effort rate"])
    add_flow(
        "effort_rate",
        "learning_function * (estimated_total_effort - cumulative_effort)")
    add_auxiliary("learning_function", "manpower_buildup_parameter * time")
    add_auxiliary("manpower_buildup_parameter", 'random.uniform(.2, .7)')
    add_auxiliary("estimated_total_effort", f'random.uniform(25, 35)')

    run_model(verbose=False)
    end_times.append(get_stop_time())

print(f'End Time Mean for {number_runs} runs = ', np.mean(end_times))

# PDF histogram with Matplotlib
fig, axis = plt.subplots()
axis.hist(end_times)
axis.set(title=f'End Times PDF', xlabel='End Time', ylabel='Frequency')
plt.savefig(f"end_times_PDF")

# CDF with NumPy and Matplotlib
fig, axis = plt.subplots()
# Use the histogram function to bin the data
histogram_bin_counts, bin_edges = np.histogram (end_times, density=True)
cdf = np.cumsum (histogram_bin_counts)
axis.plot (bin_edges[1:], cdf/cdf[-1])
axis.set(title=f'End Times CDF', xlabel='End Time', ylabel='Cumulative Probability')
plt.savefig(f"end_times_CDF")

End Time Mean for 100 runs =  3.4560000000000004

--------------

PNG Image 19


--------------

PNG Image 20


Special Functions


from selib import *

init_sd_model(start=0, stop=6, dt=.2, stop_when="effort_rate <= 0")
add_stock("cumulative_effort", 0, inflows=["effort rate"])
add_flow("effort_rate",
    "learning_function * (estimated_total_effort - cumulative_effort)")
add_auxiliary("learning_function", "manpower_buildup_parameter * time")
add_auxiliary("manpower_buildup_parameter", 'random.uniform(.2, .7)')
add_auxiliary("estimated_total_effort", 'random.uniform(25, 35)')

run_model()

print(f"Project end time is {get_stop_time()}")

Stop When Condition


# demonstrate stop_when condition to end simulation and the get_stop function
# collect time when the stock reaches zero

from selib import *
init_sd_model(start=0, stop=10, dt=.5, stop_when="stock <= 0")
add_stock("stock", 50, outflows=["outflow_rate"])
add_flow("outflow_rate", "20")

run_model()

print('stop time = ', get_stop_time())

Simulation ends at time = 2.5

      INITIAL TIME  FINAL TIME  TIME STEP  SAVEPER  outflow_rate  stock
time
0.0              0          10        0.5      0.5            20   50.0
0.5              0          10        0.5      0.5            20   40.0
1.0              0          10        0.5      0.5            20   30.0
1.5              0          10        0.5      0.5            20   20.0
2.0              0          10        0.5      0.5            20   10.0
2.5              0          10        0.5      0.5            20    0.0


stop time =  2.5

If Then Else


# demonstrate if then else function

from selib import *
init_sd_model(start=0, stop=5, dt=1)

# set conditional variable
# if time > 2 then 1 else 0
add_auxiliary("conditional", "if_then_else(time > 2, 1, 0)")
add_auxiliary("conditional2", "if_then_else(time < 4, .5, .7)")
run_model()

      INITIAL TIME  FINAL TIME  TIME STEP  SAVEPER  conditional  conditional2
time
0                0           5          1        1            0           0.5
1                0           5          1        1            0           0.5
2                0           5          1        1            0           0.5
3                0           5          1        1            1           0.5
4                0           5          1        1            1           0.7
5                0           5          1        1            1           0.7


      INITIAL TIME  FINAL TIME  TIME STEP  SAVEPER  conditional  conditional2
time
0                0           5          1        1            0           0.5
1                0           5          1        1            0           0.5
2                0           5          1        1            0           0.5
3                0           5          1        1            1           0.5
4                0           5          1        1            1           0.7
5                0           5          1        1            1           0.7