On this page
SDSir: your first stock-and-flow model
Manual contents | Stocks, flows, and integration | SDMain reference | Stock and Store reference
This tutorial builds the classic SIR epidemic as a NuWebKit system-dynamics
model. You will write the two parts every SD app has — a Project that
declares inputs, displays, and the integration settings, and a Main class
extending SDMain that declares stocks and flows — then run the model and
check its behavior against known properties of SIR.
The finished model ships as the SDSir app with its project definition; this chapter reconstructs it piece by piece.
1. The model on paper
SIR divides a closed population of size N into three compartments:
susceptible S, infected I, and recovered R. In a stock-and-flow diagram
each compartment is a stock (a rectangle), and two flows (valves on
pipes) move population between them:
infection recovery
[ S ] ======>>====== [ I ] ======>>====== [ R ]
infection = Beta * S * I / N
recovery = Sigma * I
As differential equations:
dS/dt = -Beta * S * I / N
dI/dt = Beta * S * I / N - Sigma * I
dR/dt = Sigma * I
Two facts give us checks to run later:
- Conservation. Nothing enters or leaves the system, so
S + I + R = Nat all times. - Threshold. An epidemic grows only while
Beta * S / N > Sigma. With everyone initially susceptible, the basic reproduction number isR0 = Beta / Sigma; ifR0 <= 1the outbreak dies out immediately.
2. The Project
The Project is a plain JavaScript object the hosting page reads to build the user interface and configure the clock. The SD-relevant parts of the shipped sdsirPrj.js are:
"use strict";
const Project = {
name: "SDSir",
title: "SD Sir",
visitMode: "SDRK4",
dt: 0.1,
timeDisplay: 1,
maxTime: 15,
inputs: [
{type: "slider", min: 0, max: 10000000, step: 1000, value: 1000000,
legend: "N", code: "N", vtype: "Integer"},
{type: "slider", min: 0, max: 100, step: 1, value: 4,
legend: "Beta", code: "Beta", vtype: "Integer"},
{type: "slider", min: 0, max: 100, step: 1, value: 1,
legend: "IO", code: "IO", vtype: "Integer"},
{type: "slider", min: 0, max: 100, step: 1, value: 2,
legend: "Sigma", code: "Sigma", vtype: "Integer"},
],
displays: [
{
title: "Population",
code: "SIR",
site: "display_01",
type: "linechart",
dom: [0, 15],
ran: [0, 1000],
width: 400,
height: 400,
legend: ["S", "I", "R"],
palette: ['#0000ff', '#4daf4a', '#ff0000'],
tolerance: 0
}
]
}
Reading it as an SD modeler:
visitMode: "SDRK4"selects the system-dynamics runtime with fourth-order Runge-Kutta integration."SD"selects the same runtime with Euler integration. See integration methods.dt: 0.1is the integration time step, andmaxTime: 15is the stop time. The run ends once the clock reachesmaxTime(the runtime stops within half adtof it).timeDisplaycontrols how the time readout is rounded in the UI.inputsdeclares the model's exogenous parameters, each with acode. Those codes are exactly what the Main class reads back withthis.getParam("Beta"). Sliders stay live while the model runs.displaysdeclares output charts, each with acode. The Main class pushes a tuple of values to a chart withthis.show("SIR", s, i, r); the order of values matcheslegendandpalette.
The shipped project also carries hosting-page settings (frame sizes, local
tool paths, and an api.sens sensitivity block) that are not part of the
SDMain contract; this tutorial omits them.
3. The Main class
The Main class is where the stock-and-flow diagram becomes code. The complete shipped sdsir.js:
"use strict";
import {SDMain} from "@nwk/api/sdmain.js"
import {Stock} from "@nwk/api/stock.js"
class SDSir extends SDMain {
constructor() {super();}
init() {
super.init();
this.s = Stock.create(this.N - this.IO, this);
this.i = Stock.create(this.IO, this);
this.r = Stock.create(0, this);
this.s.flow = () => -this.Beta * this.s.get() * this.i.get() / this.N;
this.i.flow = () => this.Beta * this.s.get() * this.i.get() / this.N
- (this.Sigma * this.i.get());
this.r.flow = () => this.Sigma * this.i.get();
this.setup();
}
step() {
super.step();
}
reset() {
super.reset();
}
setup() {
super.setup();
this.show("SIR", this.s.get(), this.i.get(), this.r.get());
}
update() {
super.update();
this.show("SIR", this.s.get(), this.i.get(), this.r.get());
}
get N() {return this.getParam("N");}
get Beta() {return this.getParam("Beta");}
get IO() {return this.getParam("IO");}
get Sigma() {return this.getParam("Sigma");}
}
export {SDSir}
Studio generates this file with numbered marker comments (//1s … //8e)
around the regions you edit; they are omitted here for readability and are
described in the SDMain reference.
Stocks
this.s = Stock.create(this.N - this.IO, this);
Stock.create(initial, main) makes one stock, registers it with the runtime
(main.stocks), and arranges synchronized commitment of its value. The first
argument is the initial value — here computed from the N and IO
sliders, so "everyone susceptible except the first cases". A stock's current
level is read with this.s.get().
Flows
this.i.flow = () => this.Beta * this.s.get() * this.i.get() / this.N
- (this.Sigma * this.i.get());
Each flow is a function returning the stock's net rate of change — the
right-hand side of its differential equation, inflows minus outflows. The SIR
diagram has two flow processes; in code each appears in every stock it
touches: the infection term leaves s (negative) and enters i (positive),
and the recovery term leaves i and enters r.
Flow functions are evaluated by the integrator, possibly several times per
dt under RK4, so they must be pure: read stocks with .get() and read
parameters, but do not assign state, draw random numbers, or produce output
here. See the simulation cycle for
exactly when flows run.
Parameters
get N() {return this.getParam("N");}
Each getter mirrors one input code from the Project. Because flows call the
getters on every evaluation, dragging a slider while the model runs changes
the system immediately — no restart required.
Lifecycle hooks
init()runs once when the app launches: create stocks, attach flows. Callingthis.setup()at its end makes launch and Reset share one path.setup()runs at launch (via that call) and again on every Reset: publish the initial chart point here. If a stock's initial value should track the current slider positions after Reset, re-initialize it here withthis.s.initialize(this.N - this.IO)(see initialization and reset).update()runs once per completeddt, after the stocks commit their new values: read the stocks andshowthem.step()runs once perdtbefore integration; SIR needs no discrete per-step logic, so it only callssuper.step().
show(code, ...values) buffers one tuple for the display with that code;
the runtime drains the buffer into the chart after each step.
4. Run it
Open the app page (for the shipped model, sdsir);
press Run. With the default parameters (N = 1,000,000, Beta = 4,
IO = 1, Sigma = 2, so R0 = 2) you should observe:
Sfalls along a sigmoid from nearN;Irises to a single peak and decays;Rrises monotonically toward its final size.- The three curves always sum to
N— conservation. If they do not, a flow term is missing from one of the stocks it touches. Ipeaks exactly whenS/NcrossesSigma/Beta = 1/R0 = 0.5— the threshold property.- The run halts by itself at
maxTime = 15.
Step advances a single dt at a time — useful for watching early
dynamics. Reset returns the clock to zero and replays setup().
5. Experiments
- Kill the epidemic. Set
Beta = 2soR0 = 1:Ishould never grow. Below 1 it decays from the start. - Live feedback. While running, drag
Betadown mid-epidemic — the trajectory bends immediately because flows reread the parameter getters every evaluation. - Integration accuracy. Switch
visitModeto"SD"(Euler) and raisedt. Euler with a coarse step visibly distorts the peak; RK4 atdt = 0.1is effectively exact for this system. This trade-off is the subject of Euler and RK4 integration. - Stop on a condition. In
init()addthis.stopWhenTrue = () => this.i.get() < 0.5;to end the run when the epidemic is effectively over instead of waiting formaxTime.
6. Where to go next
- Stocks, flows, and integration maps every SD concept to its code construct and walks through Lotka-Volterra and a stochastic SIR.
- SDMain reference documents every hook and method used above.
- Stock and Store reference documents the two-phase value
machinery underneath
Stock.create,flow, andget.