On this page

5. Programming Main

Manual contents | Wandering turtles | NLSimMain reference | Next: Programming patches

Main is the observer-like part of a model. It creates populations, reads parameters, coordinates shared work, and publishes results. It should not contain a second timer, animation loop, or repeated calls to individual init hooks. This chapter assumes lifecycle: "aligned".

5.1 Choose a home for each rule

Question Best home
What does one turtle do? Turtle turtleStep()
What happens at one environmental site? Patch patchStep()
What shared decision must precede all individual rules? Main coordinate()
What should the chart show after this tick? Main update()
What requires the completed populations at time zero/reset? Main setup()
What local Main variable must return to a starting value? Main reset()

Keep a local rule on its owner. For example, Main can count hungry turtles, but each turtle normally decides whether it is hungry.

5.2 Create populations, once per initialization

The Walkers Main is a complete example. Its constructor saves the Patch/Turtle classes supplied by the runtime. initCells() calls initializePatches(Class); initAgents() calls initializeTurtles(Class).

The runtime owns ordering:

  1. Main receives context and its one-time init().
  2. Main creates patches; configured colors are applied before patch init.
  3. If Project has agent, Main creates turtles with defaults before turtle init.
  4. Setup runs on patches, then turtles, then Main.

A patch-only model omits Project's agent and Main's initAgents(). An agent population of zero still needs agent and initAgents() if you intend to create turtles later. Empty population and absent population are different.

Main's hooks, in plain language

Hook Runs when Put here
constructor(Patch, Turtle) Main is created Save the supplied classes; allocate stable model-owned fields
init() Once, after runtime context and inputs are installed One-time registrations; preserve super.init() if overriding
initCells() Initial patch population is created initializePatches(this.Patch)
initAgents() Initial turtle population is created, if configured initializeTurtles(this.Turtle)
setup() After all populations are ready, initially and after world reset Population-dependent calculations, cached references, initial outputs
coordinate() Before individual rules on each tick Shared decisions or a snapshot of starting state
update() After commitment and clock advancement Summaries, charts, and stopping decisions
reset() During world reset, before populations are restored Restore Main-owned state; preserve super.reset()

You need only override hooks that have work to do. In particular, an empty init() override is unnecessary: inheriting it preserves the base behavior. Do not call these hooks yourself to make the model start or reset.

The constructor is too early to read inputs or scan populations. Main init() can read inputs, but the patch objects and turtle population are not ready. Main reset() is also too early to derive new counts from the restored world. Use setup() for all three population-dependent jobs.

Optional turtle customization

initializeTurtles(Class, customize) applies customize(turtle) after that turtle's init but before coordinated setup:

initAgents() {
  this.initializeTurtles(this.Turtle, turtle => {
    turtle.color = turtle.id % 2;
  });
}

Use an agent palette with at least two entries. This initializes alternate turtles with colors 0 and 1; it is not a repeating rule. Do not call init/setup inside the callback. Reset recreates turtles and repeats customization. Relationships requiring all turtles to exist belong in setup, not this callback.

5.3 Parameters are named inputs, not magic variables

Define inputs in Project and read their exact codes:

get SPD() { return this.getParam("SPD"); }
get GROW() { return this.getBParam("GROW"); }

These are methods inside a Main class, not standalone declarations. this.main.SPD then reads the value from Patch/Turtle. getParam(code) returns the supplied value; it does not validate or invent a missing default. getBParam(code) tests whether the stored value equals 1; use it for the runtime's 0/1 toggle inputs, not arbitrary text.

Decide when each input matters:

Policy Example
Every tick Walkers reads SPD in Turtle step
At startup and world reset A reserve is initialized from an input in init/reset
During initial population creation Project agent.count
On a specific action A model-defined birth callback uses a current input

Changing a slider only changes its parameter. Code or supported configuration bindings must read it. agent.defaults.speed: "SPD" has a built-in speed binding; Walkers instead explicitly reads SPD, making its timing visible. Do not install both approaches unnecessarily.

agent.count: "PPL" reads the count when agent properties are initialized; it does not promise live resizing or a new count on every Reset. Use explicit birth/death rules for changing populations. Do not repair that distinction by calling initialization hooks from a slider listener.

5.4 Coordinate before rules; report after commitment

For an ordinary dt: 1 tick:

coordinate -> individual step rules -> shift/commit -> time advances -> update

coordinate() and steps see the starting time; update sees the new time. Use this.currentTime for a number. For example, the first update sees 1. Slash visit modes insert an intermediate commitment, so start with "C", "A", or "C_A" until your model needs that extra boundary.

Read current state and propose next state for synchronous changes. An ordinary JavaScript field changes immediately; it is suitable for a counter or an explicitly sequential claim, not automatically a synchronized variable. See state management.

5.5 Visiting patches and turtles

"A" visits the turtle population automatically. "C" permits patch steps, but patches must also be selected for visitation. "C_A" runs patch then turtle steps with their proposals committed afterward.

For a beginner's always-active environment, put this in Patch:

setup() {
  this.main.visitAlways(this);
}

This Set registration is repeatable after reset. It does not duplicate the same patch in that Set. Do not also put those patches in the transient active set: the scheduler visits persistent and transient sets separately.

For a sparse model, touchPatch(patch) or addToNextSites(patchOrCollection) selects transient sites. These sets clear at shift; a model must arrange future activation where necessary. They are not "run once at the next tick" promises if called during an already-active patch traversal. The fire tutorial avoids that advanced scheduling entirely.

For an explicit scan without calling step rules:

const patches = this.patches.toList();
const turtles = [...this.turtles.values()];
const warm = patches.filter(patch => patch.color > 0).length;
const red = turtles.filter(turtle => turtle.color === 1).length;

This fragment assumes an agent world. toList() is an array; turtle values() is an iterator, converted here to an array. this.patches.forEach(patch => ...) also scans patches. this.turtleVisit(turtle => ...) scans turtles without inventing a second loop.

Advanced visitation caution: visitAll = true makes each scheduler patchVisit scan the whole grid, and the scheduler has multiple patchVisit passes. It is not a guarantee of exactly one call per patch per tick. Prefer persistent registration for the examples here. hasSeen(patch) / doneWith(patch) support explicit duplicate-step guards in sparse scheduling designs.

5.6 Charts and display values

Project declares a display's code, type, site, labels, and ranges. Main supplies values using that exact code:

this.show("TOTAL", 12);
this.show("STATES", 7, 5);
this.showList("STATES", [7, 5]);

These are method-body fragments requiring matching configured displays. The last two calls publish the same two-value series shape. Use showList for an existing array; show("STATES", [7, 5]) instead wraps that array as one argument and produces a different, nested shape. The series order must match Project's legend.

Publish initial values in setup and committed values in update. show replaces the pending value for its code; it does not accumulate every call into a history before the next status publication. The browser receives output when the runtime publishes status. Do not call statusToJson() just to inspect your model: it consumes output and clears reporting bookkeeping.

For a configured logger display, log(code, ...messages) appends messages to its pending batch; clearLogger(code) requests clearing it. createLogger(code) returns an object with log(text), clear(), and scrollToTop(). These are logging tools, not replacements for numeric chart series; use a compatible configured logger display.

5.7 Stopping a run

The browser's pause/stop control halts execution; it does not reset state. this.halt() requests the same kind of stop from model code. For example, inside update:

if (this.currentTime >= 100) this.halt();

There is also a callback property:

this.stopWhenTrue = () => this.currentTime >= 100;

Assign that function in one-time Main init after super.init(). The runtime evaluates it when preparing status, not at every expression or before every individual step. It signals the browser to stop. Neither mechanism substitutes for a correctly advancing clock. Do not start a separate setInterval or recursively call step().

5.8 Reset ownership and references

Browser Reset calls resetWorld(). Aligned Main reset() is local: preserve super.reset(), then restore Main-owned fields/Stores without recreating populations or calling init/setup.

Main and patches survive world reset; turtles do not. Rebuild cached turtle references in setup, and dispose any turtle-owned listeners attached to longer-lived objects. Synchronizing a custom Store does not define its reset. See the reserve example.

Birth/death requests also have delayed population boundaries, covered in Programming turtles.

5.9 Worked example: count turtles and remember a peak

Return to Walkers. Keep its Project settings, Patch, and Turtle, except for the display change below. We will replace Main so the chart shows three series:

This separates a measurement (where turtles are now) from memory (the largest count seen earlier). Measuring belongs after commitment; the memory is an ordinary Main-owned number because no individual rule reads or proposes it. It does not need a Store.

Configure the three chart series

Replace the existing entry in Project's displays array with this object. The variable declaration below is only a way to name the example; put the object itself in displays.

const populationDisplay = {
  code: "RIGHT",
  site: "display_01",
  type: "linechart",
  dom: [0, 100],
  ran: [0, 40],
  width: 460,
  height: 275,
  legend: ["Left half", "Right half", "Peak right-half count"],
  palette: ["#e67e22", "#1f77b4", "#333333"]
};

The output code stays "RIGHT" so we reuse the same display site. Three legend entries now require three values in that same order. The range assumes the original forty-turtle population; enlarge it if your model can exceed forty.

Replace Main

import {NLSimMain} from "@nwk/api/nlsimmain.js";

class PopulationMonitor extends NLSimMain {
  constructor(Patch, Turtle) {
    super();
    this.Patch = Patch;
    this.Turtle = Turtle;
    this.peakRight = 0;
  }

  initCells() {
    this.initializePatches(this.Patch);
  }

  initAgents() {
    this.initializeTurtles(this.Turtle);
  }

  setup() {
    const counts = this.countHalves();
    this.peakRight = counts.right;
    this.publishCounts(counts);
  }

  update() {
    const counts = this.countHalves();
    this.peakRight = Math.max(this.peakRight, counts.right);
    this.publishCounts(counts);
  }

  countHalves() {
    let left = 0;
    let right = 0;
    for (const turtle of this.turtles.values()) {
      if (turtle.x < this.cols / 2) left++;
      else right++;
    }
    return {left, right};
  }

  publishCounts(counts) {
    this.show("RIGHT", counts.left, counts.right, this.peakRight);
  }

  reset() {
    super.reset();
    this.peakRight = 0;
  }

  get SPD() { return this.getParam("SPD"); }
  get TURN() { return this.getParam("TURN"); }
}

export default PopulationMonitor;

countHalves() and publishCounts() are our own helper methods, not runtime hooks. Naming a method does not schedule it: setup and update call them. countHalves() scans the population without moving turtles or calling their rules. The loop avoids allocating a new array just to count.

At startup, setup records the initial right-half count as the first peak. At each update, Math.max keeps the larger of the old peak and the new count. The peak can rise or stay unchanged, but cannot fall during a run. Left plus right always equals the current population size, including zero. An empty population publishes [0, 0, 0] at startup rather than a missing value. If turtles die later, the peak still remembers the larger earlier population.

Reset first clears Main's memory locally, then the runtime restores the world. Finally, setup measures the recreated turtles and starts a new peak. Do not call setup from reset: it would measure the old population before restoration. The constructor and one-time init are not replayed.

Check the example without relying on random motion

In Project, set TURN's value to 0, leave SPD at 0.5, and replace agent.start: "random" with this two-turtle start list:

const twoStarts = [
  [0, 14.5, 5.5, 0, 0.6, 0, 0.5, 0],
  [1, 1.5, 5.5, 0, 0.6, 0, 0.5, 0]
];

Use the array itself as agent.start. As in the one-turtle exercise, an explicit start list determines the population size. Both turtles face east and travel half a world unit each tick. With the original thirty-column wrapped world:

Time Left half Right half Peak right-half count
0 2 0 0
1 1 1 1
27 0 2 2
31 1 1 2

At time 31 the first turtle has wrapped to x = 0; the second is at x = 17. The right count falls but the peak remains 2. Reset should restore the first row, including peak 0. Change SPD to 0 before resetting and both turtles should remain in the left half on subsequent steps: reset uses the current speed input, not the slider's original value.

For comparison, try publishing from coordinate() instead of update(). That hook sees positions before the move, so a chart point labeled time 1 would still describe time-zero positions. Restore the update version afterward. Use coordinate for information individual rules need before acting, not for reporting the results of actions that have not happened yet.

Model-facing Main operations at a glance

Operation Purpose / return
initializePatches(ClassOrFactory) Populate the patch matrix during initCells
initializeTurtles(Class, customize?) Populate turtles during initAgents
getParam(code), getBParam(code) Read an input / test its 0/1 toggle value
currentTime Read current clock time
patch(col, row) Find a patch; see boundary rules in the neighborhood reference
patches.toList(), turtles.values() Read patch array / turtle iterator
visitAlways(patch), touchPatch(patch) Persistent / transient activation
show(code, ...values), showList(code, values) Replace pending display values
log(code, ...text), clearLogger(code) Append / clear logger output
halt(), stopWhenTrue = fn Stop now / stop on status condition
resetWorld() Coordinate whole-world reset, not a local hook

Implementation: NLSimMain. Transport/serialization helpers are runtime plumbing, not additional initialization hooks for model authors.