Creating Custom Compartmental Models: Building an SIR Model
This tutorial demonstrates how to create custom compartmental models in simcronomicon by implementing the classic SIR (Susceptible-Infectious-Recovered) model from scratch. You’ll learn the essential components needed to build any compartmental model and understand the inheritance structure that makes simcronomicon extensible.
Understanding the SIR Model
The SIR model is the simplest epidemic model, dividing the population into three compartments:
- S - Susceptible
Healthy individuals who can become infected.
- I - Infectious
Individuals who can transmit the disease to susceptible people.
- R - Recovered
Individuals who have recovered and gained immunity.
Disease Flow: S → I → R (unidirectional)
Why Build SIR When SEIR Exists?
While simcronomicon includes a more complex SEIR model, building SIR teaches you:
Model Architecture: Understanding the inheritance hierarchy
Parameter Design: How to define model-specific parameters
Agent Behavior: Implementing custom disease progression logic
Validation: Ensuring your model works correctly
This knowledge enables you to create any compartmental model (SIRD, SEIQR, etc.) for your specific research needs.
Step 1: Define Model Parameters
Every compartmental model needs a parameters class that inherits from AbstractModelParameters
:
# File: sir_model.py
from simcronomicon.compartmental_models.abstract_model import AbstractModelParameters
class SIRModelParameters(AbstractModelParameters):
"""
Parameters for the SIR compartmental model.
Parameters
----------
max_energy : int
Maximum daily energy for agents (affects movement patterns)
beta : float
Transmission rate - probability of infection per contact
gamma : float
Recovery rate - days until recovery from infectious status
"""
def __init__(self, max_energy, beta, gamma):
super().__init__(max_energy)
# Validate parameters
assert isinstance(beta, (int, float)) and 0 <= beta <= 1, \
"beta must be a number between 0 and 1"
assert isinstance(gamma, (int, float)) and gamma > 0, \
"gamma must be a positive number"
self.beta = beta
self.gamma = gamma
def to_metadata_dict(self):
"""Convert parameters to dictionary for saving/loading simulations."""
return {
'max_energy': self.max_energy,
'beta': self.beta,
'gamma': self.gamma,
'model_type': 'SIR'
}
Key Design Principles:
Validation: Always validate parameter types and ranges
Documentation: Clear docstrings explain each parameter’s meaning
Metadata: The
to_metadata_dict()
method enables simulation persistence
Step 2: Create the Agent Class
The agent class defines how individuals behave and transition between disease states:
from simcronomicon.compartmental_models.abstract_model import AbstractFolk
class FolkSIR(AbstractFolk):
"""
SIR agent with disease-specific behaviors.
Inherits all basic agent functionality from AbstractFolk and adds
SIR-specific interaction and status transition logic.
"""
def interact(self, folks_here, current_place_type, status_dict_t, model_params, dice):
"""
Handle agent interactions at a location, including disease transmission.
Parameters
----------
folks_here : list of FolkSIR
All agents at this location
current_place_type : str
Type of location (e.g., 'commercial', 'accommodation')
status_dict_t : dict
Current status counts for the simulation
model_params : SIRModelParameters
Model configuration parameters
dice : float
Random number between 0 and 1 for stochastic decisions
"""
# Only susceptible agents can become infected
if self.status == 'S':
# Check if any infectious people are present
num_contact = len(
[folk for folk in folks_here if folk != self and folk.status in ['I']])
# conversion_prob * I / N is the non-linear term that defines conversion
# This inverse bernoulli function is an interpretation of the term
# in agent-based modeling
transmission_prob = self.inverse_bernoulli(
num_contact, conversion_prob / len(folks_here))
# Attempt infection
if transmission_prob > dice:
self.convert('I', status_dict_t)
# Reduce energy from social interaction
if self.energy > 0:
self.energy -= 1
def sleep(self, status_dict_t, model_params):
"""
End-of-day processing: energy reset and disease progression.
Parameters
----------
status_dict_t : dict
Current status counts for the simulation
model_params : SIRModelParameters
Model configuration parameters
"""
# Call parent sleep method (resets energy, increments status_step_streak)
super().sleep()
# Handle disease progression based on time in status
if self.status == 'I' and self.status_step_streak >= model_params.gamma:
# Infectious agents recover after gamma days
self.convert('R', status_dict_t)
Key Agent Concepts:
Interaction Logic: Only susceptible agents can become infected
Inverse Bernoulli: Handles multiple infectious contacts realistically
Time-Based Transitions: Status changes based on
status_step_streak
Energy Management: Social interactions consume energy
Step 3: Implement the SIR Model
Custom models need to override the initialize_sim_population
method to handle their specific status assignments and initial conditions:
class SIRModel(AbstractCompartmentalModel):
"""SIR compartmental model implementation."""
def __init__(self, model_params, step_events=None):
# Define model-specific attributes BEFORE calling super().__init__
self.infected_statuses = ['I'] # Only infectious status
self.all_statuses = ['S', 'I', 'R'] # All possible statuses
self.folk_class = FolkSIR # Agent class to use
self.step_events = step_events # Custom or default events
# Initialize parent class (validates our definitions)
super().__init__(model_params)
def initialize_sim_population(self, town):
import random as rd
# Get basic population parameters from parent
num_pop, num_init_spreader, num_init_spreader_rd, folks, household_node_indices, assignments = super().initialize_sim_population(town)
# Randomly assign initial spreaders (not on specified nodes)
for i in range(num_init_spreader_rd):
node = rd.choice(town.accommodation_node_ids)
assignments.append((node, 'I')) # Infectious status
# Assign the rest as susceptible
for i in range(num_pop - num_init_spreader):
node = rd.choice(town.accommodation_node_ids)
assignments.append((node, 'S')) # Susceptible status
# Assign initial spreaders to specified nodes (if any)
for node in town.town_params.spreader_initial_nodes:
assignments.append((node, 'I'))
# Create folks and update graph/node info
for i, (node, status) in enumerate(assignments):
folk = self.create_folk(i, node, self.model_params.max_energy, status)
folks.append(folk)
town.town_graph.nodes[node]["folks"].append(folk)
# Track household nodes (nodes with 2+ people)
if len(town.town_graph.nodes[node]["folks"]) == 2:
household_node_indices.add(node)
# Create initial status dictionary for timestep 0
status_dict_t0 = {
'current_event': None,
'timestep': 0,
'S': num_pop - num_init_spreader, # Susceptible count
'I': num_init_spreader, # Infectious count
'R': 0 # Recovered count (starts at 0)
}
return folks, household_node_indices, status_dict_t0
Key Implementation Points:
Call Parent Method: Use
super().initialize_sim_population(town)
to get base setupStatus Assignment: Assign initial statuses based on your model’s compartments
Node Assignment: Distribute agents across accommodation nodes in the town
Status Dictionary: Initialize counts for all possible statuses at timestep 0
Household Tracking: Update household indices for nodes with multiple residents
Step 4: Test Your SIR Model
Create a simple test script to validate your implementation:
# File: test_sir_model.py
import simcronomicon as scon
from sir_model import SIRModel, SIRModelParameters
def test_sir_model():
"""Test basic SIR model functionality."""
# Create a simple town
point = (50.7753, 6.0839) # Aachen coordinates
town_params = scon.TownParameters(100, 5) # 100 people, 5 initial infected
town = scon.Town.from_point(point, 500, "test_sir", town_params)
# Configure SIR model
sir_params = SIRModelParameters(
max_energy=5,
beta=0.3, # 30% transmission probability
gamma=7 # Recovery after 7 days
)
# Create model and simulation
sir_model = SIRModel(sir_params)
simulation = scon.Simulation(town, sir_model, timesteps=50)
# Run simulation
print("Running SIR simulation...")
simulation.run()
# Analyze results
print("SIR simulation completed successfully!")
# Visualize results
scon.plot_status_summary_from_hdf5("simulation_output.h5")
return True
if __name__ == "__main__":
test_sir_model()
Step 5: Compare with Mathematical SIR
Validate your agent-based model against the classic SIR differential equations:
import numpy as np
from scipy.integrate import solve_ivp
import matplotlib.pyplot as plt
def compare_sir_models(sir_params, population_size=100):
"""Compare agent-based SIR with ODE solution."""
# ODE system for SIR
def sir_ode(t, y):
S, I, R = y
N = S + I + R
dS_dt = -sir_params.beta * S * I / N
dI_dt = sir_params.beta * S * I / N - I / sir_params.gamma
dR_dt = I / sir_params.gamma
return [dS_dt, dI_dt, dR_dt]
# Initial conditions (normalized)
S0 = 0.95 # 95% susceptible
I0 = 0.05 # 5% infectious
R0 = 0.00 # 0% recovered
# Solve ODE
t_span = (0, 50)
t_eval = np.arange(0, 51)
solution = solve_ivp(sir_ode, t_span, [S0, I0, R0], t_eval=t_eval)
# Plot comparison
plt.figure(figsize=(12, 5))
# ODE solution
plt.subplot(1, 2, 1)
plt.plot(solution.t, solution.y[0], 'b-', label='Susceptible')
plt.plot(solution.t, solution.y[1], 'r-', label='Infectious')
plt.plot(solution.t, solution.y[2], 'g-', label='Recovered')
plt.xlabel('Time (days)')
plt.ylabel('Proportion')
plt.title('SIR ODE Model')
plt.legend()
plt.grid(True)
# Agent-based results (load from your simulation)
plt.subplot(1, 2, 2)
# Add code to plot your simulation results here
plt.title('Agent-Based SIR Model')
plt.tight_layout()
plt.show()
print("📊 Model comparison complete!")
print(f" Beta (transmission rate): {sir_params.beta}")
print(f" Gamma (recovery time): {sir_params.gamma} days")
print(f" Basic reproduction number R₀: {sir_params.beta * sir_params.gamma:.2f}")
Advanced Features
Once your basic SIR model works, you can extend it with advanced features:
Custom Movement Patterns
# Add custom step events for different behaviors
sir_step_events = [
scon.StepEvent(
"morning_commute",
FolkSIR.interact,
scon.EventType.DISPERSE,
10000, # Travel up to 10km
['workplace', 'education'],
scon.log_normal_mobility # Distance-based probability
),
scon.StepEvent(
"evening_social",
FolkSIR.interact,
scon.EventType.DISPERSE,
5000,
['commercial', 'religious']
)
]
sir_model = SIRModel(sir_params, step_events=sir_step_events)
Behavioral Interventions
class FolkSIRWithMasks(FolkSIR):
"""SIR agents with mask-wearing behavior."""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.wearing_mask = False
self.mask_effectiveness = 0.5 # 50% transmission reduction
def interact(self, folks_here, current_place_type, status_dict_t, model_params, dice):
# Modify transmission probability based on mask wearing
if self.status == 'S':
num_contact = len(
[folk for folk in folks_here if folk != self and folk.status in ['I']])
# conversion_prob * I / N is the non-linear term that defines conversion
# This inverse bernoulli function is an interpretation of the term
# in agent-based modeling
base_prob = self.inverse_bernoulli(
num_contact, conversion_prob / len(folks_here))
# Reduce transmission if either person wears a mask
if self.wearing_mask or any(folk.wearing_mask for folk in infectious_folks):
base_prob *= (1 - self.mask_effectiveness)
if base_prob > dice:
self.convert('I', status_dict_t)
if self.energy > 0:
self.energy -= 1
Validation and Testing
Always validate your custom model:
- Unit Tests
Test individual components (parameter validation, agent transitions)
- Integration Tests
Verify the complete model runs without errors
- Mathematical Validation
Compare with known analytical solutions when possible
- Sensitivity Analysis
Test how parameter changes affect outcomes
- Edge Cases
Test extreme parameter values and unusual scenarios
def validate_sir_model():
"""Comprehensive SIR model validation."""
# Test 1: Parameter validation
try:
SIRModelParameters(max_energy=5, beta=1.5, gamma=7) # Should fail
assert False, "Should have caught invalid beta"
except AssertionError:
print("Parameter validation working")
# Test 2: Status transitions
agent = FolkSIR(id=1, home_address=0, max_energy=5, status='S')
status_dict = {'S': 100, 'I': 0, 'R': 0}
agent.convert('I', status_dict)
assert agent.status == 'I' and agent.status_step_streak == 0
print("Status transitions working")
# Test 3: Disease progression
params = SIRModelParameters(max_energy=5, beta=0.3, gamma=3)
agent.status_step_streak = 3 # At recovery threshold
agent.sleep(status_dict, params)
if agent.status_step_streak >= params.gamma:
agent.convert('R', status_dict)
assert agent.status == 'R'
print("Disease progression working")
print("All validation tests passed!")
Best Practices
When creating custom compartmental models:
- 1. Start Simple
Begin with basic functionality, then add complexity incrementally
- 2. Validate Early
Test each component before building the complete model
- 3. Document Everything
Clear docstrings and comments make models maintainable
- 4. Follow Conventions
Use consistent naming and structure with existing simcronomicon models
- 5. Test Thoroughly
Validate against mathematical models when possible
- 6. Consider Performance
Profile your model with large populations to identify bottlenecks
Common Pitfalls
- Incorrect Inheritance Order
Always define model attributes BEFORE calling
super().__init__()
- Missing Status Validation
Ensure
all_statuses
includes every possible agent status- Energy Management
Don’t forget to decrement energy during interactions
- Status Transition Logic
Use
status_step_streak
correctly for time-based transitions- Parameter Validation
Always validate parameter ranges and types
Next Steps
Now that you can create custom compartmental models, explore:
SIRD Model: Add death compartment to your SIR model
Age-Structured Models: Different parameters for age groups
Spatial Models: Location-dependent transmission rates
Multi-Strain Models: Competing variants with different characteristics
Economic Models: Incorporate economic factors and interventions
The simcronomicon framework makes it straightforward to implement any compartmental model structure. Your SIR implementation provides the foundation for understanding how to build increasingly sophisticated epidemic models tailored to your specific research questions.