Examples¶
Complete models with abstract names. Each example runs on its own, shows one technique, and reads or draws its results. They follow the order of the guide: rates and stratifications first, then other engines, then a fit, its uncertainty and a comparison of scenarios.
| Example | Technique | Guide |
|---|---|---|
| 1. A chain of states | Absolute and proportional rates; read and plot the results | Core Concepts |
| 2. A periodic rate | sin of step in a rate |
Mathematical Expressions |
| 3. A rate per group | Stratified rates; a figure of the sum of a bin | Transitions |
| 4. Rates of an intersection | Two stratifications, total_<category>, per_compartment |
Transitions |
| 5. A conditional stratification | A stratification that splits one group only | Core Concepts |
| 6. A capacity limit | max in a rate |
Mathematical Expressions |
| 7. A cycle in continuous time | A transition back to the first state, run as differential equations | Running Simulations |
| 8. Rates that follow a pattern | AxisPattern as a rate, and a schedule per group |
Axis Patterns |
| 9. Runs at random | Replicates of a stochastic engine, their mean and band | Stochastic Simulation |
| 10. A fit, its uncertainty and two scenarios | Calibrator, a bootstrap, Predictive, Scenarios, plot_compared, threads |
Model Calibration, Uncertainty |
Example 1: A chain of states¶
A moves to B at an absolute rate, which names compartments, and B moves
to C at a proportional rate, which names none. A fourth state M sits
between A and B:
from commol import Aggregate, ModelBuilder, Simulation
model = (
ModelBuilder(name="Chain Model", version="1.0")
.add_bin(id="A", name="State A")
.add_bin(id="M", name="Intermediate")
.add_bin(id="B", name="State B")
.add_bin(id="C", name="State C")
.add_parameter(id="k1", value=0.4, description="A to M rate")
.add_parameter(id="k2", value=0.2, description="M to B rate")
.add_parameter(id="k3", value=0.1, description="B to C rate")
.add_transition(id="flow_AM", source=["A"], target=["M"], rate="k1 * A * B / total")
.add_transition(id="flow_MB", source=["M"], target=["B"], rate="k2")
.add_transition(id="flow_BC", source=["B"], target=["C"], rate="k3")
.build(
Aggregate(bin_amounts={"A": 999, "M": 0, "B": 1, "C": 0}),
engine="DifferenceEquations",
)
)
results = Simulation(model).run(num_steps=100)
peak = max(results["B"])
print(f"B peaks at {peak:.0f} at step {results['B'].index(peak)}")
results.plot.series(output_file="chain.png")
Example 2: A periodic rate¶
The rate of A to B varies with a period of 365 steps; step is the
position on the axis and pi a constant of the
expressions:
from commol import Aggregate, ModelBuilder, Simulation
model = (
ModelBuilder(name="Periodic Model", version="1.0")
.add_bin(id="A", name="State A")
.add_bin(id="B", name="State B")
.add_bin(id="C", name="State C")
.add_parameter(id="k_mean", value=0.3)
.add_parameter(id="k_amp", value=0.2)
.add_parameter(id="k2", value=0.1)
.add_transition(
id="periodic_flow",
source=["A"],
target=["B"],
rate="k_mean * (1 + k_amp * sin(2 * pi * step / 365)) * A * B / total",
)
.add_transition(id="flow_BC", source=["B"], target=["C"], rate="k2")
.build(
Aggregate(bin_amounts={"A": 990, "B": 10, "C": 0}),
engine="DifferenceEquations",
)
)
results = Simulation(model).run(num_steps=365 * 3)
results.plot.series(series=["B"], tick_every=365, output_file="periodic.png")
Example 3: A rate per group¶
A stratification splits every bin into three groups, and the transition from
B to C has one rate per group. In a figure, a bin id such as B draws the
sum of its compartments:
from commol import Aggregate, ModelBuilder, Simulation
model = (
ModelBuilder(name="Stratified Model", version="1.0")
.add_bin(id="A", name="State A")
.add_bin(id="B", name="State B")
.add_bin(id="C", name="State C")
.add_stratification(id="group", categories=["g1", "g2", "g3"])
.add_parameter(id="k1", value=0.0003)
.add_parameter(id="k2_g1", value=0.15)
.add_parameter(id="k2_g2", value=0.12)
.add_parameter(id="k2_g3", value=0.08)
.add_transition(id="flow_AB", source=["A"], target=["B"], rate="k1")
.add_transition(
id="flow_BC",
source=["B"],
target=["C"],
stratified_rates=[
{"conditions": [{"stratification": "group", "category": "g1"}], "rate": "k2_g1"},
{"conditions": [{"stratification": "group", "category": "g2"}], "rate": "k2_g2"},
{"conditions": [{"stratification": "group", "category": "g3"}], "rate": "k2_g3"},
],
)
.build(
Aggregate(
bin_amounts={"A": 9900, "B": 100, "C": 0},
stratification_fractions={"group": {"g1": 0.25, "g2": 0.55, "g3": 0.2}},
),
engine="DifferenceEquations",
)
)
results = Simulation(model).run(num_steps=100)
for name in ["B_g1", "B_g2", "B_g3"]:
print(f"{name}: {results[name][-1]:.2f}")
results.plot.series(series=["B", "B_g1", "B_g3"], output_file="groups.png")
Example 4: Rates of an intersection¶
Two stratifications, and a rate for the compartments of type t2 only. In
group g1, the rate divides by the total of the subgroup, total_g1, instead
of the total of the model. per_compartment=True makes A and B in the
rate the compartments of each flow, so each compartment of A loses an amount
computed from its own amount:
from commol import Aggregate, ModelBuilder, Simulation
model = (
ModelBuilder(name="Intersection Model", version="1.0")
.add_bin(id="A", name="State A")
.add_bin(id="B", name="State B")
.add_bin(id="C", name="State C")
.add_stratification(id="group", categories=["g1", "g2"])
.add_stratification(id="type", categories=["t1", "t2"])
.add_parameter(id="k1_low", value=0.3)
.add_parameter(id="k1_high", value=0.6)
.add_parameter(id="k2", value=0.1)
.add_transition(
id="flow_AB",
source=["A"],
target=["B"],
rate="k1_low * A * B / total",
stratified_rates=[
{
"conditions": [{"stratification": "group", "category": "g1"}],
"rate": "k1_low * A * B / total_g1",
},
{
"conditions": [{"stratification": "type", "category": "t2"}],
"rate": "k1_high * A * B / total",
},
],
per_compartment=True,
)
.add_transition(id="flow_BC", source=["B"], target=["C"], rate="k2")
.build(
Aggregate(
bin_amounts={"A": 990, "B": 10, "C": 0},
stratification_fractions={
"group": {"g1": 0.6, "g2": 0.4},
"type": {"t1": 0.8, "t2": 0.2},
},
),
engine="DifferenceEquations",
)
)
results = Simulation(model).run(num_steps=100)
print(sorted(name for name in results if name.startswith("B_")))
# ['B_g1_t1', 'B_g1_t2', 'B_g2_t1', 'B_g2_t2']
Example 5: A conditional stratification¶
The stratification subtype splits only the compartments of group g2; the
compartments of g1 are not split further:
from commol import Aggregate, ModelBuilder, Simulation
model = (
ModelBuilder(name="Conditional Stratification Model", version="1.0")
.add_bin(id="A", name="State A")
.add_bin(id="B", name="State B")
.add_bin(id="C", name="State C")
.add_stratification(id="group", categories=["g1", "g2"])
.add_stratification(
id="subtype",
categories=["s1", "s2"],
conditions=[{"stratification": "group", "category": "g2"}],
)
.add_parameter(id="k1", value=0.3)
.add_parameter(id="k2_default", value=0.1)
.add_parameter(id="k2_s1", value=0.15)
.add_parameter(id="k2_s2", value=0.05)
.add_transition(
id="flow_AB",
source=["A"],
target=["B"],
rate="k1 * A * B / total",
per_compartment=True,
)
.add_transition(
id="flow_BC",
source=["B"],
target=["C"],
rate="k2_default",
stratified_rates=[
{"conditions": [{"stratification": "subtype", "category": "s1"}], "rate": "k2_s1"},
{"conditions": [{"stratification": "subtype", "category": "s2"}], "rate": "k2_s2"},
],
)
.build(
Aggregate(
bin_amounts={"A": 9900, "B": 100, "C": 0},
stratification_fractions={
"group": {"g1": 0.6, "g2": 0.4},
"subtype": {"s1": 0.7, "s2": 0.3},
},
),
engine="DifferenceEquations",
)
)
results = Simulation(model).run(num_steps=100)
print(sorted(name for name in results if name.startswith("B")))
# ['B_g1', 'B_g2_s1', 'B_g2_s2']
Example 6: A capacity limit¶
The flow from B slows as C fills, and stops when C reaches capacity.
The rate names B, so it is the flow itself; the factor
max(0, 1 - C / capacity) never turns it negative. A difference model moves
the flow of a whole step at once, so the last step can pass capacity by a
little:
from commol import Aggregate, ModelBuilder, Simulation
model = (
ModelBuilder(name="Capacity Model", version="1.0")
.add_bin(id="A", name="State A")
.add_bin(id="B", name="State B")
.add_bin(id="C", name="State C")
.add_parameter(id="k1", value=0.5)
.add_parameter(id="k2_max", value=0.15)
.add_parameter(id="capacity", value=100.0)
.add_transition(id="flow_AB", source=["A"], target=["B"], rate="k1 * A * B / total")
.add_transition(
id="flow_BC",
source=["B"],
target=["C"],
rate="k2_max * B * max(0, 1 - C / capacity)",
)
.build(
Aggregate(bin_amounts={"A": 950, "B": 50, "C": 0}),
engine="DifferenceEquations",
)
)
results = Simulation(model).run(num_steps=100)
print(round(results["C"][-1], 1)) # 100.0
Example 7: A cycle in continuous time¶
Amounts in C return to A, so the process never ends. The same process
runs as differential equations: only the engine changes, and the results can
be read at any position, here every half step:
from commol import Aggregate, ModelBuilder, Simulation
model = (
ModelBuilder(name="Cyclic Model", version="1.0")
.add_bin(id="A", name="State A")
.add_bin(id="B", name="State B")
.add_bin(id="C", name="State C")
.add_parameter(id="k1", value=0.3)
.add_parameter(id="k2", value=0.1)
.add_parameter(id="k3", value=0.01, description="C to A rate")
.add_transition(id="flow_AB", source=["A"], target=["B"], rate="k1 * A * B / total")
.add_transition(id="flow_BC", source=["B"], target=["C"], rate="k2")
.add_transition(id="return_CA", source=["C"], target=["A"], rate="k3")
.build(
Aggregate(bin_amounts={"A": 990, "B": 10, "C": 0}),
engine="DifferentialEquations",
)
)
simulation = Simulation(model)
results = simulation.run_at([step / 2 for step in range(2001)])
print(results.positions[:3]) # [0.0, 0.5, 1.0]
print(round(sum(results[name][-1] for name in ["A", "B", "C"]))) # 1000
Example 8: Rates that follow a pattern¶
An AxisPattern is a rate that changes along the axis. The first model
combines an opening pulse, an event every 30 steps and a sinusoid. The second
gives each group its own schedule: the first add_group call is on the
class, the next ones on the pattern it returns:
from commol import Aggregate, AxisPattern, ModelBuilder, Simulation
combined = AxisPattern.combine(
AxisPattern.pulse(at=0, value=0.3),
AxisPattern.periodic(period=30, value=0.05),
AxisPattern.sinusoid(amplitude=0.005, period=365, baseline=0.01),
)
model = (
ModelBuilder(name="Pattern Model")
.add_bin(id="A", name="Source")
.add_bin(id="B", name="Sink")
.add_transition(id="flow", source=["A"], target=["B"], rate=combined)
.build(Aggregate(bin_amounts={"A": 10000, "B": 0}), engine="DifferenceEquations")
)
results = Simulation(model).run(num_steps=365)
per_group = AxisPattern.add_group(
conditions=[{"stratification": "group", "category": "cat1"}],
schedule=AxisPattern.periodic(period=7, value=0.05),
).add_group(
conditions=[{"stratification": "group", "category": "cat2"}],
schedule=AxisPattern.periodic(period=30, value=0.05),
)
grouped = (
ModelBuilder(name="Schedule Model")
.add_bin(id="A", name="Source")
.add_bin(id="B", name="Sink")
.add_stratification(id="group", categories=["cat1", "cat2"])
.add_transition(id="flow", source=["A"], target=["B"], rate=per_group)
.build(
Aggregate(
bin_amounts={"A": 1000, "B": 0},
stratification_fractions={"group": {"cat1": 0.5, "cat2": 0.5}},
),
engine="DifferenceEquations",
)
)
grouped_results = Simulation(grouped).run(num_steps=60)
print(grouped_results["B_cat1"][-1] > grouped_results["B_cat2"][-1]) # True
Example 9: Runs at random¶
The StochasticSteps engine moves whole units at random, one step at a time.
replicates runs the model once per seed; the mean, the band and the figure
reduce the runs position by position:
from commol import Aggregate, ModelBuilder, Simulation
model = (
ModelBuilder(name="Stochastic Model")
.add_bin(id="A", name="State A")
.add_bin(id="B", name="State B")
.add_bin(id="C", name="State C")
.add_parameter(id="k1", value=0.3)
.add_parameter(id="k2", value=0.1)
.add_transition(id="t_ab", source=["A"], target=["B"], rate="k1 * A * B / total")
.add_transition(id="t_bc", source=["B"], target=["C"], rate="k2")
.build(Aggregate(bin_amounts={"A": 990, "B": 10, "C": 0}), engine="StochasticSteps")
)
replicates = Simulation(model).replicates(40, seed=7).run(num_steps=100)
mean = replicates.mean()
spread = replicates.member_statistics("C", confidence_level=0.9)
print(round(mean["C"][-1]), round(spread["ci_lower"][-1]), round(spread["ci_upper"][-1]))
replicates.plot.series(series=["B", "C"], confidence_level=0.9, output_file="replicates.png")
Example 10: A fit, its uncertainty and two scenarios¶
A fit of k1 to noisy observations of B, a sample of k1 from a Bayesian
bootstrap, its predictive band, and a comparison of two scenarios of k2. The
work of the bootstrap and of the draws runs on two threads:
import numpy as np
import commol
from commol import (
Aggregate,
BayesianBootstrap,
CalibrationParameter,
CalibrationProblem,
Calibrator,
ModelBuilder,
NelderMeadConfig,
ObservedDataPoint,
Predictive,
Scenarios,
Simulation,
ValueAt,
plot_compared,
)
def basic_model(k1: float | None):
return (
ModelBuilder(name="Basic Model")
.add_bin(id="A", name="State A")
.add_bin(id="B", name="State B")
.add_bin(id="C", name="State C")
.add_parameter(id="k1", value=k1)
.add_parameter(id="k2", value=0.1)
.add_transition(id="t_ab", source=["A"], target=["B"], rate="k1 * A * B / total")
.add_transition(id="t_bc", source=["B"], target=["C"], rate="k2 * B")
.build(
Aggregate(bin_amounts={"A": 990, "B": 10, "C": 0}),
engine="DifferenceEquations",
)
)
truth = Simulation(basic_model(0.4)).run(num_steps=40)
rng = np.random.default_rng(1)
observed_data = [
ObservedDataPoint(at=step, output="B", value=truth["B"][step] * float(np.exp(rng.normal(0.0, 0.05))))
for step in range(2, 41, 2)
]
problem = CalibrationProblem(
observed_data=observed_data,
parameters=[
CalibrationParameter(id="k1", parameter_type="parameter", min_bound=0.0, max_bound=1.0),
],
loss_function="sse",
optimization_config=NelderMeadConfig(max_iterations=500),
seed=42,
)
calibrator = Calibrator(Simulation(basic_model(None)), problem)
with commol.threads(2):
fit = calibrator.run()
bootstrap = calibrator.uncertainty(BayesianBootstrap(replicates=20, block_length=1))
predictive = Predictive(calibrator, bootstrap.sample, positions=list(range(61)))
scenarios = Scenarios(
calibrator,
bootstrap.sample,
{"fitted": {}, "slower": {"k2": 0.05}},
positions=list(range(61)),
)
print(round(fit.best_parameters["k1"], 2)) # close to 0.4
lower, upper = bootstrap.sample.interval(0.9)["k1"]
print(round(lower, 3), round(upper, 3)) # the 90 % interval of k1
predictive.plot.series(series=["B"], observed_data=observed_data, output_file="band.png")
[difference] = scenarios.compare("slower", "fitted", [ValueAt(series="C", at=60)], 0.9)
print(difference.label, round(difference.median)) # how much less C reaches by step 60
plot_compared(scenarios, series=["B", "C"], output_file="scenarios.png")
Next steps¶
| To learn | Read |
|---|---|
| Every builder option | Building Models |
| Reading, aggregating and plotting results | Running Simulations |
| Calibration in depth | Model Calibration |
| The uncertainty methods, the predictive and scenarios | Uncertainty |
| Every class and argument | API Reference |