Chapter 19: Discrete-Event Simulation with salabim¶
Part III — Foundations of Spatial Simulation
Learning Objectives¶
By the end of this chapter you will be able to:
- Explain how discrete-event simulation differs from the fixed-tick loop Chapter 18 used
- Build a
salabimmodel withEnvironment,Component, and the scheduler clock - Run several components concurrently in one
Environment - Explain what
salabimbuys you over Chapter 18's hand-rolled loop, and what it doesn't
# Standard imports — add chapter-specific imports below
import random
import salabim as sim
Chapter 18's Game of Life advanced in perfectly regular ticks: generation 0, then 1, then 2, every step the same fixed size, every component (there was only one — the grid itself) updating in lockstep. Real systems rarely cooperate with that regularity — a customer arrives at an unpredictable time, a machine breaks down after a random interval, two independent processes need to run side by side without either one dictating the other's pace. salabim is a discrete-event simulation library built for exactly that, and DisSModel's own engine, Chapter 21 already noted, took inspiration from libraries in this same family.
What Is Discrete-Event Simulation?¶
A discrete-event simulation doesn't advance in fixed ticks at all — it advances directly from one event to the next, wherever on the timeline that next event happens to fall. If a Producer schedules its next action for t=4.7 and a Consumer schedules its next one for t=9.2, the simulation clock jumps straight to 4.7, does whatever the Producer does, then jumps straight to 9.2 — nothing happens, and no time is spent computing anything, for the gap in between. Chapter 18's fixed-tick loop spent equal effort on every single generation whether or not anything meaningful happened that generation; discrete-event simulation spends effort exactly where the events are.
salabim's two central objects map directly onto that idea: a Component is anything with its own independent behavior over time, and an Environment is the shared clock and event queue every Component schedules itself against.
salabim: Environment, Component, Clock¶
A minimal salabim model overrides process() — the method describing one component's behavior — and calls self.hold(duration) wherever that behavior should pause before continuing:
class Ticker(sim.Component):
def process(self):
while True:
print(f"t={self.env.now():.1f} tick")
self.hold(1)
env = sim.Environment(trace=False)
Ticker()
env.run(till=5)
t=0.0 tick t=1.0 tick t=2.0 tick t=3.0 tick t=4.0 tick t=5.0 tick
sim.Environment() creates the clock and scheduler; constructing Ticker() registers it with whichever Environment is currently active — the identical "construction implies registration" pattern Chapter 21 already established for DisSModel's own Model. self.hold(1) doesn't block execution the way time.sleep(1) would — it tells the scheduler "wake this component up again one time unit from now," and control returns immediately to whatever else is scheduled in between. env.run(till=5) then drives the clock forward, processing every scheduled event in timestamp order until it reaches t=5.
Did you know?
Older salabim tutorials write process() as a generator, with yield self.hold(1) instead of a plain self.hold(1) call. Recent salabim versions default to this "yieldless" style instead — no yield needed — which is what every example in this chapter uses. If you find a yield-based example elsewhere, it's not wrong, just an older calling convention for the identical underlying scheduler.
Multiple Components, One Environment¶
Nothing changes to run several components side by side — each keeps its own independent process(), and the shared Environment interleaves them by timestamp, not by which one was constructed first:
class Producer(sim.Component):
def process(self):
while True:
print(f"t={self.env.now():.1f} Producer works")
self.hold(2)
class Consumer(sim.Component):
def process(self):
while True:
print(f"t={self.env.now():.1f} Consumer works")
self.hold(3)
env = sim.Environment(trace=False)
Producer()
Consumer()
env.run(till=10)
t=0.0 Producer works t=0.0 Consumer works t=2.0 Producer works t=3.0 Consumer works t=4.0 Producer works t=6.0 Consumer works t=6.0 Producer works t=8.0 Producer works t=9.0 Consumer works t=10.0 Producer works
Notice the output interleaves — Producer at t=2, 4, 6, ..., Consumer at t=3, 6, 9, ... — with both landing on t=6 in the same tick, resolved in construction order. Neither component has any idea the other exists; the Environment is entirely responsible for weaving their independent timelines into one consistent history.
Game of Life with salabim (Vector)¶
Chapter 18's Game of Life fits naturally into exactly one Component: the whole grid re-evaluated once every hold(1). Reusing Chapter 18's step() function unchanged, only the driving loop changes — a while True / hold(1) pair instead of a plain Python for loop:
def count_live_neighbors(grid, r, c, rows, cols):
count = 0
for dr in (-1, 0, 1):
for dc in (-1, 0, 1):
if dr == 0 and dc == 0:
continue
nr, nc = r + dr, c + dc
if 0 <= nr < rows and 0 <= nc < cols:
count += grid[nr][nc]
return count
def step(grid, rows, cols):
next_grid = [[0] * cols for _ in range(rows)]
for r in range(rows):
for c in range(cols):
alive = grid[r][c] == 1
n = count_live_neighbors(grid, r, c, rows, cols)
if alive and n in (2, 3):
next_grid[r][c] = 1
elif not alive and n == 3:
next_grid[r][c] = 1
return next_grid
class GameOfLifeComponent(sim.Component):
def setup(self, rows, cols, grid):
self.rows = rows
self.cols = cols
self.grid = grid
def process(self):
while True:
alive = sum(sum(row) for row in self.grid)
print(f"t={self.env.now():.0f} alive={alive}")
self.grid = step(self.grid, self.rows, self.cols)
self.hold(1)
random.seed(42)
rows, cols = 15, 15
grid = [[1 if random.random() < 0.3 else 0 for _ in range(cols)] for _ in range(rows)]
env = sim.Environment(trace=False)
GameOfLifeComponent(rows=rows, cols=cols, grid=grid)
env.run(till=5)
t=0 alive=77 t=1 alive=90 t=2 alive=67 t=3 alive=63 t=4 alive=62 t=5 alive=67
setup() here plays the same role Chapter 21's Model.setup() will — one-time initialization, called automatically at construction — right down to the name. That's not a coincidence: salabim's Component lifecycle and DisSModel's own Model lifecycle share a common ancestor in this style of scheduler, which is exactly why Chapter 21 could credit salabim as one of the influences on DisSModel's engine even after removing it as a dependency.
Composing Models and Visualization¶
Nothing stops the Game of Life Component above from sharing an Environment with something running on a genuinely different clock — a sensor logging every 0.5 time units, an external event injected at an irregular timestamp — exactly the flexibility Chapter 18's fixed-tick loop had no way to express, since everything in that loop was forced onto the same rigid step size.
salabim also ships its own real-time animation layer (sim.AnimateRectangle, sim.Animate, and similar), which can render a running model directly rather than printing state to text — a different tool from Chapter 15's matplotlib/folium techniques, built specifically for watching a discrete-event simulation unfold live rather than producing a static or web map. This chapter sticks to print() for one reason worth stating plainly: right now, the point is to feel the scheduler underneath, not to make it pretty.
What salabim Buys Us¶
Three real things, compared to Chapter 18's hand-rolled loop:
- Multiple independent timelines, composed automatically. Chapter 18 could only ever have one thing happening — the grid, ticking as a whole.
salabimcomposes any number of components, each with its own pace, without hand-writing a merge of their schedules. - Irregular timing, for free. Chapter 18's loop meant "every generation is exactly 1 unit apart" by construction.
salabimcomponents canhold()for different, even random, durations — ahold(random.expovariate(0.5))costs nothing extra to add. - A shared vocabulary with DisSModel's own scheduler.
setup(),process()/execute(), construction-implies-registration — Chapter 21'sModellifecycle will feel immediately familiar, because it descends from exactly this family of ideas.
What it does not buy: none of Chapter 18's core performance problem goes away. step() is still a pure-Python nested loop, still checking up to 8 neighbors per cell by hand — salabim changed how time is managed, not how space is computed. Chapter 20 picks that half of the problem up next, entirely independent of which scheduler is driving the clock.
Exercises¶
- A third component. Add a
Loggercomponent to the Multiple Components example that printsenv.now()every 0.5 time units. Run the sameenv.run(till=10)and observe how its events interleave withProducerandConsumer. - Irregular holds. Modify
Producertohold(random.uniform(1, 3))instead of a fixedhold(2). Run it a few times with different random seeds — does the order of Producer/Consumer events ever change, or only the timestamps? - Two grids, one Environment. Construct two separate
GameOfLifeComponents, with different random seeds, in the sameEnvironment. Confirm both advance independently and their printedalivecounts diverge. - Where salabim's help ends. Using the What salabim Buys Us section, write two sentences: one describing a problem
salabimsolves that Chapter 18 couldn't, and one describing a problem Chapter 18 had thatsalabimdoes not solve.
# Your code here
Summary¶
Key concepts introduced¶
- Discrete-event simulation: the clock jumps directly from one scheduled event to the next, rather than advancing in fixed ticks
salabim'sEnvironment(shared clock and scheduler) andComponent(independent behavior,process()+self.hold(duration))- Multiple components sharing one
Environment, interleaved automatically by timestamp - Chapter 18's Game of Life ported into a single
Component, itsstep()function reused completely unchanged - What a scheduler like
salabimsolves (composing independent, irregularly-timed processes) versus what it doesn't (Chapter 18's per-cell computation is exactly as slow as it was)
Chapter 20 returns to that unsolved half directly — profiling Chapter 18's step() function for real, and fixing what the profiler finds.
Further Reading¶
- salabim documentation: https://www.salabim.org/
- Law, A. M. (2014). Simulation Modeling and Analysis (5th ed.). McGraw-Hill — the standard graduate reference for discrete-event simulation theory
- Banks, J. et al. (2009). Discrete-Event System Simulation (5th ed.). Pearson