346 lines
11 KiB
Python
346 lines
11 KiB
Python
#!/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()
|