Advanced Step Events and Movement Patterns

This tutorial covers how to create custom step events and design realistic movement patterns for your simulations. Step events control when, where, and how agents move and interact during each simulation day.

We recommend that along with reading this, you should look at the tutorial `disease_spread_mobility.ipynb`` to see how StepEvent looks like in action

Understanding Step Events

Step events are the building blocks of agent behavior in simcronomicon. They define:

  • When agents perform activities (event sequence)

  • Where agents go (place types and distances)

  • How agents choose destinations (probability functions)

  • What agents do when they arrive (interaction functions)

Basic Step Event Creation

import simcronomicon as scon

# Simple end-of-day event (SEND_HOME)
# This event exists by default at the end of step_events list without your input.
sleep_event = scon.StepEvent(
    name="end_day",
    folk_action=scon.FolkSEIR.sleep
)

# Basic movement event (DISPERSE)
work_event = scon.StepEvent(
    name="work_commute",
    folk_action=scon.FolkSEIR.interact,
    event_type=scon.EventType.DISPERSE,
    max_distance=15000,  # 15km max travel
    place_types=['workplace']
)

Important: Automatic End-of-Day Event

You never need to manually add a sleep/end-of-day event to your step events list.

The simcronomicon framework automatically appends an “end_day” event to your step events sequence. This event:

  • Calls the sleep() method for all agents

  • Sends all agents back to their home addresses

  • Resets agent energy for the next day

  • Handles end-of-day status transitions

# DON'T do this - the end_day event is added automatically!
step_events = [
    scon.StepEvent("work", scon.FolkSEIR.interact, ...),
    scon.StepEvent("shopping", scon.FolkSEIR.interact, ...),
    # scon.StepEvent("end_day", scon.FolkSEIR.sleep)  # ← NOT NEEDED!
]

# DO this - just define your activity events
step_events = [
    scon.StepEvent("work", scon.FolkSEIR.interact, ...),
    scon.StepEvent("shopping", scon.FolkSEIR.interact, ...)
    # End-of-day automatically added by the model
]

This automatic behavior ensures that:

  • Agents always return home at day’s end

  • Energy is properly reset for the next simulation day

  • You can focus on defining meaningful daily activities

  • The simulation maintains proper day/night cycles

Event Types Explained

SEND_HOME Events
  • All agents return directly to their home addresses

  • No distance limitations or place type filtering

  • Used for: end-of-day, emergency evacuations, curfews

DISPERSE Events
  • Agents move to locations within specified constraints

  • Enables agent interactions at destinations

  • Used for: work, shopping, social activities

Built-in Probability Functions

We have 2 built-in functions to simulate agent movement patterns:

Log-Normal Mobility Models realistic human travel patterns based on research literature. Best for: - Work commutes and regular activities - Shopping and errands - Healthcare visits - Any activity with preferred typical distances

# Log-normal mobility with intuitive parameters
shopping_event = scon.StepEvent(
    name="shopping",
    folk_action=scon.FolkSEIR.interact,
    event_type=scon.EventType.DISPERSE,
    max_distance=8000,
    place_types=['commercial'],
    probability_func=lambda distances, agent: scon.log_normal_mobility(
        distances, agent, median_distance=2000, sigma=1.2)
)

Energy-Dependent Exponential Mobility Models agent movement based on current energy levels. Best for: - Social activities after work - Leisure activities - Any energy-dependent behavior

# Energy-dependent mobility with distance scaling
social_event = scon.StepEvent(
    name="evening_social",
    folk_action=scon.FolkSEIR.interact,
    event_type=scon.EventType.DISPERSE,
    max_distance=15000,
    place_types=['commercial', 'entertainment'],
    probability_func=lambda distances, agent: scon.energy_exponential_mobility(
        distances, agent, distance_scale=2000)
)

Parameter Guidelines:

Log-Normal Mobility: - median_distance: 400m (local), 1100m (neighborhood), 3000m (city-wide), 8000m (regional) - sigma: 0.8 (consistent), 1.0 (moderate), 1.5 (variable)

Energy Exponential Mobility: - distance_scale: 200 (very local), 1000 (moderate), 3000 (wide range)

Creating Custom Probability Functions

But maybe you might want to use other types of function to define the probability of an agent going somewhere that is dependent with the distances. You can define them yourselves!

Your probability function must:

  1. Accept exactly 2 non-default arguments: (distances, agent)

  2. Return probabilities between 0 and 1

  3. Probabilities should sum to 1. This means you must normalize the probabilities!

  4. Handle numpy arrays for distances

  5. Be robust to edge cases (empty arrays, zero distances)

Here is an example of how you can define your own simple probability function:

 def distance_preference_mobility(distances, agent, preference="nearby"):
     import numpy as np
     distances = np.array(distances)

     if preference == "nearby":
         # Exponential decay - prefer closer locations
         probs = np.exp(-distances / 2000)  # 2km characteristic distance
     elif preference == "far":
         # Prefer moderate to far distances
         probs = distances / np.max(distances) if len(distances) > 1 else [1.0]
     else:
         # Uniform - all distances equally likely
         probs = np.ones_like(distances)

# Use custom function
exploration_event = scon.StepEvent(
    name="exploration",
    folk_action=scon.FolkSEIR.interact,
    event_type=scon.EventType.DISPERSE,
    max_distance=20000,
    place_types=['commercial', 'religious', 'education'],
    probability_func=lambda dists: distance_preference(dists, "far")
)

Agent-Dependent Probability Functions

The power of the 2-parameter system is enabling agent-specific behavior. For example, if you have an SEIR model, you can make assumption about agent’s mobility dependence with their status:

def status_based_mobility(distances, agent):
    """
    Movement patterns that depend on agent health status.
    """
    import numpy as np
    distances = np.array(distances)

    # Quarantined agents cannot move (handled elsewhere)
    # Sick agents prefer shorter distances
    if hasattr(agent, 'status'):
        if agent.status == 'I':  # Infectious - stay closer to home
            probs = np.exp(-distances / 1000)  # 1km characteristic distance
        elif agent.status == 'R':  # Recovered - normal mobility
            probs = np.exp(-distances / 3000)  # 3km characteristic distance
        else:  # Susceptible - slightly more adventurous
            probs = np.exp(-distances / 4000)  # 4km characteristic distance
    else:
        # Default behavior for other statuses
        probs = np.exp(-distances / 2000)

    return probs / probs.sum() if probs.sum() > 0 else np.ones_like(probs) / len(probs)

Complete Example: Daily Routine

# Define a realistic daily schedule with varied movement patterns
def create_daily_events():
    return [
        # Morning commute - log-normal for realistic work travel
        scon.StepEvent(
            "morning_commute",
            scon.FolkSEIR.interact,
            scon.EventType.DISPERSE,
            max_distance=20000,
            place_types=['workplace', 'education'],
            probability_func=lambda distances, agent: scon.log_normal_mobility(
                distances, agent, median_distance=5000, sigma=1.0)
        ),

        # Lunch break - energy-dependent for tired workers
        scon.StepEvent(
            "lunch_break",
            scon.FolkSEIR.interact,
            scon.EventType.DISPERSE,
            max_distance=3000,
            place_types=['commercial'],
            probability_func=lambda distances, agent: scon.energy_exponential_mobility(
                distances, agent, distance_scale=800)
        ),

        # Evening activities - custom preference function
        scon.StepEvent(
            "evening_social",
            scon.FolkSEIR.interact,
            scon.EventType.DISPERSE,
            max_distance=15000,
            place_types=['commercial', 'religious', 'entertainment'],
            probability_func=lambda distances, agent: distance_preference_mobility(
                distances, agent, "far")
        ),
    ]

# Use in simulation
step_events = create_daily_events()
model = scon.SEIRModel(model_params, step_events)

Tips for Effective Step Events

Event Timing
  • Order events logically (commute → work → lunch → home)

  • Consider realistic time constraints for each activity

Distance Constraints
  • Match max_distance to activity type (nearby shopping vs. long commutes)

  • Consider transportation modes in your model area

Place Type Selection
  • Be specific: [‘workplace’] vs. [‘commercial’, ‘workplace’]

  • Ensure your town has the required place types

Probability Function Parameters
  • Log-normal median_distance: Set to typical travel distance for the activity

  • Log-normal sigma: Lower for consistent behavior, higher for varied patterns

  • Energy exponential distance_scale: Lower for local activities, higher for wide-range movement

  • Test with sample distances before using in simulation

Parameter Testing Example

# Test your probability functions with sample data
import numpy as np

class TestAgent:
    def __init__(self, energy=5, max_energy=10):
        self.energy = energy
        self.max_energy = max_energy

test_distances = np.array([100, 500, 1000, 2000, 5000])
test_agent = TestAgent()

# Test log-normal mobility
log_probs = scon.log_normal_mobility(test_distances, test_agent,
                                    median_distance=1500, sigma=1.0)
print(f"Log-normal probabilities: {log_probs}")

# Test energy exponential mobility
energy_probs = scon.energy_exponential_mobility(test_distances, test_agent,
                                               distance_scale=1000)
print(f"Energy exponential probabilities: {energy_probs}")

Debugging Step Events

# Test your probability function
test_distances = [100, 500, 1000, 5000, 10000]
test_probs = distance_preference(test_distances, "nearby")
print(f"Distances: {test_distances}")
print(f"Probabilities: {test_probs}")
print(f"Sum: {sum(test_probs)}")  # Should be close to 1.0

# Validate step events before simulation
events = create_daily_events()
for event in events:
    print(f"Event: {event.name}")
    print(f"  Type: {event.event_type}")
    print(f"  Max distance: {event.max_distance}m")
    print(f"  Place types: {event.place_types}")

Next Steps

  • Experiment with different probability functions for the same activity

  • Create event sequences that reflect real-world daily patterns

  • Combine step events with advanced model features (vaccination, quarantine)

  • Consider seasonal or policy-driven changes to movement patterns

For more complex scenarios, see the SEIQRDV advanced features tutorial and the full API documentation.