Debugging SimPy: Finding What's Wrong
Something's broken. You need to find it. Here's how.
The Print Statement Method
Start simple:
def customer(env, cid, server):
print(f"[{env.now:.2f}] Customer {cid} arrives")
with server.request() as req:
print(f"[{env.now:.2f}] Customer {cid} requests server")
yield req
print(f"[{env.now:.2f}] Customer {cid} gets server")
service_time = random.expovariate(1/5)
print(f"[{env.now:.2f}] Customer {cid} service time: {service_time:.2f}")
yield env.timeout(service_time)
print(f"[{env.now:.2f}] Customer {cid} leaves")
Output tells the story:
[0.00] Customer 0 arrives
[0.00] Customer 0 requests server
[0.00] Customer 0 gets server
[3.21] Customer 1 arrives
[3.21] Customer 1 requests server
[4.87] Customer 0 leaves
[4.87] Customer 1 gets server
Structured Logging
Better than print:
import logging
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger('simulation')
def customer(env, cid, server):
logger.debug(f"[{env.now:.2f}] Customer {cid} arrives")
with server.request() as req:
logger.debug(f"[{env.now:.2f}] Customer {cid} queuing")
yield req
logger.info(f"[{env.now:.2f}] Customer {cid} served")
yield env.timeout(5)
logger.debug(f"[{env.now:.2f}] Customer {cid} complete")
Control verbosity:
# Show everything
logging.getLogger('simulation').setLevel(logging.DEBUG)
# Show important stuff only
logging.getLogger('simulation').setLevel(logging.INFO)
# Silence
logging.getLogger('simulation').setLevel(logging.WARNING)
Event Tracer
Wrap the environment to trace all events:
class TracedEnvironment(simpy.Environment):
def step(self):
# Log next event before processing
if self._queue:
next_event = self._queue[0]
print(f"[{self.now:.2f}] Processing event: {next_event}")
super().step()
# Use instead of simpy.Environment()
env = TracedEnvironment()
Resource Monitor
Watch resource state:
def resource_monitor(env, resources, interval=10):
"""Periodically log resource states."""
while True:
print(f"\n=== Resource State at {env.now:.2f} ===")
for name, resource in resources.items():
print(f" {name}:")
print(f" Capacity: {resource.capacity}")
print(f" In use: {resource.count}")
print(f" Queue length: {len(resource.queue)}")
if hasattr(resource, 'queue') and resource.queue:
for i, req in enumerate(resource.queue[:5]):
print(f" Waiting[{i}]: {req}")
yield env.timeout(interval)
# Start monitor
env.process(resource_monitor(env, {'server': server, 'checkout': checkout}))
Process Tracker
Track process lifecycle:
class ProcessTracker:
def __init__(self):
self.active = {}
self.completed = []
def start(self, pid, name):
self.active[pid] = {'name': name, 'start': None}
def running(self, pid, time):
if pid in self.active:
self.active[pid]['start'] = time
def complete(self, pid, time):
if pid in self.active:
info = self.active.pop(pid)
info['end'] = time
self.completed.append(info)
def status(self):
print(f"Active processes: {len(self.active)}")
for pid, info in self.active.items():
print(f" {pid}: {info['name']} (started: {info['start']})")
tracker = ProcessTracker()
def customer(env, cid, server, tracker):
tracker.start(cid, 'customer')
with server.request() as req:
yield req
tracker.running(cid, env.now)
yield env.timeout(5)
tracker.complete(cid, env.now)
Breakpoint Debugging
Use Python's debugger:
def customer(env, cid, server):
with server.request() as req:
yield req
# Drop into debugger
import pdb; pdb.set_trace()
yield env.timeout(5)
In debugger:
(Pdb) env.now # Current time
(Pdb) server.count # Resources in use
(Pdb) len(server.queue) # Queue length
(Pdb) c # Continue
Conditional Debugging
Debug only when something interesting happens:
def customer(env, cid, server):
arrival = env.now
with server.request() as req:
yield req
wait = env.now - arrival
# Debug long waits
if wait > 50:
print(f"DEBUG: Customer {cid} waited {wait:.2f}")
print(f" Queue length: {len(server.queue)}")
print(f" Server busy: {server.count}/{server.capacity}")
import pdb; pdb.set_trace()
yield env.timeout(5)
State Snapshots
Capture simulation state at intervals:
def state_snapshot(env, resources, stores, interval=100):
"""Capture full state periodically."""
snapshots = []
while True:
snapshot = {
'time': env.now,
'resources': {},
'stores': {}
}
for name, res in resources.items():
snapshot['resources'][name] = {
'count': res.count,
'capacity': res.capacity,
'queue_length': len(res.queue)
}
for name, store in stores.items():
snapshot['stores'][name] = {
'level': store.level if hasattr(store, 'level') else len(store.items),
'capacity': store.capacity
}
snapshots.append(snapshot)
yield env.timeout(interval)
return snapshots
Assertion Checks
Catch problems early:
def customer(env, cid, server):
arrival = env.now
with server.request() as req:
yield req
wait = env.now - arrival
# Sanity checks
assert wait >= 0, f"Negative wait time: {wait}"
assert server.count <= server.capacity, "Over capacity!"
service = random.expovariate(1/5)
assert service > 0, f"Invalid service time: {service}"
yield env.timeout(service)
# Check departure time makes sense
assert env.now >= arrival, "Time went backwards!"
Event Queue Inspection
Look at what's pending:
def inspect_queue(env, n=10):
"""Print next n pending events."""
print(f"\nEvent queue at {env.now}:")
for i, (time, priority, eid, event) in enumerate(env._queue[:n]):
print(f" {i}: time={time:.2f}, event={event}")
if len(env._queue) > n:
print(f" ... and {len(env._queue) - n} more")
Test Harness
Isolate components for testing:
def test_customer_service():
"""Test customer process in isolation."""
env = simpy.Environment()
server = simpy.Resource(env, capacity=1)
results = []
def test_customer(env, server):
arrival = env.now
with server.request() as req:
yield req
yield env.timeout(5)
results.append({
'arrival': arrival,
'departure': env.now,
'wait': env.now - arrival - 5
})
# Single customer - should have no wait
env.process(test_customer(env, server))
env.run()
assert len(results) == 1
assert results[0]['wait'] == 0, "Expected no wait for single customer"
print("Test passed!")
test_customer_service()
Debug Mode Pattern
class Simulation:
def __init__(self, env, debug=False):
self.env = env
self.debug = debug
def log(self, message, level='DEBUG'):
if self.debug:
print(f"[{self.env.now:.2f}] {level}: {message}")
def customer(self, cid, server):
self.log(f"Customer {cid} arrives")
with server.request() as req:
yield req
self.log(f"Customer {cid} starts service")
yield self.env.timeout(5)
self.log(f"Customer {cid} done")
# Normal run
sim = Simulation(env, debug=False)
# Debug run
sim = Simulation(env, debug=True)
Summary
Debugging toolkit: 1. Print statements - simple, effective 2. Logging - controllable verbosity 3. Resource monitors - watch state 4. Breakpoints - interactive inspection 5. Assertions - catch problems early 6. Test harness - isolate and verify
Find the bug. Fix it. Move on.
Next Steps
Strengthen Your Python Skills
If you're finding Python tricky, get up to speed quickly with the 10-Day Python Bootcamp. It's designed to give you the confidence and skills to write clean, effective code.
Start the Python Bootcamp