On this page
- 1. Statement anatomy
- 2. Model variables: deriv, next, and initial
- 3. Terms
- 4. Binding inputs: sliders and switches
- 5. Routing output: update
- 6. Stopping early: quitWhen
- 7. Primops and special operators
- 8. Blocks, functions, and the iterator tool
- 9. How NuML becomes a running model
- 10. Rules and pitfalls
Model logic with @=
Manual contents | Tutorial | Dashboard configuration | Platforms | Where NuML runs
The @= operator carries all of a NuML program's model logic: state
variables and their dynamics, intermediate terms, input bindings, output
routing, and the stop condition. This chapter specifies each statement form
and then explains how the compiler turns them into a running simulation.
Sources: uberbuilder.js,
numlTransformer.js,
apiTools.js.
1. Statement anatomy
A NuML program is a sequence of statements, one per line (a trailing ; is
optional), parsed with JavaScript syntax extended by the two operators:
LHS @= RHS // model logic (this chapter)
LHS #= RHS // configuration (next chapter)
The first line of the program must be the string 'numl' (or "numl") —
that header is how the runtime recognizes the text as NuML
(the engine contract). // line
comments and /* */ block comments are allowed anywhere.
The @= statement forms:
| Form | Declares |
|---|---|
deriv(X) @= expr |
X is a continuous model variable with derivative expr |
next(X) @= expr |
X is a discrete model variable; expr is its value at the next step |
initial(X) @= expr |
the starting value of model variable X (required for every deriv/next) |
name @= expr |
name is a term — a named intermediate value |
name @= Slider1 |
binds term name to an input component |
update(a, b, …) @= Display |
routes values to a display once per step |
quitWhen @= condition |
stops the run when the condition becomes true |
name @= iterator(fn, x0, n, burn) |
creates an iteration tool (section 8) |
Right-hand sides are ordinary JavaScript expressions over numbers, strings,
model variables, terms, inputs, primops,
and Math.* calls. All multiplication must be explicit — 2 * S, never
2S.
2. Model variables: deriv, next, and initial
A model variable is a function of model time — an SD stock. It is
declared by deriv or next and must receive a matching initial:
deriv(S) @= -beta * S * I / N // continuous: dS/dt = …
initial(S) @= N - I0
next(L) @= R * L * (1 - L) // discrete: L(t+1) = …
initial(L) @= Init
derivasserts a differential equation. The integrator (Euler or RK4, chosen in integration configuration) advances the variable eachdt. Usemethod("RK4")forderivmodels.nextasserts a difference equation: the right-hand side replaces the value at each step, with nodtscaling. Usemethod("Euler"), conventionally withdt(1).initialmay be any expression, including slider-bound terms; it is re-evaluated on every Reset, so each run starts from the current dashboard settings.
Always write the deriv/next statement before its initial. Every
variable referenced in any equation must be defined somewhere — as a model
variable, a term, an input binding, or a numeric constant you assign; there
are no phantom variables.
Within one time step, every equation reads the same committed values of all model variables; new values take effect together at the end of the step. Declaration order therefore never changes the result.
3. Terms
A term is a named value recomputed from current state — an SD auxiliary:
lambda @= beta * I / N
NewInf @= binomial(S, lambda)
Semantics that matter in practice:
- A term is evaluated at most once per integration step (once per stage
under RK4) and the value is shared by every reference in that step. This
memoization is what makes stochastic terms sound: in the stochastic SIR
preset,
NewInfis drawn once and the same draw is subtracted fromSand added toI— conservation holds. - Terms are not carried through time. To accumulate, use a model variable
(
next(Total) @= Total + NewInf). - A term may be defined as an arrow function; it is invoked when read:
f @= (x => x * x)is legal, but plain expressions are the normal style.
4. Binding inputs: sliders and switches
N @= Slider1
vacc @= Switch1
A term whose right-hand side is exactly an input component's name reads that
component's current value on every evaluation — live, mid-run. Switches
yield 0 or 1. Give every input you bind a configuration statement
(Slider1 #= [legend("N"), …]) so the dashboard shows a labeled, ranged
control (sliders and switches).
Input names may also appear directly inside expressions
(deriv(x) @= Slider2 * x), but binding them to a named term keeps the
model readable.
5. Routing output: update
update(S) @= TimeGraph
update(I, R) @= TimeGraph1
update(N, Survivors, Recruits) @= Table01
Once per completed step, each update sends the listed values — model
variables or terms — to the named display. Write one update per
display, listing all of its series; their order matches the display's
legend. Phase graphs consume the values as coordinate tuples
(update(x, z) @= PhaseGraph1 plots z against x).
A value can also be recorded without occupying a visible graph using the
synthetic target update(TotNewInf) @= TimeSeries("TotNewInf") — used by
the stochastic platform
(recording without a graph).
6. Stopping early: quitWhen
quitWhen @= (N <= 0)
The condition is checked after every step; when it is true the run halts
(before maxTime). The time at which a run stopped is available to the
stochastic platform as quitTime
(quitWhen and quitTime). Never use
quitWhen or quitTime as a sensitivity target.
7. Primops and special operators
Primops are convenience functions usable in any @= expression without
the Math. prefix. They are the APIRoot statics documented in the
SD manual's utilities table:
| Group | Primops |
|---|---|
| Random draws | random(x, y?), irandom(x, y?), normal(m, s), flip(p), binomial(n, p), poisson(lambda), choose(…), multinomial(N, …) |
| Trigonometry | cos, sin, tan, acos, asin, atan, atan2, cosh, sinh, tanh, acosh, asinh, atanh |
| Constants | PI, PI2 (2π), Pi2 (π/2), Pi34 (3π/4), degToRad, radToDeg |
Everything else from JavaScript's Math object is available with the prefix:
Math.exp, Math.max, Math.log, Math.sqrt, and so on.
Special operators expand in place:
| Operator | Meaning |
|---|---|
currentTime |
the current model time |
nextTime |
currentTime + dt |
initial(X) (on a right-hand side) |
the initial value of model variable X |
timeWhen(cond) |
the current time while cond is true, otherwise null; used to record event times for stochastic histograms |
8. Blocks, functions, and the iterator tool
A right-hand side may be a brace-enclosed block; the normalizer wraps it as
an immediately invoked function, so it may contain statements and must
return its value:
next(r) @= { loop.step(r); return r + rdelta; }
The iterator tool runs an inner iteration each outer step — the engine behind the bifurcation-diagram preset:
loop @= iterator(x => r * x * (1 - x), x0, iterations, burnin)
next(r) @= { loop.step(r); return r + rdelta; }
initial(r) @= r0
update(loop) @= PhaseGraph
iterator(fn, x0, n, burn) iterates fn from x0 n times, discarding the
first burn values; loop.step(p) tags the retained values with parameter
p, and update(loop) @= PhaseGraph plots the point cloud. Iterator state
resets with the run.
9. How NuML becomes a running model
The @= compiler (uberbuilder.js)
translates the program onto the SD runtime documented in the
SD API User Manual:
| NuML | Runtime construct |
|---|---|
deriv(X) + initial(X) |
a Stock with flow = () => expr |
next(X) + initial(X) |
a sequence stock (replaced, not accumulated) |
| term | a getter memoized per integration step |
| input binding | a parameter read (getParam) against the hosting app's input codes |
update(…) @= D |
a per-step show(D, …) call |
quitWhen |
the main class's stopWhenTrue predicate |
Identifiers in each expression are resolved in this order: special operators,
primops, trig/Math shorthands, input codes, model variables, terms, then
iterator tools; anything left unresolved is passed through to JavaScript and
fails at run time as an undefined variable. The two-phase step cycle —
simultaneous reads, synchronized commitment, Euler versus RK4 — is exactly
the one specified in
the SD simulation cycle.
Because RK4 evaluates every deriv expression four times per dt at
provisional states, randomness belongs in next models run under Euler,
never inside a deriv under RK4
(integration guidance).
10. Rules and pitfalls
- Start with
'numl'. The header string must be the first line. - Explicit multiplication.
0.5 * G2,0.5 * (1 - r)— never0.5G2or0.5(1 - r). - Every model variable needs
initial, and thederiv/nextcomes first. - No phantom variables. Every right-hand-side identifier must be a
model variable, term, input, primop, or
Mathmember. Assign invented constants explicitly (gravity @= 9.81). - Names must match exactly across
deriv/next,initial,update, and expressions —CentralandcentralCompartare different variables. - One
updateper display, withlegendarity to match. Math.prefix for JavaScript math in model expressions; no prefix inside#=configuration directives.- Match method to statement type:
derivwithmethod("RK4");nextor stochastic draws withmethod("Euler"). - Parse and runtime errors surface in a Numl Error(s) dialog with the offending line; fix the first error reported and re-apply.