Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 08:30:10 +08:00
commit f0bd18fb4e
824 changed files with 331919 additions and 0 deletions

View 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

View 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

View 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

View 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

View 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