On this page

3. Execution and lifecycle

Compatibility appendix

Manual contents | Next: Managing model state

This guide assumes top-level Project.lifecycle: "aligned" for patch/agent models based on NLSimMain, not system-dynamics integration. Start with the quick guide here, then read the full aligned contract in section 3.10. The first-model tutorial uses these rules without requiring knowledge of runtime messages.

Aligned lifecycle: the model author's quick guide

Hook What belongs here What does not
Constructor Allocate stable fields/Stores; call super(...) Calling init/setup, reading Main parameters before runtime initialization
init() One-time initialization/registration after context/defaults Depending on populations from Main init; replaying it on reset
setup() Relationships, patch activation, derived totals, initial output after populations are ready Allocating replacement Stores or appending long-lived listeners each time
reset() Restore this object's local state; preserve base reset Resetting other populations, calling init/setup manually

Initial order is Main constructor/context/init, patch allocation/configured colors/init, then (if configured) turtle allocation/defaults/init/customization. Setup runs patches → turtles → Main. Use only the hooks your model needs. An overridden patch color setter must already have its dependencies in the constructor, because configured color is applied before patch init.

For a tick, rules read current values and write next values. The runtime commits pending state at shift and publishes update() after time advances. Keep super.patchStep() / super.turtleStep() for automatic built-in updater evaluation; turtle movement is scheduled before its built-in updaters. Custom Store updaters are still explicit.

The browser Reset command coordinates world restoration through resetWorld(); programmatic whole-world resets use that same method. It invokes local resets, recreates turtles, and runs coordinated setup again. Main and patch identities survive; old turtle references do not. Custom state requires explicit reset.

Compatibility background follows: sections 3.1–3.9 describe legacy behavior for diagnosing unconverted apps. They are not instructions for a new aligned model. In particular, legacy repeated init/setup and whole-world reset() must not be copied into aligned code. The runtime still defaults to legacy when the flag is absent; the remaining app migrations are deferred. Do not change an existing model's visit mode or initialization ordering merely to make it resemble an example.

3.1 Who owns execution?

Project supplies configuration. Main creates the patch world and initial turtle population through its initialization hooks. The clock advances the model. Patches and turtles implement individual rules.

The browser runtime constructs Main and sends initialization and control messages through SimDispatcher. Model authors normally subclass the API classes; they do not call the runtime's initialize(msg) or create a separate timer or animation loop.

The browser prepares the cell view before sending initialization messages. Local JavaScript replies are synchronous: redrawing the hex lattice after those replies would erase the newly painted agents and initial cell colors. This order applies to both legacy and aligned models; no model-specific reset is needed before the first run.

Use the hooks below for model behavior. Keep the distinction between an overridable hook such as coordinate() and runtime orchestration such as step() or shift().

3.2 Initial creation happens in stages

The normal dispatcher path is:

  1. Construct Main, passing the supplied Patch and Turtle classes.
  2. Initialize runtime services, parameters, the clock, world dimensions, collections, and scheduling structures. This calls Main's init().
  3. Apply cell properties and the selected visit mode, then call Main's initCells(). A typical implementation calls this.initializePatches(this.MyPatch).
  4. For an agent model, apply agent properties, then call Main's initAgents(). A typical implementation calls this.initializeTurtles(this.MyTurtle).

Cell and agent initialization arrive as separate messages. Do not assume the population already exists while constructing patches or during Main's first init() call.

Main constructor and init()

In the constructor, call super() first, retain the supplied class references, and allocate persistent model-owned objects. Parameters, the clock, and populated world objects are not yet available.

Main's init() runs after runtime parameters are installed and again during reset. Preserve super.init(): it clears queued births/deaths and initializes the clock, including simulation time zero.

Use init() for repeatable model-state initialization, not unconditional listener registration. Main survives reset; another listener added on each call will accumulate.

Patch construction

The patch base constructor establishes coordinates, references to Main and the world, and the built-in color Store. Supplied patch templates explicitly call their own init() from the subclass constructor.

NLPatch does not itself call an init() hook. Its setup() is an empty method, and the ordinary matrix initialization path does not automatically call it. Do not put required patch initialization there unless your model explicitly invokes it.

Turtle construction, defaults, and setup

For each initial turtle:

  1. The subclass constructor runs. The base constructor has allocated built-in Stores, but their values may still be unset.
  2. The population initializer fills uninitialized built-in state from configured defaults/start data.
  3. The initializer calls the turtle's init().
  4. After inserting the population, the population helper calls setup() on its turtles.

Two current compatibility details matter:

Allocate one-time state and listeners once per object. Make setup repeatable. Do not assume location, color, or other configured state is populated during the constructor's init() call.

Main's setup() is different: its base reset path calls it, but the normal dispatcher initialization path does not. A model needing a shared setup operation on the first run and reset must explicitly arrange both calls after the required objects exist.

3.3 One ordinary clock tick

For a patch/agent model using the ordinary non-RK4 clock iterator:

sequenceDiagram
    participant Clock
    participant Main
    participant Objects as Patches / turtles
    participant State as Registered Stores
    Clock->>Main: step()
    Main->>Main: coordinate()
    Main->>Objects: selected patchStep() / turtleStep() calls
    Objects->>State: write pending next values
    Note over Main,State: Slash modes add an intermediate shift here
    Clock->>Main: shift()
    Main->>State: commit touched Stores
    Main->>Main: clear transient visit bookkeeping
    Main->>Main: remove queued deaths
    Main->>Objects: enabled shift hooks, if mapped
    Main->>Main: insert queued births
    Clock->>Clock: time += dt
    Clock->>Main: update()

coordinate() and individual step rules observe the tick's starting time. update() observes the advanced time and committed state. For dt: 1, the first tick calls coordinate() at time 0 and update() at time 1.

Output/status publication follows a tick through the runtime. Main's statusToJson() is not a read-only inspector: it consumes pending display data, clears patch-report bookkeeping, and evaluates stopping conditions.

3.4 Visit modes and visibility of changes

The following assumes both relevant populations exist:

Project visitMode Step order Commitment
"C" Patches Final shift
"A" Turtles Final shift
"C_A" Patches, then turtles Final shift; turtles normally read old patch state
"A_C" Turtles, then patches Final shift; patches normally read old turtle state
"C/A" Patches, shift, then turtles Intermediate and final shifts
"A/C" Turtles, shift, then patches Intermediate and final shifts
"No" No automatic patch/turtle step calls Main still coordinates, commits touched Stores, and updates

"Old state" assumes rules read current values and write next values. Immediate writes and ordinary JavaScript properties are visible immediately and therefore can make results order-dependent.

An intermediate shift is the full Main shift operation: it can commit unrelated touched Stores, remove deaths, and insert births. For example, a patch-created child can step during the same tick in "C/A", but not in "C_A".

Current shift-hook limitations

Never manually shift built-in Stores inside turtleShift() or patchShift() just because the hook is named "shift". The commitment has already occurred. Writing a next value in a shift hook is not equivalent to writing it in a step rule; prefer step rules unless the later scheduling is intentional and tested.

3.5 Which patches receive steps?

Choosing a patch visit mode does not necessarily visit every patch.

The scheduler makes separate patch-visitation passes. The sets are not globally deduplicated; a patch can be reached more than once. With visitAll, each pass can traverse the entire matrix. Do not assume "one tick" means "exactly one call per patch". Active-cell examples such as Life guard with hasSeen/doneWith. Transient visit/seen bookkeeping is cleared during shift.

Sparse Life rules schedule each initially live patch and its neighbors, then repeat that registration for changed patches in patchShift(). Registering only the ring misses isolated live cells at startup and can skip a newly born cell's next survival check when none of its neighbors changed. The Life and HexLife library models therefore include this as well as this.ring() in visitHereNextTime().

3.6 Movement belongs in the step rule

In "Trajectory" movement mode, super.turtleStep() schedules movement using the pending speed and heading. Call ordering determines which heading it uses. For a turn that affects this move, use this method inside your Turtle subclass:

turtleStep() {
    this.nextTheta = this.theta + 0.1;
    super.turtleStep();
}

Putting the heading assignment after super.turtleStep() schedules the move first and the turn afterwards. The supplied random-walk template intentionally uses that ordering.

For "Next_Location", your rule chooses the pending location. Do not schedule another trajectory move unless two movement operations are intentional. Use documented next-state movement operations rather than legacy forward() or fd(); those methods have known unresolved defects.

3.7 Births and deaths

Creation helpers return a new object immediately, but population insertion is queued. Use their customization callbacks to initialize child-specific state. The birth path constructs the class but does not automatically perform the initial population's post-default init() and setup() sequence.

Use a regular callback function if you need the helper's callback this: createTurtleFromParent binds it to the parent; createTurtleFromPatch binds it to the patch. An arrow function retains its lexical this.

Calling turtle.die() marks the turtle as a zombie and queues its ID for removal. Patch agent-list queries exclude zombies immediately, although the turtle remains in the population until shift. Do not assume every population iteration filters zombies.

Newborns are inserted after shift hooks and indexed spatially. Patch agent-list queries exclude them while their newbie flag is true. In a mapped turtle shift hook, the base turtleShift() clears that flag. Insertion, ability to execute a step, and visibility in queries are therefore distinct events.

3.8 What Reset actually does

The base Main reset currently:

  1. Halts the clock and calls Main's init().
  2. Resets coordinate bookkeeping and clears the spatial grid.
  3. Calls reset() on existing patches.
  4. Recreates the turtle population from its stored starts using the population initializer and turtle setup.
  5. Resets stocks, if present, and calls Main's setup().

Main and patch identities survive. Turtle identities do not: do not retain references to old turtles in Main collections or surviving patch fields. Rebuild relationships to the new population explicitly.

Main reset does not achieve population recreation by calling each old turtle's reset() method. The individual turtle reset method is a separate operation: it restores Stores in that turtle's reset list, then calls setup().

The patch base reset restores its color only. Custom patch state requires an explicit reset policy. Main's base init() does not automatically reinitialize all custom Main Stores either. See Managing model state.

Reset is not a promise to reproduce an identical random run. Some starts are retained, other custom state may be drawn again, and the random utilities do not provide a single documented reproducibility policy.

3.9 Hook checklist

Hook Preserve / remember
Main init() Call super.init(); avoid assuming the first population exists
Main initCells() Explicitly initialize the required patch class
Main initAgents() Explicitly initialize turtles; the base has an unresolved legacy overload, so do not call super.initAgents() as a no-op
Main coordinate() Global pre-step work; base implementation is empty
Main update() Post-commit work and outputs; base implementation is empty
Main reset() Preserve super.reset() unless deliberately replacing the entire lifecycle
Patch patchStep() / patchShift() Legacy base methods are empty; aligned patchStep() evaluates the color updater. Visitation and shift hooks must be enabled as needed
Patch reset() Preserve super.reset() to restore color; reset custom state explicitly
Turtle turtleStep() Base schedules trajectory movement; omission or reordering changes movement
Turtle turtleShift() Preserve the base newborn bookkeeping
Turtle reset() Base restores its reset-list Stores and calls setup(); not the ordinary Main population-reset path

3.10 Opt-in aligned lifecycle

New or deliberately migrated patch/agent models can set this top-level Project option:

const Project = {
  lifecycle: "aligned",
  // Existing cell, agent, input, and display configuration...
};

Only the exact string "aligned" enables this contract. Omitting the option, using "legacy", or using any other value retains legacy behavior. The canonical Agents, Life, Antz, Diffuse, Dla, and EGOL3 apps and their embedded model-library copies, plus standalone Ani2, have deliberately opted in. Other shipped apps retain legacy behavior unless explicitly configured otherwise. This option does not change SD initialization, visit modes, movement, or step/shift boundaries.

EGOL3 allocates and synchronizes its level Store in the patch constructor: configured color is assigned before patch init, and its overridden setter needs the Store immediately. init enables patch shifting without replacing that Store or its configured value. Main setup recounts the restored world after patch reset; reset itself does not reconstruct the RAM or its listener. See the EGOL3 evidence.

Responsibilities and ordering

Aligned step hooks also own built-in updater evaluation. super.patchStep() evaluates an assigned colorUpdater. super.turtleStep() schedules trajectory movement first, then evaluates heading, color, size, and shape updaters in that order. Only assigned updaters run, and they write pending state. Remove explicit built-in updater evaluations when migrating, but keep the base calls. This preserves Antz's movement-before-heading-update ordering. Legacy base-step behavior and custom Store updater evaluation are unchanged. See public updater properties.

Hook Aligned responsibility
constructor(...) Allocate Stores, collections, and stable object fields once per object. Call super(...); do not call init() or setup().
init() One-time initialization/registration after runtime context and built-in defaults exist. Do not call it from reset.
reset() Restore the existing object's local state only. Call super.reset(). Do not invoke init/setup or reset other populations.
setup() Establish relationships and derived state once per completed world initialization/reset; the runtime owns invocation.

Main construction precedes runtime context. Its init() runs after initialize() and initCellProps() establish dimensions, parameters, clock, coordinates, transport, visit mode, cell size, and palette. Populations and agent configuration are not yet available to Main init. Main initCells() and, when configured, initAgents() still create the populations through initializePatches(Class) and initializeTurtles(Class, customization). Provide those model-specific population hooks as in existing models.

Patch initialization is constructor → initial cell colors → init(). All patches have been allocated before their init hooks run, but turtles need not exist yet. Turtle initialization is constructor → built-in defaults/start values → init() → optional population customization callback. Aligned patch generators may be ES classes or factories returning a patch instance (including subclass instances). Antz uses a factory selecting ordinary, food, and nest patches. The runtime calls the returned instance's polymorphic init() once, after allocation and initial colors; subclass init hooks must call super.init() when they extend parent initialization. Turtle generators are constructors/classes. Aligned allocation does not use a constructor-to-factory retry, so a thrown allocation error propagates without executing the generator twice. Cartesian and hex factory paths are tested.

After both configured populations exist (including an agent population of size zero), setup runs in this order:

  1. Each patch's setup().
  2. Each turtle's setup().
  3. Main's setup().

Patch-only projects complete this sequence immediately after initCells(). The presence of Project.agent, not the visit mode or population count, determines whether initialization waits for agents.

Reset ownership

The browser's Reset operation calls main.resetWorld(), the aligned world coordinator. Programmatic clients should do the same, or send {op: "reset"}. Calling main.reset() directly is deliberately local in aligned mode; it no longer means resetting the entire world. Its base implementation clears birth/death and pending scheduling queues and resets the clock. Override it to restore custom Main state without reallocating objects.

resetWorld() halts execution, invokes Main's local reset, resets coordinate bookkeeping and the spatial grid, resets surviving patches, recreates turtles from their starts, resets stocks, then runs the patch → turtle → Main setup sequence. Main and patch identities survive; turtle identities still do not. Each replacement turtle gets its own constructor and init, followed by population customization and one setup. An individual turtle's local reset restores its Store reset list without invoking setup.

Do not register long-lived Main/parameter listeners from setup. Turtle recreation also means turtle registrations on a longer-lived object need an explicit ownership/disposal policy; opt-in is not automatic leak repair.

Births

All three customization helpers (createTurtleFromParent, createTurtleFromPatch, createTurtleFromSpecs) now run constructor → inherited/default built-in values → init → customization. Children still queue for insertion at the existing shift boundary. Once the whole birth batch is inserted and indexed, each child's setup runs once. The lower-level createTurtle() follows the same init/deferred-setup rule.

Setup may create another birth; it remains queued until the next insertion boundary. Existing newborn visibility, stepping, and newbie-flag behavior is unchanged. Do not manually call child init/setup in aligned customizers.

Browser/runtime protocol

The browser forwards Project.lifecycle in initCells only for aligned mode, along with awaitAgents: !!Project.agent. External message producers opting in must supply the same boolean (omitted means patch-only). For agent projects the first cellinit is a population-request handshake, without evaluating displays before setup. After agents and setup complete, agentinit carries final agents and worldinit carries display data and a full patch snapshot. The browser handles worldinit without requesting agents again. Patch-only initialization uses the normal cellinit reply after setup. Legacy messages and their ordering are unchanged. After aligned world reset, the normal status reply includes every patch, including colors changed directly by setup.

Migration checklist

The executable contract is nlapi-aligned-lifecycle.test.mjs. The Agents source-derived legacy comparison and the representative app tests are in nlapi-apps.test.mjs. That comparison exercises dispatcher startup and reset with deterministic random draws; it confirms matching population, trajectories, status output, and speed parameter behavior in its controlled scenario. It also confirms the expected difference in hook multiplicity: the aligned runtime initializes Main once, initializes each new turtle once after defaults, and runs world setup once per world initialization/reset. Life and Antz now have additional source-derived legacy comparisons for their canonical and embedded copies; see the migration evidence. These checks do not certify every saved or externally supplied application.

Diffuse and Dla now also have canonical/library comparisons and repeated-reset regressions. Diffuse Main setup replays its original pre-tick turtle step at startup and reset; patch registration stays in one-time init. Dla reset explicitly restores its ordinary radius/count fields, resets patch status and both color slots, and publishes count from Main setup. Studio Dla retains its class-scoped Status and init-time turtle binding. See the migration evidence.

Sources and evidence

The behavior above is grounded in Main, Clock, dispatcher, patch matrix, turtle population, Patch, and Turtle.

Compatibility tests cover lifecycle ordering, visit modes, defaults/setup multiplicity, birth/death timing, movement, and reset. Shipped-app tests cover Life, Agents, Follower, and a controlled Fng scenario. Dispatcher startup details and other cautions identified from source are not certification of every browser, topology, collision mode, or externally saved app.