On this page
- 5.1 Choose a home for each rule
- 5.2 Create populations, once per initialization
- 5.3 Parameters are named inputs, not magic variables
- 5.4 Coordinate before rules; report after commitment
- 5.5 Visiting patches and turtles
- 5.6 Charts and display values
- 5.7 Stopping a run
- 5.8 Reset ownership and references
- 5.9 Worked example: count turtles and remember a peak
- Model-facing Main operations at a glance
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:
- Main receives context and its one-time
init(). - Main creates patches; configured colors are applied before patch init.
- If Project has
agent, Main creates turtles with defaults before turtle init. - 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:
- how many turtles are in the left half;
- how many are in the right half;
- the largest right-half count observed so far, including time zero.
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.