On this page
Tools and shared utilities
Manual contents | First-model tutorial | Neighborhoods | NLRoot reference | Supporting types
Use named states to make rules readable, arrays/Sets to organize collections, and random draws to express modeling assumptions. You do not need to use every helper below. All examples follow the aligned guide; these utility operations themselves do not select a lifecycle mode.
Importing and calling helpers
Helpers from tools.js are named imports:
import {Enum, EnumSym, Property} from "@nwk/api/tools.js";
Shared model utilities are on NLRoot, which inherits static math/random utilities from APIRoot:
import {NLRoot} from "@nwk/api/nlroot.js";
const chance = NLRoot.random();
Static means call the class, as in NLRoot.random(), not
this.random(). Instance methods such as this.any(...) are inherited by
Main, Patch, and Turtle. A function passed to a helper is a callback:
patch => patch.color === 1 means "given a patch, return whether it is red."
Named states and choices
Enum(...slots)
Creates an object whose named entries are the supplied slot objects.
Each slot needs a unique name. Entries receive:
.ord: zero-based declaration position..toString(): returns the slot's name.- Any extra fields you supplied, such as a label or count.
The returned enum has values() (the slot array in order) and
ordinal(slot) (the slot's .ord).
const Health = Enum(
{name: "Susceptible", label: "Can catch infection"},
{name: "Infected", label: "Infectious"},
{name: "Recovered", label: "No longer infectious"}
);
let state = Health.Infected;
const isInfected = state === Health.Infected;
const colorIndex = state.ord; // 1
const name = String(state); // "Infected"
const first = Health.values()[0]; // Health.Susceptible
const ordinal = Health.ordinal(state); // 1
Store the slot object when you want named states; store .ord when a palette
or numeric format needs an index. They are different types:
Health.Infected !== 1. To convert an index back, use Health.values()[index]
after checking the index is valid.
The fire tutorial uses names in rules and ordinals as patch colors, without introducing a custom Store.
Mutation and reset: the enum container is frozen, but its slots are not.
The helper mutates your supplied slots when it installs ordinals/toString,
and values() returns the original array, not a copy. Do not sort or splice
that array; copy it first if needed. Avoid reserved names values and ordinal;
names are not validated. Duplicate names overwrite the named lookup.
ordinal does not check membership.
Slots can carry mutable fields, but a module-level Health.Infected.count
is shared by every model object using that module. It is not automatically
reset or synchronized. For most beginners, put changing counts on Main and
compute them from the population instead of treating the enum as a population.
Do not change declaration order if saved numeric states rely on .ord.
EnumSym(...names)
Creates named Symbol values instead of objects:
const Mode = EnumSym("Searching", "Resting");
let mode = Mode.Searching;
const isSearching = mode === Mode.Searching;
const ordinal = Mode.ordinal(mode); // 0
values() returns the original Symbol array; ordinal(value) returns its
index, or -1 for an unknown value. The container is shallow-frozen.
There are no .ord or custom per-slot fields on the Symbols.
Use unique names other than values/ordinal.
Symbols are not ordinary JSON state values: serialize an agreed name or index
deliberately, not the Symbol itself.
Supplied choices: Shape, Dir, VisitMode, BoundaryMode
import {Shape, Dir, VisitMode} from "@nwk/api/nlsimmain.js";
import {BoundaryMode} from "@nwk/api/nlmatrix.js";
| Type | Representation | Examples |
|---|---|---|
Shape |
EnumSym | Shape.Circle, Shape.Rectangle |
VisitMode |
EnumSym | VisitMode.C, VisitMode.A, VisitMode.C_A, VisitMode["C/A"], VisitMode.No |
BoundaryMode |
EnumSym | BoundaryMode.Wrap, BoundaryMode.Edge |
Dir |
Enum slots | Dir.E, Dir.SE, Dir.S, Dir.SW, Dir.W, Dir.NW, Dir.N, Dir.NE |
Each Dir slot has theta (radians), dx, and dy.
East is angle zero, positive y points down, and South is Math.PI / 2.
For example this.nextTheta = Dir.N.theta schedules a northward heading.
Project uses configuration strings, not these imported runtime objects:
visitMode: "C", cell.boundary: "Edge",
agent.defaults.shape: "circle". In model code, turtle shape is a Shape
Symbol, not the string "circle". Keep configuration and runtime values apart.
Collections: arrays and Sets
An array is an ordered list: [a, b, c]. A Set holds unique object identities.
patch.ring(1) returns a Set; main.patches.toList() returns an array.
const neighbors = [...this.ring(1)];
const burning = neighbors.filter(patch => patch.color === 1);
const colors = neighbors.map(patch => patch.color);
const anyBurning = neighbors.some(patch => patch.color === 1);
const count = burning.length;
[...set] makes an array copy of a Set. Array filter, map, some, and
reduce do not step patches or synchronize state. for...of works on both
arrays and Sets. A Set uses .size; an array uses .length.
Do not assume every API collection supports every array method.
Random selection and summaries
| Signature | Result and important behavior |
|---|---|
NLRoot.one_of(array) |
One random element; undefined if empty; non-array throws TypeError |
NLRoot.n_of(size, array) |
Shuffled shallow sample of at most size positions, without replacement; input unchanged; size must be a nonnegative integer (RangeError otherwise); non-array throws TypeError |
NLRoot.shuffle(array) |
Shuffles the supplied array in place and returns that array |
NLRoot.sum(fn, array) |
Sum of fn(element); zero for an empty array |
NLRoot.mean(array) |
Arithmetic mean; NaN for empty input |
NLRoot.meanPMStd(array) |
[mean, mean + populationSD, mean - populationSD]; all NaN for empty input |
this.any(predicate, elements) |
True if any value satisfies the predicate; uses elements.values() |
this.count(predicate, array) |
Number satisfying the predicate; uses array filter |
this.max_of(elements, score) / this.min_of(elements, score) |
Selected element, not score; first tied element retained; null if empty |
this.max_one_of(score, elements) / this.min_one_of(score, elements) |
Selected element; randomized tie decisions, not a promise of uniform tie sampling; null if empty |
this.min_one_of_value(score, elements) |
Minimum score, not element; Number.MAX_VALUE if empty |
Selection methods with elements.values() accept arrays or Sets.
Use finite numeric scores; these helpers do not validate score results.
n_of samples positions: equal values or repeated object references in the
input can appear more than once in the result.
Methods have different argument orders; copy the signature rather than
assuming every helper takes collection first.
const neighbors = [...this.ring(1)];
const chosen = NLRoot.one_of(neighbors);
if (chosen !== undefined) this.nextColor = chosen.color;
const pair = NLRoot.n_of(2, neighbors);
Compatibility caveat: this.max_one_of_value(fn, elements) currently expands
each element as an argument list (fn.apply(this, element)), unlike the
minimum counterpart. It works for tuple elements such as [x, y], not an
ordinary patch => patch.color callback. For nonempty patch arrays, use
Math.max(...patches.map(patch => patch.color)); decide your own empty policy.
The tuple form remains preserved for existing apps.
this.map(elements, mapper) and this.forEach(elements, visitor) simply
delegate to those collection methods. this.filter(elements, predicate)
has several legacy overloads; prefer ordinary array .filter after toList()
or [...set] rather than relying on its iterator/class dispatch.
Random draws
These are static methods inherited from APIRoot. Give valid numeric inputs; most do not validate ranges.
| Call | Intended modeling use |
|---|---|
NLRoot.random() |
Uniform number from 0 inclusive to 1 exclusive |
NLRoot.random(high) |
Uniform from 0 to positive high, excluding high |
NLRoot.random(low, high) |
Uniform from low to high, excluding high, for ordinary positive bounds |
NLRoot.irandom(high) |
Integer from 0 through high minus one, for positive integer high |
NLRoot.irandom(low, high) |
Integer from low through high minus one, for ordered integer bounds |
NLRoot.flip(p) / NLRoot.bernoulli(p) |
Number 1 with probability p, otherwise 0 (not a Boolean) |
NLRoot.normal(mean, sd) |
Normally distributed value; sd is standard deviation |
NLRoot.poisson(lambda) |
Poisson draw for positive lambda |
NLRoot.binomial(n, p) |
Count of successes; exact Bernoulli summation through n = 100000, approximated above that |
NLRoot.choose(n) |
Random integer palette/choice index from 0 through n minus one |
NLRoot.choose(...weights) |
Index selected in proportion to positive weights |
The simplest probability test is NLRoot.random() < p, with 0 <= p <= 1.
Choose where to draw deliberately: one draw per exposed tree is a different
model from one draw per burning neighbor.
The random implementation uses truthiness defaults: random(0) means a unit
draw, not a constant zero; a zero lower bound in the two-argument form also
does not behave as an ordinary interval API would. For general ordered bounds
use low + (high - low) * NLRoot.random(). irandom tests its second argument
by truthiness too; avoid using that overload with upper bound zero.
There is no promised single seeded/replayable random stream: random() uses
Math.random, while irandom() uses a separate module generator. Reset does
not rewind random draws. For a beginner's reproducible check, first make the
rule deterministic, as in the tutorial.
Strings
NLRoot.parse(text)
Splits text into an array of whitespace-separated tokens, keeping double-quoted groups together and removing their outer quotes:
const words = NLRoot.parse('tree "burning patch" ""');
// ["tree", "burning patch", ""]
Empty/whitespace-only text yields []. Backslashes and single quotes are
literal characters, not escape/grouping syntax. Double quotes must start a
token and a closing quote must be followed by whitespace or the end of input.
Malformed quoting throws SyntaxError; a non-string throws TypeError.
This is not a CSV, JSON, or JavaScript parser.
NLRoot.unquotify(text)
Removes one matching pair of outer double quotes; otherwise preserves the string, including whitespace. It does not parse escapes or strip single quotes. Pass a string.
Change notifications: Property
new Property(value = null) holds an immediate value and a Set of listeners.
It is not a Store: it has no pending value, shift, synchronization, or
automatic reset.
const selected = new Property("Tree");
const listener = (property, oldValue, newValue) => {
console.log(oldValue, "->", newValue);
};
selected.addListener(listener);
selected.value = "Burning";
const current = selected.get();
selected.removeListener(listener);
get()/set(value) are aliases for reading/writing .value.
Listeners receive (property, oldValue, newValue) synchronously when setting
a value not loosely equal to the old value (old == new suppresses notification).
For example changing numeric 1 to string "1" stores the new value but does
not notify. In-place mutation of an object is not detected.
Repeated registration of the same function is deduplicated; a fresh arrow
function is a different listener. Allocate long-lived Property/listener wiring
once and reset owned values explicitly if needed.
Smaller supporting tools
These named exports are also from tools.js:
| Helper | Contract |
|---|---|
isIterable(value) |
True if non-null value has a callable Symbol.iterator; arrays, Sets, and strings qualify |
isClass(value) |
Source-text test for a function whose string starts with "class "; not a general type/constructor check |
new Complex(re, im) |
Complex number with realPart, imaginaryPart, norm, theta (radians), and conjugate |
complex.add(other) |
New Complex containing the sum |
complex.times(other) |
New Complex containing the product |
complex.equals(other) |
True for another Complex with exactly equal real/imaginary parts |
complex.toString() |
Text in the implementation's re + imi form |
Complex operations do not mutate their operands, but the object is not frozen.
Do not change .re/.im after construction: cached norm/angle/conjugate would
then be stale. Use new Complex values. The third constructor argument is an
internal conjugate link, not an ordinary model parameter.
For synchronous model variables use Store and public state accessors, not Property or mutable enum fields as substitutes for pending state.