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:

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.