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:

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:

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

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:

Step advances a single dt at a time — useful for watching early dynamics. Reset returns the clock to zero and replays setup().

5. Experiments

6. Where to go next