Initial commit
This commit is contained in:
423
skills/simpy/SKILL.md
Normal file
423
skills/simpy/SKILL.md
Normal file
@@ -0,0 +1,423 @@
|
||||
---
|
||||
name: simpy
|
||||
description: Process-based discrete-event simulation framework in Python. Use this skill when building simulations of systems with processes, queues, resources, and time-based events such as manufacturing systems, service operations, network traffic, logistics, or any system where entities interact with shared resources over time.
|
||||
---
|
||||
|
||||
# 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)
|
||||
|
||||
## 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')
|
||||
```
|
||||
|
||||
## Reference Documentation
|
||||
|
||||
Detailed guides for specific topics:
|
||||
|
||||
- **`references/resources.md`** - All resource types with examples
|
||||
- **`references/events.md`** - Event system and patterns
|
||||
- **`references/process-interaction.md`** - Process synchronization
|
||||
- **`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
|
||||
374
skills/simpy/references/events.md
Normal file
374
skills/simpy/references/events.md
Normal file
@@ -0,0 +1,374 @@
|
||||
# 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
|
||||
475
skills/simpy/references/monitoring.md
Normal file
475
skills/simpy/references/monitoring.md
Normal file
@@ -0,0 +1,475 @@
|
||||
# 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
|
||||
424
skills/simpy/references/process-interaction.md
Normal file
424
skills/simpy/references/process-interaction.md
Normal file
@@ -0,0 +1,424 @@
|
||||
# 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
|
||||
395
skills/simpy/references/real-time.md
Normal file
395
skills/simpy/references/real-time.md
Normal file
@@ -0,0 +1,395 @@
|
||||
# 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
|
||||
275
skills/simpy/references/resources.md
Normal file
275
skills/simpy/references/resources.md
Normal file
@@ -0,0 +1,275 @@
|
||||
# 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
|
||||
193
skills/simpy/scripts/basic_simulation_template.py
Normal file
193
skills/simpy/scripts/basic_simulation_template.py
Normal file
@@ -0,0 +1,193 @@
|
||||
#!/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.arrival_rate = 5.0 # Average time between arrivals
|
||||
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 distribution)
|
||||
inter_arrival_time = random.expovariate(1.0 / config.arrival_rate)
|
||||
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"Average arrival rate: {config.arrival_rate:.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.arrival_rate = 2.0
|
||||
config.service_time_mean = 3.0
|
||||
|
||||
# Run simulation
|
||||
stats = run_simulation(config)
|
||||
|
||||
# Report statistics
|
||||
stats.report()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
345
skills/simpy/scripts/resource_monitor.py
Normal file
345
skills/simpy/scripts/resource_monitor.py
Normal file
@@ -0,0 +1,345 @@
|
||||
#!/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 collections import defaultdict
|
||||
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()
|
||||
Reference in New Issue
Block a user