Life & perception
Spatial Epidemic (SIRD) Model
An agent-based Monte Carlo simulation of epidemic spread: people move on a torus, infect each other only at close range, recover or die, and the S/I/R/D curves emerge from those local contacts.
- Epidemiology
- Agent-based models
- Monte Carlo
Susceptible, infected, recovered, dead
Model
Monte Carlo
Repeats the epidemic with the current model settings and new randomness. Each run continues until no one is infected (at most 2,000 s of simulated time), in a background thread.
No results yet.
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/D 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 conditions). If someone exits from the right, they re-enter from the left (same for top/bottom), which keeps density and contact structure unbiased.
Each agent keeps its own speed, between 0.5 and 1.5 times the speed setting, and turns a little at random every step. The turning angle has variance proportional to (rotational diffusion), so the time step is only a numerical choice: halving it does not change how the agents wander.
Transmission is then local: only pairs with can infect, with Bernoulli probability per step.
- Blue (S): susceptible agents
- Orange (I): infected agents; the faint ring around each is the contact radius
- Green (R): recovered agents
- Dead (D): not drawn. When an agent dies its point disappears; the grey line in the chart counts the dead
The panel holds the controls (, , , contact radius, speed, , number of agents, initial infected, and the Monte Carlo settings). The square canvas shows the live agents, the readouts give the current S/I/R/D counts, and the chart 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, R, or D). 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, recovery and death probabilities are the usual exponential forms, so the model stays interpretable while remaining stochastic and spatial:
Recovery and death are two competing clocks with rates and : an agent stays infected for on average, and a share of all infections end in death. A dead agent stops moving, can no longer infect or be infected, and its point disappears from the picture.
For comparison, if everyone were well mixed (each agent meeting a random share of the population), the counts would follow the SIRD equations
The important part is that these probabilities are applied to individuals in local neighborhoods, not to a fully mixed population.
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 presets set up exactly these three experiments. Raise , and a larger share of each outbreak ends in deaths; since dying also ends an infection, it shortens the time an agent can pass it on, so the spread itself depends on .
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. Each run continues until no one is infected, so the mean final size (and the mean number of deaths) is that of the whole epidemic, not a snapshot.
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 or death is sampled per infected person, and then S/I/R/D 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 for transmission and for leaving the infected state (death with share ), 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
// one typed array per field; index i is agent i
x, y: Float64Array; // position on the unit torus
heading: Float64Array; // direction of motion
speedFactor: Float64Array; // own speed = speed × (0.5 … 1.5)
state: Uint8Array; // 0 = S, 1 = I, 2 = R, 3 = D (dead)This is the minimum unit of the model. Every person has a location, a heading, their own speed, and a health state. Nothing is abstracted away at this stage.
2) Torus update + torus distance
const torus = (x: number) => x - Math.floor(x);
heading += (rng() - 0.5) * Math.sqrt(12 * D * dt); // variance D·dt per step
x = torus(x + v * Math.cos(heading) * dt);
y = torus(y + v * Math.sin(heading) * 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 and death (per step)
if (d2 <= r*r && infected && susceptible) {
if (rng() < 1 - Math.exp(-beta * dt)) infect();
}
if (infected) {
const pOut = 1 - Math.exp(-(gamma + mu) * dt); // leaves I this step
const u = rng();
if (u < pOut * mu / (gamma + mu)) die(); // vanishes from the picture
else if (u < pOut) recover();
}This is the stochastic engine. Infection, recovery and death 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.