The kernel contract#
solprop.translate.problem.KernelProblem is the only object that crosses from the physics and
translation layers into a backend. It is a frozen dataclass of plain NumPy arrays and scalars,
validated, versioned, and documented here field by field. A backend may assume everything
stated here and nothing else; the translation layer promises everything stated here and
nothing else. Changing this file and the dataclass together is the only way to extend the
contract, and a test checks that the fields listed here and the fields of the dataclass agree.
Axis names follow conventions.md: n_batch, n_modes, n_pol, n_omega
(coarse spectral), n_omega_fine, n_t, n_to (fine time), n_nodes, n_z, n_intervals,
n_deg (Chebyshev degree + 1). The algorithms behind the fields — the cumulative
operator, the quadrature, the compiled responses — are in algorithms.md.
Sign convention. Every complex array in this contract and in Observation is in the
FFT convention of conventions.md §2 (raw forward DFTs, propagation
e^{i(ωt − βz)}, linear operator −iβ). The translation layer supplies field0 and noise0
already converted from the optics-convention user inputs; the output layer converts
observations back. Backends never conjugate.
Current kernel schema: 36; saved-output schema: 25. These versions are independent.
Internal modal amplitudes are vacuum normalised; the launch and observation boundaries
carry the true-power conversion, and every backend reads the modal power weight (§3) in its
statistics alone. Radial columns are Hankel
frequencies and Cartesian columns are transverse FFT frequencies, the latter flattened as
ix + n_x*iy. Every scalar transverse transform is applied independently to each state
polarisation component.
Agreement is a rounding bound. The contract fixes quantities, not operation order. A
backend reproduces the reference’s results within a rounding bound fixed from its
operation and reduction counts, never bit for bit
(SEP 25). That includes the plasma
column’s running sums against plasma_scan and plasma_current, under either
running-integral rule (§5); the Raman pass against RamanResponse.evaluate and
raman_spectrum against RamanKernels.spectrum; and the statistics evaluated on one
state. What the contract states exactly stays exact: integer and identity fields,
hard-mask zeros, and explicitly exact zeros and no-ops.
1. Identity and validation#
field |
type |
meaning |
|---|---|---|
|
|
incremented on every change to this contract, to the protected surface of |
|
|
selects r2c/c2r vs c2c transforms and the response formulas |
|
|
canonical state shape |
|
frozen dataclass of static flags and counts |
|
|
frozen dataclass |
section 2 ( |
|
frozen dataclass |
section 3 ( |
|
frozen dataclass |
section 4 ( |
|
frozen dataclass |
section 5 ( |
|
frozen dataclass |
section 6 ( |
|
frozen dataclass |
section 7 ( |
|
|
immutable atomic specification, resolved response lattice and scientific identity; section 5 |
|
|
independent current coefficient numerator, |
|
|
a spatial run’s plane measure (SEP 34, |
|
|
the arithmetic the run is held in (SEP 33, ADR-054): |
|
||
above: |
||
of a fixed set of arrays, and false when either took the z table. |
How a stage is evaluated, and why device_ready exists. Three of the quantities a
stage needs are arrays that depend on z — the propagator’s exponent ΔΦ, the response
prefactor, and the noise’s unitary phase — and a handful are scalars: the number
density, the pre-ionisation, the mode set’s taper, the Raman decay rates. The seam splits
them in two, and translate.stage is the split:
terms (
stage.terms(problem)), the fixed arrays, uploaded once per run. A constant quantity is one term; an expansion isn_termsof them.StageTerms.separablecarries the shared spatial coefficients instead when both array term groups are absent. Its stage weights are the distance, position and unit prefactor weight.scalars (
stage.scalars(problem, z0, z1)), a small frozen record of host floats per stage: the weights that multiply the terms, and every scalar path value the responses read.stage.anchor(problem, z0)hoists the part every stage of one step shares.
A backend evaluates ΔΦ = Σ_j w_j · terms_j (stage.combine) and knows nothing else
about the representation. It is the arithmetic of delta_phi and
prefactor_at, rearranged, so a backend that goes through the bundle reproduces them to
rounding.
The z table has no such split — its evaluation is a Clenshaw pass over an array of the
state’s size times the degree. NumPy evaluates it directly;
Numba uses preallocated host stage arrays and the same Clenshaw order. The native CPU
build holds the coefficients resident and evaluates the table at each native call’s
distinct positions into term slots, which the stage weights then select (+-1 for an
increment, one-hot for a value), so the bundle’s arithmetic is unchanged and no field of
the contract changes (SEP 29 section 6.6). A device build calls
stage.require_device_ready(problem, name) and refuses a table, naming its size and,
for a mixture, the labels whose independent profiles exceed the qualified tensor
expansion. A
total-degree expansion is not part of the contract: it failed the joint memory and
build-cost criterion on the short fixture.
The current version is 36 (solprop.translate.problem.SCHEMA_VERSION), and the
tables of this document describe it. A version bump that adds an optional field
leaves every problem that does not use it unchanged, schema_version itself being the
one thing that moves. Removing an admitted value needs no bump, because no consumer can
misread a problem that no longer carries it.
Batches and device memory. BatchBackend batches independently translated and loaded
member backends, which carry their own parameter tables and noise across construction;
successful batch construction adopts their lifetime. observe_members() returns one
ordinary host Observation per member, preserving independent error histories and
periodic-statistics rows. member_metadata() returns each member’s resolved policies plus
batch settings. Backend.available_device_memory() returns current free device bytes,
excluding live allocations and allocator reserves, or None for host execution;
StepDriver provides the host default and a device backend overrides it. Common batch
admission uses that probe before allocating a complete group, with 20% headroom and a
conservative per-member upper bound from the complete memory ledger. Only compatible
layouts under the same explicit fixed schedule can share a batch; mixed backend names are
refused. The batch’s max_batch is its constructed member count. Calling its
single-observation API is refused because it would discard the independent diagnostic
histories. The StepDriver signatures, stage bundles and singleton output schema are
those of a singleton run.
The general-modal leaves. linear.coupled_flow and linear.modal_metric (§3),
transform.modal_tables (§4) and responses.regional_responses (§5) are optional. Each
defaults to None, and a problem with all four None carries no field-sized or dense
buffer for them. The leaves carry int32 region and support indices beside float64,
complex128, bool and intp. A backend refuses by name each leaf it does not execute,
through solprop.backends.driver.refuse_general_leaves(..., admits=...), or its own
equivalent check in a compiled backend’s adapter (as native’s does), because the version check alone cannot fire
for a backend compiled against the same constant. The combinations a backend executes
are one shared list, solprop.backends.numpy_ref.admission.refuse_general_route, run
under the caller’s name with the section methods it executes and whether it evaluates
moving maps (moving_maps=False by default), so every backend refuses the same problems
in the same words. On the general route response_order is (accumulate,) alone — the
response is the regional leaf and not a fused-pass stage — and the prefactor row is the
real crop scale. What a backend reads across the seam on that route is the leaves’ arrays,
grids.crop_index, grids.pad_scale, n_to and the prefactor row.
Stage-bundle rows of a coupled flow. StageScalars.flow_section is which section of
linear.coupled_flow the step’s anchor lies in — every stage of one step carries the
same value, because the exponential the interaction picture is built on is frozen for the
whole step — and flow_weights, float64 of shape (n_intervals * n_deg,), the weights
the flow’s own (n_intervals, n_deg) coefficient block is combined over at the stage’s
position, flattened in that order and zero outside the one section the stage is evaluated
on. A problem carrying no coupled flow reports flow_section = -1 and
flow_weights = None. The rows are the seam’s own evaluation form applied to the flow: a
backend evaluates G(z₁) − C(z₁) = Σ_j w_j · terms_j exactly as it evaluates ΔΦ, and
the explicit residual is that combination minus the frozen block flow_section names.
Which section a stage on a section edge belongs to is normative: it is the step’s own
whenever the position lies in that section’s closed interval, matched within 1e-10 of the
flow’s span, and the section the stage closes in otherwise. A section edge is a kink —
the declared taper corner the sections were placed on — so a stage arriving at one must
read the side it came from; reading the opening side instead drops a whole run from fifth
order to first (measured 2.1e-3 at ten steps of a qualification taper, against
3.45e-9 once the tie is broken this way).
A "cf4" section stores exactly what a "frozen_residual" one does — the same Chebyshev
coefficients of G and C along z — and differs in how a backend integrates it and in
what its rows mean: no explicit residual (residual_norm_bound is zero) and a pair bound
for inverse_condition_bound. A CF4 step of length l from z exponentiates the whole
generator L = G − C at the two Gauss nodes z + c_j l, which are not stage
positions, so the stage bundle’s flow_weights cannot supply them; numpy_ref builds them
from the leaf itself — solprop.translate.coupled.flow_weights of the node against the
step’s flow_section, the same Chebyshev evaluation the bundle row is, from z_edges and
the n_deg axis the leaf already carries. A compiled backend would need up to twelve such
positions per step (two nodes at each of six distinct stage fractions); no bundle row
carries them, which is why CF4 runs on numpy_ref alone.
The Schur factors of a section’s frozen block and the actions formed from them are
backend-local and never cross the seam. The compiled backends form the actions in their
own kernels, from the eigenbasis of each section’s triangular factor
(solprop.translate.coupled.schur_eigenbasis, host-side at construction and shared) or by
the reference’s scaling and squaring on the bins where that basis is ill-conditioned, so
their actions are within a derived formation bound of the reference’s rather than bitwise
its (tests/flow_parity.py); the certificate is checked per step, on a compiled
measurement, by the reference’s rule. FSAL is absent on a flow rather than disabled by
a flag: a coupled flow would need the analogous certified forward action at the old
endpoint before any backend could reuse a stage (stepper.md §2).
The vector wall block is the same "constant_schur" (or, on a varying guide,
"frozen_residual") leaf with a different dense generator — one azimuthal order of a
capillary rather than one HE_1m family — and stores no factored wall pair, because
its wall term is rank two and the pair’s w (v x v) is rank one. Its gauge_id names the
order it was built for ("capillary azimuthal order nu=1") rather than
"capillary HE_1m", which is how a resume tells the two blocks apart.
Stage-bundle row of moving modal maps. StageScalars.transform_weights, float64 of
shape (n_intervals * n_deg,), holds the weights transform.modal_tables’ synthesis
and projection are combined over at the stage’s position z1, their
(n_intervals, n_deg) coefficient axes flattened in that order and zero outside the one
section z1 lies in (solprop.translate.modal_path.transform_weights). A backend
evaluates S(z1) = Σ_j w_j S_j and Q(z1) = Σ_j w_j Q_j in the seam’s own evaluation
form, exactly as it evaluates ΔΦ. The row is separate from flow_weights, so that
the transform’s sectioning is its own: the moving route puts the two on the same edges,
which is natural and not required. No one-sided rule is needed for it, unlike
flow_weights: the maps are continuous across a section edge even at a kink of the
geometry, where only their slope jumps, so a position on an edge takes the section
starting there (within 1e-10 of the span) and the value differs from the other side’s by
the representation error alone. A problem whose maps do not move — no modal tables, or
maps of one coefficient — reports None. A backend that does not evaluate moving maps
refuses such a problem by name through the shared admission list.
The frozen literals and the contract tuples. tests/backends/test_driver.py freezes
DRIVER_SURFACE, LINEAR_OPERATOR_FIELDS (dataclasses.fields(LinearOperator)),
STAGE_TERMS_FIELDS, STAGE_SCALARS_FIELDS and SEPARABLE_OPERATOR_FIELDS under
SCHEMA_VERSION; a bump rewrites the literal it changes. solprop-native exports its own
tuples of the names it relies on — LINEAR_OPERATOR_FIELDS, TRANSFORM_FIELDS,
RESPONSE_FIELDS, STAGE_SCALARS_FIELDS, and MODAL_TABLES_FIELDS (electric,
omega_fine, projection, region_index, source_frequency_factor, synthesis),
REGIONAL_RESPONSE_FIELDS (n_terms) and COUPLED_FLOW_FIELDS (connection,
generator, inverse_condition_bound, n_deg, n_modes, section_methods) for the
members of ModalTransformTables, RegionalResponseTables and CoupledFlowTables its
adapter reads itself — and its check_build() refuses a solprop without one (a dataclass
field, or a property for the derived counts), so a renamed leaf or member fails there with
a message naming it rather than as a missing attribute inside the adapter. The two sets are
separate objects that this document keeps in step. Members read only inside solprop’s own
derivations the adapter calls (node_kerr_tensor, node_delayed_maps, metric_at,
frozen_generator, schur_blocks) are solprop’s to keep consistent and are not listed;
for that reason ModalMetricTables has no tuple, the adapter handing the leaf to
metric_at whole.
The dataclass tree is exactly KernelProblem and the six array groups grids,
linear, transform, responses, initial and stepper; every other value a field
holds — a Flags, a StatsSpec, a Tableau, a PIController, a RateTable, a
ZProfile table — is a leaf whose own fields are not rows of this document. Fields whose
name begins with an underscore are implementation detail and are outside the contract
too. The test that compares this document with the dataclass walks the tree by that rule,
in both directions; a second test checks the flags and counts named in the flags row of this section
against the fields of Flags.
Source reservations in the shared Adams march. The protected hooks
StepDriver._reserve_source_calls(count) and StepDriver._release_source_calls()
bound the source evaluations of a trial. Before starting a trial, the shared march
reserves its required source evaluations through these hooks; on every exit it
releases unused capacity. The ordinary backend implementation needs no atomic
reservation. NumPy’s TDSE implementation translates the reservation into the
required persistent-plan chunks, while provider attempts remain charged before
invocation, including failures. Logical source-call and atomic-attempt ledgers
remain separate. The base driver owns _adams, initially None, and exports and
restores its logical ledger on every Adams backend. Loading a checkpoint resets
numerical history but preserves charged calls; startup is charged again. Atomic
ledger restoration remains exclusive to the bound NumPy TDSE path.
The lab-frame source of the exponential formulation.
StepDriver._adams_lab_source(lab, position, *, purpose) is the complete source
assembly at a lab-frame field and position – the modified noise draw advanced to
that position’s absolute phase, the in-stage absorbers and, on the reference backend,
the TDSE adapter with its labels – returned in the lab frame, with no propagator
applied. The exponential Adams formulation
(SEP 16) stores these untransported samples and
integrates their polynomial against the linear flow; the base driver’s hook refuses,
naming the backend, so a backend that has not implemented it refuses the formulation
rather than stepping it with another method. Multistep.formulation is
"interaction" or "exponential".
The Adams engine. With StepDriver._adams_engine(method, compensation),
AdamsMarch keeps the controller – orders, slots, rejection,
restarts, the call budget and the ledger – and hands every state-sized operation of
its fused and exponential paths to an engine
(solprop.backends.multistep.AdamsEngine): the startup sample, a trial’s predictor,
source, corrector and scored defect norms, the adoption and transport of an accepted
step, holding and restoring the accepted state around a dense observation, and the
observation itself. Only scalars, the coefficient tables and the norms cross the
interface. The default returns None, and the march uses HostHistory, the host
engine, over the other Adams hooks; native returns one resident in its native state.
The general path (PECE and the comparison variants) keeps host arrays.
StepDriver._adams_source(lab, anchor, position, *, purpose) takes a
TDSEPurpose label so startup, predictor, corrected-field and diagnostic
observations retain their actual provenance. This is a host seam operation:
the shared march neither accesses NumPy’s private binding nor adds atomic
physics to a compiled backend. Numba and native continue to refuse TDSE by name.
A corrected-field diagnostic still needs an identical cached response or a
separately charged solve; a predictor label never implies accepted-field data.
If the corrected-field solve fails after candidate adoption, _adams_restore
restores the previous accepted field and position before a checkpoint is written.
A ZProfile table is how anything that varies along z crosses the seam: a
scalar-valued translate.chebyshev.PiecewiseChebyshev over the same z range as Phi,
built by translate.linop.tabulate. A backend evaluates it with one Clenshaw pass on the
host and never sees a Python ZProfile. Where a path is exactly zero at an interval edge
(a declared vacuum), translate.linop.profile_table makes its table exactly zero there
too, within the table’s tolerance, so the non-negativity check on density tables never
meets the fit’s rounding.
validate() checks: all arrays finite, C-contiguous, read-only, and of the stated dtype
(float64, complex128, bool and intp everywhere, and int32 at exactly three declared
index leaves — transform.modal_tables.region_index,
responses.regional_responses.response_region and
responses.regional_responses.kernel_valid_samples, named in
translate.problem._INT32_LEAVES; the dtype rule applies to every leaf the tree walk
reaches, so int32 is admitted by name and not globally)
and shape; masks Boolean; breakpoints strictly increasing and covering [0, z_max]; hard
kinks are breakpoints; every ZProfile table covers the same z range as Phi; every
general-modal leaf tabulated along z covers [0, z_max] and carries grids.omega
itself; a replayed
schedule lands on every save plane and hard kink; schema_version equals the backend’s
expected version. A failed check raises TranslationError naming the field. Quadrature
weights and mode matrices are checked in modes.quadrature; physical modal weights
are also carried as transform.node_weights for species inventories.
2. Grids and transforms#
field |
shape / dtype |
meaning |
|---|---|---|
|
|
coarse angular frequencies (field: non-negative half spectrum; envelope: FFT order about |
|
float |
reference frequency, coarse and fine time steps |
|
int |
coarse and fine time lengths, oversampling factor |
|
|
where coarse bins sit in the fine spectrum (field: leading bins; envelope: leading and trailing halves) |
|
float |
the scaling factors of the zero-pad/crop operations |
|
|
physical frequencies inside the wavelength limits, the requested band |
|
|
Luna’s |
|
|
the taper applied to the nonlinear polarisation in the spectral collar: the grid’s collar profile (1 in band) when |
|
|
temporal window applied inside the response pass |
|
|
analytic derivative of the temporal collar, s⁻¹; the held field-plasma source uses this for its second current stream |
|
|
|
|
|
the bins the error norm scores: |
3. Linear operator#
The scalar parameters an expansion runs over are named by the mode set’s
parameter_paths: a capillary filled with one gas declares core_radius and density;
a fill of several species at one total pressure declares core_radius and pressure,
so the two-parameter expansion still applies and a one-component fill stays exactly the
single-gas case; independent per-species density profiles declare one
density:<label> path per component, which exceeds the two-parameter tensor expansion
and falls to the z table below. The names are the vocabulary of
solprop.modes.protocol.ParameterisedMode and of the reuse check in
Simulation.with_mixture; a backend never sees them, only the representation flag.
field |
shape / dtype |
meaning |
|---|---|---|
|
|
the representation consumed by |
|
|
interval edges in z — of the table itself, or of the expansion’s scalar tables |
|
|
degree used per interval (diagnostic) |
|
|
Chebyshev coefficients of Φ(ω, z) = ∫D dz − ½ ln[w(z)/w(0)] along z (the batch axis is 1 when members share a fibre); present for |
|
frozen dataclass or |
Φ as an expansion in the mode set’s scalar paths ( |
|
|
z positions where D, or a scalar path a response consumes (a plasma channel’s preionisation, an explicit density), jumps or kinks; steps land on them |
|
|
|
|
|
modal power weight representation, present exactly for modal transforms and absent for spatial geometry |
|
|
positive dimensionless |
|
|
the weight with plain basis-product weights; expansion representation only |
|
|
exact constant weight, with a matching degree-zero table; constant representation only. It carries no batch axis, unlike |
|
|
frame quantities along z (diagnostic; already inside Φ) |
|
|
shared constant spatial coefficients, present exactly for |
|
|
the generator of a guide whose channels couple, dense or factored, with its residual, conditioning and error certificate and one method per section; a leaf in |
|
|
the explicit Hermitian flux metric W, a documented leaf here rather than a record nested inside the flow, shared by identity with the controller’s norm, the linear flow and the output layer so that no second W is allocated. |
The separable representation carries no phi_coefficients, phi_constant or
phi_expansion. It has one interval, degree 1, its declared z_span as the two
breakpoints and no hard kinks. Free propagation requires this representation and
refuses varying media and propagator_mode="tabulate" explicitly.
SeparableOperator is a frozen leaf in translate.spatial. Its frequency vectors
are k_squared and k_squared_low (complex128, material wave-number square
and its residual in m^-2), frame
(float64, frame subtraction in m^-1), spectral_rate (float64, nonnegative power
absorption in m^-1), prefactor_numerator and prefactor_clamped (complex128,
purely imaginary, with temporal crop scaling), and active (bool). Each has
n_omega entries. kperp_sq is a nonnegative float64 vector of n_modes
transverse wave-number squares in m^-2; kperp_sq_low has the same shape and
dtype and stores their signed residuals. Both low parts are rounded from exact
rational squares of the unsquared binary inputs. The scalars are evanescent_rate
(positive finite field attenuation cap in m^-1), n_pol (1 or 2), and z_span
(two finite increasing positions in m). There are no arrays proportional to
n_modes*n_omega in this leaf. kappa_sq is None except on the
hybrid planar kind (§4): complex128 (n_channels, n_omega) in m^-2, each slab
channel’s transverse square κ_p(ω)² = k²ε_co − β_p(ω)² from the mode’s own complex
propagation constant (its wall term under its loss policy), zero at inactive
frequencies; column c belongs to channel c % n_channels, and kperp_sq is then
k_y² per column with the channel fastest. kperp_rate is float64,
n_modes entries, finite and nonnegative, a transverse collar’s power rate per column
in m^-1 (solprop.boundaries.TransverseCollar); None is stored
as zeros, and a nonzero one is refused beside kappa_sq. evanescent_rate is
solprop.spatial.EVANESCENT_RATE unless the run sets SimulationSettings.evanescent_rate.
For each column and frequency form
q=(k_squared-kperp_sq)+(k_squared_low-kperp_sq_low) in that order, then, when
kappa_sq is present, q = q - kappa_sq[c % n_channels], and its principal
complex square root s. Where Re(q)>0,
D=-Im(s)-i*(Re(s)-frame) and the prefactor is
prefactor_numerator/Re(s). Else the attenuation is
min(abs(Im(s)), evanescent_rate) and the prefactor is prefactor_clamped.
Subtract kperp_rate[c]/2 from the attenuation term, D = -(a + kperp_rate/2) - i*(...),
formed as -a - kperp_rate/2 so that a zero rate leaves D exactly as it was. Multiply both
physical terms by active (the transverse rate therefore acts on the active band
alone), then subtract spectral_rate/2 from D. Propagating material gain retains its sign. Evanescent components decay.
The exact cumulative increment is D*(z2-z1); modified noise takes only its
imaginary part. Compiled backends evaluate these formulas per element.
KernelProblem.validate holds a step the run cannot shrink – a replayed schedule’s
longest step, or the floor dz_min – to the propagator’s exponent budget
(SEP 22 §4.5): with λ₋ and λ₊ bounds on max(-Re D) and max(Re D) from
translate.spatial.rate_bounds (a sorted search over the columns, O(n_omega log n_modes)), a step h with h max(λ₋, σλ₊) > ln(DBL_MAX) is refused naming the
binding term and the admissible step and rate, and one past nine tenths of it warns
(ExponentBudgetWarning); σ is 1 for the Runge-Kutta and low-storage marches and
q + 1 for an Adams method of largest order q. Adaptive runs are never refused.
The radial kind carries a dense operator rather than a separable one, and every backend
forms the same inverse factor exp(-ΔΦ) from it, so it is held to the same budget with
bounds read off the operator (translate.radial.radial_rate_bounds): a
constant table’s exact extremes of Re D, and, for a varying medium, a sufficient bound
on each interval of the rate’s Chebyshev series (|T_j| ≤ 1) for a z table or for an
expansion’s scalar weights against its terms. The refusal names the radial operator’s
attenuation and the admissible step. The modal kinds are not checked.
The imaginary part of Φ is also the unitary phase for the noise field. Modal Φ
includes the real endpoint term -1/2 [ln w(z) - ln w(0)]. It vanishes identically
for a constant fill and for the GNLSE unit-index override. Statistics evaluate
weights only through translate.linop.power_weight_at(problem, z). Radial Φ in a varying
gas carries the spatial counterpart, -1/2 ln[Y_z(ω,k_r,z)/Y_z(ω,k_r,0)] with
Y_z ∝ Re k_z/ω, evaluated from endpoint admittances in both the table and the
parameter-expansion representations (SEP 34 §4.4); it
is exactly zero in a uniform medium, and a radial statistic reads the admittance at its
plane through flux.at(z).
An expansion may append an endpoint block with endpoint_degrees on the same
parameter box. Its terms follow the integral block; its scalar weights are plain
basis products minus their entrance values. n_terms is then the sum of both
blocks’ tensor sizes. Reuse reweights each block with its original weight kind.
phi_expansion is a leaf, like a PlasmaChannel, so its fields are not rows of
this document; they are parameters (the path names, in the order of the term
multi-index), ranges ((n_params, 2) float64, the values each path reaches over the
tabulated range), degrees ((n_params,) intp, the Chebyshev degree per parameter),
terms ((n_terms, n_batch_or_1, n_modes, n_pol, n_omega) complex128, with
n_terms = Π(degrees + 1) + Π(endpoint_degrees + 1) when the endpoint block
is present, otherwise just the first product, and each multi-index flattened in C
order) and weights, the concatenated integral and endpoint tables along z as a (n_terms,)-valued PiecewiseChebyshev. Its
breakpoints, degrees and hard kinks are the three rows above, which validate
checks, so the local step cap and the schedule read one field whichever representation a
problem got.
Why there are two representations. A mode whose whole z dependence runs through
declared scalar paths — a Marcatili mode’s core radius a(z) and number density ρ(z) —
makes D a function of those parameters, D(ω, z) = D(ω; ρ(z), a(z)), and the two
dependences are nothing alike. D is analytic in the parameters; ρ(z) is what carries
the square-root cusp at a near-vacuum entrance, the stage boundaries and the taper
corners. So D is expanded once over the range each path spans and Φ is that expansion
against scalar integrals I_j(z) = ∫₀ᶻ Π T(p̂(z')) dz', which cost 8 bytes per term per
coefficient instead of 16 bytes per state element. The dispersion contribution is
Σ_j d_j(ω) I_j(z), exact to the truncation in the parameters. The appended log-weight
block adds its plain basis-product endpoint differences to form modal Φ. On a gradient
capillary the expansion is a few terms and a fraction of a megabyte where the z table is
tens of megabytes, and it agrees with the z table well inside the tolerance both are
built to (accuracy.md §1).
The z table stays as the general representation, and the translation layer chooses it from the mode set’s traits: for a mode that declares no parameters, for a set that declares different ones or disagrees about their values, for a mode whose declared parameters are not the whole of its z dependence, and for a quantity that kinks in a parameter rather than being analytic in it — a loss clamp crossing its limit inside the range, the evanescent index floor binding partway along it — where the degree search reaches its ceiling. Which representation was built, and why, is in the debug log.
The third of those is the optional parameters_own_z trait, read by name and
defaulting to true, which translate.linop.parameters_own_z collects and the two
expansion-reuse gates read as well. The case is a
solprop.nonlinear.gnlse.SpecifiedMode whose dispersion follows a declared path along
z beside its effective area: the area is still its kinks, and is still what
declared_paths resolves first, but freezing the area at a value would leave the
dispersion moving, so there is no parameter space to expand in and the operator is
tabulated along z. The contract needs nothing for it — no field, no representation, no change to
what a backend sees beyond the flag it already reads. Every CPU backend runs it, the
native one evaluating the table in its own kernels; a device build refuses it through
translate.stage.require_device_ready, as it refuses any z table. The same fibre with a
constant dispersion and a varying area keeps the one-parameter expansion, and the
device routes with it.
The tolerance is not the number asked for but the larger of it and a hundred times
the measured round-off floor of D: the frame subtracts β₀ + β₁(ω − ω₀) from a β
that reaches 8·10⁷ m⁻¹ at the top of a wide field grid’s active band, so D is only known
to about 1e-11 relative and no interpolant can resolve it below its own noise. The floor
is a property of one evaluation of D and serves both representations. See
docs/guide/numerics/linear-operator.md, “The table”.
The coupled-flow leaf#
linear.coupled_flow (CoupledFlowTables or None) is the generator of a guide whose
channels couple, dense or factored, with its residual, conditioning and error certificate
and one method per section. The generator and every term of it are complex128 in 1/m,
the scalar weights dimensionless complex128, the three certificate rows float64; omega
is grids.omega itself and z_edges covers the run. A leaf in translate.coupled, so
its own fields are not rows here.
It is None except on three routes. The coherent wall of a capillary, which
solprop.translate.coupled.constant_schur_flow (constant geometry) or
frozen_residual_flow (a guide that varies along z) builds from a condensed Robin
boundary operator when prop_capillary is given a wall=. A general modal guide that
declares a unitary basis of a split block, where U^H diag(beta) U is not diagonal and
solprop.translate.general.coupled_block reads the transformed generator relative to one
reference channel for constant_schur_flow to certify. A moving solid guide, whose
route carries every channel’s generator less the reference channel’s entrance diagonal
and the measured connection Γ = W⁻¹S (solprop.modes.connection.band_connection), both
expanded on the maps’ sections, for frozen_residual_flow (certificate_id
"general-moving-frozen-residual"); Φ beside it is the reference channel’s entrance
dispersion, a one-term expansion whose declared path puts the section edges into
hard_kinks. connection is Γ as measured and not its conjugate — the provider’s
fields are internal-convention and the synthesis stores them unconjugated, so the kernel’s
amplitude is their coefficient — and needs no power conversion, the Poynting amplitude
being the true-power one on this route (W^(-1/2) C W^(1/2) is the identity there, and
Re Γ_nn = W'_nn/2 is each channel’s own normalisation). The producers share the leaf,
the executor and nothing else: the split block carries no wall pair, no connection and no
validity ratio, and its gauge_id is the tables’ composed one rather than "capillary HE_1m". The leaf itself is provenance-blind and neither the stage bundle nor the backend
asks which built it.
The n_deg axis is Chebyshev coefficients along z within each section, in the basis
numpy.polynomial.chebyshev.chebval evaluates on x = (2z − lo − hi)/(hi − lo), and
connection shares those axes exactly; the frozen block of a section is that
expansion at its midpoint (solprop.translate.coupled.frozen_generator), read out of the
stored coefficients rather than carried beside them, so the certificate and the executor’s
Schur factors are the same matrix to the last bit. z_edges are the sections, placed on
the operator table’s own breakpoints — which land on every declared kink of the radius and
the density — subdivided CoherentWall.sections times. In the rank-one wall pair the
vectors are dimensionless and the weight carries the whole 1/m, so that w (v x v) is the generator’s own 1/m, and the constructor checks the pair reconstructs the dense
off-diagonal.
The three certificate rows are per section: generator_error is the frozen block’s Schur
diagnostic plus the relative Chebyshev tail of the z expansion, residual_norm_bound
is the enclosure Σ_k ‖r_k‖₂ of the explicit residual over the whole section (|T_k| ≤ 1, so it bounds the extrema no sample sees), and inverse_condition_bound is
exp(dz_max (μ_max − μ_min)) of the frozen block. On a "cf4" section the same three
rows mean: the Chebyshev tail alone, zero (the whole generator is in the exponential),
and the pair bound exp((2/√3) dz_max (m_hi − m_lo)) on the product of the four
factor norms of any forward-and-inverse CF4 pair a step anchored there can form, [m_lo, m_hi] enclosing the Hermitian-part spectrum of G − C over the step’s reach by Weyl’s
inequality on the stored coefficients; its growth bound exp((2√3 − 3)/6 · dz_max (m_hi − m_lo)) is that row to the power (2 − √3)/4 and needs no row of its own
(solprop.translate.coupled.cf4_growth_bound). 0.9/(K · cond) from the last two is the
advisory step solprop.translate.problem.validate_coupled_flow warns dz_init, dz_max
and a fixed schedule against; no row carries it.
What executes. Two families of combinations, either of which may carry the vector
block (which differs only in the generator it stores and in carrying no factored pair; on
a varying guide its connection is block diagonal over the order’s two radial families,
each family’s own and exactly zero between them, stored like the scalar one in the
true-power amplitude): section_methods == ("constant_schur",), the dense generator at
one interval of degree zero, no connection and an identically zero residual_norm_bound,
beside a Phi that is the exact constant representation; and section_methods that is
"frozen_residual" or "cf4" on every section, the dense generator at any degree
along z, an optional connection and, on a frozen-residual section, a residual the
interaction-picture source carries, beside a Phi of any device-ready representation. A
"cf4" section is integrated by the commutator-free fourth-order exponential `exp(l(a A₁
b A₂)) exp(l(b A₁ + a A₂))
of the whole generator at the Gauss nodes (a, b = (3 ∓ 2√3)/12, the early-weighted factor acting first;solprop.translate.coupled.CF4_NODES,CF4_WEIGHTS) with no residual, its inverse the product of the negated factors in reverse order. A step is integrated by the method of the section its anchor lies in, so a flow whose declared fallback mixed the two is the same equation throughout. For a CF4 section the backend measures the four factor norms of every stage's pair and refuses a product above the declared pair bound; the flow's ownO(l⁵)` defect is invisible to the embedded pair, so a run through CF4 sections is at most fourth order.
Both need no noise draw and a scalar state (n_pol = 1, the action contracting the
channel axis alone; the synthesis may resolve two components, which the flow never reads),
and a Phi that is a multiple of the identity in the channel axis – the flow action
and the cumulative diagonal are applied as a product, which is the same equation only when
they commute. Because Phi is then one reference channel’s, its normalisation term −½ ln[w(z)/w(0)] is that channel’s for all of them, and the capillary wall’s varying
generator carries the rest of each channel’s own on its diagonal, −½ d ln(w_n/w_1)/dz
(real, zero on the reference channel and on a constant guide), so exp(ΔΦ) times the flow
renormalises every channel by its own power weight; the term is part of the stored
generator and so of the frozen block, the residual and the CF4 exponent alike. The backend
measures norm(U) norm(U^-1) of the whole step itself and refuses above the declared
inverse_condition_bound rather than trusting it.
Which section a stage that lands on a section edge is evaluated on is normative: it is the step’s own section wherever the position lies in that section’s closed interval, matched within 1e-10 of the flow’s span, so that a schedule arriving a few ulp short of a declared kink and one arriving a few ulp past name the same section. A position genuinely outside the step’s section — which only a step spanning an edge produces — takes the section containing it, resolved on the side that closes there when the position is itself on an edge: a stage that ran past its step’s section arrived from below, and the residual it is measured against is the one-sided limit on the side it came from. An edge is a kink, so reading the opening side instead drops a whole run from fifth order to first (§1).
A linear.modal_metric beside it is refused on the mode-set route, whose flow is a
condensed boundary block with no bound reciprocal basis and therefore no metric this
package has certified, and admitted on the general modal route, where
transform.modal_tables is present and the metric is the guide’s own sampled basis Gram,
which a declared unitary rotation leaves where it was. A flow that mixes a
"constant_schur" section with any other, a factored generator, a "diagonal" section, a
varying degree or a connection beside a constant section, and a Phi that took the z
table beside a varying one, are refused by name. numpy_ref executes every admitted
combination; numba_cpu and the native backend execute the constant-Schur and frozen-residual
sections and refuse a "cf4" section by name
4. Multimode transform#
field |
shape / dtype |
meaning |
|---|---|---|
|
|
mode fields at nodes (element type of the time-domain field) |
|
|
weights folded in, and the basis normalisation |
|
same |
the embedded coarse-rule projection for the error estimate |
|
|
the scalar rescaling of synthesis/projection along z for scale-invariant modes |
|
int |
nodes per chunk for the response pass |
|
|
effective area for mode-averaged normalisation ( |
|
|
the spatial leaf below; absent for modal transforms |
|
|
physical transverse weights in m² at z=0 for resolved modal populations; divide by taper scale squared at later planes; mode-averaged runs use |
|
|
what the transform represents, checked on construction against the geometry, the tables, the node count and the node weights; |
|
|
the full E/H reference of a general modal guide — separate synthesis S and projection Q, both on |
For radial geometry, synthesis is the inverse raw QDHT times the temporal pad
scale, projection is the forward QDHT, and projection_coarse equals
projection. Their departure from an exact inverse is the independently derived
truncation-plus-roundoff contract in the Hankel manual.
For free geometry all three matrices have shape (0, 0); the transform uses the
geometry’s FFT axes and scalings instead. Spatial transforms have _n_pol=1,
no taper_scale, and share the scalar map between the state’s polarisation
components. The node-buffer budget applies (1 GiB by default on spatial grids, 64 MiB on
modal routes).
SpatialGeometry is a frozen leaf in translate.spatial, with the following fields:
kind:"radial","free"or"slab": the hybrid planar transform (solprop.modes.slab.PlanarGrid), slab modes in x on the transform’s ownsynthesis(n_x, n_p),projectionandprojection_coarse(n_p, n_x)(the slab rule, pad scale in the synthesis), times a raw DFT in y. Its state columns are(p, k_y), flattenedp + n_p*iywith the mode index fastest, amplitudes in sqrt(W/m) per y frequency; its nodes(x, y),ix + n_x*iy.n_modes = n_p*n_yandn_nodes = n_x*n_y, so its column vectors (spectral_weights,on_axis) and its node vectors (the rest) have different lengths. Itsspectral_axesis(k_y,)alone; itsspectral_weightsare2 dy/(ε₀ c n_y)in m·Ω, the raw DFT’sdy/n_ydivided byε₀c/2, so the commonε₀c/2energy measure gives joules; itswindowandabsorber_ratedepend on y alone (checked), so a backend may apply them per(p, y)row; its scalings are the y FFT’s,1and1/n_y. A constant gap and constant media only; the operator is §3’s withkappa_sq, and the prefactor is §3’s per column.numpy_ref,numba_cpuand native CPU execute it.shape:(n_r,)or(n_x, n_y), each dimension at least 2, exceptn_y = 1for a Cartesian line uniform in y (grid.LineGrid), whose one y sample isy = 0withk_y = 0and unit measure: its weights are then in m and its integrals per metre of y.axes: tuple of physicalror(x, y)float64 axes in m.spectral_axes: tuple ofk_ror(k_x, k_y)float64 axes in rad/m; Cartesian frequencies have FFT order.spatial_weightsandspectral_weights: positive float64 vectors in flattened column order. Area weights have units m^2; spectral weights have units m^-2 for Hankel spectra and m^2 for raw Cartesian DFT spectra.coordinates: float64(n_nodes, 2)pairs(r, 0)or(x, y)in m.window: float64(n_nodes,)collar profile in [0, 1].absorber_rate: nonnegative float64(n_nodes,)spatial power loss in m^-1, applied as-absorber_rate*E/2inside each nonlinear evaluation, independently of the nonlinear material prefactor.on_axis: complex128(n_modes,)linear weights reconstructing the physical axis from the transverse spectrum. Temporal oversampling is not included.forward_scale,inverse_scale: positive finite common transverse scalings; m^2 and m^-2 for the radial pair, dimensionless for Cartesian transforms.
The spectral weights define spatial energy and noise normalisation. The physical weights, coordinates and on-axis reconstruction support fluence-based spatial statistics. Physical spatial fields are in V/m; radial spectra are in V m, and Cartesian raw DFT spectra are in V/m.
synthesis carries the pad scale n_to/n_t of the oversampling round trip, so that
the field handed to the response pass is the physical one in V/m, and the crop scale goes
into prefactor (§5); why each sits where it does is in
the kernel problem.
For the mode-averaged case the synthesis is pad_scale / sqrt(ε₀ c A_eff(0) / 2) and the
projection is 1, the reciprocal being already inside prefactor; taper_scale(z) = sqrt(A_eff(0)/A_eff(z)), which for a scale-invariant mode is the a(0)/a(z) of
modes.quadrature.taper_scale, so one mechanism serves both geometries.
5. Responses (the compiled nonlinear term)#
field |
shape / dtype |
meaning |
|---|---|---|
|
tuple of enum |
the fused-pass sequence (e.g. |
|
frozen dataclass |
|
|
|
the frequency-dependent electronic response, a leaf described below: one channel per population with its own fine-grid multipliers and density table, |
|
tuple of |
rate tables in plasma-channel order, identical to each channel’s |
|
tuple of |
labelled independent exposure channels, each with its own density, initial fraction, rate, potential and free-current coefficient; empty without plasma |
|
|
the running-integral rule of the plasma column, a leaf described below, one per problem because the channels share the current integrals; the trapezoid without plasma |
|
tuple of |
Raman-active components in species order, each with its amplitude density, label, undamped rows, decay tables, fraction and drive form |
|
scalar Chebyshev table |
total number density along z in m^-3, or the dimensionless normalisation for a solid/GNLSE |
|
tuple of str |
unique stable component labels, in species order |
|
tuple of str |
chemical species names in the same order; names may repeat |
|
tuple of scalar Chebyshev tables |
one number density per label, including zero and non-ionising populations |
|
tuple of |
explicit post-transform source assembly, empty or the same object as |
|
|
labelled envelope channels with validated instantaneous rate/cycle rules and coarse-axis Drude/loss multipliers; ordinary polarisation remains a separate stream until spectral assembly |
|
|
which representation carries the prefactor, following §3 |
|
|
the UPPE/envelope prefactor |
|
frozen dataclass or |
the same quantity as §3’s |
|
|
the prefactor itself, present exactly when |
|
|
rate α(t) on the coarse grid, for ADR-025’s split-step scheme |
|
|
the same rate on the fine grid, for ADR-025’s in-the-nonlinear-term scheme; both are carried so that switching schemes is not a retranslation |
|
|
the temporal-absorber application: the default in-stage term ( |
|
|
present exactly for |
|
|
which material responds at each node of |
The envelope source leaves are frozen in
tests/translate/test_envelope_plasma_compilation.py. The full field route keeps
its own plasma_channels; population_channels selects those or the envelope
assembly’s channels for position-dependent density and initial-fraction tuples.
No population history crosses the seam or survives an RHS evaluation.
EnvelopePlasmaAssembly(channels, factors)has ordered, uniquely labelled populations and one coarse-axis multiplier record.EnvelopePlasmaChannel(label, species_identity, ionisation_potential, density, preionisation, cycle, charge_mass_ratio, electrons_per_event)carries compiled scalar paths and SI constants. Single ionisation fixeselectrons_per_event=1.CycleIntegralData(rate, rules, validation, identity, moment_rtol, negligible_rate)retains the explicitly instantaneous rate convention and species binding.AngularRule(nodes, weights)contains the 64, 128, 256 and 512 point split rules.CycleValidation(probes, residuals, cutoff_rate, format_version)records independent Cartesian-cycle checks of the averaged rate and both directional moments, with the model cutoff audited separately.EnvelopeSourceFactors(active, drude, loss, carrier_policy, omega0)owns read-only contiguous arrays on the coarse absolute-frequency axis. The multipliers are-1/omega**2and-1j/omegainside the positive active mask, zero elsewhere.KernelProblem.validate()binds these values to its grid.shock=Falsefreezes their denominators and the explicit UPPE numerator atomega0, retaining mode-index dispersion and masks.
Execution order is cycle moments, a fresh trapezoidal population scan per channel and physical node, formation of Drude drive and loss current, material collar, spatial projection where applicable, two forward temporal transforms, coarse cropping, spectral addition to ordinary polarisation, and the UPPE prefactor. Cartesian propagation transforms the currents within each physical node chunk before the transverse forward transform. The embedded quadrature alarm projects and transforms both streams too. There is no inverse transform to manufacture a time-domain plasma polarisation.
The CPU workspace owns (2, chunk*n_pol, n_to) complex sources, five real
(chunk, n_to) histories, one integer status per node, two projected complex
streams and their spectra, projection scratch, and optional embedded streams.
Its plans keep the full chunk width; unused rows are cleared for a final partial
chunk. Rules are packed once, and all owned buffers and measured FFT plan allocator
growth enter the memory ledger. native owns the corresponding resident
streams, rules, histories and status arrays. A planned scalar status readback
per channel/chunk rejects failed angular refinement before any current transform.
NumPy, Numba CPU and native scan the observed envelope field afresh for population
statistics. backends.md §9 records qualification separately for each
execution space.
Each PlasmaChannel is a leaf, like kerr_coefficients, so its fields are not rows
of this document; they are charge_mass_ratio (e²/mₑ in C²/kg, the constant of the
free-electron current (e²/mₑ)∫f E dt), ionisation_potential (in J, the constant of the
loss current I_p W (1−f) E/|E|²), preionisation (a ZProfile table of f₀, a constant
table for the plain number a response carries, so a backend reads one value per plane), and
rate, the ionisation rate table. That table is the ionisation_table row above — the
same object, so a run carries one rate and validate checks the identity. The ionised
fraction is f = 1 − (1 − f₀)e^{−∫W dt}, with the initial neutral fraction
multiplying the survival exponential. The
group is present only on a field grid, which validate also checks.
plasma_integral is a leaf (nonlinear.ionisation.RunningIntegral), designed by
translate.plasma_integration.design_running_integral. Its fields are policy
("band" or "trapezoid"), taps (float64 (K+1,), centre first, (1.0,) for the
trapezoid), dt and band_edge (the fine spacing in s and the band edge
\(\omega^{\rm win}_{\max}\) in rad/s the taps were designed for, None for the
trapezoid), certified_error (the certified \(\max|H-1|\) in band), boundary and
exposure. Each running integral is the cumulative trapezoid of the filtered
integrand \(\tilde y_n = c_0 y_n + \sum_{k=1}^K c_k (y_{n-k} + y_{n+k})\). The reference
sums it centre first, then each pair added before it is weighted; a backend may sum the
stencil and the running integrals in any order, held to the stencil’s
\(\gamma_{2K+2} A_n\) bound and the absolute prefix bounds of the scans’ signed increments
(SEP 25 §4.6). The \(K\) samples past
each end are \(y_{-m} = 2y_0 - y_m\) and \(y_{N-1+m} = 2y_{N-1} - y_{N-1-m}\). The exposure
is floored at zero pointwise, \(X_n = \max(S_n, 0)\), before the fraction is formed; the
loss current keeps the raw rate. So plasma_scan finalises the fraction at sample \(n\)
from rates up to \(n+K\), the free current from samples up to \(n+2K\), and
the production column stops at the total current. The physical oracle
PlasmaResponse.evaluate also integrates that current to polarisation, from samples up
to \(n+3K\). The "trapezoid" rule has no filter, no boundary and no floor.
The held field-plasma source (SEP 27).
The field-plasma column writes three complete streams: the collared non-plasma
polarisation P, the collared current X = gJ and its collar-derivative companion
Z = g'J, with g' the analytic grids.t_window_fine_derivative (§2). On active bins
their spectra combine as P + X/(iω) − Z/(iω)²; inactive bins are exactly zero and no
zero-frequency division executes. Modal routes project before transforming; radial
routes transform on nodes, combine and crop, then project once; Cartesian, line and
planar routes combine before their transverse transform. The derivative and every
additional buffer are charged to memory admission. The steady-current certificate uses
the complete fine transform (DC included) in its denominator, must not exceed 1e-10 and
is recorded under plasma_source.
response_order reaches intensity, ionisation_rate, plasma_scan, plasma_current, kerr, accumulate for a problem declaring both a Kerr and a plasma response; dispersive_kerr
follows kerr when a dispersive electronic response is compiled; raman_drive joins
it after them when a Raman response is compiled, so a gas-filled capillary with all three
reaches intensity, ionisation_rate, plasma_scan, plasma_current, kerr, raman_drive, accumulate.
Each member of raman_terms is a leaf, like a plasma channel, so its fields are not
rows of this document; they are undamped ((n_terms, n_to_doubled) float64: each
independently damped term’s undamped response, causal from t = 0, tapered by the grid’s
temporal collar, zero-padded to the doubled grid, sampled once per grid by
translate.raman.undamped_kernels; n_terms is 1 for a glass or a vibration-only gas
and 2 for a gas with a rotation and a vibration), decay_rates (one ZProfile table per
row: the decay rate 1/τ₂ along z, composed by the translation layer from the term’s
dephasing and the density path, translate.raman.decay_rate_profiles, finite everywhere
and capped at MAX_DECAY_EXPONENT / dt_fine), fraction (f_R multiplying the summed
kernel, in (0, 1]; 1 for an absolute molecular response) and thg, the drive form. The
kernel damps row k by exp(−γ_k(z) t), sums, scales by fraction and transforms
(rfft · dt, one real transform of n_to_doubled) whenever a rate changes, memoising the
spectrum; the convolution is irfft(spectrum · rfft(drive padded)) on the doubled grid.
Each leaf also carries label and density, the component identity and its amplitude
density, separate from the density the linewidth reads.
The leaf splits the kernel into the expensive part, which is fixed, and the cheap part,
which is not, and hands a backend arrays and scalar paths rather than physics objects.
The decay rates are paths rather than dephasing objects so that the contract stays a
description of arrays and scalar paths along z, as it is for the density itself, and the
physics that turns a linewidth into a rate stays in the physics layer; a coherence that
collapses at zero density carries the cap MAX_DECAY_EXPONENT / dt_fine
(708/dt_fine), which damps it within one sample. In the scalar case a backend evaluates
per chunk of quadrature nodes (solprop.backends.numpy_ref.passes.raman_polarisation),
with \(D\) the real drive of the grid written into the first half of an array of twice the
length. The damped kernel spectrum \(\tilde H\) does not depend on the nodes, so a stage
forms it once and shares it between the chunks
(solprop.backends.numpy_ref.passes.raman_spectrum: each row damped at the rate its
table gives at this plane, summed, scaled by fraction and transformed). A backend
memoises it on the rates, not on the density, because the rates are what it depends
on: a glass’s are zero at every density, so its kernel is transformed once for a whole
run, and a gas on a pressure gradient refreshes it when its stage positions present new
rates. Only the most recent set of rates is remembered: a gradient presents a new density
at every stage, so a growing cache would hold thousands of spectra and hit none of them.
A change of rate costs one exponential per term over n_to samples and one or two real
transforms of length 2 n_to, against n_modes · n_pol transforms of length n_to per
stage for the field itself.
Each Raman leaf also carries tensor_weights, a read-only contiguous float64
(n_terms, 2) array, and tensor_capable, a boolean. Isotropic rows carry
(1, 0) and rank-two rows (1/4, 3/4). A scalar-only row carries (1, 0) with
capability false; a response is vector-capable only if every term is qualified.
The weights form trace/traceless effective kernels after each physical row is
damped; amplitude densities and fractions apply before combining components.
Scalar execution keeps the ordered unweighted kernel sum. Vector execution
uses S=Qxx+Qyy, D=Qxx-Qyy, C=2Qxy and three convolutions, or just S
when all traceless weights vanish. These are local Cartesian components after
synthesis. Unsupported scalar-only vector responses are refused before execution.
The drive form is carried, and the two refusals that make it true stand. Which real
quadratic form drives the coherence on a field grid — E², or the cycle-averaged
|E_a|²/2 — is a property of the problem and not of each response: a drive that keeps the
2ω beat radiates a third harmonic through the later multiplication by the field, and a
grid built without one does not oversample enough to hold it. So the translation layer
checks that the declared Kerr and Raman responses agree about thg on a field grid
and refuses a field-grid Raman response with no Kerr response beside it; the answer
reaches a backend as each Raman leaf’s thg rather than being inferred from
kerr_coefficients’ analytic_signal flag. An envelope
grid’s drive is |A|²/2 whatever the flag says, so neither check applies there and a Raman
response may stand alone.
The undamped rows are h itself and carry no ε₀: the response writes it outside
the convolution integral (docs/guide/media/raman.md), so the reference backend is the one place
a backend reads a physical constant. Folding it into the rows would change their stated
units and loosen the rounding-level agreement with RamanResponse.evaluate that pins the
pass.
dispersive_kerr is a leaf (translate.responses.DispersiveKerrTerms), so its
fields are not rows of this document. It is the frequency-dependent electronic
response of SEP 6 reduced to arrays, sampled by the translation layer on the problem’s own fine
spectral layout at absolute angular frequency: the one-sided (n_to // 2 + 1,)
layout on a field grid, the full FFT-order (n_to,) layout on an envelope grid. Its
fields are evaluator ("symmetric" or "polynomial"), sector ("full", every
quartet, for thg=True; "conserving", the quartets with two positive and two negative
signed legs, for thg=False), carrier (true exactly for the full sector on an
envelope grid, which needs grids.carrier_2w0), labels and coefficients (one per
channel: the population and ε₀ γ₀, the static coefficient per unit density),
positive (bool, Π₊: the bins of strictly positive absolute frequency, the field
grid’s DC and Nyquist bins excluded), filters (float64 (n_channels, n_omega_fine):
the symmetric factor g, or the polynomial’s bounded leg weight d̃ = min((ω/2πc)², ν_c²)
in m⁻²), outputs (float64 (n_channels, 2, n_omega_fine), the polynomial’s output
multipliers 1 + a d̃ + (b + b′) d̃² and 3a + 6b d̃, None for the symmetric
evaluator), products (float64 (n_channels, 2), the polynomial’s constant weights
3(b + b′) and 6b in m⁴, None for the symmetric evaluator) and densities (one
scalar Chebyshev table per channel, m⁻³, read through the stage bundle’s
dispersive_kerr_densities) and regions (int32 (n_channels,), present exactly on
the general modal route: the node region of transform.modal_tables.region_index each
channel acts in, a region’s gas fill giving one channel per species; None on every
other route, where a channel is a population and acts at every node). The channels
combine by evaluator, in the groups of DispersiveKerrTerms.groups(). For the
polynomial a backend evaluates, per channel and node,
P += coefficient · ρ · cubic(E); for the symmetric evaluator, per group of the
channels acting at the same nodes (every channel, or one general modal region’s), one
P += F · cubic(E) under the mixture filter g_mix = [Σ_s f_s g_s⁴ / F]^(1/4), with
f_s = coefficient_s · ρ_s at the stage and F = Σ_s f_s, rebuilt whenever the
densities change (solprop.nonlinear.responses.mixture_filter: the species summed in
channel order, the fourth root as two square roots, a unit filter where F = 0; one
channel is its own g and coefficient · ρ unchanged). cubic is
solprop.nonlinear.responses.dispersive_cubic on the multipliers: the
filtered fields, their isotropic-Kleinman trilinear products in the sector (on an
envelope grid the three-photon group carries carrier_2w0 and the one-into-three group
its conjugate, and Π₊ acts on absolute frequency), the temporal collar
grids.t_window_fine on each product, each product’s spectrum times its output
multiplier, and one inverse transform: G[w (G E)³], the collar before the output
multipliers, and the response is not windowed again with the other responses’
polarisation. It reads the bare field and changes no other response’s source. The
polynomial’s channels are the exact per-species sum, which a backend may rewrite
exactly – species with equal leg bounds share their derivative fields and add their
coefficient tables, as numba_cpu does – but not approximate. The symmetric
evaluator’s mixture filter is SEP 6 §6’s. numpy_ref and numba_cpu execute it on every route: the fused node path
(single-mode, mode-averaged multimode and radial), the Cartesian free-space, line and
hybrid planar pipelines, and the general modal route at the nodes of each channel’s
region, on the real three-component field, beside the region tensors of
regional_responses (the route has no collar). numba_cpu moves the multipliers off
the nodes – onto the modal columns, onto each chunk’s columns or rows, or into the
general route’s synthesis (backends.md
§3.11). native executes it on every
route with the same structure (backends.md §4).
Vector polarisation: n_pol == 2 selects the vector-isotropic Kerr form; tensor responses
(χ⁽²⁾) carry their own coefficient arrays when they exist. The transverse components are
carried by §4’s synthesis rows or by the state’s polarisation axis and never by both,
so the product of transform’s component count and n_pol is at most two; validate
checks it.
prefactor follows z, and the two halves of it are carried in different places.
A_eff(z) is carried by §4’s taper_scale, which rescales the synthesis and divides the
projection; the table therefore holds the area at the reference position and lets only
Re n_eff(ω, z) vary. The identity prefactor(z) = mode_average_prefactor(z) · taper_scale(z) · crop_scale holds at every position and is a test; on a fill, where the
bore is constant, the table simply is mode_average_prefactor(z) · crop_scale.
The prefactor is built by the machinery of §3 minus the parts it does not need. On the
expansion it is the same construction with the same parameters and the plain products as
weights; on the z table the mode set’s scalar paths are resolved first, so each interval
holds a lower degree. No round-off floor is measured and no tolerance relaxed, unlike Φ:
those exist to recover from the eight digits the co-moving frame cancels out of β, and
−iω√(ε₀cA_eff/2)/(2ε₀c·Re n_eff) has no such cancellation, so the build converges at
the tolerance it is asked for. Why the prefactor follows z, and what carrying it costs, is
in the kernel problem.
A backend evaluates the prefactor only through translate.responses.prefactor_at, as it
evaluates the linear flow only through translate.linop.delta_phi and unitary_phase,
never by reading the coefficient arrays. That is what lets the representation change —
a z table, an expansion in the scalar parameters, a constant — without a backend
noticing.
Each plasma channel also carries label, density and electrons_per_event=1.0. Dissociation and multiple electrons per event are unsupported. Channel
labels select species metadata by name, and rates are identity-checked per channel.
Kerr coefficients carry their own scalar density table: for several components
it contains sum rho_s gamma3_s, with coefficient=epsilon_0; one component keeps
the single-species coefficient and density multiplication order. Raman components carry
label and density beside their other fields. Their linewidths use total
number density, an explicit approximation; their amplitudes use their own populations.
CompiledResponses.n_species is a property, the number of labels. Compatibility
properties plasma_coefficients and raman_coefficients return the sole leaf only
when that specialised path applies; channel-aware code consumes the tuples.
The StageScalars fields, in order, are z0, z1, phi_delta, phi_at,
prefactor, density, taper_scale, raman_rates, kerr_density,
plasma_densities, plasma_preionisation, raman_densities, flow_section,
flow_weights, transform_weights (these three described in §1) and
dispersive_kerr_densities, each channel of dispersive_kerr its number density in
m^-3 at the stage, in the leaf’s order, empty without the leaf. The plasma tuples have
one entry per channel; Raman densities one per component; flattened Raman rates
visit every kernel row in component order. The preionisation property serves the
one-channel specialisation. All changing Raman amplitudes and rates participate in
kernel-cache validity; the one-component kernel keeps the single-species amplitude order.
6. Initial state, noise and batch parameters#
Optional TDSE source binding#
tdse describes the atomic basis, prepared ground state, discrete shadow order,
output selection, calibration and response lattice as canonical JSON and a SHA-256
identity. The scalar fields are checked against this record. It contains no
native handle. The driver passes a translate.tdse.TDSEBinding explicitly to
registry.select(..., tdse_binding=...), constructs and closes it outside the
backend, and refuses an unsupported route before expensive setup.
The binding accepts chronological, real SI fields (n_nodes, n_to) and returns
owning read-only TDSEResult arrays. Each consumed sample is valid and finite.
Its atomic counter is charged immediately before every provider invocation,
including failed calls and requested diagnostics. Stable chunks are reserved for
the entire Adams trial before startup. Unused reservations are released on every
exit path. restore_call_ledgers(logical, atomic) restores cumulative counters
after loading a field; it restores no Adams history or atomic state.
The host binding protocol also labels its evaluations for an optional raw archive. Each reservation opens one optical trial, evaluate takes
an optional density label (m⁻³), and record_trial(accepted=...) reports the
stepper’s decision on the latest trial. The reference backend calls it from its
step record (_record), which every Adams attempt reaches, rejected or accepted;
it changes no step, charge or reservation. None of this is part of the protected
StepDriver surface or the kernel problem, which compiled backends see, so it is
outside the kernel schema: Numba and the native backend refuse TDSE by name and never hold a
binding.
The backend independently projects macroscopic P and J with the existing modal
weights and crop scale. It subtracts unscaled discrete shadows, then applies
density and the declared scales once. The ordinary optmat cubic keeps its
existing temporal source window. Raw atomic dipole/current histories are never
modified or mean-subtracted. A native descriptor’s source is always gated and
edge-free (SEP 26; TDSEDescriptor.gated); a cubic replacement’s never is.
Every descriptor has one layout, solprop.tdse-descriptor.v5; any other schema tag is
refused. Its response record holds every
response field but the consistency policy: response.model, a haCC basis tagged
kind="hacc_helium" and response.atomic_window (null for the full lattice); a
recorded source_filter is refused. lattice.time_collar records the grid collar’s
exact Planck breakpoints in seconds and lattice.tail_policy="unchanged" the
untouched raw history; the seam checks the collar against the actual fine-grid gate,
so a collar change changes the scientific identity. A windowed response’s
lattice.atomic_window records the resolved duration, taper, nominal bounds, retained
sample range, t_s, t_e and the gate’s Planck breakpoints; a haCC setup is the
pinned record {"initial": "ground_root", "restart": "every_evaluation", "growing": "refuse_below_cut", "norm_growth_limit": 2.0}. The binding drives the atom with the
retained samples multiplied by the gate and pads the returned currents with zeros;
TDSEResult keeps its full-lattice shape. The consistency policy of a haCC response
never enters the descriptor.
For a gated source the gate is the atomic window’s when one is set, else
lattice.time_collar. The backend forms S = j_total - j1, the gated current g S
and the edge array g' S, with g' the analytic gate derivative of
TDSEDescriptor.source_gate_derivative() (solprop.grid.planck_taper_derivative),
applies density and scale once to both, projects, transforms and crops both, and on
the active bins forms X - E/(i omega) before the current factor. It changes the
macroscopic current source; the atomic driving field and separate in-stage field
collar remain unchanged. Operand-derived roundoff indicators include the gate’s
multiplication and the edge term. Translation refuses a steady-current certificate
above 1e-10, relative to the full transform of the gate
(solprop.translate.tdse_source.steady_current_certificate); the certificate is run
metadata (tdse_source) and part of the last-evaluation edge diagnostic, never part
of the descriptor or an identity. The gate is an explicit approximation: its 1 % full-source width robustness is not
established and the atomic-cut uncertainty remains
(TDSE accuracy and limitations).
Same-row embedded quadrature diagnostics identify the actual evaluated field
(startup, predictor, corrected or diagnostic) and carry absolute cancellation
floors. They are not accepted-field statistics. An exact accepted-field diagnostic
needs an identical cached input or another charged solve.
The initial-state fields below are independent of that optional binding.
field |
shape / dtype |
meaning |
|---|---|---|
|
|
initial state in the lab frame, FFT convention |
|
same or |
modified shot-noise realisation |
|
|
|
|
dict of |
per-member scalars recorded in the output; per-member table indices where tables differ |
|
|
absolute initial position in m, non-negative and before the first requested save; true-power input is normalised at this position |
7. Stepper settings and schedule#
field |
type |
meaning |
|---|---|---|
|
frozen dataclass |
|
|
frozen dataclass |
|
|
float |
caps on the controller’s proposal; |
|
float |
landing stretch factor ( |
|
|
fixed schedule to replay (lockstep batches require it); its edges must contain every save plane and hard kink, which |
|
|
z positions the driver will ask for; steps land on them unless |
|
|
ADR-004 and backends.md §3.6. |
|
|
The method stepping the problem instead of the tableau, or |
|
bool |
whether a step is capped at the width of the operator-table interval it starts in. |
|
bool |
whether the start-up rule takes the step after the first from the first step’s own scaled norm ( |
|
bool |
steps across the save planes and observes each inside its accepted step, through DOPRI5’s continuous extension or, with an |
|
int |
a positive |
8. Statistics requests#
field |
type |
meaning |
|---|---|---|
|
frozen dataclass |
|
The periodic subset and its period are read when the backend is built, not when a
run is started: the shared step loop arms the between-planes channel from
problem.stats_spec, so a spec handed to run_problem that disagrees with the problem’s
own about either is refused. Every name in the subset must carry the catalogue’s
between_planes trait, which StatsSpec enforces at construction and by name; a backend
that cannot form one of them at all refuses the run the same way its plane statistics do.
The driver exposes three storage hooks; the shared march decides which accepted steps produce rows and passes their exact history indices:
hook |
contract |
|---|---|
|
Discard rows on a fresh load ( |
|
Evaluate a row after boundary treatments, state commitment and position advancement. |
|
Return owning host rows at observation, then reset the row count while retaining capacity. A device implementation copies packed values once here. An empty drain transfers nothing. |
The defaults keep a host buffer and call stats(). No backend needs to
override the march or read its private row buffer. Device arrays remain behind the
seam. The native backend inherits the host defaults. A backend must implement the current
schema (§1),
including the batch admission interface of §1, explicit handling or refusal of envelope
source assemblies, and an explicit refusal of each general-modal leaf, section method and
spatial kind it does not execute.
9. What comes back#
Observation (host arrays only, FFT convention): z, field in the lab frame (n_batch, n_modes, n_pol, n_omega), noise_phase (if a noise model is active, so processing can restore the
background), both None for an observation made with observe(field=False): nothing then crosses to the host and nothing scans the field, so a
diverged field-free run is caught by the step loop’s finite-norm rule and the
statistics; the spatial collar diagnostic is not evaluated there, and the backend
counts such planes in metadata() (collar_diagnostic_skipped) and logs the first, step_history (arrays of z, h, error norm, accepted flag, wall time since the
last observation; recorded whether the run was adaptive or replayed a schedule, since the
embedded estimate is computed either way), n_steps, n_rejected, and step_stats. stats() returns a dict of small host arrays
keyed by statistic name with a leading batch axis.
step_stats is a StepStats: z (n_rows,) and step (n_rows,) — the index of the
accepted step each row was taken after, into this observation’s step_history, which
counts rejected attempts — and values[name] shaped (n_rows, n_batch, *shape), the row
axis first and then the batch axis every statistic crossing this seam carries. Empty
unless the spec named a periodic subset, which is the default; the field has a default so
an Observation built without it is unchanged. It is flushed at every observation, like
the step history and over the same interval, and a row may not index past it.
10. Extension procedure#
Add the field to the dataclass with a default under which a problem that does not use it is unchanged.
Add the row here and bump
schema_version.Extend
validate().Implement in
numpy_reffirst, with a test; then in the other backends, with the equivalence test.Update the memory budget in architecture.md §8 if a buffer is added.