An agent-based model for simulating rotavirus genetic diversity and vaccination interventions. Rotasim is built on the Starsim v3 framework and uses genetic modeling to track pathogen evolution and immune responses across multiple strains.
Current Status: V2 architecture complete with unified scenario API for easy strain configuration and improved performance.
- Unified scenario API: Easy-to-use predefined scenarios with strain definitions, fitness, and prevalence
- Multi-strain architecture: Each rotavirus G,P combination is modeled as a separate disease instance
- Cross-strain immunity: High-performance vectorized immunity tracking with bitmask operations
- Genetic reassortment: Realistic genetic recombination between co-infecting strains
- Population dynamics: Birth, death, and aging processes
- Comprehensive analysis: Built-in analyzers for strain statistics, events, and demographics
- Clean codebase: Recently refactored to remove 435+ lines of deprecated code
pip install -e .This installs Rotasim as an importable module in development mode.
import rotasim as rs
# Run a simple simulation
sim = rs.Sim(scenario='simple') # G1P8 and G2P4 strains
sim.run()
print(f"Simulation completed with {len(sim.diseases)} diseases")Rotasim uses a multi-strain architecture where each unique G,P combination is represented as a separate Rotavirus disease instance:
- G genotype: Major outer capsid protein determining antigenic properties
- P genotype: Protease-sensitive protein determining antigenic properties
- Backbone: Non-antigenic segments (A,B) inherited during reassortment
Each strain is automatically named using the format G{G}P{P} (e.g., "G1P8", "G2P4").
The model starts with a set of initial strains and generates all possible G,P combinations through reassortment:
# 3 initial strains create 9 total combinations
initial_strains = [(1, 8), (2, 4), (3, 6)]
# Results in: G1P8, G1P4, G1P6, G2P8, G2P4, G2P6, G3P8, G3P4, G3P6- Active strains: Present in the initial population with non-zero prevalence
- Dormant strains: Created through reassortment but initially absent from the population
Rotasim tracks immunity using a high-performance bitmask system for computational efficiency:
Each agent maintains exposure history using bitmasks:
exposed_G_bitmask: Integer tracking all G genotypes the agent has been exposed toexposed_P_bitmask: Integer tracking all P genotypes the agent has been exposed toexposed_GP_bitmask: Integer tracking all specific G,P combinations the agent has been exposed to
When an agent is exposed to a new strain, protection is determined by previous exposures using a hierarchical matching system with maximum protection principle:
-
Homotypic Protection: Exact G and P match with previous infection
- Default protection: 90% (
homotypic_immunity_efficacy = 0.9) - Example: Previous G1P8 infection protects against new G1P8 exposure
- Default protection: 90% (
-
Partial Heterotypic Protection: Shared G OR P genotype (but not both)
- Default protection: 50% (
partial_heterotypic_immunity_efficacy = 0.5) - Key feature: Uses the most protective partial match available
- Example: Agent previously infected with G1P8 and G2P4, now exposed to G1P6
- G1P6 shares G=1 with G1P8 infection → partial G-type protection
- G1P6 has P=6, no previous P=6 exposure → no P-type protection
- Result: Receives G1-based partial protection (the available match)
- Default protection: 50% (
-
Complete Heterotypic Protection: No shared G or P genotypes but has prior immunity
- Default protection: 30% (
complete_heterotypic_immunity_efficacy = 0.3) - Example: Previous G1P8 infection, exposed to G2P4 (no shared antigens)
- Default protection: 30% (
-
Naive Susceptibility: No prior immunity to any strain
- Default protection: 0% (
naive_immunity_efficacy = 0.0) - Fully susceptible to first rotavirus infection
- Default protection: 0% (
Immunity wanes through exponential decay based on time since recovery from each strain:
- Waning process: Individual strain immunity decays exponentially over time
- Default waning rate: ~273 days (
full_waning_rate = ss.freqperyear(365/273)) - No waning delay: Decay begins immediately after recovery (
immunity_waning_delay = ss.days(0)) - Gradual waning: Unlike complete portfolio loss, immunity decays gradually for each strain
- Maximum protection principle: For partial heterotypic protection, uses the strongest remaining immunity from either G-type or P-type matches
The cross-strain protection calculation uses bitwise operations for efficiency:
# Check for homotypic match (exact G,P match)
has_exact_match = (exposed_GP_bitmask & disease_GP_mask) != 0
# Check for partial heterotypic match (G match OR P match, but not both)
has_G_match = (exposed_G_bitmask & disease_G_mask) != 0 & ~has_exact_match
has_P_match = (exposed_P_bitmask & disease_P_mask) != 0 & ~has_exact_match
# For partial matches, use maximum decay factor from G or P immunity
max_decay_factor = max(G_max_decayed_immunity_factor, P_max_decayed_immunity_factor)
# Calculate final protection
protection_factor = strain_match_immunity_efficacy Ă— decay_factorThe rel_sus (relative susceptibility) parameter controls infection probability and is modified by:
- Cross-strain immunity: Reduced susceptibility based on previous exposures and their decay
- Infection history: Scaling factor based on total number of previous infections
When an agent is exposed to a strain, their rel_sus is calculated as:
rel_sus = (1 - strain_immunity_efficacy Ă— decay_factor) Ă— infection_history_factor
Where:
strain_immunity_efficacycomes from the hierarchical matching system (0.0-0.9)decay_factorrepresents time-based immunity waning (0.0-1.0)infection_history_factorscales based on total previous infections
import rotasim as rs
# Simple simulation using predefined scenario
sim = rs.Sim(scenario='simple') # Two-strain scenario (G1P8, G2P4)
sim.run()
# Access results
print(f"Simulation completed with {len(sim.diseases)} total diseases")
print(f"Scenario: {sim.scenario}")
print(f"Initial strains: {sim.initial_strains}")import rotasim as rs
# List all available scenarios
scenarios = rs.list_scenarios()
for name, description in scenarios.items():
print(f"{name}: {description}")
# Use different scenarios
sim_baseline = rs.Sim(scenario='baseline') # 3 common global strains
sim_diverse = rs.Sim(scenario='high_diversity') # 12 strains with varied fitness
sim_competition = rs.Sim(scenario='realistic_competition') # G1P8 dominant with competitionimport rotasim as rs
# Override scenario parameters
sim = rs.Sim(
scenario='baseline',
override_prevalence=0.02, # Set all strains to 2% prevalence
override_fitness={(1,8): 0.95, (2,4): 0.8}, # Override specific strain fitness
base_beta=0.15, # Adjust base transmission rate
n_agents=50000
)
sim.run()
# Add new strain to existing scenario
sim = rs.Sim(
scenario='baseline',
override_strains={(9,6): {'fitness': 0.7, 'prevalence': 0.003}}, # Add G9P6
verbose=True
)
# View strain summary
summary = sim.get_strain_summary()
print(f"Total diseases: {summary['total_diseases']}")
print(f"Active strains: {len(summary['active_strains'])}")
print(f"Dormant reassortants: {len(summary['dormant_strains'])}")import rotasim as rs
# Define custom scenario
custom_scenario = {
'strains': {
(1, 8): {'fitness': 1.0, 'prevalence': 0.015},
(2, 4): {'fitness': 0.8, 'prevalence': 0.010},
(3, 6): {'fitness': 0.9, 'prevalence': 0.005}
},
'default_fitness': 0.3 # For dormant reassortants
}
sim = rs.Sim(
scenario=custom_scenario,
base_beta=0.1,
n_agents=25000
)
sim.run()import rotasim as rs
from rotasim import RotaImmunityConnector
# Test different protection scenarios
high_cross_protection = RotaImmunityConnector(
homotypic_immunity_efficacy=0.9,
partial_heterotypic_immunity_efficacy=0.8, # High cross-protection
complete_heterotypic_immunity_efficacy=0.6
)
low_cross_protection = RotaImmunityConnector(
homotypic_immunity_efficacy=0.9,
partial_heterotypic_immunity_efficacy=0.3, # Low cross-protection
complete_heterotypic_immunity_efficacy=0.1
)
# Compare strain diversity outcomes
for immunity, label in [(high_cross_protection, "High"), (low_cross_protection, "Low")]:
sim = rs.Sim(
scenario='high_diversity', # Use built-in scenario
connectors=[immunity],
n_agents=25000,
dt=rs.days(1), # Daily timesteps
verbose=0
)
sim.run()
print(f"{label} cross-protection: simulation completed")import rotasim as rs
from rotasim.analyzers import StrainStats, EventStats
# Add analyzers to track detailed statistics
analyzers = [
StrainStats(), # Strain proportions and counts
EventStats() # Births, deaths, infections, waning events
]
sim = rs.Sim(
scenario='realistic_competition', # Use built-in scenario
analyzers=analyzers,
dt=rs.days(1), # Daily timesteps
stop='2030-01-01', # 10-year simulation
verbose=1
)
sim.run()
# Export results
strain_stats = analyzers[0].to_df()
event_stats = analyzers[1].to_df()
strain_stats.to_csv("strain_proportions.csv", index=False)
event_stats.to_csv("simulation_events.csv", index=False)
# Examine immunity waning events
waning_events = event_stats['wanings'].sum()
print(f"Total immunity waning events: {waning_events}")
print(f"Final scenario: {sim.final_scenario}")# Run from tests directory
cd tests
python simple.py# Test multi-strain architecture and unified scenarios
cd tests
python -m pytest test_integration.py -v
python test_utils.py # Test scenario system# Test performance with large strain numbers
python tests/test_performance.pyimport rotasim as rs
from rotasim import RotaImmunityConnector
# Asymmetric protection scenarios
strong_homotypic = RotaImmunityConnector(
homotypic_immunity_efficacy=0.95, # Very strong same-strain
partial_heterotypic_immunity_efficacy=0.4, # Moderate cross-protection
complete_heterotypic_immunity_efficacy=0.1, # Minimal heterotypic
full_waning_rate=rs.freqperyear(365/180) # 6-month waning
)
sim = rs.Sim(
scenario='balanced_competition', # Use built-in 4-strain scenario
connectors=[strong_homotypic],
base_beta=0.12,
verbose=2 # Detailed output
)import rotasim as rs
from rotasim.analyzers import EventStats
# Track immunity and reassortment events
analyzer = EventStats()
sim = rs.Sim(
scenario='emergence_scenario', # Weak background for emergence studies
analyzers=[analyzer],
stop='2035-01-01', # 15-year simulation
dt=rs.days(1), # Daily timesteps
verbose=1 # Show summary information
)
sim.run()
# Analyze cross-strain dynamics
events = analyzer.to_df()
print(f"Immunity waning events: {events['wanings'].sum()}")
print(f"Reassortment events: {events['reassortments'].sum()}")
print(f"Peak coinfected agents: {events['coinfected_agents'].max()}")
print(f"Final scenario used: {sim.final_scenario['strains']}")import rotasim as rs
import numpy as np
# Test scenario and immunity parameter sensitivity
scenarios = ['simple', 'baseline', 'realistic_competition']
cross_protection_levels = [0.2, 0.4, 0.6, 0.8]
results = []
for scenario in scenarios:
for cross_prot in cross_protection_levels:
immunity = rs.RotaImmunityConnector(
partial_heterotypic_immunity_efficacy=cross_prot
)
sim = rs.Sim(
scenario=scenario,
connectors=[immunity],
dt=rs.days(1),
stop='2030-01-01',
verbose=0
)
sim.run()
results.append({
'scenario': scenario,
'cross_protection': cross_prot,
'total_diseases': len(sim.diseases),
'initial_strains': len(sim.initial_strains)
})
print("Scenario and cross-protection sensitivity:")
for result in results:
print(f"Scenario: {result['scenario']}, Protection: {result['cross_protection']:.1f}, "
f"Total diseases: {result['total_diseases']}, Initial: {result['initial_strains']}")- Built-in scenarios: 8 predefined scenarios from simple 2-strain to complex 12-strain setups
- Easy customization: Override fitness, prevalence, or add new strains to existing scenarios
- Clean API: Single
scenarioparameter replaces multiple configuration options - Backward compatibility: All existing functionality preserved
- Removed 435+ lines of deprecated code after unified scenario implementation
- Eliminated legacy dictionaries:
INITIAL_STRAIN_SCENARIOS,FITNESS_HYPOTHESES - Streamlined imports: Clean import structure with only necessary functions
- Updated examples: All documentation updated to use new unified API
import rotasim as rs
# List all built-in scenarios
print(rs.list_scenarios())
# Output:
# {
# 'simple': 'Simple two-strain scenario - G1P8 and G2P4 with equal fitness and prevalence',
# 'baseline': 'Baseline scenario - common global strains with equal fitness',
# 'realistic_competition': 'G1P8 dominant with realistic strain competition',
# 'balanced_competition': 'G1P8 dominant with moderate balanced competition',
# 'high_diversity': 'High diversity with 12 strains and varied fitness',
# 'low_diversity': 'Low diversity with 4 main competitive strains',
# 'emergence_scenario': 'Scenario for studying strain emergence with weak background'
# }This project follows standard development practices:
- Testing: Run
python -m pytest tests/andpython tests/test_utils.pybefore submitting changes - Performance: Use
python tests/test_performance.pyto check performance - Style: Follow existing code patterns and documentation standards
- Scenarios: Use built-in scenarios when possible, create custom scenarios for specific needs
- Verbosity: Use
verbose=1for summary info,verbose=2for detailed strain creation output
starsim: Core simulation frameworknumpy: Numerical computations and bitmask operationspandas: Data analysis and exportmatplotlib: Plotting and visualizationnumba: Performance optimization (optional)sciris: Utilities and parameter management