Loading learning content...
How do you model a busy airport? A network of routers? A hospital emergency room? A supply chain? These systems involve entities arriving unpredictably, competing for resources, and triggering chains of consequences.
Discrete Event Simulation (DES) provides the answer: instead of simulating every moment in time, we jump from event to event. A priority queue—implemented as a heap—efficiently tracks which event happens next, enabling simulation of systems far too complex for closed-form analysis.
By the end of this page, you will understand how discrete event simulation works, master the event queue pattern using heaps, implement simulations for queuing systems and networks, and recognize when simulation is the right tool.
Core Concept:
In DES, time advances in jumps from one event to the next. Between events, nothing changes. This is far more efficient than time-stepped simulation where we check everything at regular intervals.
Key Components:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364
import heapqfrom dataclasses import dataclass, fieldfrom typing import Callable, Anyfrom abc import ABC, abstractmethod @dataclass(order=True)class Event: """A simulation event.""" time: float priority: int = 0 # Tie-breaker for same-time events handler: Callable = field(compare=False) data: Any = field(default=None, compare=False) class Simulation: """ Core discrete event simulation engine. The event queue is a min-heap ordered by (time, priority). Each event handler can schedule future events. """ def __init__(self): self.clock = 0.0 self.event_queue: list[Event] = [] self.running = False def schedule(self, delay: float, handler: Callable, data: Any = None, priority: int = 0) -> Event: """Schedule event to occur after delay from current time.""" event = Event( time=self.clock + delay, priority=priority, handler=handler, data=data ) heapq.heappush(self.event_queue, event) return event def schedule_at(self, time: float, handler: Callable, data: Any = None, priority: int = 0) -> Event: """Schedule event at absolute time.""" event = Event(time=time, priority=priority, handler=handler, data=data) heapq.heappush(self.event_queue, event) return event def run(self, until: float = float('inf')) -> None: """Run simulation until time limit or no more events.""" self.running = True while self.running and self.event_queue: event = heapq.heappop(self.event_queue) if event.time > until: heapq.heappush(self.event_queue, event) break self.clock = event.time event.handler(self, event.data) self.running = False def stop(self) -> None: """Stop simulation.""" self.running = FalseThe M/M/1 queue is the 'hello world' of simulation: customers arrive randomly, wait in line, and are served one at a time. This models bank tellers, checkout lines, and CPU scheduling.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990
import randomfrom collections import deque class MM1Queue: """ M/M/1 Queue Simulation - Poisson arrivals (exponential inter-arrival times) - Exponential service times - Single server Uses discrete event simulation with heap-based event queue. """ def __init__(self, arrival_rate: float, service_rate: float): self.arrival_rate = arrival_rate self.service_rate = service_rate self.sim = Simulation() # State self.queue = deque() self.server_busy = False self.customers_served = 0 # Statistics self.total_wait_time = 0.0 self.total_system_time = 0.0 def run(self, duration: float) -> dict: """Run simulation and return statistics.""" # Schedule first arrival self.sim.schedule( self._exp_random(self.arrival_rate), self._handle_arrival ) self.sim.run(until=duration) return { 'customers_served': self.customers_served, 'avg_wait': self.total_wait_time / max(1, self.customers_served), 'avg_system_time': self.total_system_time / max(1, self.customers_served), 'utilization': self.service_rate / self.arrival_rate if self.arrival_rate else 0 } def _handle_arrival(self, sim: Simulation, data): """Customer arrives.""" arrival_time = sim.clock if not self.server_busy: # Start service immediately self.server_busy = True service_time = self._exp_random(self.service_rate) sim.schedule(service_time, self._handle_departure, arrival_time) else: # Join queue self.queue.append(arrival_time) # Schedule next arrival sim.schedule( self._exp_random(self.arrival_rate), self._handle_arrival ) def _handle_departure(self, sim: Simulation, arrival_time: float): """Customer departs after service.""" wait_time = sim.clock - arrival_time - self._exp_random(self.service_rate) self.total_wait_time += max(0, wait_time) self.total_system_time += sim.clock - arrival_time self.customers_served += 1 if self.queue: # Serve next customer next_arrival = self.queue.popleft() service_time = self._exp_random(self.service_rate) sim.schedule(service_time, self._handle_departure, next_arrival) else: self.server_busy = False def _exp_random(self, rate: float) -> float: """Exponential random variable.""" return random.expovariate(rate) # Run simulationqueue = MM1Queue(arrival_rate=0.8, service_rate=1.0)stats = queue.run(duration=10000)print(f"Customers served: {stats['customers_served']}")print(f"Average wait time: {stats['avg_wait']:.2f}")print(f"Average system time: {stats['avg_system_time']:.2f}")Network simulators use DES to model packet transmission, queuing, and routing. The event queue tracks packet arrivals and departures at each node.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889
from dataclasses import dataclassfrom typing import Dict, Listimport random @dataclassclass Packet: id: int source: str destination: str size: int # bytes created_at: float class NetworkSimulator: """ Simple network simulation with routers and links. Events: packet_arrival, packet_departure, link_free Demonstrates: DES for network modeling """ def __init__(self): self.sim = Simulation() self.links: Dict[tuple, dict] = {} # (from, to) -> {bandwidth, delay, queue} self.delivered = [] self.packet_id = 0 def add_link(self, from_node: str, to_node: str, bandwidth: float, delay: float): """Add directional link between nodes.""" self.links[(from_node, to_node)] = { 'bandwidth': bandwidth, # bytes/sec 'delay': delay, # seconds 'queue': [], 'busy': False } def send_packet(self, source: str, dest: str, size: int = 1000): """Create and send a packet.""" self.packet_id += 1 packet = Packet(self.packet_id, source, dest, size, self.sim.clock) if (source, dest) in self.links: self._enqueue_packet(source, dest, packet) def _enqueue_packet(self, from_node: str, to_node: str, packet: Packet): """Add packet to link queue, start transmission if idle.""" link = self.links[(from_node, to_node)] link['queue'].append(packet) if not link['busy']: self._start_transmission(from_node, to_node) def _start_transmission(self, from_node: str, to_node: str): """Start transmitting next packet on link.""" link = self.links[(from_node, to_node)] if not link['queue']: link['busy'] = False return link['busy'] = True packet = link['queue'].pop(0) # Transmission time = size / bandwidth tx_time = packet.size / link['bandwidth'] total_delay = tx_time + link['delay'] self.sim.schedule(total_delay, self._handle_arrival, (to_node, packet)) def _handle_arrival(self, sim: Simulation, data): """Packet arrives at destination.""" node, packet = data if node == packet.destination: latency = sim.clock - packet.created_at self.delivered.append((packet, latency)) # Could route to next hop here for multi-hop networks def run(self, duration: float) -> dict: self.sim.run(until=duration) if self.delivered: latencies = [lat for _, lat in self.delivered] return { 'packets_delivered': len(self.delivered), 'avg_latency': sum(latencies) / len(latencies), 'max_latency': max(latencies) } return {'packets_delivered': 0}For production use, SimPy is the standard Python DES library. It uses generators for elegant process modeling. The core still uses a heap-based event queue—the same pattern we've implemented here.
Why Heaps Are Essential for DES:
A simulation might process millions of events. Each event can schedule multiple future events. The event queue constantly grows and shrinks.
For a simulation with 10 million events, this is the difference between seconds and hours.
| Operation | Heap | Sorted List | Unsorted List |
|---|---|---|---|
| Get next event | O(log n)* | O(1) | O(n) |
| Schedule event | O(log n) | O(n) | O(1) |
| Cancel event | O(log n)** | O(n) | O(n) |
| n events total | O(n log n) | O(n²) | O(n²) |
*Technically O(log n) for extract, but peek is O(1) **With indexed heap or lazy deletion
You've mastered the five major heap patterns: k-th element selection, k-way merge, median maintenance, priority scheduling, and event-driven simulation. These patterns appear constantly in interviews and production systems. The heap is now a core tool in your algorithmic toolkit.