Initial commit
This commit is contained in:
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