On this page
- Aligned lifecycle: the model author's quick guide
- 3.1 Who owns execution?
- 3.2 Initial creation happens in stages
- 3.3 One ordinary clock tick
- 3.4 Visit modes and visibility of changes
- 3.5 Which patches receive steps?
- 3.6 Movement belongs in the step rule
- 3.7 Births and deaths
- 3.8 What Reset actually does
- 3.9 Hook checklist
- 3.10 Opt-in aligned lifecycle
- Sources and evidence
3. Execution and lifecycle
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:
- Construct Main, passing the supplied Patch and Turtle classes.
- Initialize runtime services, parameters, the clock, world dimensions,
collections, and scheduling structures. This calls Main's
init(). - Apply cell properties and the selected visit mode, then call Main's
initCells(). A typical implementation callsthis.initializePatches(this.MyPatch). - For an agent model, apply agent properties, then call Main's
initAgents(). A typical implementation callsthis.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:
- The subclass constructor runs. The base constructor has allocated built-in Stores, but their values may still be unset.
- The population initializer fills uninitialized built-in state from configured defaults/start data.
- The initializer calls the turtle's
init(). - After inserting the population, the population helper calls
setup()on its turtles.
Two current compatibility details matter:
- The supplied template also calls
this.init()in its constructor, soinit()can run twice: once before defaults and once after them. initializeTurtles()currently invokes population setup again, so the first population creation calls each turtle'ssetup()twice. Population recreation during Main reset calls it once.
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
- Patch shift hooks require
this.doPatchShift = trueon Main and a mode with a patch shift mapping. Only the selected patch sets are visited. - Mapped turtle shift hooks run after pending Stores have been committed.
Preserve
super.turtleShift()for newborn bookkeeping. - The current
"C/A"and"A/C"mappings do not call shift hooks. Their aliases are assigned before their target mappings exist. Store commitment still happens. This is a known defect retained for compatibility. - Do not rely on
doTurtleShiftto suppress mapped turtle hooks; the current scheduler does not consult that flag.
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.
main.visitAlways(patch)registers a persistent patch visit.main.addToNextSites(patchOrIterable)adds patches to the transient visit set.main.visitAll = truemakes patch visitation traverse the whole world.hasSeen(patch)anddoneWith(patch)let a rule guard against duplicate work.
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:
- Halts the clock and calls Main's
init(). - Resets coordinate bookkeeping and clears the spatial grid.
- Calls
reset()on existing patches. - Recreates the turtle population from its stored starts using the population initializer and turtle setup.
- 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:
- Each patch's
setup(). - Each turtle's
setup(). - 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
- Opt in only after removing constructor calls to init/setup.
- Keep allocation and one-time registrations out of reset/setup.
- Move population-dependent Main initialization into setup.
- Give custom Stores an explicit local reset policy; call the base reset.
- Replace programmatic world-reset calls with
resetWorld(). - Remove manual population/child setup calls.
- Test repeated reset and births; do not assume old turtle references survive.
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.