Understanding Agent Behavior and Simulation Flow
This section explains the fundamental building blocks of how individual agents work and how multiple agents interact in the simulation environment. Understanding these core concepts will help you grasp the advanced features covered in later tutorials.
The Agent (Folk) Architecture
Individual Agent Attributes
Every agent in simcronomicon has several key attributes that govern their behavior:
# Example of a basic agent
agent = FolkSEIR(
id=42, # Unique identifier
home_address=156, # Node ID of their home
max_energy=5, # Maximum daily energy
status='S' # Disease status (S, E, I, R, etc.)
)
# Key behavioral attributes:
print(f"Current energy: {agent.energy}") # 0-5 (random daily start)
print(f"Current location: {agent.address}") # Current node ID
print(f"Days in status: {agent.status_step_streak}") # Time tracking
print(f"Movement restricted: {agent.movement_restricted}") # Quarantine flag
Energy System: The Foundation of Movement
The most important concept is the energy system. Each agent has:
max_energy: Maximum energy points (e.g., 5)
energy: Current energy (randomly reset each morning between 0 and max_energy)
Energy determines movement capability and action capability of an agent. If an agent has 0 energy, they cannot move or participate in any interaction with other agents.
This creates realistic daily patterns where: - Some agents are more active (higher starting energy) - Agents become less active as the day progresses - Energy resets each night during sleep
Daily Agent Lifecycle
Each simulation day follows this pattern for every agent:
Morning: Energy Reset
def sleep(self):
self.status_step_streak += 1 # Track time in current status
self.energy = random.randint(0, self.max_energy) # Reset energy
Day: Event Participation
For each step event in the day:
Movement Decision: Can the agent move?
if agent.movement_restricted == False and agent.alive and agent.energy > 0:
# Agent can participate in this event
# ...
Location Selection: Where does the agent go?
# Find valid destinations within travel distance
candidates = [node for node in town_graph.nodes
if distance_to_node <= event.max_distance
and node.place_type in event.place_types]
# Move to chosen location
agent.address = chosen_location
3. Interaction: What happens at the location? The simplest answer is, the agent has a likelihood of getting infected! This is govern by very simple block of if else condition and a touch of randomness:
def interact(self, folks_here, current_place_type, status_dict_t, model_params, dice):
# Disease transmission logic
if self.status == 'S' and any(folk.status == 'I' for folk in folks_here):
if transmission_probability > dice:
self.convert('E', status_dict_t) # Become exposed
self.energy -= 1 # Lose energy from interaction
Mathematical Foundation: Inverse Bernoulli Probability
The inverse Bernoulli function bridges the gap between continuous ODE dynamics and discrete agent interactions:
def inverse_bernoulli(self, folks_here, conversion_prob, infectious_statuses):
num_infectious = len([folk for folk in folks_here
if folk != self and folk.status in infectious_statuses])
# Key formula: P(infection) = 1 - (1 - β/N)^k
contact_prob = conversion_prob / len(folks_here) # β/N
return 1 - (1 - contact_prob) ** num_infectious # 1 - (1 - β/N)^k
Why This Formula Works:
β/N: Base transmission probability scaled by location density
k: Number of infectious people present (multiple exposure opportunities)
1 - (1 - β/N)^k: Probability of at least one successful transmission
Real-World Examples:
# Scenario 1: Small household (β=0.4)
# 1 infectious person, 5 total people
P = 1 - (1 - 0.4/5)^1 = 0.08 (8% infection risk)
# Scenario 2: Crowded restaurant
# 3 infectious people, 30 total people
P = 1 - (1 - 0.4/30)^3 = 0.039 (3.9% infection risk)
# Scenario 3: Large event
# 10 infectious people, 100 total people
P = 1 - (1 - 0.4/100)^10 = 0.039 (3.9% infection risk)
Key Insights:
Location matters: Smaller venues (higher β/N) create higher per-contact risk
Multiple contacts: More infectious people increases overall risk non-linearly
Crowd dilution: Larger crowds can actually reduce individual infection risk
ODE compatibility: As population grows, results converge to traditional SEIR equations
This mathematical foundation ensures that our agent-based results align with classical epidemiological theory while capturing the spatial heterogeneity that makes simcronomicon powerful for policy analysis.
Evening: Status Transitions
The most important concept for disease progression is status_step_streak - this tracks how many days an agent has been in their current status:
def sleep(self, ...):
super().sleep() # Reset energy and increment status streak
# Time-based disease progression using status_step_streak
if self.status == 'E' and self.status_step_streak == model_params.sigma:
self.convert('I', status_dict_t) # Exposed → Infectious after incubation period
elif self.status == 'I' and self.status_step_streak == model_params.gamma:
self.convert('R', status_dict_t) # Infectious → Recovered after infectious period
Example Disease Progression Timeline:
# Day 1: Agent becomes exposed
agent.status = 'E'
agent.status_step_streak = 0
# Day 2-3: Still incubating (sigma = 3 days)
agent.status_step_streak = 1, then 2
# Day 4: Becomes infectious
if agent.status_step_streak == 3: # sigma = 3
agent.convert('I', status_dict_t) # Convert and reset counter for new status
# Day 5-11: Infectious period (gamma = 7 days)
agent.status_step_streak = 1, 2, 3, 4, 5, 6
# Day 12: Recovers
if agent.status_step_streak == 7: # gamma = 7
agent.convert('R', status_dict_t) # Convert and reset counter for new status
Key Points: - status_step_streak increments every night during sleep() - When it reaches the model parameter threshold, status changes occur - The counter resets to 0 when an agent changes status - Different statuses have different duration parameters (sigma, gamma, etc.)
This creates predictable disease timelines: exposed agents become infectious after exactly sigma days, and infectious agents recover after exactly gamma days, mimicking real epidemiological patterns.
Simulation Orchestra: How Multiple Agents Coordinate
The main simulation loop in sim.py
coordinates thousands of agents:
Step 1: Reset Locations
# Clear all locations
for node in town_graph.nodes:
node["folks"] = [] # Empty all locations
Step 2: Agent Movement (DISPERSE Events)
def _disperse_for_event(self, step_event):
for person in self.folks:
if person.movement_restricted == False and person.alive and person.energy > 0:
# Find valid destinations
candidates = [node for node in reachable_nodes
if node.place_type in step_event.place_types]
# Choose destination (uniform random or custom probability function)
if step_event.probability_func:
distances = [distance_to_node for node in candidates]
probs = step_event.probability_func(distances, person)
new_location = np.random.choice(candidates, p=probs)
else:
new_location = random.choice(candidates)
# Move agent
person.address = new_location
# Add agent to their chosen location
town_graph.nodes[person.address]["folks"].append(person)
Step 3: Interactions at Each Location
# Process each active location
for node in active_locations:
folks_here = town_graph.nodes[node]["folks"]
place_type = town_graph.nodes[node]["place_type"]
# Each agent interacts with environment and others
for agent in folks_here:
if agent.alive and agent.energy > 0:
agent.interact(folks_here, place_type, status_dict, model_params, random.random())
Step 4: Return Home (SEND_HOME Events)
# End of day: everyone goes home
for agent in self.folks:
agent.address = agent.home_address
agent.sleep(...) # Reset energy, handle status transitions
Key Simulation Concepts
- Energy-Driven Participation
Only agents with energy > 0 can move and interact. This creates natural activity patterns and prevents unrealistic behavior.
- Location-Based Interactions
Agents only interact with others at the same location node. Disease spreads through shared physical spaces.
- Time-Dependent Transitions
Status changes happen during
sleep()
based on how long an agent has been in their current status.- Movement Restrictions
Quarantined agents (
movement_restricted = True
) stay home but can still interact with visiting agents (delivery, family).- Stochastic Behavior
Random elements (energy levels, movement choices, disease transmission) create realistic population-level patterns from simple rules.
Understanding Movement Patterns
The basic movement system uses uniform random selection:
# Basic movement: choose randomly among valid destinations
valid_destinations = [node for node in town if meets_criteria(node)]
chosen_destination = random.choice(valid_destinations)
But you can customize this with probability functions of your own, which is the topic of the next tutorial!
Next Steps
Now that you understand how individual agents work and how the simulation coordinates multiple agents, you’re ready to explore:
Advanced Step Events and Movement Patterns: Customize agent movement with sophisticated probability functions
SEIQRDV Model Features: Understand complex disease models with vaccination (priority place system) and quarantine
Custom Model Development: Create your own compartmental models and agent behaviors
The key insight is that complex population-level patterns emerge from simple agent-level rules. By understanding energy, movement, interactions, and status transitions, you can design realistic epidemic simulations and analyze intervention strategies.