Calculus
D, ic, and the rewrite-target sugar grad / div / laplacian / integral.
D — derivative
D has two tiers, and which one a node is in depends entirely on wrt.
Structural time derivative — evaluable
wrt: "t" (or wrt absent, which means t) is the structural time derivative.
It is strictly unary: args holds exactly the differentiated variable.
Text
D(N)/Dt
JSON
{ "op": "D", "args": ["N"], "wrt": "t" }
This is what makes a variable an ODE state, and it belongs on an equation’s left-hand side, where system assembly consumes it:
Text
D(N)/Dt = (-lambda) * N
JSON
{
"lhs": { "op": "D", "args": ["N"], "wrt": "t" },
"rhs": { "op": "*", "args": [{ "op": "-", "args": ["lambda"] }, "N"] }
}
A common mistake is writing the independent variable as a second argument —
{"op":"D","args":["N","t"]}. That is wrong: t goes in wrt, and a wrt:"t"
node with more than one argument is rejected by the schema.
Spatial derivative — rewrite target
Any other wrt — or any D appearing on a right-hand side — has no
evaluator. It must be lowered to a stencil by a
rewrite rule
before the document can run.
Text
D(C)/Dx
JSON
{ "op": "D", "args": ["C"], "wrt": "x" }
A spatial D may carry trailing operands after args[0]:
{ "op": "D", "args": ["C", "C_west", "C_east"], "wrt": "x" }
These are auxiliary data for the discretization rule — canonically the per-face
boundary or halo values of a closure. They carry no evaluator semantics:
they do not change the derivative’s value, its dimension, or its eligibility for
lowering. A dimensional checker applies the D rule to args[0] alone and
ignores the rest entirely.
Their count is unbounded and their meaning is positional and rule-defined — the format assigns no role to any position, and a binding must not infer one. By convention rule libraries list per-face operands in ascending order along the differentiated axis, but that is readability only, never enforced.
Because pattern and node args must match in length, a rule written for a bare
D(f, wrt:x) does not fire on a D carrying trailing operands, and vice
versa. That is intended: the closure is part of the rule’s identity, so a rule
that does not consume the boundary data cannot silently discard it.
ic — initial condition
Used as an equation LHS. args[0] is the ODE state; the RHS is its initial
field.
Text
ic(N) = 100
JSON
{
"lhs": { "op": "ic", "args": ["N"] },
"rhs": 100.0
}
An initial condition may be an expression, not just a literal — which is how a spatially varying initial field is written:
Text
ic(C) = exp(-x^2)
JSON
{
"lhs": { "op": "ic", "args": ["C"] },
"rhs": { "op": "exp", "args": [{ "op": "-", "args": [{ "op": "^", "args": ["x", 2] }] }] }
}
A variable’s default and an ic equation both set an initial value; ic is
the one that can carry an expression.
grad, div, laplacian — sugar, not operators
These are not built-in operators. They are optional rewrite-target sugar,
and they mean exactly what their D expansions mean:
| Sugar | Expansion |
|---|---|
grad(f, dim: x) | D(f, wrt: x) |
div(F) | Σᵢ D(Fᵢ, wrt: xᵢ) |
laplacian(u) | Σᵢ D(D(u, wrt: xᵢ), wrt: xᵢ) |
Text
laplacian(u)
JSON
{ "op": "laplacian", "args": ["u"] }
The axis-carrying form puts the axis name in dim, an ordinary scalar field
with no privileged status:
{ "op": "grad", "args": ["f"], "dim": "x" }
This format ships no rewrite rules for any of them. The discretization standard library lives in EarthSciDiscretizations .
As open-tier rewrite targets they get no privileges: their result dimension is
undeterminable until lowered, so a checker reports it as unknown and skips the
enclosing check rather than inventing a coordinate-divided dimension. A binding
must not single these names out — they are matched, lowered, and checked by the
same machinery as any operator you invent yourself. A custom operator carries
its scheme parameters in attrs.
integral
A spatial partial integral, for partial integro-differential equations.
args[0] is the integrand; var names the spatial variable integrated over;
lower and upper are the bounds.
Whole-domain — both bounds constant, giving a spatially uniform value, consumed through an auxiliary variable plus boundary extraction:
Text
integral(f, x, 0, 1)
JSON
{ "op": "integral", "args": ["f"], "var": "x", "lower": 0, "upper": 1 }
Cumulative — upper is the spatial variable itself, giving a field cumulative
up to the current grid point:
Text
integral(u, x, x_min, x)
JSON
{ "op": "integral", "args": ["u"], "var": "x", "lower": "x_min", "upper": "x" }
integral carries no in-repo lowering: a file using it loads,
pretty-prints, and validates, then fails at evaluation with
unlowered_operator unless you supply a rewrite rule. Its bounds carry no
measure, because the format has none.
Do not reach for integral to write a discrete cumulative sum. A running
total, a cumulative distribution, or a column-integrated burden over a
discretized axis is an ordinary faq
with a monotone
filter — in the evaluable core, and running in every executing binding today.