icon_sc.core.plan — execution plans¶
Execution plans: negotiation/execution split, op algebra, T1 interpreter (§8.2-§8.3).
Bind-time plan compiler: the §8.2 negotiation/execution split (SPEC S05).
ExecutionPlan.bind(composition, schema, ctx) walks the composition tree via
the visit(plan_builder) double-dispatch protocol implemented by every
S03/S04 container, runs the full sympl negotiation exactly once (S02/S03
DynamicChecker/IngressPlan machinery, one negotiation per component
occurrence) and emits symbolic op drafts over a stable slot table. Federations
and pure-control-flow wrappers dissolve (§8.2):
SequentialUpdateSplittingflattens into the op list;ParallelSplitting’s recombination ψⁿ⁺¹ = Σψₗ - L·ψⁿ becomes one k-aryAxpyper field, term order replayed from the T0 implementation (seed ψ_l1, then per further section +ψ_l, -ψⁿ) so T0≡T1 stays bitwise;SequentialTendencySplittinggets provisional slots plus oneDiffScaleper stepped field per section;SSUSdoubles into the reversed λ·Δt pre-list, the core, and the forward (1-λ)·Δt post-list;CallingFrequencybecomes per-signature cadence masks with its cached output living in persistent vault/tendency slots;SubcycleandDynamicalCorestage/substep tiers unroll with bound dt;ScalingWrapperfolds into constant-coefficientAxpyops.
State evolution is in-place where elementwise-safe (tendency integration) and
ping-pong for kernel-written outputs: every swapped field owns exactly one
(vault cell, alternate cell) pair, and the compiler emits one op-list
variant per plan phase (even/odd x cadence signature) with buffer references
pre-bound — no runtime indirection (§8.2). run_step interprets the phase’s
variant; plan_hash is a stable content hash of the symbolic plan.
- class icon_sc.core.plan.bind.ExecutionPlan(compiler)¶
The frozen product of one bind (frozen interface, SPEC S05).
ExecutionPlan.bind(composition, schema, ctx)compiles;run_step(vault, step_index)interprets the pre-bound op variant of the step’s signature (T1). Materialization against a concrete vault happens lazily on the firstrun_step(buffers become available only then);plan_hashandsignaturesare properties of the symbolic plan and stable across processes.- Parameters:
compiler (_Compiler)
- classmethod bind(composition, schema, ctx)¶
Compile
compositionagainstschema/ctx(frozen interface).- Parameters:
composition (Any)
schema (StateSchema)
ctx (ComputeContext)
- Return type:
- property signatures: tuple[str, ...]¶
Labels of the distinct step signatures (one op-list variant each).
- property schema: StateSchema¶
The bind-time state schema (input to
bind()).
- run_step(vault, step_index, *, on_segment=None)¶
Execute one step’s pre-bound op list (frozen interface, SPEC S05).
step_indexmust advance sequentially from 0 (modulo the signature period): the ping-pong swap state of the vault is phase-dependent. Materializes againstvaulton first use.on_segment(S14, additive keyword with default) is the host-step seam: a callback invoked with everySegmentMarkerthe interpreter reaches (seeicon_sc.core.plan.interpreter); monitors and time advancement live there, outside the plan.- Raises:
StalePlanError – After any out-of-band façade mutation (materialization against
vaultdetects a changed epoch).- Parameters:
vault (StateVault)
step_index (int)
on_segment (Any | None)
- Return type:
None
- class icon_sc.core.plan.bind.PlanBuilder(*args, **kwargs)¶
Double-dispatch surface of the plan compiler (PLAN S05 item 3).
Every S03/S04 container implements
visit(plan_builder)forwarding to exactly one of these hooks; the same walk is reused by the post-slice halo validator.
The execution-plan op algebra (architecture §8.2, SPEC S05).
Exactly six op types — BoundCall, Swap, Axpy, DiffScale,
CadenceMask, SegmentMarker — and nothing else. Every op is a flat
NamedTuple with pre-bound references: buffers, callables and scalars are
resolved when the plan is materialized against a vault, so interpreting an op
performs no name lookups, no allocation and no negotiation.
The docstrings below are normative: the T2/T3 emitters (post-slice) treat them as the definition of each op’s semantics, and the T0≡T1 bitwise-equivalence contract of SPEC S05 is stated against them. In particular, every arithmetic op is specified as an exact sequence of numpy ufunc applications; emitters must preserve that sequence (per-element evaluation order), because reordering changes floating-point results (AGENTS.md: no reduction-order changes).
Semantics donor: tasmania’s DataArrayDictOperator kernel set (iadd /
iaddsub / fma / fused sts_*_0 stage kernels, see REFERENCES.lock)
— Axpy is the k-ary assign-or-accumulate generalization of the first
three and DiffScale is the (ψ_prv - ψⁿ)/Δt forcing tasmania fuses
into its first-stage kernels.
- class icon_sc.core.plan.ops.Axpy(y, init, terms, scratch, divisor, tag)¶
Fused k-ary axpy:
y ← (init + Σᵢ aᵢ·xᵢ) / divisor, exact ufunc sequence.Normative evaluation order (numpy ufuncs, all with
out=):If
init is not None(assign form):np.multiply(x₀, a₀, out=y)whereinit = (a₀, x₀).a₀ = 1.0is an exact copy (IEEE-754 multiplication by one is the identity). Ifinit is None(accumulate form),y’s current content is the seed.For each
(aᵢ, xᵢ)interms, in order:np.multiply(xᵢ, aᵢ, out=scratch)thennp.add(y, scratch, out=y).If
divisor != 1.0:np.divide(y, divisor, out=y).
This reproduces bit-for-bit the T0 dict arithmetic it compiles from — e.g.
phi + dt*k1(tendency-stepper stages),acc += 1.0*x(dict_axpy),0.75*phi + 0.25*(phi1 + dt*k2)(ssprk3) and(phi + 2.0*(phi2 + dt*k3))/3.0(ssprk3 final; the trailing division is whydivisorexists —x/3.0andx*(1/3)differ in the last ulp).ymay aliasx₀and may appear intermsonly when the aliased read happens before the first write toyunder the sequence above (the compiler guarantees this; step 1 withx₀ is yis safe, later aliased terms are not emitted).scratchis a plan-owned buffer ofy’s shape/dtype; it carries no state between ops.- Parameters:
- class icon_sc.core.plan.ops.BoundCall(fn, args, tag)¶
One pre-specialized component-kernel invocation:
fn(*args).fnis the component’s raw kernel entry (the boundarray_call/stage_array_call/substep_array_callmethod, resolved once at materialization),argsthe frozen argument pack. For thearray_callABI the pack is(inputs, outputs, timestep)whereinputs/outputsare dicts built once at bind time with contract field names mapping to pre-resolved raw buffers; buffer identity is stable across steps (vault contract), so the same pack is valid for the plan’s lifetime. The interpreter performs exactly one Python call perBoundCalland never inspects the pack.
- class icon_sc.core.plan.ops.CadenceMask(period, phase, ops, tag)¶
Cadence guard: run
opsiffstep_index % period == phase.The compiler resolves every
CadenceMaskinto the per-signature op lists (one flattened list per distinct step signature, architecture §8.2), so the interpreter’s hot path never evaluates the guard; the op type exists so a symbolic plan remains a single legible list and so debug tooling can interpret an unexpanded plan. When interpreted directly, the guard is one integer modulo — the firing rule of a freshCallingFrequencywrapper under the S03 rounding-to-multiple rule (fires at step indicesphase (mod period); see REFERENCES.lock, symplUpdateFrequencyWrapper).
- class icon_sc.core.plan.ops.DiffScale(y, minuend, subtrahend, divisor, tag)¶
Provisional-tendency forcing:
y ← (minuend - subtrahend) / divisor.Normative sequence:
np.subtract(minuend, subtrahend, out=y)thennp.divide(y, divisor, out=y). This is thesis eq. (2.11b)’s constant forcing(ψ_prv - ψⁿ)/Δtfeeding every SequentialTendencyStepper evaluation (S04 computes the same two-op sequence per field).ymust alias neither input.
- class icon_sc.core.plan.ops.SegmentMarker(kind, tag)¶
Boundary of an exchange-free plan segment (architecture §8.3).
A runtime no-op at T1. T2 graph capture and the T3 native driver split the op list at these markers: everything between two consecutive markers is capturable as one graph / emittable as one native block.
kindlabels the boundary reason ("step_end"in S05; halo exchanges, bridge calls and framework seams add kinds post-slice).
- class icon_sc.core.plan.ops.Swap(vault, slot, alt_store, alt_index, tag)¶
Exchange a vault slot’s buffer with a plan-held alternate buffer.
vault.buffers[slot]andalt_store[alt_index]exchange contents and the vault’sgenerationcounter is bumped (vault.note_swap()) so the lazy façade rebuilds its DataArray view of the swapped slot on next access. Swaps carry no data movement — they retarget which buffer the façade exposes for a ping-pong field after a step whose kernels wrote the alternate buffer. Ops never read through the vault at runtime (references are pre-bound per even/odd variant, architecture §8.2), so aSwaponly keeps the vault’s public view coherent.
The T1 plan interpreter (architecture §8.3, SPEC S05).
A boring, measurable for loop over pre-bound ops with a match on the op
type — per-op cost is one Python dispatch plus the op’s kernel calls. No dict
lookups on state names, no xarray, no contract logic, no per-step allocation
beyond CPython transients (iterators, boxed scalars) that are freed within the
step (the SPEC’s tracemalloc criterion: traced memory is step-invariant after
warmup).
Every arithmetic op executes the exact ufunc sequence its
icon_sc.core.plan.ops docstring declares (normative; bitwise T0≡T1).
The host-step seam (S14). run_ops(..., on_segment=...) yields to a host
callback at every SegmentMarker — the minimal
seam the T2 graph-replay tier needs: under stream capture each exchange-free
segment becomes one captured graph, and the interpreter’s per-marker yield is
exactly where T2 stops replaying and returns control to Python (monitors, time
advancement, MPI, bridge calls — everything excluded from the plan). At T1 the
callback costs one is not None check per marker; ctx.timeloop uses it
to run monitors against the vault façade in the step_end host step (design
note only — no T2 code in the slice).
- icon_sc.core.plan.interpreter.run_ops(ops, step_index, on_segment=None)¶
Interpret one pre-bound op list (one step signature) at
step_index.on_segment(S14, additive) is invoked with everySegmentMarkerreached — the host-step seam described in the module docstring.- Parameters:
step_index (int)
on_segment (Callable[[SegmentMarker], None] | None)
- Return type:
None
Plan staleness guards and debug renegotiation (architecture §8.2, SPEC S05).
A plan is valid for one (composition, schema, ctx) triple, enforced by:
the vault
schema_hash+epochrecorded at materialization — any out-of-band mutation through the façade (field rebind/delete) bumps the epoch and the nextrun_stepraisesStalePlanErrorinstead of silently binding dead buffers;plan_hash— a stable content hash over the canonical serialization of the symbolic op lists (names and slots, never object ids), the schema, and the context configuration; identical inputs hash identically across processes;debug renegotiation —
renegotiate_and_diff()re-runs the full bind against the live composition every N steps and diffs the result against the bound plan, raisingPlanDriftErroron any divergence (ctx.timeloop(..., debug_renegotiate_every=N)wires it into the loop).
- exception icon_sc.core.plan.guards.PlanCompileError¶
The composition cannot be compiled to an execution plan (bind-time).
- exception icon_sc.core.plan.guards.PlanDriftError¶
Debug renegotiation produced a plan differing from the bound one.
- exception icon_sc.core.plan.guards.StalePlanError¶
The vault mutated out of band since the plan was bound (§8.2 guard).
- icon_sc.core.plan.guards.renegotiate_and_diff(plan, composition, ctx)¶
Re-run the full negotiation and diff against
plan(debug builds, §8.2).Binds the live
compositionafresh against the plan’s schema and context and compares plan hashes and the canonical op serialization. Returns silently when the plans agree.- Raises:
PlanDriftError – Naming the first divergence between the re-bound plan and
plan.- Parameters:
plan (ExecutionPlan)
composition (Any)
ctx (ComputeContext)
- Return type:
None
- icon_sc.core.plan.guards.schema_fingerprint(schema)¶
Stable content hash of a
StateSchema(sorted canonical text).- Parameters:
schema (StateSchema)
- Return type: