The operator interface
Status: M1 draft, 2026-10-23; §8 (operators a plan supplies) added for M2, 2026-10-25; §8.4 (forcing, M2) added 2026-11-02 (D87). Normative for both implementations (D1, D8).
A leaf (spec/run-config.md §4.2) advances the sum of its operators. This chapter defines what a leaf calls on each operator: the right-hand side, its derivatives, and the structure the linear solvers rely on. From M2 the planner builds operators from the flattened model, with kernels compiled by EarthSciAST and band models from EarthSciASTDiff (architecture §2, §3.2 pass 6), and the runtime constructs them from the plan’s derived documents (§8). At M1 the toy problems of toy-problems.md
implement the interface by hand (D53), and they stay as a second supplier. Nothing in the methods, controllers, schedule interpreter or adjoint may depend on which of the two supplied an operator: both satisfy this chapter’s calls with the same meaning, and the M2 gate checks that they give the same results (spec/toy-problems.md §4).
1. What an operator is
An operator $k$ of a leaf contributes an additive term $f_k$ to the leaf’s right-hand side:
f(u, p, t) = \sum_k f_k(u, p, t).
- $u$ is the leaf’s state vector: every degree of freedom (DOF) the leaf’s operators write (architecture §3.2 pass 3), of length $N$ and element type $T$ (
f32orf64, spec/run-config.md §7.1). - $p$ is the parameter vector: the active scalar parameters, of length $P$, always
f64. Data-fed parameters are not in $p$: they are the forcing set $F$ below (M2). Array-valued (gridded) parameters as gradient targets join at M4 (U7); §6 reserves their place. - $t$ is time in seconds, always
f64(D13). - $F$ is the forcing set: the values of the data-fed parameters the operator reads (spec/plan.md §4.1), one
f64buffer per parameter, chosen by the call’s $t$ and constant within each forcing epoch (§8.4.1). It joins the calls at M2 (§8.4); an operator that reads no data-fed parameter takes an empty set, and every call below then means exactly what it means without $F$. - State an operator reads but no operator of the leaf writes is a constant of the leaf over its interval. The schedule interpreter supplies it; at M1 no toy problem has any.
- The adjoint through a leaf’s constants. The reverse of a leaf that advances part of the model state passes the cotangents of the states it does not advance through unchanged. That is exact only when no operator of the leaf reads them, and the cotangent a constant would receive is not implemented. So an adjoint through such a leaf is accepted only when every operator of the leaf declares
reads(§3) and every DOF it reads is one of the leaf’s. Otherwise the run is refused when it is built, as not implemented, naming the leaf and the first such operator in the leaf’s declared order:leaf <id>: operator <name> may read states the leaf does not advance; the adjoint of a leaf through its constants is not implemented (operator-interface.md §1). A leaf that advances every DOF meets this trivially.
An operator never stores state between calls. Every call is a pure function of its arguments, so one operator object may be called from several threads at once (U4). Workspace an operator needs is passed in by the caller.
2. Layout
The leaf’s state is one flat vector. Its order is part of the operator set’s declaration:
dense: no structure is promised; DOF order is the operator set’s own.block_diagonalwithncellscells ofnDOFs each: every operator’s Jacobian is zero between different cells. DOFs are species-major: DOF $i$ of cell $c$ (both 0-based) is at index $i \cdot \text{ncells} + c$. This is the struct-of-arrays layout that batched kernels vectorize across (architecture §6.1, theLaneBlockslayout of U10), and reseact’s.
The leaf’s structure is the coarsest that all its operators share: a leaf whose operators are all block_diagonal with the same ncells and n is block_diagonal; any other mix is dense. Operators a plan supplies declare one of these two at M2 (§8.3); banded and stencil storage arrive with their solvers (M3, M4b).
3. Calls
Every call accumulates into its output: out ← out + (value). The leaf zeroes an output once, then calls each operator in its declared order. Accumulation in a fixed operator order makes the sum bitwise reproducible (D10).
All arguments are as in §1. From M2 every call also takes the forcing set $F$ as its last argument (§8.4.1), left out of the table below and of §8.3 for brevity: rhs(out, u, p, t, F), jac(out, u, p, t, F) and so on. An operator that reads no data-fed parameter, such as every hand-written toy operator of M1 (D53), takes the empty set and ignores it, so both suppliers of §1 satisfy the same calls. $v$ and $w$ are vectors of length $N$, element type $T$; $\mu$ is a list of pairs $(\mu_i, w_i)$; outputs have the shape of the cotangent or tangent they hold. A $u$-shaped output has element type $T$; a $p$-shaped output is f64.
| Call | Accumulates | Required |
|---|---|---|
rhs(out, u, p, t) | $f_k(u, p, t)$ | always |
jvp_u(out, u, p, t, v) | $J_k(u), v$ | always |
vjp_u(out, u, p, t, w) | $J_k(u)^\top w$ | always |
vjp_p(out, u, p, t, w) | $f_{k,p}(u)^\top w$ | when an adjoint is taken with respect to $p$ |
jac(out, u, p, t) | $J_k(u)$ into the leaf’s matrix storage (§4) | for Rosenbrock, SDIRK, IMEX and θ-method leaves with a direct linsolve, from the operators whose Jacobian the method forms: for IMEX Euler the implicit part only, for the θ-method at θ = 0 none |
ft(out, u, p, t) | $\partial f_k/\partial t$ | for Rosenbrock leaves whose operator depends on $t$ explicitly |
hess_u(out, u, p, t, μ) | $\nabla_u \sum_i \mu_i^\top J_k(u), w_i$ | for Rosenbrock adjoints, and for adjoints of IMEX Euler with implicit_solve: "linearized" from its implicit part only (spec/adjoint.md §4.4) |
hess_p(out, u, p, t, μ) | $\nabla_p \sum_i \mu_i^\top J_k(u), w_i$ | likewise, with respect to $p$ |
ft_vjp_u(out, u, p, t, ν) | $\nabla_u, (\nu^\top \partial f_k/\partial t)$ | for Rosenbrock adjoints of $t$-dependent operators |
ft_vjp_p(out, u, p, t, ν) | $\nabla_p, (\nu^\top \partial f_k/\partial t)$ | likewise, with respect to $p$ |
These are the primitives of spec/adjoint.md §3, per operator. The leaf’s primitive is the sum over its operators.
Each operator also declares:
depends_on_t: whether $f_k$ depends on $t$ explicitly. When false,ftandft_vjp_*are zero and need not be implemented.linear_in_u: whether $J_k$ is independent of $u$. When true,hess_uis zero and need not be implemented (no adjoint calls it for that operator), and IMEX Euler’s Newton iteration converges its implicit part in one linear solve (spec/adjoint.md §4.4). The other second-order calls need not vanish:hess_pis zero only when $J_k$ also does not depend on $p$, andft_vjp_uonly when it does not depend on $t$. For $f_k = -k,u$, $J_k = -k$ andhess_pis $-\sum_i \mu_i^\top w_i$.Its structure (§2).
writes: the indices of the state vector it writes, those where $f_k$ can be nonzero. The leaf’s DOFs, over which the error norm and first step run (spec/methods.md §7, §9), are the union over its operators, in ascending order. At M1 operators take the full state vector; from M2 the plan’swriteslists give the same set (spec/plan.md §3). Deviation: OrdinaryDiffEq norms over the whole state, so a leaf whose operators leave some entries unwritten divides by a smaller $N$ than an OrdinaryDiffEq solve of the same equations, and its step sizes can differ from that solve’s.reads(optional): the DOFs of the model state that $f_k$ may depend on. An operator that does not declare them may read any state, which matters for the adjoint of a leaf that advances part of the state (§1).
A leaf whose method or adjoint needs a call its operators do not provide is refused when the leaf is built, naming the operator and the call.
4. Matrix storage
jac accumulates into storage the leaf’s linsolve owns:
block_diagonal:ncellsdensen × nblocks, stored lane-innermost: entry $(i, j)$ of cell $c$ is at offset $(i \cdot n + j) \cdot \text{ncells} + c$. Batched LU (block_lu, architecture §6.1) operates on this layout, one operation across all lanes at a time.dense: anN × Ncolumn-major matrix. This is for toy problems and tests only; architecture §6.1 never assembles a dense W for a real model.
The leaf, not the operator, forms $W$ from $J$ and factors it (spec/methods.md).
5. Bindings
The names above are the spec’s. Each binding spells them idiomatically and documents the mapping:
- Julia: methods on an operator type,
rhs!(op, out, u, p, t, F)and so on, generic overT. At M1 the leaf’s sum of operators is wrapped as aSciMLBase.ODEFunction(D33) so OrdinaryDiffEq can integrate the same problem as an oracle, withjacandvjpfilled from the operator calls. - Rust: a trait
Operator<T>with&selfmethods taking slices,Send + Sync(U4).
6. Reserved for later milestones
- Forcing derivatives (M4):
vjp_F,hess_Fandft_vjp_F(U7, U8). The forcing-set argument $F$ itself is M2’s (§1, §8.4). - Array parameters (M4): gridded parameters as additional $p$-shaped arguments with their own
vjp_*(D5, U7). - Range restriction (M3): calls over a tile with halos or a list of lanes rather than the whole state (U3).
- Maps, limits, conditions and residuals (M1 maps; M4b the rest): §7 defines maps; the others follow with U6.
7. Map operators
A map operator (spec/run-config.md §3.2) provides:
apply(out, u, p, t, Δ): writes $M(u, p, t, \Delta)$ intoout(overwrites, unlike §3), and records into a caller-provided mask any discrete decision it takes;map_vjp(out_u, out_p, u, p, t, Δ, mask, w): accumulates $(\partial M/\partial u)^\top w$ and $(\partial M/\partial p)^\top w$ at the pre-map state with the recorded decisions held fixed (spec/adjoint.md §5.7);map_jvp(out, u, p, t, Δ, mask, v_u, v_p): $(\partial M/\partial u), v_u + (\partial M/\partial p), v_p$, for tangent checks;apply_frozen(out, u, p, t, Δ, mask): likeapply, but with the decisions inmaskimposed rather than taken, so finite differences on the frozen tape (spec/adjoint.md §7.2) hold them fixed.
$\Delta$ is the value bind_dt gives the map (spec/run-config.md §4.4).
8. Operators supplied by a plan (M2)
A runtime that executes a plan (spec/plan.md) builds each operator the schedule names from the plan’s entry operators.<name> and the operator’s two derived documents. The result implements §1–§4 exactly as a hand-written operator does.
8.1 Building the kernels
- Right-hand side. Load
derived/<name>.rhs.esmwith EarthSciAST and build its right-hand side as the public reentrant program of U4 (EarthSciAST 0.5.0), the same way in both languages:rhs_program(esm_problem(path, tspan))with the native compiler, which returns anRhsProgram.esm_problemflattens the document, so both languages build from the flattened document, whose names and parameter order §8.2 refers to.tspanis the run’s interval; it does not affect the program, which takes $t$ as an argument. Derived documents carry no data and no data location: a data-fed parameter is declared with a stub source (spec/plan.md §6.3), bound at construction to placeholder values that no evaluation reads, and supplied at every call through the forcing set (§8.4). Runtimes do not use the problem’sf!, which is not reentrant (architecture §7.3), norbuild_evaluator, deprecated since EarthSciAST 0.4.0 (spec/plan.md §10 item 3). A runtime calls oneRhsProgramfrom as many threads as it runs shards on. - Jacobian. Load and build
derived/<name>.jacobian.esmthe same way. It has noicequations, and every state in it has adefault, the model’s or1.0where the model gives none (spec/plan.md §6.2 item 4), so it needs no calleru0; a runtime builds it as written. Its right-hand side at $(u, p, t)$ holds band fieldJ_kin the slots of the stateJacobianEval.J_k(spec/plan.md §6.2). - A build failure refuses the leaf, naming the operator and the document. A native refusal (
compiler_refused_rule) is such a failure: a runtime never falls back to EarthSciAST’s interpreter. Only tests build with the interpreter, and they ask for it by name (D89).
8.2 Layout and arguments
- State. The operator acts on the whole model state in the plan’s canonical order (spec/plan.md §4): state names sorted by code point, each array’s cells column-major. Every derived document of a plan declares the same states, so one permutation from the binding’s own build order (its name map) to canonical order serves all of them; the runtime computes it once per document at build time. The leaf’s state vector $u$ (§1) is the union of its operators’
writes, in canonical order; every other state is a constant of the leaf over its interval (§1). writes. The operator’swrites(§3) are the canonical indices of every cell of every variable in the plan’swriteslist. The right-hand side document is zero outside them by construction.reads. The operator’sreads(§3) are the columns of its band points (§8.3) that land on a DOF, in canonical order. A ghost read (a column outside the column variable’s extent) reads nothing.- Initial state. The initial state is the one EarthSciAST builds for a right-hand-side document (its defaults and
icequations; every right-hand-side document of a plan builds the same, spec/plan.md §6.1.2), in canonical order, except when anicequation reads a data-fed parameter: the run’s initial state then uses the data at $t_0$ (§8.4.5). - Parameters. $p$ is the flattened model’s scalar parameters other than data-fed ones, in code-point order of their names, at their defaults (the order of
rhs_program’sparameters, which leaves externally fed parameters out); array parameters are fixed at their defaults, since array-parameter gradients wait for U7 (§6). - Declarations.
depends_on_tandlinear_in_uare the plan’sstructure.depends_on_tandstructure.linear_in_u.
8.3 Calls
Each call below also takes the forcing set $F$ last (§3, §8.4.1) and evaluates its document with it; an operator with no forcing takes the empty set.
rhs(out, u, p, t): evaluate the right-hand-side document at the full state (the leaf’s $u$ scattered into the model state, the constants elsewhere), and add its values at the operator’swritesintoout.jac(out, u, p, t): evaluate the Jacobian document, then for each band $k$ (1-based), withb = structure.bands[k-1], addJ_kat each point of the band into the entry $(\text{row}(r), \text{col}(\text{col_idx}(r, c)))$, exactly as esm-jacobian-spec §4 defines a band’s meaning; a column index outside the column variable’s extent is a ghost read and contributes nothing. Entries outside the leaf’s DOFs are dropped. The storage isblock_diagonal(§4) when every operator of the leaf is pointwise withstructure.blockand the leaf’s own state variables (the variables of its operators’writes) all have the same extents,ncellsbeing the product of those extents, or are all scalars,ncellsbeing 1;nis the number of the leaf’s state variables. Otherwise it isdense. The model’s other states do not matter: a leaf over two states of shape[cell]isblock_diagonalwithn= 2 when the model also has a scalar state, or states of other shapes. The leaf’s state vector (§8.2) is then species-major (§2): cell $c$ of the leaf’s $i$-th variable in code-point order (both 0-based) is entry $i \cdot \text{ncells} + c$. Ablock_luorsparse_block_luleaf whose storage is notblock_diagonalis refused when it is built. Adense_luleaf factors the same entries in either storage, so a runtime may store itdense(an all-scalar pointwise leaf, such asrobertson’s, isblock_diagonalwithncells= 1).jvp_uandvjp_u: $J v$ and $J^\top w$ from the same assembled entries, restricted to the leaf’s DOFs.ft,vjp_p,hess_u,hess_p,ft_vjp_u,ft_vjp_p: not provided at M2. They need time-derivative bands (U17), parameter bands (U7) and second-order bands (U8). A leaf whose method or adjoint needs one of them is refused when it is built (§3), naming the operator and the call; so are Rosenbrock leaves on operators withdepends_on_ttrue.- Adjoints. With
vjp_uandjacprovided and the calls above not, an adjoint (spec/adjoint.md) through a leaf of planned operators is accepted exactly when all three hold; each refusal is the one §1 or §3 gives:- its
wrtisu0alone, so the gradient is with respect to the initial state only. A parameter inwrtneedsvjp_p, and the run is refused naming the operator andvjp_p; - its method’s adjoint needs no other call: explicit Runge–Kutta, SDIRK, θ-method and converged IMEX Euler leaves; Rosenbrock leaves, and the implicit part of linearized IMEX Euler leaves, only on operators with
linear_in_utrue, whosehess_uis zero (§3). Any other is refused naming the operator andhess_u; - every operator of the leaf reads only the leaf’s DOFs (§1, with
readsfrom §8.2).
- its
8.4 Forcing (M2; D87)
An operator whose plan entry has forcing (spec/plan.md §5.3) reads data. Both runtimes implement this section at M2: they fetch the data, build forcing sets and run leaves of such operators. The planner reads no data (spec/plan.md §4.1); everything here happens when the runtime builds and runs a plan.
8.4.1 The calls
- Every call of §3 takes the forcing set as a last argument,
rhs(out, u, p, t, F)and so on. $F$ is anRhsProgramforcing set (EarthSciAST API_SPEC §5.8, “Forcing sets”): one buffer per slot of the program’sforcing_slots, in that order, each column-major over the parameter’s declared shape (one value for a scalar). Julia evaluates withrhs!(du, prog, ws, u, aux, p, t, fs), Rust withRhsProgram::eval_into(&self, u, aux, p, t, &forcing, du, ws). - The runtime holds one buffer per data-fed parameter of the plan’s
forcing(JuliaVector{Float64}, RustArc<[f64]>) and builds each program’s set from the buffers its slots name (forcing_set(prog, bufs),ForcingSet::new(&prog, bufs)), so one buffer is shared by every operator’s right-hand-side and Jacobian programs. A program whose slots name a parameter that is not in the plan’sforcing, or a slot whose shape differs from it, refuses the leaf. - Which buffers a call takes are fixed by its $t$ and its step (D88): a call at $t$ inside a step $[t_n, t_n + h]$, with $B_k \le t_n < B_{k+1}$ (§8.4.3; the last epoch for $t_n$ at or after its start), takes epoch $k$’s buffers. Steps never cross an epoch boundary (§8.4.3), so every stage of a step takes epoch $k$’s, except a stage at exactly $t_n + h = B_{k+1}$, the step’s right end (a stage with $c = 1$, or an FSAL stage), where each parameter takes the buffer that is its limit from the left: a parameter with
records2 takes epoch $k+1$’s, one withrecords1 epoch $k$’s. Withrecords2 the model’s time blend weight $\mathrm{frac}(\cdot)$ is 0 at a record time, so epoch $k$’s bracket would read record $h$ instead of $h+1$, a jump the step’s error estimate would see and the reference checks of spec/toy-problems.md §4.4 would fail; epoch $k+1$’s gives record $h+1$, the limit from the left (architecture §7.5). Withrecords1 the data is constant on the epoch, and its limit from the left is epoch $k$’s record; epoch $k+1$’s would carry the next record into the step with the right-end stage’s weight (about 2.3 forTsit5), the error data_monthly’s reference check (spec/toy-problems.md §4.5) found. A step that starts at $B_{k+1}$ takes epoch $k+1$’s buffers for both. Every call made for a step takes that step’s buffers: its evaluations in the forward pass, in replay and in the step’s adjoint, and the initial step-size estimate, which belongs to a first step from $t_0$; a call outside any step (a check at the initial state, say) takes the epoch its $t$ falls in. A one-record parameter’s buffer is chosen by the step’s start, never by comparing a stage’s time with $B_{k+1}$, so a right-end stage time $t_n + h$ that rounds above $B_{k+1}$ still takes epoch $k$’s. Since the right-end stage of the step that ends at a forcing epoch boundary is not what the next step’s first stage takes, a step never reuses its first-same-as-last stage across a forcing epoch boundary: the first stage of a step that starts at an epoch start $B_k$, $k \ge 2$, is evaluated anew (spec/methods.md §3.3), in both languages and in replay. So $F$ is constant on each step’s half-open interval $[t_n, t_n + h)$; a binding never holds one set for a whole step.jac,jvp_uandvjp_uare with respect to $u$ with $F$ fixed at the buffers their call takes. Inside an epoch $F$ contributes nothing toft: the operator depends on $t$ only through the model’s own expressions, whichdepends_on_tand the time-derivative bands (U17) describe. - Time interpolation is the model’s. With
records2 (the parameter’supdate.from.records_per_sample, spec/plan.md §4.1) a buffer holds the record at or before the epoch’s start and its successor along the parameter’s first dimension, and the model blends them with a weight it computes from $t$, as reseact’s GEOS-FP fields do. Withrecords1 it holds the one record at or before. The runtime never interpolates.
8.4.2 The data layer
The runtime owns the data; an operator and a derived document own none. It writes no data layer of its own: it calls EarthSciAST and EarthSciIO for everything an esm data source can declare (esm-spec §8), so that a planned run reads exactly what EarthSciAST’s own solve of the model reads. The calls below arrived upstream on 2026-11-02 (spec/plan.md §10 items 18–20). What is still missing (item 19’s records_per_sample follow-up, item 21’s EarthSciIO gaps, item 22’s Rust EarthSciAST bugs) is fixed upstream, not reimplemented here.
- Locate. Load the model that
inputs.modelnames (the plan’s model, whose hash the plan checks) with EarthSciAST, as the planner does (spec/plan.md §4 steps 3–5), never the derived documents. Itsdata_sourcesare then resolved: a relativeurl_templateormirrorsentry is an absolutefile://URL relative to the file that declared it, and a custom substitution a mount binds (url_substitutions, esm-spec §4.7), such as NEI’s{sector}, is expanded at load. So a run locates its data by the model’s own declaration, and what is read is fixed by hashed inputs. The runtime’s API takes three run-time options that are in neither the run config nor the plan, since none changes what is computed: EarthSciIO’s cache directory (default itsdatadir), offline mode, and a map from source name to replacement URL template (EarthSciAST’surl_overrides). The override is kept for deployment choices a shared model should not encode: a site’s local copy of a public archive, or a cluster’s staging area. esm-spec §8.2.1 names a binding’s URL override as the sanctioned way to express those. It is not for making a model’s templates name the right files: a template that names the wrong data, for example the wrong grid resolution, is fixed in the model. A custom substitution that load left unbound is refused when the providers are built, naming the source and the substitution: the value would change which data is read, so it belongs in the model. - Providers. Build them with EarthSciAST’s
providers_from_document(model; cache_root, url_overrides)(Julia: from its EarthSciIO extension, withEarthSciIOloaded; Rust:earthsci_ast::esio_provider::providers_from_document, featureesio), restricted to the parameters the plan’sforcinglists, which are the used data-fed parameters only (spec/plan.md §3, §4.1): a source or parameter that nothing reads is never built, fetched or checked against the run window. It returns one provider per listed parameter, keyed by the parameter’s flattened name, and covers each source as the model declares it, through EarthSciIO’s document constructor (item 19): the reader from the source’smetadata.esio_format(item 18); theurl_templateandmirrorswith{date:<strftime>}expanded at each record’s anchor; thetemporalcadence, calendar cadences included (P1M), with records located by the static count (by the file’s time axis,records_per_file: "auto", only in Python so far, item 21); the parameter’s effectiverecords_per_samplefromloader_fields(item 18; passing it through is item 19’s follow-up);select,codes,record_filterandextent;unit_conversion, a number or an expression, applied by the provider;pointsandstaticsources as well asgrid; andreader_optionsas EarthSciIO’s readers take them (determinismis not yet checked, item 21). The model’s time origin is EarthSciAST’stime_origin(D74; the POSIX epoch when the model declares none): UTC seconds = origin + $t$. The providers count time from their own origin, whichproviders_from_documenttakes asdomain.temporal.reference_time, elsedomain.temporal.start, else the start of the window it is given (item 21); so for a model with no declared origin the two differ by $t_0$. The runtime always passes the run window, computes the providers’ origin by the same rule, and converts every time it passes to or reads from a provider (sample times, refresh times) by $p = t + \text{origin} - \text{provider origin}$, and back. The run window $[t_0, t_f]$, so converted, bounds each discrete provider’s anchors (EarthSciIO’s window is half-open, $[\text{start}, \text{stop})$, and read to the millisecond, so the runtime passes as its stop the converted $t_f$ plus one second, which keeps a refresh time at $t_f$ among the anchors, §8.4.3, and keeps only the refresh times in $(t_0, t_f]$); a window outside a source’stemporal.start/endis refused before the first step, naming the source. - Sample to buffer. At an epoch’s start $B_k$ each provider is sampled (Julia
provider_sample(p, t), RustCadenceProvider::refresh(t)ormaterialize()for a const one, at the time of $B_k$ in the providers’ clock), and the sample is turned into the slot’s buffer by EarthSciAST’s public conversion, the functionesm_problemuses (item 20): Juliaforcing_from_sample(sample, slot), Rustearthsci_ast::forcing_from_sample(&sample, &slot), withslotthe program’sForcingSlotfor the parameter. It maps the file variable’s dimensions, in the file’s order, onto the parameter’s declared dimensions in order (the record dimension first when two records are asked for), checks sizes, and writes column-major over the declared shape. It does not applyunit_conversion: the provider already has, so conversion happens once, in the provider, and a runtime never applies it again. The runtimes’ tests check that on every M2 data case each buffer at $t_0$ equals, bit for bit, the forcing buffer ofesm_problem(model, (t₀, t_f); providers)(Juliaforcing_buffers(prob), Rust the problem’s forcing array). - Extents. A source with
extentbinds a metaparameter from the delivered record count (esm-spec §8.9.4). The planner reads no data, so a model whose index set is sized by such a metaparameter is planned only when the metaparameter has a value at load (a default or a binding), and the plan’s shapes use it. At run timeproviders_from_documentand the conversion discover the count, and a count that differs from the planned one refuses the run before its first step (EarthSciAST already refuses a contradicting binding; CONFORMANCE_SPEC §5.5), naming the source, the metaparameter and both values.
8.4.3 Epochs
constbuffers are filled once, before the run. The forcing epochs are those of architecture §7.5: the sorted union of the refresh times (provider_refresh_timesin Julia,refresh_timesin Rust) inside $(t_0, t_f]$, converted to model time, of thediscreteproviders of the parameters some operator reads (the union of the operators’forcing, spec/plan.md §5.3), plus $t_0$; epoch $k$ is $[B_k, B_{k+1})$, and its buffers are each such provider’s sample at $B_k$. A data-fed parameter that only anicequation reads is sampled once, at $t_0$ (§8.4.5), and adds no epochs. A refresh time at $t_f$ is an epoch start like any other (the interval is closed on the right, architecture §7.5): the last epoch is $[t_f, t_f]$, read only by the stages at exactly $t_f$ that end the run’s last step, and they do read it (§8.4.1). Its sample is requested like the others, so withrecords2 the run needs the record at $t_f$ and its successor; a source whosetemporal.endstops short of the records a run requests is refused before the first step (below), naming the source and the time.- At M2 a runtime samples epoch $k$ on the calling thread before the first call whose $t$ is $B_k$ or later, which is the right-end stage of the step that ends at $B_k$ (§8.4.1). EarthSciIO’s
PrefetchGroup(JuliaPrefetchGroup(members; depth),start!,take!; RustPrefetchGroup::builder().member(key, provider)…,start,take), whosetake(k)is bitwise equal tosample_at(boundaries()[k]), may replace this without changing a result; asynchronous prefetch and rank windows are U11’s (M5). - At each boundary the runtime builds new sets (Julia
replace_forcing, RustForcingSet::replace, a new epoch stamp, never an in-place write), so a workspace never sees a stale buffer. forcing_refreshis a tstop source (spec/run-config.md §5), required instep.hitwhenever an operator reads a discrete data-fed parameter (spec/plan.md §7.3), so no step crosses $B_k$; a stage evaluated at exactly $B_{k+1}$, the right end of the step that ends there, reads each parameter’s limit from the left: epoch $k+1$’s buffer withrecords2, epoch $k$’s withrecords1 (§8.4.1). These times are the ones rule 9 is checked against when the runtime builds the run (spec/plan.md §7.3), and the runtime adds them to the macro-step boundaries as it adds every tstop. A data request outside the source’stemporal.start/endrefuses the run before its first step, naming the source and the time.- The tape does not yet record the epochs: spec/tape.md defers forcing epochs to M3, whose tape records each epoch’s $B_k$ and, per discrete parameter, the anchor of its sample, so a replay re-reads the same records. Until then a replay uses the epochs its run read when it was built. The adjoint’s per-record accumulation (D5) is M4’s.
8.4.4 Ranks and tiles
At M2 (one rank, one tile) every buffer is held whole. A rank’s window of a buffer and the halo a stencil operator’s forcing reads need are M5’s (architecture §7.5, U11); the state bands do not give the reach of a forcing read.
8.4.5 The initial state
When an ic equation of the plan’s model reads a data-fed parameter (directly or through observed definitions), the run’s initial state is evaluated with that parameter’s data at $t_0$: its provider’s sample at $t_0$, converted as in §8.4.2 step 3. The runtime takes it from EarthSciAST’s initial state of esm_problem(model, (t₀, t_f); providers) (Rust initial_state, spec/plan.md §10 item 15; Julia the problem’s initial state), built from the plan’s model with the providers of §8.4.2, the problem the buffer check of §8.4.2 step 3 already builds, and permutes it into canonical order (§8.2). So a planned run starts from the state EarthSciAST’s own solve of the model starts from. It is not the planner’s $u_0$, which binds each data-fed parameter to its default because the planner reads no data (spec/plan.md §4), and which serves only as the scale of the sampled states (spec/plan.md §5.5.1); nor is it the initial state a derived document builds, whose data-fed parameters are bound to their defaults too (spec/plan.md §6.3). A model whose ic equations read no data-fed parameter starts as it does without data. The runtimes’ tests check this on a variant of data_decay whose $u(t_0)$ is $S_i$ (spec/toy-problems.md §4.4).