Claude Skill

alterlab-simpy

Process-based discrete-event simulation in Python with SimPy — processes, queues, shared resources, and time-based events. Use when simulating systems where entities contend for shared resources over time, such as manufacturing systems, service operations, network traffic, or log

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download alterlab-ieu-alterlab-academic-skills-skills_data-science_alterlab-simpy-e4836c0.zip · 25 KB
Part of alterlab-ieu/alterlab-academic-skills — 94 skills

Install

skills CLI npx skills add https://github.com/AlterLab-IEU/AlterLab-Academic-Skills/tree/main/skills/data-science/alterlab-simpy
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install alterlab-ieu-alterlab-academic-skills@llmmart
Git git clone https://github.com/AlterLab-IEU/AlterLab-Academic-Skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole alterlab-ieu/alterlab-academic-skills collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

SimPy - Discrete-Event Simulation

Overview

SimPy is a process-based discrete-event simulation framework based on standard Python. Use SimPy to model systems where entities (customers, vehicles, packets, etc.) interact with each other and compete for shared resources (servers, machines, bandwidth, etc.) over time.

Core capabilities:

  • Process modeling using Python generator functions
  • Shared resource management (servers, containers, stores)
  • Event-driven scheduling and synchronization
  • Real-time simulations synchronized with wall-clock time
  • Comprehensive monitoring and data collection

When to Use This Skill

Use the SimPy skill when:

  1. Modeling discrete-event systems - Systems where events occur at irregular intervals
  2. Resource contention - Entities compete for limited resources (servers, machines, staff)
  3. Queue analysis - Studying waiting lines, service times, and throughput
  4. Process optimization - Analyzing manufacturing, logistics, or service processes
  5. Network simulation - Packet routing, bandwidth allocation, latency analysis
  6. Capacity planning - Determining optimal resource levels for desired performance
  7. System validation - Testing system behavior before implementation

Not suitable for:

  • Continuous simulations with fixed time steps (consider SciPy ODE solvers)
  • Independent processes without resource sharing
  • Pure mathematical optimization (consider SciPy optimize)

Does NOT Trigger

Scenario Use Instead
Agent-based models of social systems with heterogeneous agents on grids or networks (segregation, opinion dynamics, diffusion) alterlab-abm-mesa
Searching for Pareto-optimal designs or parameters (e.g. cost vs. waiting time) with evolutionary algorithms alterlab-pymoo
Fitting time-series or regression models to observed queue or throughput data alterlab-statsmodels
Training a reinforcement-learning agent to control a system alterlab-stable-baselines3

Quick Start

Basic Simulation Structure

import simpy

def process(env, name):
    """A simple process that waits and prints."""
    print(f'{name} starting at {env.now}')
    yield env.timeout(5)
    print(f'{name} finishing at {env.now}')

# Create environment
env = simpy.Environment()

# Start processes
env.process(process(env, 'Process 1'))
env.process(process(env, 'Process 2'))

# Run simulation
env.run(until=10)

Resource Usage Pattern

import simpy

def customer(env, name, resource):
    """Customer requests resource, uses it, then releases."""
    with resource.request() as req:
        yield req  # Wait for resource
        print(f'{name} got resource at {env.now}')
        yield env.timeout(3)  # Use resource
        print(f'{name} released resource at {env.now}')

env = simpy.Environment()
server = simpy.Resource(env, capacity=1)

env.process(customer(env, 'Customer 1', server))
env.process(customer(env, 'Customer 2', server))
env.run()

Core Concepts

1. Environment

The simulation environment manages time and schedules events.

import simpy

# Standard environment (runs as fast as possible)
env = simpy.Environment(initial_time=0)

# Real-time environment (synchronized with wall-clock)
import simpy.rt
env_rt = simpy.rt.RealtimeEnvironment(factor=1.0)

# Run simulation
env.run(until=100)  # Run until time 100
env.run()  # Run until no events remain

2. Processes

Processes are defined using Python generator functions (functions with yield statements).

def my_process(env, param1, param2):
    """Process that yields events to pause execution."""
    print(f'Starting at {env.now}')

    # Wait for time to pass
    yield env.timeout(5)

    print(f'Resumed at {env.now}')

    # Wait for another event
    yield env.timeout(3)

    print(f'Done at {env.now}')
    return 'result'

# Start the process
env.process(my_process(env, 'value1', 'value2'))

3. Events

Events are the fundamental mechanism for process synchronization. Processes yield events and resume when those events are triggered.

Common event types:

  • env.timeout(delay) - Wait for time to pass
  • resource.request() - Request a resource
  • env.event() - Create a custom event
  • env.process(func()) - Process as an event
  • event1 & event2 - Wait for all events (AllOf)
  • event1 | event2 - Wait for any event (AnyOf)

Resources

SimPy provides several resource types for different scenarios. For comprehensive details, see references/resources.md.

Resource Types Summary

Resource Type Use Case
Resource Limited capacity (servers, machines)
PriorityResource Priority-based queuing
PreemptiveResource High-priority can interrupt low-priority
Container Bulk materials (fuel, water)
Store Python object storage (FIFO)
FilterStore Selective item retrieval
PriorityStore Priority-ordered items

Quick Reference

import simpy

env = simpy.Environment()

# Basic resource (e.g., servers)
resource = simpy.Resource(env, capacity=2)

# Priority resource
priority_resource = simpy.PriorityResource(env, capacity=1)

# Container (e.g., fuel tank)
fuel_tank = simpy.Container(env, capacity=100, init=50)

# Store (e.g., warehouse)
warehouse = simpy.Store(env, capacity=10)

Common Simulation Patterns

Pattern 1: Customer-Server Queue

import simpy
import random

def customer(env, name, server):
    arrival = env.now
    with server.request() as req:
        yield req
        wait = env.now - arrival
        print(f'{name} waited {wait:.2f}, served at {env.now}')
        yield env.timeout(random.uniform(2, 4))

def customer_generator(env, server):
    i = 0
    while True:
        yield env.timeout(random.uniform(1, 3))
        i += 1
        env.process(customer(env, f'Customer {i}', server))

env = simpy.Environment()
server = simpy.Resource(env, capacity=2)
env.process(customer_generator(env, server))
env.run(until=20)

Pattern 2: Producer-Consumer

import simpy

def producer(env, store):
    item_id = 0
    while True:
        yield env.timeout(2)
        item = f'Item {item_id}'
        yield store.put(item)
        print(f'Produced {item} at {env.now}')
        item_id += 1

def consumer(env, store):
    while True:
        item = yield store.get()
        print(f'Consumed {item} at {env.now}')
        yield env.timeout(3)

env = simpy.Environment()
store = simpy.Store(env, capacity=10)
env.process(producer(env, store))
env.process(consumer(env, store))
env.run(until=20)

Pattern 3: Parallel Task Execution

import simpy

def task(env, name, duration):
    print(f'{name} starting at {env.now}')
    yield env.timeout(duration)
    print(f'{name} done at {env.now}')
    return f'{name} result'

def coordinator(env):
    # Start tasks in parallel
    task1 = env.process(task(env, 'Task 1', 5))
    task2 = env.process(task(env, 'Task 2', 3))
    task3 = env.process(task(env, 'Task 3', 4))

    # Wait for all to complete
    results = yield task1 & task2 & task3
    print(f'All done at {env.now}')

env = simpy.Environment()
env.process(coordinator(env))
env.run()

Workflow Guide

Step 1: Define the System

Identify:

  • Entities: What moves through the system? (customers, parts, packets)
  • Resources: What are the constraints? (servers, machines, bandwidth)
  • Processes: What are the activities? (arrival, service, departure)
  • Metrics: What to measure? (wait times, utilization, throughput)

Step 2: Implement Process Functions

Create generator functions for each process type:

def entity_process(env, name, resources, parameters):
    # Arrival logic
    arrival_time = env.now

    # Request resources
    with resource.request() as req:
        yield req

        # Service logic
        service_time = calculate_service_time(parameters)
        yield env.timeout(service_time)

    # Departure logic
    collect_statistics(env.now - arrival_time)

Step 3: Set Up Monitoring

Use monitoring utilities to collect data. See references/monitoring.md for comprehensive techniques.

from scripts.resource_monitor import ResourceMonitor

# Create and monitor resource
resource = simpy.Resource(env, capacity=2)
monitor = ResourceMonitor(env, resource, "Server")

# After simulation
monitor.report()

Step 4: Run and Analyze

# Run simulation
env.run(until=simulation_time)

# Generate reports
monitor.report()
stats.report()

# Export data for further analysis
monitor.export_csv('results.csv')

Advanced Features

Process Interaction

Processes can interact through events, process yields, and interrupts. See references/process-interaction.md for detailed patterns.

Key mechanisms:

  • Event signaling: Shared events for coordination
  • Process yields: Wait for other processes to complete
  • Interrupts: Forcefully resume processes for preemption

Real-Time Simulations

Synchronize simulation with wall-clock time for hardware-in-the-loop or interactive applications. See references/real-time.md.

import simpy.rt

env = simpy.rt.RealtimeEnvironment(factor=1.0)  # 1:1 time mapping
# factor=0.5 means 1 sim unit = 0.5 seconds (2x faster)

Comprehensive Monitoring

Monitor processes, resources, and events. See references/monitoring.md for techniques including:

  • State variable tracking
  • Resource monkey-patching
  • Event tracing
  • Statistical collection

Scripts and Templates

basic_simulation_template.py

Complete template for building queue simulations with:

  • Configurable parameters
  • Statistics collection
  • Customer generation
  • Resource usage
  • Report generation

Usage:

from scripts.basic_simulation_template import SimulationConfig, run_simulation

config = SimulationConfig()
config.num_resources = 2
config.sim_time = 100
stats = run_simulation(config)
stats.report()

resource_monitor.py

Reusable monitoring utilities:

  • ResourceMonitor - Track single resource
  • MultiResourceMonitor - Monitor multiple resources
  • ContainerMonitor - Track container levels
  • Automatic statistics calculation
  • CSV export functionality

Usage:

from scripts.resource_monitor import ResourceMonitor

monitor = ResourceMonitor(env, resource, "My Resource")
# ... run simulation ...
monitor.report()
monitor.export_csv('data.csv')

Import note: these from scripts.... paths are relative to the skill directory. Copy the script next to your simulation, or add the skill directory to PYTHONPATH before importing.

Reference Documentation

Detailed guides, loaded on demand:

  • references/resources.md - All resource types with examples
  • references/events.md - Event system and patterns
  • references/process-interaction.md - Process synchronization (signals, yields, interrupts)
  • references/monitoring.md - Data collection techniques
  • references/real-time.md - Real-time simulation setup

Best Practices

  1. Generator functions: Always use yield in process functions
  2. Resource context managers: Use with resource.request() as req: for automatic cleanup
  3. Reproducibility: Set random.seed() for consistent results
  4. Monitoring: Collect data throughout simulation, not just at the end
  5. Validation: Compare simple cases with analytical solutions
  6. Documentation: Comment process logic and parameter choices
  7. Modular design: Separate process logic, statistics, and configuration

Common Pitfalls

  1. Forgetting yield: Processes must yield events to pause
  2. Event reuse: Events can only be triggered once
  3. Resource leaks: Use context managers or ensure release
  4. Blocking operations: Avoid Python blocking calls in processes
  5. Time units: Stay consistent with time unit interpretation
  6. Deadlocks: Ensure at least one process can make progress

Example Use Cases

  • Manufacturing: Machine scheduling, production lines, inventory management
  • Healthcare: Emergency room simulation, patient flow, staff allocation
  • Telecommunications: Network traffic, packet routing, bandwidth allocation
  • Transportation: Traffic flow, logistics, vehicle routing
  • Service operations: Call centers, retail checkout, appointment scheduling
  • Computer systems: CPU scheduling, memory management, I/O operations

Part of the AlterLab Academic Skills suite.

Files (alterlab-academic-skills)
  • evals
    • evals.json 5.1 KB
      {
        "skill": "alterlab-simpy",
        "evals": [
          {
            "id": "queue-server-simulation",
            "prompt": "I need to simulate a call center where calls arrive randomly and wait for one of 3 agents. I want average wait time and agent utilization over an 8-hour shift.",
            "expected_output": "Invokes alterlab-simpy: builds a discrete-event model with a simpy.Environment, a simpy.Resource(capacity=3) for the agents, a customer generator with randomized inter-arrival and service times (with a seeded random), uses 'with resource.request() as req:' context managers, runs env.run(until=...), and reports average wait time and resource utilization via a ResourceMonitor.",
            "assertions": [
              { "type": "should_trigger", "value": true },
              { "type": "behavior", "value": "Models agents as a SimPy Resource, collects per-customer wait times, and reports average wait time and utilization." }
            ]
          },
          {
            "id": "manufacturing-priority-preemption",
            "prompt": "Model a machine shop where rush orders should be able to interrupt normal jobs already on a machine. I want to see how preemption affects throughput.",
            "expected_output": "Invokes alterlab-simpy: uses a simpy.PreemptiveResource so high-priority (rush) requests can interrupt lower-priority jobs mid-service, models the interrupt handling in the process generator, runs the simulation, and reports throughput differences with monitoring.",
            "assertions": [
              { "type": "should_trigger", "value": true },
              { "type": "output_contains", "value": "PreemptiveResource" }
            ]
          },
          {
            "id": "producer-consumer-store",
            "prompt": "Simulate a warehouse where a producer adds finished items to a buffer and a consumer pulls them out on a different schedule, so I can see when the buffer fills up or empties.",
            "expected_output": "Invokes alterlab-simpy: implements a producer-consumer pattern using a simpy.Store as the shared buffer, with producer (store.put) and consumer (store.get) generator processes on different timeouts, runs env.run(until=...), and tracks buffer level over time.",
            "assertions": [
              { "type": "should_trigger", "value": true },
              { "type": "behavior", "value": "Uses a SimPy Store as a shared buffer with put/get processes and tracks buffer occupancy over simulated time." }
            ]
          },
          {
            "id": "capacity-planning-sweep",
            "prompt": "For our clinic, I want to find the minimum number of nurses needed so that 95% of patients wait less than 10 minutes. Can you run the simulation across different staffing levels?",
            "expected_output": "Invokes alterlab-simpy: parameterizes nurse count as a Resource capacity, runs the queue simulation across multiple staffing levels (with seeded reproducibility), collects wait-time distributions per level, and identifies the minimum capacity meeting the 95%-under-10-minutes service target.",
            "assertions": [
              { "type": "should_trigger", "value": true }
            ]
          },
          {
            "id": "near-miss-alterlab-statsmodels",
            "prompt": "I have observed inter-arrival times and service times from my call center logs. Fit an M/M/c queueing model and estimate the parameters analytically with confidence intervals.",
            "expected_output": "Does NOT invoke this skill; defers to a classical statistical modeling skill (alterlab-statsmodels). The user wants analytical parameter estimation with confidence intervals from observed data, not a process-based discrete-event simulation of resource contention.",
            "assertions": [
              { "type": "should_not_trigger", "value": true },
              { "type": "output_contains", "value": "alterlab-statsmodels" }
            ]
          },
          {
            "id": "near-miss-alterlab-pymoo",
            "prompt": "Find the staffing configuration across five departments that simultaneously minimizes total labor cost and minimizes average customer wait, giving me the Pareto front of trade-offs.",
            "expected_output": "Does NOT invoke this skill; defers to alterlab-pymoo. The user wants multi-objective optimization and a Pareto front of competing objectives, which is pymoo's territory, not a single discrete-event simulation run.",
            "assertions": [
              { "type": "should_not_trigger", "value": true },
              { "type": "output_contains", "value": "alterlab-pymoo" }
            ]
          },
          {
            "id": "near-miss-alterlab-abm-mesa",
            "prompt": "I want to model how a rumor spreads through a town where each resident has their own trust level and friends network, and see how the share of believers evolves when I change the trust distribution. What framework should I build this in?",
            "expected_output": "Does NOT invoke this skill; defers to alterlab-abm-mesa. The request is an agent-based social model (heterogeneous residents on a network, opinion/rumor diffusion, parameter sweeps over agent attributes), which is Mesa's territory rather than SimPy's process/resource-contention discrete-event simulation.",
            "assertions": [
              { "type": "should_not_trigger", "value": true },
              { "type": "output_contains", "value": "alterlab-abm-mesa" }
            ]
          }
        ]
      }
      
  • references
    • events.md 8.4 KB
      # SimPy Events System
      
      This guide covers the event system in SimPy, which forms the foundation of discrete-event simulation.
      
      ## Event Basics
      
      Events are the core mechanism for controlling simulation flow. Processes yield events and resume when those events are triggered.
      
      ### Event Lifecycle
      
      Events progress through three states:
      
      1. **Not triggered** - Initial state as memory objects
      2. **Triggered** - Scheduled in event queue; `triggered` property is `True`
      3. **Processed** - Removed from queue with callbacks executed; `processed` property is `True`
      
      ```python
      import simpy
      
      env = simpy.Environment()
      
      # Create an event
      event = env.event()
      print(f'Triggered: {event.triggered}, Processed: {event.processed}')  # Both False
      
      # Trigger the event
      event.succeed(value='Event result')
      print(f'Triggered: {event.triggered}, Processed: {event.processed}')  # True, False
      
      # Run to process the event
      env.run()
      print(f'Triggered: {event.triggered}, Processed: {event.processed}')  # True, True
      print(f'Value: {event.value}')  # 'Event result'
      ```
      
      ## Core Event Types
      
      ### Timeout
      
      Controls time progression in simulations. Most common event type.
      
      ```python
      import simpy
      
      def process(env):
          print(f'Starting at {env.now}')
          yield env.timeout(5)
          print(f'Resumed at {env.now}')
      
          # Timeout with value
          result = yield env.timeout(3, value='Done')
          print(f'Result: {result} at {env.now}')
      
      env = simpy.Environment()
      env.process(process(env))
      env.run()
      ```
      
      **Usage:**
      - `env.timeout(delay)` - Wait for specified time
      - `env.timeout(delay, value=val)` - Wait and return value
      
      ### Process Events
      
      Processes themselves are events, allowing processes to wait for other processes to complete.
      
      ```python
      import simpy
      
      def worker(env, name, duration):
          print(f'{name} starting at {env.now}')
          yield env.timeout(duration)
          print(f'{name} finished at {env.now}')
          return f'{name} result'
      
      def coordinator(env):
          # Start worker processes
          worker1 = env.process(worker(env, 'Worker 1', 5))
          worker2 = env.process(worker(env, 'Worker 2', 3))
      
          # Wait for worker1 to complete
          result = yield worker1
          print(f'Coordinator received: {result}')
      
          # Wait for worker2
          result = yield worker2
          print(f'Coordinator received: {result}')
      
      env = simpy.Environment()
      env.process(coordinator(env))
      env.run()
      ```
      
      ### Event
      
      Generic event that can be manually triggered.
      
      ```python
      import simpy
      
      def waiter(env, event):
          print(f'Waiting for event at {env.now}')
          value = yield event
          print(f'Event received with value: {value} at {env.now}')
      
      def triggerer(env, event):
          yield env.timeout(5)
          print(f'Triggering event at {env.now}')
          event.succeed(value='Hello!')
      
      env = simpy.Environment()
      event = env.event()
      env.process(waiter(env, event))
      env.process(triggerer(env, event))
      env.run()
      ```
      
      ## Composite Events
      
      ### AllOf - Wait for Multiple Events
      
      Triggers when all specified events have occurred.
      
      ```python
      import simpy
      
      def process(env):
          # Start multiple tasks
          task1 = env.timeout(3, value='Task 1 done')
          task2 = env.timeout(5, value='Task 2 done')
          task3 = env.timeout(4, value='Task 3 done')
      
          # Wait for all to complete
          results = yield simpy.AllOf(env, [task1, task2, task3])
          print(f'All tasks completed at {env.now}')
          print(f'Results: {results}')
      
          # Alternative syntax using & operator
          task4 = env.timeout(2)
          task5 = env.timeout(3)
          yield task4 & task5
          print(f'Tasks 4 and 5 completed at {env.now}')
      
      env = simpy.Environment()
      env.process(process(env))
      env.run()
      ```
      
      **Returns:** Dictionary mapping events to their values
      
      **Use cases:**
      - Parallel task completion
      - Barrier synchronization
      - Waiting for multiple resources
      
      ### AnyOf - Wait for Any Event
      
      Triggers when at least one specified event has occurred.
      
      ```python
      import simpy
      
      def process(env):
          # Start multiple tasks with different durations
          fast_task = env.timeout(2, value='Fast')
          slow_task = env.timeout(10, value='Slow')
      
          # Wait for first to complete
          results = yield simpy.AnyOf(env, [fast_task, slow_task])
          print(f'First task completed at {env.now}')
          print(f'Results: {results}')
      
          # Alternative syntax using | operator
          task1 = env.timeout(5)
          task2 = env.timeout(3)
          yield task1 | task2
          print(f'One of the tasks completed at {env.now}')
      
      env = simpy.Environment()
      env.process(process(env))
      env.run()
      ```
      
      **Returns:** Dictionary with completed events and their values
      
      **Use cases:**
      - Racing conditions
      - Timeout mechanisms
      - First-to-respond scenarios
      
      ## Event Triggering Methods
      
      Events can be triggered in three ways:
      
      ### succeed(value=None)
      
      Marks event as successful.
      
      ```python
      event = env.event()
      event.succeed(value='Success!')
      ```
      
      ### fail(exception)
      
      Marks event as failed with an exception.
      
      ```python
      def process(env):
          event = env.event()
          event.fail(ValueError('Something went wrong'))
      
          try:
              yield event
          except ValueError as e:
              print(f'Caught exception: {e}')
      
      env = simpy.Environment()
      env.process(process(env))
      env.run()
      ```
      
      ### trigger(event)
      
      Copies another event's outcome.
      
      ```python
      event1 = env.event()
      event1.succeed(value='Original')
      
      event2 = env.event()
      event2.trigger(event1)  # event2 now has same outcome as event1
      ```
      
      ## Callbacks
      
      Attach functions to execute when events are triggered.
      
      ```python
      import simpy
      
      def callback(event):
          print(f'Callback executed! Event value: {event.value}')
      
      def process(env):
          event = env.timeout(5, value='Done')
          event.callbacks.append(callback)
          yield event
      
      env = simpy.Environment()
      env.process(process(env))
      env.run()
      ```
      
      **Note:** Yielding an event from a process automatically adds the process's resume method as a callback.
      
      ## Event Sharing
      
      Multiple processes can wait for the same event.
      
      ```python
      import simpy
      
      def waiter(env, name, event):
          print(f'{name} waiting at {env.now}')
          value = yield event
          print(f'{name} resumed with {value} at {env.now}')
      
      def trigger_event(env, event):
          yield env.timeout(5)
          event.succeed(value='Go!')
      
      env = simpy.Environment()
      shared_event = env.event()
      
      env.process(waiter(env, 'Process 1', shared_event))
      env.process(waiter(env, 'Process 2', shared_event))
      env.process(waiter(env, 'Process 3', shared_event))
      env.process(trigger_event(env, shared_event))
      
      env.run()
      ```
      
      **Use cases:**
      - Broadcasting signals
      - Barrier synchronization
      - Coordinated process resumption
      
      ## Advanced Event Patterns
      
      ### Timeout with Cancellation
      
      ```python
      import simpy
      
      def process_with_timeout(env):
          work = env.timeout(10, value='Work complete')
          timeout = env.timeout(5, value='Timeout!')
      
          # Race between work and timeout
          result = yield work | timeout
      
          if work in result:
              print(f'Work completed: {result[work]}')
          else:
              print(f'Timed out: {result[timeout]}')
      
      env = simpy.Environment()
      env.process(process_with_timeout(env))
      env.run()
      ```
      
      ### Event Chaining
      
      ```python
      import simpy
      
      def event_chain(env):
          # Create chain of dependent events
          event1 = env.event()
          event2 = env.event()
          event3 = env.event()
      
          def trigger_sequence(env):
              yield env.timeout(2)
              event1.succeed(value='Step 1')
              yield env.timeout(2)
              event2.succeed(value='Step 2')
              yield env.timeout(2)
              event3.succeed(value='Step 3')
      
          env.process(trigger_sequence(env))
      
          # Wait for sequence
          val1 = yield event1
          print(f'{val1} at {env.now}')
          val2 = yield event2
          print(f'{val2} at {env.now}')
          val3 = yield event3
          print(f'{val3} at {env.now}')
      
      env = simpy.Environment()
      env.process(event_chain(env))
      env.run()
      ```
      
      ### Conditional Events
      
      ```python
      import simpy
      
      def conditional_process(env):
          temperature = 20
      
          if temperature > 25:
              yield env.timeout(5)  # Cooling required
              print('System cooled')
          else:
              yield env.timeout(1)  # No cooling needed
              print('Temperature acceptable')
      
      env = simpy.Environment()
      env.process(conditional_process(env))
      env.run()
      ```
      
      ## Best Practices
      
      1. **Always yield events**: Processes must yield events to pause execution
      2. **Don't trigger events multiple times**: Events can only be triggered once
      3. **Handle failures**: Use try-except when yielding events that might fail
      4. **Composite events for parallelism**: Use AllOf/AnyOf for concurrent operations
      5. **Shared events for broadcasting**: Multiple processes can yield the same event
      6. **Event values for data passing**: Use event values to pass results between processes
      
    • monitoring.md 12.6 KB
      # SimPy Monitoring and Data Collection
      
      This guide covers techniques for collecting data and monitoring simulation behavior in SimPy.
      
      ## Monitoring Strategy
      
      Before implementing monitoring, define three things:
      
      1. **What to monitor**: Processes, resources, events, or system state
      2. **When to monitor**: On change, at intervals, or at specific events
      3. **How to store data**: Lists, files, databases, or real-time output
      
      ## 1. Process Monitoring
      
      ### State Variable Tracking
      
      Track process state by recording variables when they change.
      
      ```python
      import simpy
      
      def customer(env, name, service_time, log):
          arrival_time = env.now
          log.append(('arrival', name, arrival_time))
      
          yield env.timeout(service_time)
      
          departure_time = env.now
          log.append(('departure', name, departure_time))
      
          wait_time = departure_time - arrival_time
          log.append(('wait_time', name, wait_time))
      
      env = simpy.Environment()
      log = []
      
      env.process(customer(env, 'Customer 1', 5, log))
      env.process(customer(env, 'Customer 2', 3, log))
      env.run()
      
      print('Simulation log:')
      for entry in log:
          print(entry)
      ```
      
      ### Time-Series Data Collection
      
      ```python
      import simpy
      
      def system_monitor(env, system_state, data_log, interval):
          while True:
              data_log.append((env.now, system_state['queue_length'], system_state['utilization']))
              yield env.timeout(interval)
      
      def process(env, system_state):
          while True:
              system_state['queue_length'] += 1
              yield env.timeout(2)
              system_state['queue_length'] -= 1
              system_state['utilization'] = system_state['queue_length'] / 10
              yield env.timeout(3)
      
      env = simpy.Environment()
      system_state = {'queue_length': 0, 'utilization': 0.0}
      data_log = []
      
      env.process(system_monitor(env, system_state, data_log, interval=1))
      env.process(process(env, system_state))
      env.run(until=20)
      
      print('Time series data:')
      for time, queue, util in data_log:
          print(f'Time {time}: Queue={queue}, Utilization={util:.2f}')
      ```
      
      ### Multiple Variable Tracking
      
      ```python
      import simpy
      
      class SimulationData:
          def __init__(self):
              self.timestamps = []
              self.queue_lengths = []
              self.processing_times = []
              self.utilizations = []
      
          def record(self, timestamp, queue_length, processing_time, utilization):
              self.timestamps.append(timestamp)
              self.queue_lengths.append(queue_length)
              self.processing_times.append(processing_time)
              self.utilizations.append(utilization)
      
      def monitored_process(env, data):
          queue_length = 0
          processing_time = 0
          utilization = 0.0
      
          for i in range(5):
              queue_length = i % 3
              processing_time = 2 + i
              utilization = queue_length / 10
      
              data.record(env.now, queue_length, processing_time, utilization)
              yield env.timeout(2)
      
      env = simpy.Environment()
      data = SimulationData()
      env.process(monitored_process(env, data))
      env.run()
      
      print(f'Collected {len(data.timestamps)} data points')
      ```
      
      ## 2. Resource Monitoring
      
      ### Monkey-Patching Resources
      
      Patch resource methods to intercept and log operations.
      
      ```python
      import simpy
      
      def patch_resource(resource, data_log):
          """Patch a resource to log all requests and releases."""
      
          # Save original methods
          original_request = resource.request
          original_release = resource.release
      
          # Create wrapper for request
          def logged_request(*args, **kwargs):
              req = original_request(*args, **kwargs)
              data_log.append(('request', resource._env.now, len(resource.queue)))
              return req
      
          # Create wrapper for release
          def logged_release(*args, **kwargs):
              result = original_release(*args, **kwargs)
              data_log.append(('release', resource._env.now, len(resource.queue)))
              return result
      
          # Replace methods
          resource.request = logged_request
          resource.release = logged_release
      
      def user(env, name, resource):
          with resource.request() as req:
              yield req
              print(f'{name} using resource at {env.now}')
              yield env.timeout(3)
              print(f'{name} releasing resource at {env.now}')
      
      env = simpy.Environment()
      resource = simpy.Resource(env, capacity=1)
      log = []
      
      patch_resource(resource, log)
      
      env.process(user(env, 'User 1', resource))
      env.process(user(env, 'User 2', resource))
      env.run()
      
      print('\nResource log:')
      for entry in log:
          print(entry)
      ```
      
      ### Resource Subclassing
      
      Create custom resource classes with built-in monitoring.
      
      ```python
      import simpy
      
      class MonitoredResource(simpy.Resource):
          def __init__(self, env, capacity):
              super().__init__(env, capacity)
              self.data = []
              self.utilization_data = []
      
          def request(self, *args, **kwargs):
              req = super().request(*args, **kwargs)
              queue_length = len(self.queue)
              utilization = self.count / self.capacity
              self.data.append(('request', self._env.now, queue_length, utilization))
              self.utilization_data.append((self._env.now, utilization))
              return req
      
          def release(self, *args, **kwargs):
              result = super().release(*args, **kwargs)
              queue_length = len(self.queue)
              utilization = self.count / self.capacity
              self.data.append(('release', self._env.now, queue_length, utilization))
              self.utilization_data.append((self._env.now, utilization))
              return result
      
          def average_utilization(self):
              if not self.utilization_data:
                  return 0.0
              return sum(u for _, u in self.utilization_data) / len(self.utilization_data)
      
      def user(env, name, resource):
          with resource.request() as req:
              yield req
              print(f'{name} using resource at {env.now}')
              yield env.timeout(2)
      
      env = simpy.Environment()
      resource = MonitoredResource(env, capacity=2)
      
      for i in range(5):
          env.process(user(env, f'User {i+1}', resource))
      
      env.run()
      
      print(f'\nAverage utilization: {resource.average_utilization():.2%}')
      print(f'Total operations: {len(resource.data)}')
      ```
      
      ### Container Level Monitoring
      
      ```python
      import simpy
      
      class MonitoredContainer(simpy.Container):
          def __init__(self, env, capacity, init=0):
              super().__init__(env, capacity, init)
              self.level_data = [(0, init)]
      
          def put(self, amount):
              result = super().put(amount)
              self.level_data.append((self._env.now, self.level))
              return result
      
          def get(self, amount):
              result = super().get(amount)
              self.level_data.append((self._env.now, self.level))
              return result
      
      def producer(env, container, amount, interval):
          while True:
              yield env.timeout(interval)
              yield container.put(amount)
              print(f'Produced {amount}. Level: {container.level} at {env.now}')
      
      def consumer(env, container, amount, interval):
          while True:
              yield env.timeout(interval)
              yield container.get(amount)
              print(f'Consumed {amount}. Level: {container.level} at {env.now}')
      
      env = simpy.Environment()
      container = MonitoredContainer(env, capacity=100, init=50)
      
      env.process(producer(env, container, 20, 3))
      env.process(consumer(env, container, 15, 4))
      env.run(until=20)
      
      print('\nLevel history:')
      for time, level in container.level_data:
          print(f'Time {time}: Level={level}')
      ```
      
      ## 3. Event Tracing
      
      ### Environment Step Monitoring
      
      Monitor all events by patching the environment's step function.
      
      ```python
      import simpy
      
      def trace(env, callback):
          """Trace all events processed by the environment."""
      
          def _trace_step():
              # Get next event before it's processed
              if env._queue:
                  time, priority, event_id, event = env._queue[0]
                  callback(time, priority, event_id, event)
      
              # Call original step
              return original_step()
      
          original_step = env.step
          env.step = _trace_step
      
      def event_callback(time, priority, event_id, event):
          print(f'Event: time={time}, priority={priority}, id={event_id}, type={type(event).__name__}')
      
      def process(env, name):
          print(f'{name}: Starting at {env.now}')
          yield env.timeout(5)
          print(f'{name}: Done at {env.now}')
      
      env = simpy.Environment()
      trace(env, event_callback)
      
      env.process(process(env, 'Process 1'))
      env.process(process(env, 'Process 2'))
      env.run()
      ```
      
      ### Event Scheduling Monitor
      
      Track when events are scheduled.
      
      ```python
      import simpy
      
      class MonitoredEnvironment(simpy.Environment):
          def __init__(self):
              super().__init__()
              self.scheduled_events = []
      
          def schedule(self, event, priority=simpy.core.NORMAL, delay=0):
              super().schedule(event, priority, delay)
              scheduled_time = self.now + delay
              self.scheduled_events.append((scheduled_time, priority, type(event).__name__))
      
      def process(env, name, delay):
          print(f'{name}: Scheduling timeout for {delay} at {env.now}')
          yield env.timeout(delay)
          print(f'{name}: Resumed at {env.now}')
      
      env = MonitoredEnvironment()
      env.process(process(env, 'Process 1', 5))
      env.process(process(env, 'Process 2', 3))
      env.run()
      
      print('\nScheduled events:')
      for time, priority, event_type in env.scheduled_events:
          print(f'Time {time}, Priority {priority}, Type {event_type}')
      ```
      
      ## 4. Statistical Monitoring
      
      ### Queue Statistics
      
      ```python
      import simpy
      
      class QueueStatistics:
          def __init__(self):
              self.arrival_times = []
              self.departure_times = []
              self.queue_lengths = []
              self.wait_times = []
      
          def record_arrival(self, time, queue_length):
              self.arrival_times.append(time)
              self.queue_lengths.append(queue_length)
      
          def record_departure(self, arrival_time, departure_time):
              self.departure_times.append(departure_time)
              self.wait_times.append(departure_time - arrival_time)
      
          def average_wait_time(self):
              return sum(self.wait_times) / len(self.wait_times) if self.wait_times else 0
      
          def average_queue_length(self):
              return sum(self.queue_lengths) / len(self.queue_lengths) if self.queue_lengths else 0
      
      def customer(env, resource, stats):
          arrival_time = env.now
          stats.record_arrival(arrival_time, len(resource.queue))
      
          with resource.request() as req:
              yield req
              departure_time = env.now
              stats.record_departure(arrival_time, departure_time)
              yield env.timeout(2)
      
      env = simpy.Environment()
      resource = simpy.Resource(env, capacity=1)
      stats = QueueStatistics()
      
      for i in range(5):
          env.process(customer(env, resource, stats))
      
      env.run()
      
      print(f'Average wait time: {stats.average_wait_time():.2f}')
      print(f'Average queue length: {stats.average_queue_length():.2f}')
      ```
      
      ## 5. Data Export
      
      ### CSV Export
      
      ```python
      import simpy
      import csv
      
      def export_to_csv(data, filename):
          with open(filename, 'w', newline='') as f:
              writer = csv.writer(f)
              writer.writerow(['Time', 'Metric', 'Value'])
              writer.writerows(data)
      
      def monitored_simulation(env, data_log):
          for i in range(10):
              data_log.append((env.now, 'queue_length', i % 3))
              data_log.append((env.now, 'utilization', (i % 3) / 10))
              yield env.timeout(1)
      
      env = simpy.Environment()
      data = []
      env.process(monitored_simulation(env, data))
      env.run()
      
      export_to_csv(data, 'simulation_data.csv')
      print('Data exported to simulation_data.csv')
      ```
      
      ### Real-time Plotting (requires matplotlib)
      
      ```python
      import simpy
      import matplotlib.pyplot as plt
      
      class RealTimePlotter:
          def __init__(self):
              self.times = []
              self.values = []
      
          def update(self, time, value):
              self.times.append(time)
              self.values.append(value)
      
          def plot(self, title='Simulation Results'):
              plt.figure(figsize=(10, 6))
              plt.plot(self.times, self.values)
              plt.xlabel('Time')
              plt.ylabel('Value')
              plt.title(title)
              plt.grid(True)
              plt.show()
      
      def monitored_process(env, plotter):
          value = 0
          for i in range(20):
              value = value * 0.9 + (i % 5)
              plotter.update(env.now, value)
              yield env.timeout(1)
      
      env = simpy.Environment()
      plotter = RealTimePlotter()
      env.process(monitored_process(env, plotter))
      env.run()
      
      plotter.plot('Process Value Over Time')
      ```
      
      ## Best Practices
      
      1. **Minimize overhead**: Only monitor what's necessary; excessive logging can slow simulations
      
      2. **Structured data**: Use classes or named tuples for complex data points
      
      3. **Time-stamping**: Always include timestamps with monitored data
      
      4. **Aggregation**: For long simulations, aggregate data rather than storing every event
      
      5. **Lazy evaluation**: Consider collecting raw data and computing statistics after simulation
      
      6. **Memory management**: For very long simulations, periodically flush data to disk
      
      7. **Validation**: Verify monitoring code doesn't affect simulation behavior
      
      8. **Separation of concerns**: Keep monitoring code separate from simulation logic
      
      9. **Reusable components**: Create generic monitoring classes that can be reused across simulations
      
    • process-interaction.md 11.3 KB
      # SimPy Process Interaction
      
      This guide covers the mechanisms for processes to interact and synchronize in SimPy simulations.
      
      ## Interaction Mechanisms Overview
      
      SimPy provides three primary ways for processes to interact:
      
      1. **Event-based passivation/reactivation** - Shared events for signaling
      2. **Waiting for process termination** - Yielding process objects
      3. **Interruption** - Forcefully resuming paused processes
      
      ## 1. Event-Based Passivation and Reactivation
      
      Processes can share events to coordinate their execution.
      
      ### Basic Signal Pattern
      
      ```python
      import simpy
      
      def controller(env, signal_event):
          print(f'Controller: Preparing at {env.now}')
          yield env.timeout(5)
          print(f'Controller: Sending signal at {env.now}')
          signal_event.succeed()
      
      def worker(env, signal_event):
          print(f'Worker: Waiting for signal at {env.now}')
          yield signal_event
          print(f'Worker: Received signal, starting work at {env.now}')
          yield env.timeout(3)
          print(f'Worker: Work complete at {env.now}')
      
      env = simpy.Environment()
      signal = env.event()
      env.process(controller(env, signal))
      env.process(worker(env, signal))
      env.run()
      ```
      
      **Use cases:**
      - Start signals for coordinated operations
      - Completion notifications
      - Broadcasting state changes
      
      ### Multiple Waiters
      
      Multiple processes can wait for the same signal event.
      
      ```python
      import simpy
      
      def broadcaster(env, signal):
          yield env.timeout(5)
          print(f'Broadcasting signal at {env.now}')
          signal.succeed(value='Go!')
      
      def listener(env, name, signal):
          print(f'{name}: Waiting at {env.now}')
          msg = yield signal
          print(f'{name}: Received "{msg}" at {env.now}')
          yield env.timeout(2)
          print(f'{name}: Done at {env.now}')
      
      env = simpy.Environment()
      broadcast_signal = env.event()
      
      env.process(broadcaster(env, broadcast_signal))
      for i in range(3):
          env.process(listener(env, f'Listener {i+1}', broadcast_signal))
      
      env.run()
      ```
      
      ### Barrier Synchronization
      
      ```python
      import simpy
      
      class Barrier:
          def __init__(self, env, n):
              self.env = env
              self.n = n
              self.count = 0
              self.event = env.event()
      
          def wait(self):
              self.count += 1
              if self.count >= self.n:
                  self.event.succeed()
              return self.event
      
      def worker(env, barrier, name, work_time):
          print(f'{name}: Working at {env.now}')
          yield env.timeout(work_time)
          print(f'{name}: Reached barrier at {env.now}')
          yield barrier.wait()
          print(f'{name}: Passed barrier at {env.now}')
      
      env = simpy.Environment()
      barrier = Barrier(env, 3)
      
      env.process(worker(env, barrier, 'Worker A', 3))
      env.process(worker(env, barrier, 'Worker B', 5))
      env.process(worker(env, barrier, 'Worker C', 7))
      
      env.run()
      ```
      
      ## 2. Waiting for Process Termination
      
      Processes are events themselves, so you can yield them to wait for completion.
      
      ### Sequential Process Execution
      
      ```python
      import simpy
      
      def task(env, name, duration):
          print(f'{name}: Starting at {env.now}')
          yield env.timeout(duration)
          print(f'{name}: Completed at {env.now}')
          return f'{name} result'
      
      def sequential_coordinator(env):
          # Execute tasks sequentially
          result1 = yield env.process(task(env, 'Task 1', 5))
          print(f'Coordinator: {result1}')
      
          result2 = yield env.process(task(env, 'Task 2', 3))
          print(f'Coordinator: {result2}')
      
          result3 = yield env.process(task(env, 'Task 3', 4))
          print(f'Coordinator: {result3}')
      
      env = simpy.Environment()
      env.process(sequential_coordinator(env))
      env.run()
      ```
      
      ### Parallel Process Execution
      
      ```python
      import simpy
      
      def task(env, name, duration):
          print(f'{name}: Starting at {env.now}')
          yield env.timeout(duration)
          print(f'{name}: Completed at {env.now}')
          return f'{name} result'
      
      def parallel_coordinator(env):
          # Start all tasks
          task1 = env.process(task(env, 'Task 1', 5))
          task2 = env.process(task(env, 'Task 2', 3))
          task3 = env.process(task(env, 'Task 3', 4))
      
          # Wait for all to complete
          results = yield task1 & task2 & task3
          print(f'All tasks completed at {env.now}')
          print(f'Task 1 result: {task1.value}')
          print(f'Task 2 result: {task2.value}')
          print(f'Task 3 result: {task3.value}')
      
      env = simpy.Environment()
      env.process(parallel_coordinator(env))
      env.run()
      ```
      
      ### First-to-Complete Pattern
      
      ```python
      import simpy
      
      def server(env, name, processing_time):
          print(f'{name}: Starting request at {env.now}')
          yield env.timeout(processing_time)
          print(f'{name}: Completed at {env.now}')
          return name
      
      def load_balancer(env):
          # Send request to multiple servers
          server1 = env.process(server(env, 'Server 1', 5))
          server2 = env.process(server(env, 'Server 2', 3))
          server3 = env.process(server(env, 'Server 3', 7))
      
          # Wait for first to respond
          result = yield server1 | server2 | server3
      
          # Get the winner
          winner = list(result.values())[0]
          print(f'Load balancer: {winner} responded first at {env.now}')
      
      env = simpy.Environment()
      env.process(load_balancer(env))
      env.run()
      ```
      
      ## 3. Process Interruption
      
      Processes can be interrupted using `process.interrupt()`, which throws an `Interrupt` exception.
      
      ### Basic Interruption
      
      ```python
      import simpy
      
      def worker(env):
          try:
              print(f'Worker: Starting long task at {env.now}')
              yield env.timeout(10)
              print(f'Worker: Task completed at {env.now}')
          except simpy.Interrupt as interrupt:
              print(f'Worker: Interrupted at {env.now}')
              print(f'Interrupt cause: {interrupt.cause}')
      
      def interrupter(env, target_process):
          yield env.timeout(5)
          print(f'Interrupter: Interrupting worker at {env.now}')
          target_process.interrupt(cause='Higher priority task')
      
      env = simpy.Environment()
      worker_process = env.process(worker(env))
      env.process(interrupter(env, worker_process))
      env.run()
      ```
      
      ### Resumable Interruption
      
      Process can re-yield the same event after interruption to continue waiting.
      
      ```python
      import simpy
      
      def resumable_worker(env):
          work_left = 10
      
          while work_left > 0:
              try:
                  print(f'Worker: Working ({work_left} units left) at {env.now}')
                  start = env.now
                  yield env.timeout(work_left)
                  work_left = 0
                  print(f'Worker: Completed at {env.now}')
              except simpy.Interrupt:
                  work_left -= (env.now - start)
                  print(f'Worker: Interrupted! {work_left} units left at {env.now}')
      
      def interrupter(env, worker_proc):
          yield env.timeout(3)
          worker_proc.interrupt()
          yield env.timeout(2)
          worker_proc.interrupt()
      
      env = simpy.Environment()
      worker_proc = env.process(resumable_worker(env))
      env.process(interrupter(env, worker_proc))
      env.run()
      ```
      
      ### Interrupt with Custom Cause
      
      ```python
      import simpy
      
      def machine(env, name):
          while True:
              try:
                  print(f'{name}: Operating at {env.now}')
                  yield env.timeout(5)
              except simpy.Interrupt as interrupt:
                  if interrupt.cause == 'maintenance':
                      print(f'{name}: Maintenance required at {env.now}')
                      yield env.timeout(2)
                      print(f'{name}: Maintenance complete at {env.now}')
                  elif interrupt.cause == 'emergency':
                      print(f'{name}: Emergency stop at {env.now}')
                      break
      
      def maintenance_scheduler(env, machine_proc):
          yield env.timeout(7)
          machine_proc.interrupt(cause='maintenance')
          yield env.timeout(10)
          machine_proc.interrupt(cause='emergency')
      
      env = simpy.Environment()
      machine_proc = env.process(machine(env, 'Machine 1'))
      env.process(maintenance_scheduler(env, machine_proc))
      env.run()
      ```
      
      ### Preemptive Resource with Interruption
      
      ```python
      import simpy
      
      def user(env, name, resource, priority, duration):
          with resource.request(priority=priority) as req:
              try:
                  yield req
                  print(f'{name} (priority {priority}): Got resource at {env.now}')
                  yield env.timeout(duration)
                  print(f'{name}: Done at {env.now}')
              except simpy.Interrupt:
                  print(f'{name}: Preempted at {env.now}')
      
      env = simpy.Environment()
      resource = simpy.PreemptiveResource(env, capacity=1)
      
      env.process(user(env, 'Low priority user', resource, priority=10, duration=10))
      env.process(user(env, 'High priority user', resource, priority=1, duration=5))
      env.run()
      ```
      
      ## Advanced Patterns
      
      ### Producer-Consumer with Signaling
      
      ```python
      import simpy
      
      class Buffer:
          def __init__(self, env, capacity):
              self.env = env
              self.capacity = capacity
              self.items = []
              self.item_available = env.event()
      
          def put(self, item):
              if len(self.items) < self.capacity:
                  self.items.append(item)
                  if not self.item_available.triggered:
                      self.item_available.succeed()
                  return True
              return False
      
          def get(self):
              if self.items:
                  return self.items.pop(0)
              return None
      
      def producer(env, buffer):
          item_id = 0
          while True:
              yield env.timeout(2)
              item = f'Item {item_id}'
              if buffer.put(item):
                  print(f'Producer: Added {item} at {env.now}')
                  item_id += 1
      
      def consumer(env, buffer):
          while True:
              if buffer.items:
                  item = buffer.get()
                  print(f'Consumer: Retrieved {item} at {env.now}')
                  yield env.timeout(3)
              else:
                  print(f'Consumer: Waiting for items at {env.now}')
                  yield buffer.item_available
                  buffer.item_available = env.event()
      
      env = simpy.Environment()
      buffer = Buffer(env, capacity=5)
      env.process(producer(env, buffer))
      env.process(consumer(env, buffer))
      env.run(until=20)
      ```
      
      ### Handshake Protocol
      
      ```python
      import simpy
      
      def sender(env, request_event, acknowledge_event):
          for i in range(3):
              print(f'Sender: Sending request {i} at {env.now}')
              request_event.succeed(value=f'Request {i}')
              yield acknowledge_event
              print(f'Sender: Received acknowledgment at {env.now}')
      
              # Reset events for next iteration
              request_event = env.event()
              acknowledge_event = env.event()
              yield env.timeout(1)
      
      def receiver(env, request_event, acknowledge_event):
          for i in range(3):
              request = yield request_event
              print(f'Receiver: Got {request} at {env.now}')
              yield env.timeout(2)  # Process request
              acknowledge_event.succeed()
              print(f'Receiver: Sent acknowledgment at {env.now}')
      
              # Reset for next iteration
              request_event = env.event()
              acknowledge_event = env.event()
      
      env = simpy.Environment()
      request = env.event()
      ack = env.event()
      env.process(sender(env, request, ack))
      env.process(receiver(env, request, ack))
      env.run()
      ```
      
      ## Best Practices
      
      1. **Choose the right mechanism**:
         - Use events for signals and broadcasts
         - Use process yields for sequential/parallel workflows
         - Use interrupts for preemption and emergency handling
      
      2. **Exception handling**: Always wrap interrupt-prone code in try-except blocks
      
      3. **Event lifecycle**: Remember that events can only be triggered once; create new events for repeated signaling
      
      4. **Process references**: Store process objects if you need to interrupt them later
      
      5. **Cause information**: Use interrupt causes to communicate why interruption occurred
      
      6. **Resumable patterns**: Track progress to enable resumption after interruption
      
      7. **Avoid deadlocks**: Ensure at least one process can make progress at any time
      
    • real-time.md 10.5 KB
      # SimPy Real-Time Simulations
      
      This guide covers real-time simulation capabilities in SimPy, where simulation time is synchronized with wall-clock time.
      
      ## Overview
      
      Real-time simulations synchronize simulation time with actual wall-clock time. This is useful for:
      
      - **Hardware-in-the-loop (HIL)** testing
      - **Human interaction** with simulations
      - **Algorithm behavior analysis** under real-time constraints
      - **System integration** testing
      - **Demonstration** purposes
      
      ## RealtimeEnvironment
      
      Replace the standard `Environment` with `simpy.rt.RealtimeEnvironment` to enable real-time synchronization.
      
      ### Basic Usage
      
      ```python
      import simpy.rt
      
      def process(env):
          while True:
              print(f'Tick at {env.now}')
              yield env.timeout(1)
      
      # Real-time environment with 1:1 time mapping
      env = simpy.rt.RealtimeEnvironment(factor=1.0)
      env.process(process(env))
      env.run(until=5)
      ```
      
      ### Constructor Parameters
      
      ```python
      simpy.rt.RealtimeEnvironment(
          initial_time=0,      # Starting simulation time
          factor=1.0,          # Real time per simulation time unit
          strict=True          # Raise errors on timing violations
      )
      ```
      
      ## Time Scaling with Factor
      
      The `factor` parameter controls how simulation time maps to real time.
      
      ### Factor Examples
      
      ```python
      import simpy.rt
      import time
      
      def timed_process(env, label):
          start = time.time()
          print(f'{label}: Starting at {env.now}')
          yield env.timeout(2)
          elapsed = time.time() - start
          print(f'{label}: Completed at {env.now} (real time: {elapsed:.2f}s)')
      
      # Factor = 1.0: 1 simulation time unit = 1 second
      print('Factor = 1.0 (2 sim units = 2 seconds)')
      env = simpy.rt.RealtimeEnvironment(factor=1.0)
      env.process(timed_process(env, 'Normal speed'))
      env.run()
      
      # Factor = 0.5: 1 simulation time unit = 0.5 seconds
      print('\nFactor = 0.5 (2 sim units = 1 second)')
      env = simpy.rt.RealtimeEnvironment(factor=0.5)
      env.process(timed_process(env, 'Double speed'))
      env.run()
      
      # Factor = 2.0: 1 simulation time unit = 2 seconds
      print('\nFactor = 2.0 (2 sim units = 4 seconds)')
      env = simpy.rt.RealtimeEnvironment(factor=2.0)
      env.process(timed_process(env, 'Half speed'))
      env.run()
      ```
      
      **Factor interpretation:**
      - `factor=1.0` → 1 simulation time unit takes 1 real second
      - `factor=0.1` → 1 simulation time unit takes 0.1 real seconds (10x faster)
      - `factor=60` → 1 simulation time unit takes 60 real seconds (1 minute)
      
      ## Strict Mode
      
      ### strict=True (Default)
      
      Raises `RuntimeError` if computation exceeds allocated real-time budget.
      
      ```python
      import simpy.rt
      import time
      
      def heavy_computation(env):
          print(f'Starting computation at {env.now}')
          yield env.timeout(1)
      
          # Simulate heavy computation (exceeds 1 second budget)
          time.sleep(1.5)
      
          print(f'Computation done at {env.now}')
      
      env = simpy.rt.RealtimeEnvironment(factor=1.0, strict=True)
      env.process(heavy_computation(env))
      
      try:
          env.run()
      except RuntimeError as e:
          print(f'Error: {e}')
      ```
      
      ### strict=False
      
      Allows simulation to run slower than intended without crashing.
      
      ```python
      import simpy.rt
      import time
      
      def heavy_computation(env):
          print(f'Starting at {env.now}')
          yield env.timeout(1)
      
          # Heavy computation
          time.sleep(1.5)
      
          print(f'Done at {env.now}')
      
      env = simpy.rt.RealtimeEnvironment(factor=1.0, strict=False)
      env.process(heavy_computation(env))
      env.run()
      
      print('Simulation completed (slower than real-time)')
      ```
      
      **Use strict=False when:**
      - Development and debugging
      - Computation time is unpredictable
      - Acceptable to run slower than target rate
      - Analyzing worst-case behavior
      
      ## Hardware-in-the-Loop Example
      
      ```python
      import simpy.rt
      
      class HardwareInterface:
          """Simulated hardware interface."""
      
          def __init__(self):
              self.sensor_value = 0
      
          def read_sensor(self):
              """Simulate reading from hardware sensor."""
              import random
              self.sensor_value = random.uniform(20.0, 30.0)
              return self.sensor_value
      
          def write_actuator(self, value):
              """Simulate writing to hardware actuator."""
              print(f'Actuator set to {value:.2f}')
      
      def control_loop(env, hardware, setpoint):
          """Simple control loop running in real-time."""
          while True:
              # Read sensor
              sensor_value = hardware.read_sensor()
              print(f'[{env.now}] Sensor: {sensor_value:.2f}°C')
      
              # Simple proportional control
              error = setpoint - sensor_value
              control_output = error * 0.1
      
              # Write actuator
              hardware.write_actuator(control_output)
      
              # Control loop runs every 0.5 seconds
              yield env.timeout(0.5)
      
      # Real-time environment: 1 sim unit = 1 second
      env = simpy.rt.RealtimeEnvironment(factor=1.0, strict=False)
      hardware = HardwareInterface()
      setpoint = 25.0
      
      env.process(control_loop(env, hardware, setpoint))
      env.run(until=5)
      ```
      
      ## Human Interaction Example
      
      ```python
      import simpy.rt
      
      def interactive_process(env):
          """Process that waits for simulated user input."""
          print('Simulation started. Events will occur in real-time.')
      
          yield env.timeout(2)
          print(f'[{env.now}] Event 1: System startup')
      
          yield env.timeout(3)
          print(f'[{env.now}] Event 2: Initialization complete')
      
          yield env.timeout(2)
          print(f'[{env.now}] Event 3: Ready for operation')
      
      # Real-time environment for human-paced demonstration
      env = simpy.rt.RealtimeEnvironment(factor=1.0)
      env.process(interactive_process(env))
      env.run()
      ```
      
      ## Monitoring Real-Time Performance
      
      ```python
      import simpy.rt
      import time
      
      class RealTimeMonitor:
          def __init__(self):
              self.step_times = []
              self.drift_values = []
      
          def record_step(self, sim_time, real_time, expected_real_time):
              self.step_times.append(sim_time)
              drift = real_time - expected_real_time
              self.drift_values.append(drift)
      
          def report(self):
              if self.drift_values:
                  avg_drift = sum(self.drift_values) / len(self.drift_values)
                  max_drift = max(abs(d) for d in self.drift_values)
                  print(f'\nReal-time performance:')
                  print(f'Average drift: {avg_drift*1000:.2f} ms')
                  print(f'Maximum drift: {max_drift*1000:.2f} ms')
      
      def monitored_process(env, monitor, start_time, factor):
          for i in range(5):
              step_start = time.time()
              yield env.timeout(1)
      
              real_elapsed = time.time() - start_time
              expected_elapsed = env.now * factor
              monitor.record_step(env.now, real_elapsed, expected_elapsed)
      
              print(f'Sim time: {env.now}, Real time: {real_elapsed:.2f}s, ' +
                    f'Expected: {expected_elapsed:.2f}s')
      
      start = time.time()
      factor = 1.0
      env = simpy.rt.RealtimeEnvironment(factor=factor, strict=False)
      monitor = RealTimeMonitor()
      
      env.process(monitored_process(env, monitor, start, factor))
      env.run()
      monitor.report()
      ```
      
      ## Mixed Real-Time and Fast Simulation
      
      ```python
      import simpy.rt
      
      def background_simulation(env):
          """Fast background simulation."""
          for i in range(100):
              yield env.timeout(0.01)
          print(f'Background simulation completed at {env.now}')
      
      def real_time_display(env):
          """Real-time display updates."""
          for i in range(5):
              print(f'Display update at {env.now}')
              yield env.timeout(1)
      
      # Note: This is conceptual - SimPy doesn't directly support mixed modes
      # Consider running separate simulations or using strict=False
      env = simpy.rt.RealtimeEnvironment(factor=1.0, strict=False)
      env.process(background_simulation(env))
      env.process(real_time_display(env))
      env.run()
      ```
      
      ## Converting Standard to Real-Time
      
      Converting a standard simulation to real-time is straightforward:
      
      ```python
      import simpy
      import simpy.rt
      
      def process(env):
          print(f'Event at {env.now}')
          yield env.timeout(1)
          print(f'Event at {env.now}')
          yield env.timeout(1)
          print(f'Event at {env.now}')
      
      # Standard simulation (runs instantly)
      print('Standard simulation:')
      env = simpy.Environment()
      env.process(process(env))
      env.run()
      
      # Real-time simulation (2 real seconds)
      print('\nReal-time simulation:')
      env_rt = simpy.rt.RealtimeEnvironment(factor=1.0)
      env_rt.process(process(env_rt))
      env_rt.run()
      ```
      
      ## Best Practices
      
      1. **Factor selection**: Choose factor based on hardware/human constraints
         - Human interaction: `factor=1.0` (1:1 time mapping)
         - Fast hardware: `factor=0.01` (100x faster)
         - Slow processes: `factor=60` (1 sim unit = 1 minute)
      
      2. **Strict mode usage**:
         - Use `strict=True` for timing validation
         - Use `strict=False` for development and variable workloads
      
      3. **Computation budget**: Ensure process logic executes faster than timeout duration
      
      4. **Error handling**: Wrap real-time runs in try-except for timing violations
      
      5. **Testing strategy**:
         - Develop with standard Environment (fast iteration)
         - Test with RealtimeEnvironment (validation)
         - Deploy with appropriate factor and strict settings
      
      6. **Performance monitoring**: Track drift between simulation and real time
      
      7. **Graceful degradation**: Use `strict=False` when timing guarantees aren't critical
      
      ## Common Patterns
      
      ### Periodic Real-Time Tasks
      
      ```python
      import simpy.rt
      
      def periodic_task(env, name, period, duration):
          """Task that runs periodically in real-time."""
          while True:
              start = env.now
              print(f'{name}: Starting at {start}')
      
              # Simulate work
              yield env.timeout(duration)
      
              print(f'{name}: Completed at {env.now}')
      
              # Wait for next period
              elapsed = env.now - start
              wait_time = period - elapsed
              if wait_time > 0:
                  yield env.timeout(wait_time)
      
      env = simpy.rt.RealtimeEnvironment(factor=1.0)
      env.process(periodic_task(env, 'Task', period=2.0, duration=0.5))
      env.run(until=6)
      ```
      
      ### Synchronized Multi-Device Control
      
      ```python
      import simpy.rt
      
      def device_controller(env, device_id, update_rate):
          """Control loop for individual device."""
          while True:
              print(f'Device {device_id}: Update at {env.now}')
              yield env.timeout(update_rate)
      
      # All devices synchronized to real-time
      env = simpy.rt.RealtimeEnvironment(factor=1.0)
      
      # Different update rates for different devices
      env.process(device_controller(env, 'A', 1.0))
      env.process(device_controller(env, 'B', 0.5))
      env.process(device_controller(env, 'C', 2.0))
      
      env.run(until=5)
      ```
      
      ## Limitations
      
      1. **Performance**: Real-time simulation adds overhead; not suitable for high-frequency events
      2. **Synchronization**: Single-threaded; all processes share same time base
      3. **Precision**: Limited by Python's time resolution and system scheduling
      4. **Strict mode**: May raise errors frequently with computationally intensive processes
      5. **Platform-dependent**: Timing accuracy varies across operating systems
      
    • resources.md 7.2 KB
      # SimPy Shared Resources
      
      This guide covers all resource types in SimPy for modeling congestion points and resource allocation.
      
      ## Resource Types Overview
      
      SimPy provides three main categories of shared resources:
      
      1. **Resources** - Limited capacity resources (e.g., gas pumps, servers)
      2. **Containers** - Homogeneous bulk materials (e.g., fuel tanks, silos)
      3. **Stores** - Python object storage (e.g., item queues, warehouses)
      
      ## 1. Resources
      
      Model resources that can be used by a limited number of processes at a time.
      
      ### Resource (Basic)
      
      The basic resource is a semaphore with specified capacity.
      
      ```python
      import simpy
      
      env = simpy.Environment()
      resource = simpy.Resource(env, capacity=2)
      
      def process(env, resource, name):
          with resource.request() as req:
              yield req
              print(f'{name} has the resource at {env.now}')
              yield env.timeout(5)
              print(f'{name} releases the resource at {env.now}')
      
      env.process(process(env, resource, 'Process 1'))
      env.process(process(env, resource, 'Process 2'))
      env.process(process(env, resource, 'Process 3'))
      env.run()
      ```
      
      **Key properties:**
      - `capacity` - Maximum number of concurrent users (default: 1)
      - `count` - Current number of users
      - `queue` - List of queued requests
      
      ### PriorityResource
      
      Extends basic resource with priority levels (lower numbers = higher priority).
      
      ```python
      import simpy
      
      env = simpy.Environment()
      resource = simpy.PriorityResource(env, capacity=1)
      
      def process(env, resource, name, priority):
          with resource.request(priority=priority) as req:
              yield req
              print(f'{name} (priority {priority}) has the resource at {env.now}')
              yield env.timeout(5)
      
      env.process(process(env, resource, 'Low priority', priority=10))
      env.process(process(env, resource, 'High priority', priority=1))
      env.run()
      ```
      
      **Use cases:**
      - Emergency services (ambulances before regular vehicles)
      - VIP customer queues
      - Job scheduling with priorities
      
      ### PreemptiveResource
      
      Allows high-priority requests to interrupt lower-priority users.
      
      ```python
      import simpy
      
      env = simpy.Environment()
      resource = simpy.PreemptiveResource(env, capacity=1)
      
      def process(env, resource, name, priority):
          with resource.request(priority=priority) as req:
              try:
                  yield req
                  print(f'{name} acquired resource at {env.now}')
                  yield env.timeout(10)
                  print(f'{name} finished at {env.now}')
              except simpy.Interrupt:
                  print(f'{name} was preempted at {env.now}')
      
      env.process(process(env, resource, 'Low priority', priority=10))
      env.process(process(env, resource, 'High priority', priority=1))
      env.run()
      ```
      
      **Use cases:**
      - Operating system CPU scheduling
      - Emergency room triage
      - Network packet prioritization
      
      ## 2. Containers
      
      Model production and consumption of homogeneous bulk materials (continuous or discrete).
      
      ```python
      import simpy
      
      env = simpy.Environment()
      container = simpy.Container(env, capacity=100, init=50)
      
      def producer(env, container):
          while True:
              yield env.timeout(5)
              yield container.put(20)
              print(f'Produced 20. Level: {container.level}')
      
      def consumer(env, container):
          while True:
              yield env.timeout(7)
              yield container.get(15)
              print(f'Consumed 15. Level: {container.level}')
      
      env.process(producer(env, container))
      env.process(consumer(env, container))
      env.run(until=50)
      ```
      
      **Key properties:**
      - `capacity` - Maximum amount (default: float('inf'))
      - `level` - Current amount
      - `init` - Initial amount (default: 0)
      
      **Operations:**
      - `put(amount)` - Add to container (blocks if full)
      - `get(amount)` - Remove from container (blocks if insufficient)
      
      **Use cases:**
      - Gas station fuel tanks
      - Buffer storage in manufacturing
      - Water reservoirs
      - Battery charge levels
      
      ## 3. Stores
      
      Model production and consumption of Python objects.
      
      ### Store (Basic)
      
      Generic FIFO object storage.
      
      ```python
      import simpy
      
      env = simpy.Environment()
      store = simpy.Store(env, capacity=2)
      
      def producer(env, store):
          for i in range(5):
              yield env.timeout(2)
              item = f'Item {i}'
              yield store.put(item)
              print(f'Produced {item} at {env.now}')
      
      def consumer(env, store):
          while True:
              yield env.timeout(3)
              item = yield store.get()
              print(f'Consumed {item} at {env.now}')
      
      env.process(producer(env, store))
      env.process(consumer(env, store))
      env.run()
      ```
      
      **Key properties:**
      - `capacity` - Maximum number of items (default: float('inf'))
      - `items` - List of stored items
      
      **Operations:**
      - `put(item)` - Add item to store (blocks if full)
      - `get()` - Remove and return item (blocks if empty)
      
      ### FilterStore
      
      Allows retrieval of specific objects based on filter functions.
      
      ```python
      import simpy
      
      env = simpy.Environment()
      store = simpy.FilterStore(env, capacity=10)
      
      def producer(env, store):
          for color in ['red', 'blue', 'green', 'red', 'blue']:
              yield env.timeout(1)
              yield store.put({'color': color, 'time': env.now})
              print(f'Produced {color} item at {env.now}')
      
      def consumer(env, store, color):
          while True:
              yield env.timeout(2)
              item = yield store.get(lambda x: x['color'] == color)
              print(f'{color} consumer got item from {item["time"]} at {env.now}')
      
      env.process(producer(env, store))
      env.process(consumer(env, store, 'red'))
      env.process(consumer(env, store, 'blue'))
      env.run(until=15)
      ```
      
      **Use cases:**
      - Warehouse item picking (specific SKUs)
      - Job queues with skill matching
      - Packet routing by destination
      
      ### PriorityStore
      
      Items retrieved in priority order (lowest first).
      
      ```python
      import simpy
      
      class PriorityItem:
          def __init__(self, priority, data):
              self.priority = priority
              self.data = data
      
          def __lt__(self, other):
              return self.priority < other.priority
      
      env = simpy.Environment()
      store = simpy.PriorityStore(env, capacity=10)
      
      def producer(env, store):
          items = [(10, 'Low'), (1, 'High'), (5, 'Medium')]
          for priority, name in items:
              yield env.timeout(1)
              yield store.put(PriorityItem(priority, name))
              print(f'Produced {name} priority item')
      
      def consumer(env, store):
          while True:
              yield env.timeout(5)
              item = yield store.get()
              print(f'Retrieved {item.data} priority item')
      
      env.process(producer(env, store))
      env.process(consumer(env, store))
      env.run()
      ```
      
      **Use cases:**
      - Task scheduling
      - Print job queues
      - Message prioritization
      
      ## Choosing the Right Resource Type
      
      | Scenario | Resource Type |
      |----------|---------------|
      | Limited servers/machines | Resource |
      | Priority-based queuing | PriorityResource |
      | Preemptive scheduling | PreemptiveResource |
      | Fuel, water, bulk materials | Container |
      | Generic item queue (FIFO) | Store |
      | Selective item retrieval | FilterStore |
      | Priority-ordered items | PriorityStore |
      
      ## Best Practices
      
      1. **Capacity planning**: Set realistic capacities based on system constraints
      2. **Request patterns**: Use context managers (`with resource.request()`) for automatic cleanup
      3. **Error handling**: Wrap preemptive resources in try-except for Interrupt handling
      4. **Monitoring**: Track queue lengths and utilization (see monitoring.md)
      5. **Performance**: FilterStore and PriorityStore have O(n) retrieval time; use wisely for large stores
      
  • scripts
    • basic_simulation_template.py 5.6 KB
      #!/usr/bin/env python3
      """
      Basic SimPy Simulation Template
      
      This template provides a starting point for building SimPy simulations.
      Customize the process functions and parameters for your specific use case.
      """
      
      import simpy
      import random
      
      
      class SimulationConfig:
          """Configuration parameters for the simulation."""
      
          def __init__(self):
              self.random_seed = 42
              self.num_resources = 2
              self.num_processes = 10
              self.sim_time = 100
              self.mean_interarrival_time = 5.0  # Mean time between arrivals (NOT a rate)
              self.service_time_mean = 3.0  # Average service time
              self.service_time_std = 1.0  # Service time standard deviation
      
      
      class SimulationStats:
          """Collect and report simulation statistics."""
      
          def __init__(self):
              self.arrival_times = []
              self.service_start_times = []
              self.departure_times = []
              self.wait_times = []
              self.service_times = []
      
          def record_arrival(self, time):
              self.arrival_times.append(time)
      
          def record_service_start(self, time):
              self.service_start_times.append(time)
      
          def record_departure(self, time):
              self.departure_times.append(time)
      
          def record_wait_time(self, wait_time):
              self.wait_times.append(wait_time)
      
          def record_service_time(self, service_time):
              self.service_times.append(service_time)
      
          def report(self):
              print("\n" + "=" * 50)
              print("SIMULATION STATISTICS")
              print("=" * 50)
      
              if self.wait_times:
                  print(f"Total customers: {len(self.wait_times)}")
                  print(f"Average wait time: {sum(self.wait_times) / len(self.wait_times):.2f}")
                  print(f"Max wait time: {max(self.wait_times):.2f}")
                  print(f"Min wait time: {min(self.wait_times):.2f}")
      
              if self.service_times:
                  print(f"Average service time: {sum(self.service_times) / len(self.service_times):.2f}")
      
              if self.arrival_times and self.departure_times:
                  throughput = len(self.departure_times) / max(self.departure_times)
                  print(f"Throughput: {throughput:.2f} customers/time unit")
      
              print("=" * 50)
      
      
      def customer_process(env, name, resource, stats, config):
          """
          Simulate a customer process.
      
          Args:
              env: SimPy environment
              name: Customer identifier
              resource: Shared resource (e.g., server, machine)
              stats: Statistics collector
              config: Simulation configuration
          """
          # Record arrival
          arrival_time = env.now
          stats.record_arrival(arrival_time)
          print(f"{name} arrived at {arrival_time:.2f}")
      
          # Request resource
          with resource.request() as request:
              yield request
      
              # Record service start and calculate wait time
              service_start = env.now
              wait_time = service_start - arrival_time
              stats.record_service_start(service_start)
              stats.record_wait_time(wait_time)
              print(f"{name} started service at {service_start:.2f} (waited {wait_time:.2f})")
      
              # Service time (normally distributed)
              service_time = max(0.1, random.gauss(
                  config.service_time_mean,
                  config.service_time_std
              ))
              stats.record_service_time(service_time)
      
              yield env.timeout(service_time)
      
              # Record departure
              departure_time = env.now
              stats.record_departure(departure_time)
              print(f"{name} departed at {departure_time:.2f}")
      
      
      def customer_generator(env, resource, stats, config):
          """
          Generate customers arriving at random intervals.
      
          Args:
              env: SimPy environment
              resource: Shared resource
              stats: Statistics collector
              config: Simulation configuration
          """
          customer_count = 0
      
          while True:
              # Wait for next customer arrival (exponential inter-arrival times).
              # expovariate takes the rate lambda = 1 / mean, so a Poisson arrival
              # process has mean inter-arrival time config.mean_interarrival_time.
              inter_arrival_time = random.expovariate(1.0 / config.mean_interarrival_time)
              yield env.timeout(inter_arrival_time)
      
              # Create new customer process
              customer_count += 1
              customer_name = f"Customer {customer_count}"
              env.process(customer_process(env, customer_name, resource, stats, config))
      
      
      def run_simulation(config):
          """
          Run the simulation with given configuration.
      
          Args:
              config: SimulationConfig object with simulation parameters
      
          Returns:
              SimulationStats object with collected statistics
          """
          # Set random seed for reproducibility
          random.seed(config.random_seed)
      
          # Create environment
          env = simpy.Environment()
      
          # Create shared resource
          resource = simpy.Resource(env, capacity=config.num_resources)
      
          # Create statistics collector
          stats = SimulationStats()
      
          # Start customer generator
          env.process(customer_generator(env, resource, stats, config))
      
          # Run simulation
          print(f"Starting simulation for {config.sim_time} time units...")
          print(f"Resources: {config.num_resources}")
          print(f"Mean inter-arrival time: {config.mean_interarrival_time:.2f}")
          print(f"Average service time: {config.service_time_mean:.2f}")
          print("-" * 50)
      
          env.run(until=config.sim_time)
      
          return stats
      
      
      def main():
          """Main function to run the simulation."""
          # Create configuration
          config = SimulationConfig()
      
          # Customize configuration if needed
          config.num_resources = 2
          config.sim_time = 50
          config.mean_interarrival_time = 2.0
          config.service_time_mean = 3.0
      
          # Run simulation
          stats = run_simulation(config)
      
          # Report statistics
          stats.report()
      
      
      if __name__ == "__main__":
          main()
      
    • resource_monitor.py 11.4 KB
      #!/usr/bin/env python3
      """
      SimPy Resource Monitoring Utilities
      
      This module provides reusable classes and functions for monitoring
      SimPy resources during simulation. Includes utilities for tracking
      queue lengths, utilization, wait times, and generating reports.
      """
      
      import simpy
      from typing import List, Tuple, Dict, Any
      
      
      class ResourceMonitor:
          """
          Monitor resource usage with detailed statistics tracking.
      
          Tracks:
          - Queue lengths over time
          - Resource utilization
          - Wait times for requests
          - Request and release events
          """
      
          def __init__(self, env: simpy.Environment, resource: simpy.Resource, name: str = "Resource"):
              """
              Initialize the resource monitor.
      
              Args:
                  env: SimPy environment
                  resource: Resource to monitor
                  name: Name for the resource (for reporting)
              """
              self.env = env
              self.resource = resource
              self.name = name
      
              # Data storage
              self.queue_data: List[Tuple[float, int]] = [(0, 0)]
              self.utilization_data: List[Tuple[float, float]] = [(0, 0.0)]
              self.request_times: Dict[Any, float] = {}
              self.wait_times: List[float] = []
              self.events: List[Tuple[float, str, Dict]] = []
      
              # Patch the resource
              self._patch_resource()
      
          def _patch_resource(self):
              """Patch resource methods to intercept requests and releases."""
              original_request = self.resource.request
              original_release = self.resource.release
      
              def monitored_request(*args, **kwargs):
                  req = original_request(*args, **kwargs)
      
                  # Record request event
                  queue_length = len(self.resource.queue)
                  utilization = self.resource.count / self.resource.capacity
      
                  self.queue_data.append((self.env.now, queue_length))
                  self.utilization_data.append((self.env.now, utilization))
                  self.events.append((self.env.now, 'request', {
                      'queue_length': queue_length,
                      'utilization': utilization
                  }))
      
                  # Store request time for wait time calculation
                  self.request_times[req] = self.env.now
      
                  # Add callback to record when request is granted
                  def on_granted(event):
                      if req in self.request_times:
                          wait_time = self.env.now - self.request_times[req]
                          self.wait_times.append(wait_time)
                          del self.request_times[req]
      
                  req.callbacks.append(on_granted)
                  return req
      
              def monitored_release(*args, **kwargs):
                  result = original_release(*args, **kwargs)
      
                  # Record release event
                  queue_length = len(self.resource.queue)
                  utilization = self.resource.count / self.resource.capacity
      
                  self.queue_data.append((self.env.now, queue_length))
                  self.utilization_data.append((self.env.now, utilization))
                  self.events.append((self.env.now, 'release', {
                      'queue_length': queue_length,
                      'utilization': utilization
                  }))
      
                  return result
      
              self.resource.request = monitored_request
              self.resource.release = monitored_release
      
          def average_queue_length(self) -> float:
              """Calculate time-weighted average queue length."""
              if len(self.queue_data) < 2:
                  return 0.0
      
              total_time = 0.0
              weighted_sum = 0.0
      
              for i in range(len(self.queue_data) - 1):
                  time1, length1 = self.queue_data[i]
                  time2, length2 = self.queue_data[i + 1]
                  duration = time2 - time1
                  total_time += duration
                  weighted_sum += length1 * duration
      
              return weighted_sum / total_time if total_time > 0 else 0.0
      
          def average_utilization(self) -> float:
              """Calculate time-weighted average utilization."""
              if len(self.utilization_data) < 2:
                  return 0.0
      
              total_time = 0.0
              weighted_sum = 0.0
      
              for i in range(len(self.utilization_data) - 1):
                  time1, util1 = self.utilization_data[i]
                  time2, util2 = self.utilization_data[i + 1]
                  duration = time2 - time1
                  total_time += duration
                  weighted_sum += util1 * duration
      
              return weighted_sum / total_time if total_time > 0 else 0.0
      
          def average_wait_time(self) -> float:
              """Calculate average wait time for requests."""
              return sum(self.wait_times) / len(self.wait_times) if self.wait_times else 0.0
      
          def max_queue_length(self) -> int:
              """Get maximum queue length observed."""
              return max(length for _, length in self.queue_data) if self.queue_data else 0
      
          def report(self):
              """Print detailed statistics report."""
              print(f"\n{'=' * 60}")
              print(f"RESOURCE MONITOR REPORT: {self.name}")
              print(f"{'=' * 60}")
              print(f"Simulation time: 0.00 to {self.env.now:.2f}")
              print(f"Capacity: {self.resource.capacity}")
              print(f"\nUtilization:")
              print(f"  Average: {self.average_utilization():.2%}")
              print(f"  Final: {self.resource.count / self.resource.capacity:.2%}")
              print(f"\nQueue Statistics:")
              print(f"  Average length: {self.average_queue_length():.2f}")
              print(f"  Max length: {self.max_queue_length()}")
              print(f"  Final length: {len(self.resource.queue)}")
              print(f"\nWait Time Statistics:")
              print(f"  Total requests: {len(self.wait_times)}")
              if self.wait_times:
                  print(f"  Average wait: {self.average_wait_time():.2f}")
                  print(f"  Max wait: {max(self.wait_times):.2f}")
                  print(f"  Min wait: {min(self.wait_times):.2f}")
              print(f"\nEvent Summary:")
              print(f"  Total events: {len(self.events)}")
              request_count = sum(1 for _, event_type, _ in self.events if event_type == 'request')
              release_count = sum(1 for _, event_type, _ in self.events if event_type == 'release')
              print(f"  Requests: {request_count}")
              print(f"  Releases: {release_count}")
              print(f"{'=' * 60}")
      
          def export_csv(self, filename: str):
              """
              Export monitoring data to CSV file.
      
              Args:
                  filename: Output CSV filename
              """
              import csv
      
              with open(filename, 'w', newline='') as f:
                  writer = csv.writer(f)
                  writer.writerow(['Time', 'Event', 'Queue Length', 'Utilization'])
      
                  for time, event_type, data in self.events:
                      writer.writerow([
                          time,
                          event_type,
                          data['queue_length'],
                          data['utilization']
                      ])
      
              print(f"Data exported to {filename}")
      
      
      class MultiResourceMonitor:
          """Monitor multiple resources simultaneously."""
      
          def __init__(self, env: simpy.Environment):
              """
              Initialize multi-resource monitor.
      
              Args:
                  env: SimPy environment
              """
              self.env = env
              self.monitors: Dict[str, ResourceMonitor] = {}
      
          def add_resource(self, resource: simpy.Resource, name: str):
              """
              Add a resource to monitor.
      
              Args:
                  resource: SimPy resource to monitor
                  name: Name for the resource
              """
              monitor = ResourceMonitor(self.env, resource, name)
              self.monitors[name] = monitor
              return monitor
      
          def report_all(self):
              """Generate reports for all monitored resources."""
              for name, monitor in self.monitors.items():
                  monitor.report()
      
          def summary(self):
              """Print summary statistics for all resources."""
              print(f"\n{'=' * 60}")
              print("MULTI-RESOURCE SUMMARY")
              print(f"{'=' * 60}")
              print(f"{'Resource':<20} {'Avg Util':<12} {'Avg Queue':<12} {'Avg Wait':<12}")
              print(f"{'-' * 20} {'-' * 12} {'-' * 12} {'-' * 12}")
      
              for name, monitor in self.monitors.items():
                  print(f"{name:<20} {monitor.average_utilization():<12.2%} "
                        f"{monitor.average_queue_length():<12.2f} "
                        f"{monitor.average_wait_time():<12.2f}")
      
              print(f"{'=' * 60}")
      
      
      class ContainerMonitor:
          """Monitor Container resources (for tracking level changes)."""
      
          def __init__(self, env: simpy.Environment, container: simpy.Container, name: str = "Container"):
              """
              Initialize container monitor.
      
              Args:
                  env: SimPy environment
                  container: Container to monitor
                  name: Name for the container
              """
              self.env = env
              self.container = container
              self.name = name
              self.level_data: List[Tuple[float, float]] = [(0, container.level)]
      
              self._patch_container()
      
          def _patch_container(self):
              """Patch container methods to track level changes."""
              original_put = self.container.put
              original_get = self.container.get
      
              def monitored_put(amount):
                  result = original_put(amount)
      
                  def on_put(event):
                      self.level_data.append((self.env.now, self.container.level))
      
                  result.callbacks.append(on_put)
                  return result
      
              def monitored_get(amount):
                  result = original_get(amount)
      
                  def on_get(event):
                      self.level_data.append((self.env.now, self.container.level))
      
                  result.callbacks.append(on_get)
                  return result
      
              self.container.put = monitored_put
              self.container.get = monitored_get
      
          def average_level(self) -> float:
              """Calculate time-weighted average level."""
              if len(self.level_data) < 2:
                  return self.level_data[0][1] if self.level_data else 0.0
      
              total_time = 0.0
              weighted_sum = 0.0
      
              for i in range(len(self.level_data) - 1):
                  time1, level1 = self.level_data[i]
                  time2, level2 = self.level_data[i + 1]
                  duration = time2 - time1
                  total_time += duration
                  weighted_sum += level1 * duration
      
              return weighted_sum / total_time if total_time > 0 else 0.0
      
          def report(self):
              """Print container statistics."""
              print(f"\n{'=' * 60}")
              print(f"CONTAINER MONITOR REPORT: {self.name}")
              print(f"{'=' * 60}")
              print(f"Capacity: {self.container.capacity}")
              print(f"Current level: {self.container.level:.2f}")
              print(f"Average level: {self.average_level():.2f}")
              print(f"Utilization: {self.average_level() / self.container.capacity:.2%}")
      
              if self.level_data:
                  levels = [level for _, level in self.level_data]
                  print(f"Max level: {max(levels):.2f}")
                  print(f"Min level: {min(levels):.2f}")
      
              print(f"{'=' * 60}")
      
      
      # Example usage
      if __name__ == "__main__":
          def example_process(env, name, resource, duration):
              """Example process using a resource."""
              with resource.request() as req:
                  yield req
                  print(f"{name} started at {env.now}")
                  yield env.timeout(duration)
                  print(f"{name} finished at {env.now}")
      
          # Create environment and resource
          env = simpy.Environment()
          resource = simpy.Resource(env, capacity=2)
      
          # Create monitor
          monitor = ResourceMonitor(env, resource, "Example Resource")
      
          # Start processes
          for i in range(5):
              env.process(example_process(env, f"Process {i}", resource, 3 + i))
      
          # Run simulation
          env.run()
      
          # Generate report
          monitor.report()
      
  • SKILL.md 13 KB
    ---
    name: alterlab-simpy
    description: Process-based discrete-event simulation in Python with SimPy — processes, queues, shared resources, and time-based events. Use when simulating systems where entities contend for shared resources over time, such as manufacturing systems, service operations, network traffic, or logistics. Part of the AlterLab Academic Skills suite.
    license: MIT
    allowed-tools: Read Write Edit Bash(python:*) Bash(uv:*)
    compatibility: No API key required. Runs locally via `uv run python`; requires simpy 4.x (current 4.1.2 as of 2026-09; pure Python, no other dependencies).
    metadata:
        skill-author: AlterLab
        version: "1.0.1"
        last_updated: "2026-09-23"
    ---
    
    # SimPy - Discrete-Event Simulation
    
    ## Overview
    
    SimPy is a process-based discrete-event simulation framework based on standard Python. Use SimPy to model systems where entities (customers, vehicles, packets, etc.) interact with each other and compete for shared resources (servers, machines, bandwidth, etc.) over time.
    
    **Core capabilities:**
    - Process modeling using Python generator functions
    - Shared resource management (servers, containers, stores)
    - Event-driven scheduling and synchronization
    - Real-time simulations synchronized with wall-clock time
    - Comprehensive monitoring and data collection
    
    ## When to Use This Skill
    
    Use the SimPy skill when:
    
    1. **Modeling discrete-event systems** - Systems where events occur at irregular intervals
    2. **Resource contention** - Entities compete for limited resources (servers, machines, staff)
    3. **Queue analysis** - Studying waiting lines, service times, and throughput
    4. **Process optimization** - Analyzing manufacturing, logistics, or service processes
    5. **Network simulation** - Packet routing, bandwidth allocation, latency analysis
    6. **Capacity planning** - Determining optimal resource levels for desired performance
    7. **System validation** - Testing system behavior before implementation
    
    **Not suitable for:**
    - Continuous simulations with fixed time steps (consider SciPy ODE solvers)
    - Independent processes without resource sharing
    - Pure mathematical optimization (consider SciPy optimize)
    
    ### Does NOT Trigger
    
    | Scenario | Use Instead |
    |----------|-------------|
    | Agent-based models of social systems with heterogeneous agents on grids or networks (segregation, opinion dynamics, diffusion) | `alterlab-abm-mesa` |
    | Searching for Pareto-optimal designs or parameters (e.g. cost vs. waiting time) with evolutionary algorithms | `alterlab-pymoo` |
    | Fitting time-series or regression models to observed queue or throughput data | `alterlab-statsmodels` |
    | Training a reinforcement-learning agent to control a system | `alterlab-stable-baselines3` |
    
    ## Quick Start
    
    ### Basic Simulation Structure
    
    ```python
    import simpy
    
    def process(env, name):
        """A simple process that waits and prints."""
        print(f'{name} starting at {env.now}')
        yield env.timeout(5)
        print(f'{name} finishing at {env.now}')
    
    # Create environment
    env = simpy.Environment()
    
    # Start processes
    env.process(process(env, 'Process 1'))
    env.process(process(env, 'Process 2'))
    
    # Run simulation
    env.run(until=10)
    ```
    
    ### Resource Usage Pattern
    
    ```python
    import simpy
    
    def customer(env, name, resource):
        """Customer requests resource, uses it, then releases."""
        with resource.request() as req:
            yield req  # Wait for resource
            print(f'{name} got resource at {env.now}')
            yield env.timeout(3)  # Use resource
            print(f'{name} released resource at {env.now}')
    
    env = simpy.Environment()
    server = simpy.Resource(env, capacity=1)
    
    env.process(customer(env, 'Customer 1', server))
    env.process(customer(env, 'Customer 2', server))
    env.run()
    ```
    
    ## Core Concepts
    
    ### 1. Environment
    
    The simulation environment manages time and schedules events.
    
    ```python
    import simpy
    
    # Standard environment (runs as fast as possible)
    env = simpy.Environment(initial_time=0)
    
    # Real-time environment (synchronized with wall-clock)
    import simpy.rt
    env_rt = simpy.rt.RealtimeEnvironment(factor=1.0)
    
    # Run simulation
    env.run(until=100)  # Run until time 100
    env.run()  # Run until no events remain
    ```
    
    ### 2. Processes
    
    Processes are defined using Python generator functions (functions with `yield` statements).
    
    ```python
    def my_process(env, param1, param2):
        """Process that yields events to pause execution."""
        print(f'Starting at {env.now}')
    
        # Wait for time to pass
        yield env.timeout(5)
    
        print(f'Resumed at {env.now}')
    
        # Wait for another event
        yield env.timeout(3)
    
        print(f'Done at {env.now}')
        return 'result'
    
    # Start the process
    env.process(my_process(env, 'value1', 'value2'))
    ```
    
    ### 3. Events
    
    Events are the fundamental mechanism for process synchronization. Processes yield events and resume when those events are triggered.
    
    **Common event types:**
    - `env.timeout(delay)` - Wait for time to pass
    - `resource.request()` - Request a resource
    - `env.event()` - Create a custom event
    - `env.process(func())` - Process as an event
    - `event1 & event2` - Wait for all events (AllOf)
    - `event1 | event2` - Wait for any event (AnyOf)
    
    ## Resources
    
    SimPy provides several resource types for different scenarios. For comprehensive details, see `references/resources.md`.
    
    ### Resource Types Summary
    
    | Resource Type | Use Case |
    |---------------|----------|
    | Resource | Limited capacity (servers, machines) |
    | PriorityResource | Priority-based queuing |
    | PreemptiveResource | High-priority can interrupt low-priority |
    | Container | Bulk materials (fuel, water) |
    | Store | Python object storage (FIFO) |
    | FilterStore | Selective item retrieval |
    | PriorityStore | Priority-ordered items |
    
    ### Quick Reference
    
    ```python
    import simpy
    
    env = simpy.Environment()
    
    # Basic resource (e.g., servers)
    resource = simpy.Resource(env, capacity=2)
    
    # Priority resource
    priority_resource = simpy.PriorityResource(env, capacity=1)
    
    # Container (e.g., fuel tank)
    fuel_tank = simpy.Container(env, capacity=100, init=50)
    
    # Store (e.g., warehouse)
    warehouse = simpy.Store(env, capacity=10)
    ```
    
    ## Common Simulation Patterns
    
    ### Pattern 1: Customer-Server Queue
    
    ```python
    import simpy
    import random
    
    def customer(env, name, server):
        arrival = env.now
        with server.request() as req:
            yield req
            wait = env.now - arrival
            print(f'{name} waited {wait:.2f}, served at {env.now}')
            yield env.timeout(random.uniform(2, 4))
    
    def customer_generator(env, server):
        i = 0
        while True:
            yield env.timeout(random.uniform(1, 3))
            i += 1
            env.process(customer(env, f'Customer {i}', server))
    
    env = simpy.Environment()
    server = simpy.Resource(env, capacity=2)
    env.process(customer_generator(env, server))
    env.run(until=20)
    ```
    
    ### Pattern 2: Producer-Consumer
    
    ```python
    import simpy
    
    def producer(env, store):
        item_id = 0
        while True:
            yield env.timeout(2)
            item = f'Item {item_id}'
            yield store.put(item)
            print(f'Produced {item} at {env.now}')
            item_id += 1
    
    def consumer(env, store):
        while True:
            item = yield store.get()
            print(f'Consumed {item} at {env.now}')
            yield env.timeout(3)
    
    env = simpy.Environment()
    store = simpy.Store(env, capacity=10)
    env.process(producer(env, store))
    env.process(consumer(env, store))
    env.run(until=20)
    ```
    
    ### Pattern 3: Parallel Task Execution
    
    ```python
    import simpy
    
    def task(env, name, duration):
        print(f'{name} starting at {env.now}')
        yield env.timeout(duration)
        print(f'{name} done at {env.now}')
        return f'{name} result'
    
    def coordinator(env):
        # Start tasks in parallel
        task1 = env.process(task(env, 'Task 1', 5))
        task2 = env.process(task(env, 'Task 2', 3))
        task3 = env.process(task(env, 'Task 3', 4))
    
        # Wait for all to complete
        results = yield task1 & task2 & task3
        print(f'All done at {env.now}')
    
    env = simpy.Environment()
    env.process(coordinator(env))
    env.run()
    ```
    
    ## Workflow Guide
    
    ### Step 1: Define the System
    
    Identify:
    - **Entities**: What moves through the system? (customers, parts, packets)
    - **Resources**: What are the constraints? (servers, machines, bandwidth)
    - **Processes**: What are the activities? (arrival, service, departure)
    - **Metrics**: What to measure? (wait times, utilization, throughput)
    
    ### Step 2: Implement Process Functions
    
    Create generator functions for each process type:
    
    ```python
    def entity_process(env, name, resources, parameters):
        # Arrival logic
        arrival_time = env.now
    
        # Request resources
        with resource.request() as req:
            yield req
    
            # Service logic
            service_time = calculate_service_time(parameters)
            yield env.timeout(service_time)
    
        # Departure logic
        collect_statistics(env.now - arrival_time)
    ```
    
    ### Step 3: Set Up Monitoring
    
    Use monitoring utilities to collect data. See `references/monitoring.md` for comprehensive techniques.
    
    ```python
    from scripts.resource_monitor import ResourceMonitor
    
    # Create and monitor resource
    resource = simpy.Resource(env, capacity=2)
    monitor = ResourceMonitor(env, resource, "Server")
    
    # After simulation
    monitor.report()
    ```
    
    ### Step 4: Run and Analyze
    
    ```python
    # Run simulation
    env.run(until=simulation_time)
    
    # Generate reports
    monitor.report()
    stats.report()
    
    # Export data for further analysis
    monitor.export_csv('results.csv')
    ```
    
    ## Advanced Features
    
    ### Process Interaction
    
    Processes can interact through events, process yields, and interrupts. See `references/process-interaction.md` for detailed patterns.
    
    **Key mechanisms:**
    - **Event signaling**: Shared events for coordination
    - **Process yields**: Wait for other processes to complete
    - **Interrupts**: Forcefully resume processes for preemption
    
    ### Real-Time Simulations
    
    Synchronize simulation with wall-clock time for hardware-in-the-loop or interactive applications. See `references/real-time.md`.
    
    ```python
    import simpy.rt
    
    env = simpy.rt.RealtimeEnvironment(factor=1.0)  # 1:1 time mapping
    # factor=0.5 means 1 sim unit = 0.5 seconds (2x faster)
    ```
    
    ### Comprehensive Monitoring
    
    Monitor processes, resources, and events. See `references/monitoring.md` for techniques including:
    - State variable tracking
    - Resource monkey-patching
    - Event tracing
    - Statistical collection
    
    ## Scripts and Templates
    
    ### basic_simulation_template.py
    
    Complete template for building queue simulations with:
    - Configurable parameters
    - Statistics collection
    - Customer generation
    - Resource usage
    - Report generation
    
    **Usage:**
    ```python
    from scripts.basic_simulation_template import SimulationConfig, run_simulation
    
    config = SimulationConfig()
    config.num_resources = 2
    config.sim_time = 100
    stats = run_simulation(config)
    stats.report()
    ```
    
    ### resource_monitor.py
    
    Reusable monitoring utilities:
    - `ResourceMonitor` - Track single resource
    - `MultiResourceMonitor` - Monitor multiple resources
    - `ContainerMonitor` - Track container levels
    - Automatic statistics calculation
    - CSV export functionality
    
    **Usage:**
    ```python
    from scripts.resource_monitor import ResourceMonitor
    
    monitor = ResourceMonitor(env, resource, "My Resource")
    # ... run simulation ...
    monitor.report()
    monitor.export_csv('data.csv')
    ```
    
    **Import note:** these `from scripts....` paths are relative to the skill directory. Copy the script next to your simulation, or add the skill directory to `PYTHONPATH` before importing.
    
    ## Reference Documentation
    
    Detailed guides, loaded on demand:
    
    - **`references/resources.md`** - All resource types with examples
    - **`references/events.md`** - Event system and patterns
    - **`references/process-interaction.md`** - Process synchronization (signals, yields, interrupts)
    - **`references/monitoring.md`** - Data collection techniques
    - **`references/real-time.md`** - Real-time simulation setup
    
    ## Best Practices
    
    1. **Generator functions**: Always use `yield` in process functions
    2. **Resource context managers**: Use `with resource.request() as req:` for automatic cleanup
    3. **Reproducibility**: Set `random.seed()` for consistent results
    4. **Monitoring**: Collect data throughout simulation, not just at the end
    5. **Validation**: Compare simple cases with analytical solutions
    6. **Documentation**: Comment process logic and parameter choices
    7. **Modular design**: Separate process logic, statistics, and configuration
    
    ## Common Pitfalls
    
    1. **Forgetting yield**: Processes must yield events to pause
    2. **Event reuse**: Events can only be triggered once
    3. **Resource leaks**: Use context managers or ensure release
    4. **Blocking operations**: Avoid Python blocking calls in processes
    5. **Time units**: Stay consistent with time unit interpretation
    6. **Deadlocks**: Ensure at least one process can make progress
    
    ## Example Use Cases
    
    - **Manufacturing**: Machine scheduling, production lines, inventory management
    - **Healthcare**: Emergency room simulation, patient flow, staff allocation
    - **Telecommunications**: Network traffic, packet routing, bandwidth allocation
    - **Transportation**: Traffic flow, logistics, vehicle routing
    - **Service operations**: Call centers, retail checkout, appointment scheduling
    - **Computer systems**: CPU scheduling, memory management, I/O operations
    
    Part of the AlterLab Academic Skills suite.
    
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related