Multi-Output Discretization Schemes — SUPERSEDED
Status: SUPERSEDED — promoted to normative spec (2026-06-14). The design in this document (Option B, demand-driven provider resolution) has been adopted as normative §7.9 of
docs/content/rfcs/discretization.mdand encoded inesm-schema.json(kind"multi_output_stencil"+ consumerrequiresfield). Open question 2 (provider matching) was resolved 2026-06-13; open question 1 (bounded/staggered extents) and open question 3 (derived outputs) are deferred to follow-on beads.Do not edit this file. Refer to §7.9 of
discretization.mdfor the authoritative specification. This draft is preserved for design history.Authored 2026-06-12 alongside the ESD Layer-B activation work (ESD/dsc-a7b2); the per-output lowering stopgap described in §5 below is shipped and covers the numerical-convergence acceptance.
1. Problem
Several authoritative ESD catalog rules describe operators that emit multiple named outputs from one stencil application:
| Rule | Outputs | Consumer |
|---|---|---|
ppm_reconstruction | q_left_edge, q_right_edge (+ documented parabola.{a_L,a_R,da,a_6} block) | PPM flux assembly |
weno5_advection_2d | q_left_edge_x, q_right_edge_x, q_left_edge_y, q_right_edge_y | 2D advective flux |
flux_1d_ppm | F_{i+1/2} (face-located) | flux-difference tendency |
The catalog encodes these as a stencil object keyed by output name
(vs. §7.1’s flat array) plus an outputs array — a schema extension
ahead of this RFC. The §7.2 rewrite contract cannot express them: one
rule match replaces one expression occurrence with one expression. Two
rules with the same pattern cannot both fire on a single reconstruct(q, dim=x) occurrence, and the outputs are not consumed at the matched
expression anyway — they are intermediate fields a downstream rule
(flux assembly) reads.
2. Design options considered
A. Output-qualified use: ("use": "ppm_reconstruction#q_left_edge").
Minimal parser change, no AST change — but forces the model author to
write one distinct PDE op per output (reconstruct_left(q),
reconstruct_right(q)), leaking the discretization’s internal structure
into the model document. Rejected as the primary mechanism (though the
scheme#output resolution syntax is reused in option B).
B. Scheme-emitted observed fields with demand-driven provider
resolution (recommended; resolution of former open question 2 — see
§4.2). A new scheme kind: "multi_output_stencil". Two trigger paths:
Consumed directly — a
use:rule matches an expression occurrence; the engine emits one observed faq equation per named output into the enclosing model:{ "lhs": { "op": "faq", "output_idx": ["i"], "expr": { "op": "index", "args": ["q_left_edge", "i"] }, ... }, "rhs": <expansion of stencil["q_left_edge"] at $target>, "observed": true, "emitted_by": "ppm_reconstruction" }and rewrites the matched expression to the output named by the scheme’s
primaryfield (e.g.flux_1d_ppm’sF_{i+1/2}). Output variables are auto-declared withlocationfrom the scheme’semits_location(face outputs live on faces; see open question §4.1) andshapefrom the operand’s grid.Demanded by a consumer — provider-only schemes (no occurrence of their
applies_toop in any equation;reconstructis the canonical case) fire when a downstream schemerequiresone of their named outputs:"ppm_flux": { "applies_to": { "op": "flux", "args": ["$q", "$c"], "dim": "$x" }, "requires": { "q_left_edge": "ppm_reconstruction#q_left_edge", "q_right_edge": "ppm_reconstruction#q_right_edge" }, ... }On first reference the engine instantiates the provider with the consumer’s inherited bindings (the same §7.2.1 name-flow protocol that
dimensional_split.inner_rulealready uses — another instance of sibling-scheme reference, not a new mechanism) and emits the observed equations. Resolution is recursive (limiter → reconstruction → flux chains), with a resolution stack for cycle detection (E_SCHEME_CYCLE). Providers nobody demands never emit.
Downstream rules reference the emitted names as ordinary shaped
variables (they are, after emission). This composes with the existing
requires_locations / emits_location fields, which are parsed today
but unenforced.
Two mechanical sub-decisions that come with demand-driven resolution:
- Name scoping. Two consumers demanding reconstruction of
different operands need distinct emitted-field names. Emitted names
are mangled per instantiation —
<output>__<operand>__<axis>(e.g.q_left_edge__rho__x) — unless the document author pre-declares the variable, in which case the declared name wins and a second conflicting instantiation is an error (E_PROVIDER_NAME_CLASH). - Once-only emission. Provider instantiations are memoized by
(scheme name, canonical bindings)so diamond dependencies (two consumers demanding the same reconstruction) emit a single set of observed equations.
C. Tuple/record AST node (a §4 outputs bundle selected by field
access). Uniform but heavy: touches expression typing, canonical JSON,
and every binding’s evaluator. Rejected for v1.
3. Proposed schema (option B)
{
"discretizations": {
"ppm_reconstruction": {
"kind": "multi_output_stencil",
"applies_to": { "op": "reconstruct", "args": ["$q"], "dim": "$x" },
"grid_family": "cartesian",
"outputs": ["q_left_edge", "q_right_edge"],
"emits_location": "face", // per-output override allowed
"primary": null, // provider-only: demanded via requires
"stencil": {
"q_left_edge": [ { "selector": {...}, "coeff": {...} }, ... ],
"q_right_edge": [ ... ]
}
},
"ppm_flux": {
"applies_to": { "op": "flux", "args": ["$q", "$c"], "dim": "$x" },
"requires": { "q_left_edge": "ppm_reconstruction#q_left_edge",
"q_right_edge": "ppm_reconstruction#q_right_edge" },
"stencil": [ ... ] // may reference required names
}
}
}
Loader contract: outputs must equal the stencil object’s key set;
each entry list validates per §7.1; primary, when non-null, names an
output; every requires value resolves to
<sibling scheme>#<declared output> statically at load time
(requires is declared, not discovered mid-rewrite, so resolvability —
though not binding compatibility — is a parse-time check). Bindings
round-trip the block losslessly (same contract as §7.5
dimensional_split).
4. Open questions
- Staggered output extents. A face-located output on a size-
nperiodic dimension hasnfaces, butn+1on a bounded one. The faqrangesfor emitted equations need the §7.4 staggering vocabulary to be answerable; this proposal should land after (or with) staggered-location enforcement. (A deliberately scoped v1 — periodic dimensions only, where face extent equals cell extentn— sidesteps the extent question and covers the catalog’s current periodic conformance fixtures.) - Provider matching — RESOLVED (2026-06-13): demand-driven
resolution via consumer
requires, as specified in §2 option B. The alternatives were rejected: a model-leveldiscretization_usesblock leaks discretization structure into the model document (switching PPM→WENO would edit the model, not the rule set, and provider chains would need author-maintained ordering); phantomreconstruct(...)equations with discarded LHS pollute ODE/DAE classification and the canonical byte contracts. Demand-driven resolution reuses the existing §7.2.1 binding name-flow and the sibling-scheme reference pattern of §7.5/§7.6, keeps model documents scheme-agnostic, composes recursively, and never emits unconsumed providers. A document-level materialization block remains a possible orthogonal later feature for diagnostics-only outputs (an output-selection concern, not a trigger mechanism). - Derived (non-stencil) outputs. CW84’s parabola coefficients
(
a_6 = 6·(q̄ − (a_L+a_R)/2)) are expressions over other outputs, not stencil rows. Aderived: { <name>: <ExpressionNode over outputs/operand> }block would cover them and keep the AST-first authoring policy.
5. Shipped stopgap (per-output lowering)
Until this lands, ESD’s
lower_stencil_to_scheme(name, spec; output=...) lowers one named
output at a time to an ordinary §7.1 scheme; the ESD Layer-B walker
drives each output through discretize → ArrayOp → eval in its own
document and verifies convergence (ppm_reconstruction measures O(h⁴)
edge interpolation this way). What the stopgap cannot provide is the
document-level contract: one model document whose single reconstruct
occurrence yields all outputs plus their consumers — that is exactly the
Layer-A canonical byte contract the affected fixtures keep
applicable:false for.