Spatial SIR Model
An agent-based Monte Carlo simulation of epidemic spread with finite-time Lyapunov exponent analysis. Explore how spatial dynamics and chaos theory intersect in epidemiological modeling.
The short story
I built this because I wanted a model that feels closer to how outbreaks actually unfold in space. In many textbook SIR setups, everyone is effectively mixed together. That is useful, but it hides local structure. Here, people are represented as moving points, and transmission happens only when they get close enough.
The goal is not to claim perfect realism. The goal is to make the mechanism visible: movement, local contact, randomness, and then the emergent S/I/R curves that come out of those interactions.
What you are seeing here first
Before anything else: this is a moving population on a 2D torus. I use torus geometry so agents do not hit artificial walls (A reader of my website should know how much I like torus boundary condtions). If someone exits from the right, they re-enter from the left (same for top/bottom), which keeps density and contact structure unbiased.
\(d_T^2(i,j)=\min(|\Delta x|,1-|\Delta x|)^2+\min(|\Delta y|,1-|\Delta y|)^2\)
Transmission is then local: only pairs with \(d_T(i,j) \le r\) can infect, with Bernoulli probability \(1-e^{-\beta\Delta t}\) per step.
- Blue (S): susceptible agents
- Red (I): infected agents
- Green (R): recovered agents
- λ(t): finite-time divergence indicator from the twin run
Left panel = controls (\(\beta\), \(\gamma\), radius, speed, \(\Delta t\), initial infected, Monte Carlo runs). Main canvas shows the live agents, bars show instantaneous S/I/R counts, and the lower graph tracks their time evolution.
How it works in plain words
Under the hood, each person is an explicit agent with position, velocity, and a health state (S, I, or R). At each step, agents move a little, look around locally, and may transmit infection if they are close enough.
I still keep the math transparent. The transmission and recovery probabilities are the usual exponential forms, so the model stays interpretable while remaining stochastic and spatial:
\(P_{\text{recovery}} = 1 - e^{-\gamma \Delta t}\)
The important part is that these probabilities are applied to individuals in local neighborhoods, not to a fully mixed population.
The chaos part (and why I kept it)
I also wanted to test sensitivity: if two nearly identical outbreaks start from almost the same state, how quickly do they drift apart? To probe that, I run a faint twin system in parallel and track divergence over time.
In this implementation, the finite-time Lyapunov estimate is computed from divergence in the aggregate S/I/R vectors (not full phase-space trajectories). So I treat it as an operational chaos indicator rather than a strict theorem-level quantity.
What you can actually learn by playing with it
For me, the most useful part is not a single number. It is the behavior you see when you touch one parameter at a time. Increase interaction radius a little, and spread can accelerate fast. Lower speed, and local clusters can trap infection. Raise recovery, and you can watch the outbreak fail to sustain itself.
The Monte Carlo button is there for the same reason: one run can be deceptive. Repeated runs show how much randomness matters, even when the parameter set is fixed.
What makes it agent-based
The core point is simple: this is not solving only aggregate equations for S, I, and R. It is updating individuals. Every step tracks each agent's position, movement direction, and health state. Infection is tested only for nearby pairs, recovery is sampled per infected person, and then S/I/R totals are counted afterward. In other words, the curves you see are outputs of micro interactions, not assumptions.
If you want the compact form, local events use \(1 - e^{-\beta \Delta t}\) for transmission and \(1 - e^{-\gamma \Delta t}\) for recovery, but applied person-by-person in space.
Implementation notes
Here are the minimal implementation pieces, stripped to the essentials. These three blocks are enough to understand the core dynamics. Everything else in the file is mostly for rendering, controls, batching etc etc.
1) Agent state
type Agent = {
x: number; y: number;
vx: number; vy: number;
s: 0 | 1 | 2; // S, I, R
tI: number; // infection timer
};This is the minimum unit of the model. Every person has a location, a velocity, a health state, and an infection timer. Nothing is abstracted away at this stage.
2) Torus update + torus distance
const torus = (x:number) => x - Math.floor(x);
x = torus(x + vx * dt);
y = torus(y + vy * dt);
const dx = Math.abs(x1 - x2);
const dy = Math.abs(y1 - y2);
const d2 = Math.min(dx, 1 - dx)**2 + Math.min(dy, 1 - dy)**2;This part enforces periodic boundaries and computes the shortest wrapped distance. It is exactly why we avoid edge artifacts: agents crossing one side immediately re-enter from the opposite side.
3) Local infection + recovery (per step)
if (d2 <= r*r && infected && susceptible) {
if (rng() < 1 - Math.exp(-beta * dt)) infect();
}
if (infected) {
if (rng() < 1 - Math.exp(-gamma * dt)) recover();
}This is the stochastic engine. Infection and recovery are probabilistic events per time step, so two runs with the same parameters can still differ. That is why Monte Carlo averaging is meaningful here.