solprop.translate.modal_tables#

Track and interpolate dense Maxwell tables without changing the kernel seam.

Module Attributes

DEFAULT_GRAM_ATOL

Largest sampled Gram defect norm(W - I, 2) a table admits at a knot.

MIN_OVERLAP_DOMINANCE

Smallest aligned physical overlap a tracked channel may keep.

MAX_GENERATOR_COUPLING

Largest off-diagonal generator a per-channel index table admits, relatively.

MIN_Z_KNOTS

Fewest longitudinal knots a table interpolates over, beside a single snapshot.

Functions

align_modal_sample(reference, sample, nodes, *)

Align channels by physical overlap, phase and declared subspaces.

apply_channel_basis(sample, basis)

Put one tracked sample in a declared channel basis, splitting the two cases.

channel_area(sample, nodes, channel)

Return the scalar effective area of one channel of a vector sample.

composed_gauge(gauge_id, basis)

Return the gauge a declared basis leaves on top of a tracking gauge.

generator_is_coupled(table)

Return whether a tracked table's generator couples its channels anywhere.

modal_transform_tables(table, omega, *, ...)

Build the flat seam record from a dense reference table.

project_transverse_field(tables, electric)

Project a declared transverse electric field onto a guide's channels.

sample_modal_table(provider, omega, z, nodes, *)

Build dense knots with frequency and longitudinal physical-overlap tracking.

sample_provider_table(provider, channel, ...)

Sample one channel of a modal provider into a dense index snapshot.

solved_in_table_basis(table, provider, omega, z)

Solve a fresh sample and put it in the table's declared channel basis.

transformed_sample(sample, transform, *, ...)

Put a sample in new channel coordinates: e' = e U, with every partner.

validate_modal_table(table, provider, ...)

Compare dense interpolation with held-out physical provider solves.

Classes

DenseModalTable(omega, z, samples, nodes[, ...])

Retain full reference samples at every frequency and longitudinal knot.

ModalTransformTables(omega, omega_fine, ...)

Own the full E/H reference of a general guide, flat, as the seam will take it.

ProjectedLaunch(amplitudes, captured, ...)

What a declared transverse field becomes in a guide's channels.

solprop.translate.modal_tables.DEFAULT_GRAM_ATOL = 0.0001#

Largest sampled Gram defect norm(W - I, 2) a table admits at a knot.

A provider normalises its fields over the whole physical domain and deliberately does not renormalise a caller’s rule, so a rule that truncates the fields, or resolves them badly, returns a basis whose sampled Gram is not the identity – and every sample is still valid, because the provider refused nothing. The table is where that has to be caught, before an interpolated basis carries the defect into a propagation.

The value is the loosest defect a provider in this package can legitimately produce. The step-index rule certifies the cladding power outside it below 1e-12 of the total, so its defect is quadrature error alone: measured 1.33e-15, 4.68e-15 and 4.76e-15 over six mixed HE/EH/TE/TM channels at radial orders 24, 48 and 96. The radial provider admits up to solprop.modes.graded_index.MAX_EXTERIOR_TAIL_FRACTION = 1e-4 of a channel’s power beyond its matching radius, and the sampled Gram is short by exactly the power the rule discards, so that guard is the binding one and this budget is the same number.

It is a construction budget, not a physical statement: a caller who deliberately integrates part of a cross-section passes its own, and a taper whose interface leaves the frozen panel edge needs the held-out flux_atol of validate_modal_table() instead, which measures the interpolated basis.

class solprop.translate.modal_tables.DenseModalTable(omega, z, samples, nodes, degenerate_blocks=(), gram_atol=0.0001, basis=None)[source]#

Bases: Displayed

Retain full reference samples at every frequency and longitudinal knot.

Attributes:
omeganumpy.ndarray

Strictly increasing positive frequencies in rad/s, shape (n_omega_table,).

znumpy.ndarray

Strictly increasing positions in m, shape (n_z_table,); one is a snapshot, otherwise at least MIN_Z_KNOTS.

samplestuple of tuple of ModalSample

Full E/H reference knots indexed by z, then frequency, in one tracked gauge.

nodesModalNodeRule

Common physical nodes and weights for the entire table.

degenerate_blockstuple of tuple of int

The blocks the knots were tracked with, retained so that a held-out comparison can align a fresh solve in the same declared subspaces. A table tracked with a rotating degenerate pair cannot be validated without them.

gram_atolfloat

Positive budget on the sampled Gram defect of every knot, default DEFAULT_GRAM_ATOL. Construction refuses a basis above it.

basissolprop.modes.guide.ChannelBasis or None

The declared unitary change of basis the knots carry on top of the tracking gauge, or None for the provider’s own coordinates. It is retained so that a held-out comparison can put a fresh provider solve in the same coordinates before aligning it.

gram_defectfloat

The largest norm(W - I, 2) over the knots, dimensionless: how far the sampled basis is from orthonormal in true power. Reported here so that a problem built from this table can carry it into its debug summary.

basis: ChannelBasis | None = None#
degenerate_blocks: tuple[tuple[int, ...], ...] = ()#
evaluate(omega, z)[source]#

Interpolate full E/H and the generator inside the table domain.

Parameters:
omegafloat

Angular frequency in rad/s inside the knot interval.

zfloat

Position in m inside the knot interval, or the snapshot position.

Returns:
ModalSample

Dense interpolated sample; its flux is recomputed from interpolated E/H and its boundary_residual is the largest of the bracketing knots’. Held-out validation is required before using this approximation.

generator_derivative(omega, z)[source]#

Differentiate the dense generator interpolant in angular frequency.

Parameters:
omegafloat

Angular frequency in rad/s inside the knot interval.

zfloat

Position in m inside the knot interval.

Returns:
numpy.ndarray

Generator derivative in s/(rad m), shape (n_modes, n_modes).

gram_atol: float = 0.0001#
gram_defect: float = 0.0#
nodes: ModalNodeRule#
omega: Float[ndarray, 'n_omega_table']#
samples: tuple[tuple[ModalSample, ...], ...]#
z: Float[ndarray, 'n_z_table']#
solprop.translate.modal_tables.MAX_GENERATOR_COUPLING = 1e-09#

Largest off-diagonal generator a per-channel index table admits, relatively.

A channel’s propagation constant is the diagonal of the provider’s generator, so a generator that couples its channels has no such constant: its flow is a matrix exponential, which is the coupled_flow leaf and is built by the general modal route, not by this table. Both providers here are diagonal by construction and a tracked alignment is a permutation and a phase, so the measured off-diagonal norm is exactly zero; the budget is there to refuse a provider that is not, not to admit a small coupling.

What it is measured against. norm(G - diag G, 'fro') is compared with the smallest splitting between the channels, min |beta_i - beta_j| over i != j, and not with the size of beta itself. Two channels mix over a length set by the ratio of their coupling to their splitting, so that ratio is the physical quantity; comparing with |beta| ~ 1e7 1/m would have admitted an off-diagonal of 1e-3 1/m between two channels split by 1 1/m, which mixes them completely in a few metres. The splitting is floored at 64 eps |beta|_max, the rounding of the propagation constants themselves, so that an exactly degenerate pair still has a defined scale.

What this does not detect. A coupling below 1e-9 of the smallest splitting, which turns the pair by less than 1e-8 rad over a beat length and is what the budget is for. A coupling that appears between the knots: only the sampled generators are examined. Anything about the z direction: a table is a snapshot. And a physically coupled basis that a provider has already diagonalised – this refuses a generator that is not diagonal, not a guide whose channels exchange power by some other mechanism.

solprop.translate.modal_tables.MIN_OVERLAP_DOMINANCE = 0.5#

Smallest aligned physical overlap a tracked channel may keep.

The aligned diagonal of the cross-Poynting matrix is a dimensionless overlap whose correct value is one, so a dominance criterion is the right test: below a half the assignment is no longer identifying the same physical channel, whatever permutation it chose. A bound of 1e-8 would be eight orders looser than the quantity it guards and would accept an almost arbitrary mis-track.

Measured on the 250 nm strand’s degenerate HE11 pair, the weakest aligned overlap against the reference sample is 1.0, 0.99999948, 0.99985 and 0.99872 at frequency ratios 1.0, 1.01, 1.2 and 2.0 – so even a doubling of the frequency, far beyond any table knot spacing, stays three orders above this floor.

solprop.translate.modal_tables.MIN_Z_KNOTS = 4#

Fewest longitudinal knots a table interpolates over, beside a single snapshot.

SciPy’s not-a-knot cubic silently degrades to a parabola on three points and to a straight line on two, so a table built on two or three positions would report an interpolation order it does not have. One position is a snapshot and is interpolated in frequency alone.

class solprop.translate.modal_tables.ModalTransformTables(omega, omega_fine, z_edges, synthesis, projection, electric, magnetic, backward_electric, backward_magnetic, reciprocal_gram, coordinate_dual, node_xy, weights, region_index, source_frequency_factor, valid, profile_error, factors, gauge_id, model_id, policy_id)[source]#

Bases: Displayed

Own the full E/H reference of a general guide, flat, as the seam will take it.

Built from a DenseModalTable by modal_transform_tables(), or along a moving guide by solprop.translate.modal_path. It is flat – arrays, not a nest of samples – because that is what a backend can be handed, and it is the transform.modal_tables leaf of a KernelProblem.

factors is always None; the dense electric and magnetic references, the coordinate_dual and the execution maps synthesis and projection are filled. Frequency is the last axis and is contiguous. Both maps are tabulated on the coarse ``omega``: the state is a coarse spectrum and the fine grid is zero everywhere but grids.crop_index, so a synthesis tabulated on omega_fine would carry columns no run can read. The oversampled grid is where the field lives, not where the map does: the pass synthesises on the coarse grid and places the result in the fine spectrum at crop_index.

Attributes:
omeganumpy.ndarray

Coarse angular frequencies in rad/s, shape (n_omega,), increasing and non-negative. On a KernelProblem it is grids.omega itself, whose first bin is zero on a field grid, with the inactive bins explicitly zero rather than evaluated.

omega_finenumpy.ndarray

Fine angular frequencies in rad/s, shape (n_omega_fine,), increasing and non-negative; the problem’s fine spectrum, by the same rule.

z_edgesnumpy.ndarray

Stored positions in m, shape (n_z,), increasing; they are the edges the interval-indexed arrays are counted over, one interval per pair of positions and one for a single snapshot.

synthesis, projectionnumpy.ndarray or None

The execution maps, supplied together or not at all, both on omega. synthesis is S in V/(m sqrt(W)), shape (n_intervals, n_deg, n_nodes, 3, n_modes, n_omega); projection is Q in V m/sqrt(W), shape (n_intervals, n_deg, n_modes, n_nodes, 3, n_omega), carrying the area weights and the reciprocal normalisation and excluding the separate source_frequency_factor. The leading pair of axes is a Chebyshev expansion along z inside each interval, so one interval carrying a single coefficient (degree zero) is a constant map.

electric, magneticnumpy.ndarray

Reference E and H in V/m and A/m per sqrt(W), shape (n_z, n_nodes, 3, n_modes, n_omega).

backward_electric, backward_magnetic, reciprocal_gramnumpy.ndarray or None

Reciprocal partners and their bilinear pairing, supplied together or not at all.

coordinate_dualnumpy.ndarray or None

Amplitude recovery from a physical field, shape (n_z, n_modes, n_nodes, 6, n_omega): the first three components pair with E and the last three with H, in sqrt(W) m/V and sqrt(W) m/A.

node_xynumpy.ndarray

Node positions in m, shape (n_z, n_nodes, 2).

weightsnumpy.ndarray

Positive area weights in m², shape (n_z, n_nodes).

region_indexnumpy.ndarray

Non-negative int32 material index per node, shape (n_nodes,).

source_frequency_factornumpy.ndarray

The -i omega the projected polarisation is multiplied by, shape (n_omega,), in rad/s.

validnumpy.ndarray

Fine-grid support mask, shape (n_z, n_modes, n_omega_fine).

profile_errornumpy.ndarray

Declared non-negative profile error per interval and coarse frequency, shape (n_intervals, n_omega), dimensionless.

factorsobject or None

A factored representation; not implemented, so refused unless None.

gauge_id, model_id, policy_idstr

Non-empty phase/coordinate, physical-model and numeric-policy identities.

backward_electric: Complex[ndarray, 'n_z_backward_electric n_nodes_backward_electric 3 n_modes_backward_electric n_omega_backward_electric'] | None#
backward_magnetic: Complex[ndarray, 'n_z_backward_magnetic n_nodes_backward_magnetic 3 n_modes_backward_magnetic n_omega_backward_magnetic'] | None#
coordinate_dual: Complex[ndarray, 'n_z_dual n_modes_dual n_nodes_dual 6 n_omega_dual'] | None#
electric: Complex[ndarray, 'n_z_electric n_nodes_electric 3 n_modes_electric n_omega_electric']#
factors: object | None#
property frozen_rule: bool[source]#

Return whether one node rule was broadcast across every position.

A record built over several positions from one rule – the same Cartesian nodes and the same area weights at every z – is the frozen-rule case. It is legitimate and it is not free: along a path the panel edge leaves the material interface, and the interpolated basis’s flux defect grows linearly with that displacement (4.9e-7, 9.8e-7 and 1.95e-6 at a quarter, a half and the whole of a 1e-5 relative radius change). A taper needs sections with their own rules (solprop.translate.modal_path); a snapshot is trivially frozen.

Returns:
bool

True when node_xy and weights do not vary along z.

gauge_id: str#
property identity: Mapping[str, JsonValue][source]#

Return the scalar identity a run records and a resume compares.

The record is eq=False: two of them are never compared by value, and a resume must not compare megabytes of E and H field by field either. What enters identity is this mapping – the three declared IDs, the six extents, the region count, the band and position endpoints, which optional representations are present, the frozen-rule flag and the profile-error budget. Every entry is a scalar, a string, a boolean or a list of scalars; no array is included by value.

A factored map enters by its declared kind tag, never by its repr: a default repr carries a memory address, which is not stable across the two processes a resume compares.

The profile-error budget is the caller’s declared one, which on the general modal route is the guide’s measured error; it therefore enters at MEASURED_DIGITS significant digits, exactly as solprop.modes.guide.ModalSampling.identity records the same number, so that the two agree and a re-solve on another BLAS does not move either of them.

Returns:
mapping

JSON-serialisable scalars, stable across processes.

magnetic: Complex[ndarray, 'n_z_magnetic n_nodes_magnetic 3 n_modes_magnetic n_omega_magnetic']#
model_id: str#
node_xy: Float[ndarray, 'n_z_xy n_nodes_xy 2']#
omega: Float[ndarray, 'n_omega_modal']#
omega_fine: Float[ndarray, 'n_omega_fine_modal']#
policy_id: str#
profile_error: Float[ndarray, 'n_intervals_profile_error n_omega_profile_error']#
projection: Complex[ndarray, 'n_intervals_projection n_deg_projection n_modes_projection n_nodes_projection n_components_projection n_omega_projection'] | None#
reciprocal_gram: Complex[ndarray, 'n_z_reciprocal n_modes_reciprocal_out n_modes_reciprocal_in n_omega_reciprocal'] | None#
region_index: Int32[ndarray, 'n_nodes_region_index']#
source_frequency_factor: Complex[ndarray, 'n_omega_source_factor']#
synthesis: Complex[ndarray, 'n_intervals_synthesis n_deg_synthesis n_nodes_synthesis n_components_synthesis n_modes_synthesis n_omega_synthesis'] | None#
valid: Bool[ndarray, 'n_z_valid n_modes_valid n_omega_fine_valid']#
weights: Float[ndarray, 'n_z_weights n_nodes_weights']#
z_edges: Float[ndarray, 'n_edges_modal']#
class solprop.translate.modal_tables.ProjectedLaunch(amplitudes, captured, residual, residual_share)[source]#

Bases: Displayed

What a declared transverse field becomes in a guide’s channels.

The result of project_transverse_field(): the channel amplitudes, the power those amplitudes carry, and what the guide’s bound channels do not carry of the declared field. The last is named rather than discarded, because a projection that captured a tenth of what was declared and said nothing would be a silent change of the initial-value problem.

Attributes:
amplitudesnumpy.ndarray

The channel amplitudes, complex128, shape (n_modes, n_omega), in the square root of watts – the true-power amplitude every route uses (see docs/dev/decisions.md), because the basis is flux-normalised.

capturednumpy.ndarray

The longitudinal Poynting flux the projected field carries, float64, shape (n_omega,), in W. It is the physical int Re(E x conj(H))/2 . z dA of the projected field, formed from the guide’s own stored E and H, which equals a.H W a and is not sum_m |a_m|^2 unless the basis is exactly orthonormal.

residualnumpy.ndarray

Share of the declared field the bound channels do not reproduce, float64, shape (n_omega,), dimensionless and non-negative – and, like residual_share below, not clipped at one: sum_q w_q |E - sum_m a_m e_m|^2 / sum_q w_q |E|^2 at each bin, in the guide’s own quadrature norm. It is nan where the declared field is identically zero, because there is no share of nothing.

residual_sharefloat

The same quantity over the whole declared field, weighting each bin by its own declared norm rather than equally: one number for the projection. It is not clipped at one: on a basis that is not orthonormal the flux projection can overshoot, leaving a residual larger than the declared field itself, and a share above one says exactly that. Clipping it would report a projection that missed everything as one that missed all of it, which are different statements.

Raises:
ModeError

If the arrays disagree in shape, or the residual share is not finite and non-negative.

amplitudes: Complex[ndarray, 'n_modes_launch n_omega_launch']#
captured: Float[ndarray, 'n_omega_captured']#
residual: Float[ndarray, 'n_omega_residual']#
residual_share: float#
solprop.translate.modal_tables.align_modal_sample(reference, sample, nodes, *, degenerate_blocks=())[source]#

Align channels by physical overlap, phase and declared subspaces.

Parameters:
referenceModalSample

Previous sample on the same physical Cartesian nodes.

sampleModalSample

New full-vector sample with the same retained channel count.

nodesModalNodeRule

Common physical nodes and area weights in m².

degenerate_blockssequence of sequence of int, optional

Disjoint channel-index blocks permitted to rotate. The complete generator is rotated too, so a near-degenerate block remains physically equivalent.

Returns:
ModalSample

Aligned E/H, generator, flux and reciprocal data in reference coordinates.

Raises:
ModeError

If the common domain, validity, channel count or overlap is unresolved.

solprop.translate.modal_tables.apply_channel_basis(sample, basis)[source]#

Put one tracked sample in a declared channel basis, splitting the two cases.

The transformed generator is U.H diag(beta) U, which stays diagonal when the block’s propagation constants are equal and becomes a coupled matrix flow when they are not. Both are admitted, and they are not the same object:

  • an exactly degenerate block leaves the generator diagonal, and the generator is carried over rather than multiplied out (below);

  • a split block leaves it coupled, and the product is evaluated. That block is linear.coupled_flow, executed by numpy_ref and numba_cpu and built by the general modal route from the transformed generator (solprop.translate.general.coupled_block()). Any refusal is the route’s, where a flow can be built, and this function’s job is only to say which of the two the declaration produced.

The equality is judged at the rounding of the propagation constants, the same floor _channel_splitting gives a degenerate pair: 64 eps max|beta|. The two HE11 parities of a step-index strand satisfy it exactly – measured 0.0 relative – and a pair that is degenerate only to a part in 1e-9 is a physically split pair whose partners beat, which is the case this refuses. The generator must also already be diagonal to that same rounding, which is what the two conditions together make G' = G an identity rather than a product: U is the identity outside the blocks and each block is g_b times the identity inside it, so it commutes with U.

Where every block is degenerate the generator is carried over exactly, not multiplied out. Evaluating U.H G U numerically injects rounding into quantities that are exactly zero: a lossless provider has Re G = 0 to the bit, and the product put a spurious Re G = +1.1e-26 1/m there – gain, which _channel_index_table refuses outright, and an Im n_eff of the wrong sign at the 1e-33 level. Taking the exact value is the identity the two gates above have just certified – to their own floor, which is 64 eps max|beta|, 1.55e-7 1/m on a step-index strand and 1.4e-14 of the propagation constants themselves. Below that floor the carried-over generator and the evaluated product differ, and this keeps the exact one, so a degenerate declaration is bitwise independent of the split path.

Where a block is split the product is evaluated, because there is then no identity to carry over: the off-diagonal is the physics, not the rounding. The diagonal that comes out of it is checked for gain exactly as the carried-over one is, so a conjugation error is still caught.

The invariance gate is certified to the declaration’s own budget. The transform must not change norm(W - I, 2), which a unitary one cannot; the budget is the rounding of the product or BASIS_UNITARY_ATOL, whichever is larger, because a transform that is unitary only to that budget moves the measured norm by its own unitary defect and the declaration has already admitted it at exactly that size.

Parameters:
samplesolprop.modes.vector.ModalSample

A tracked sample in the provider’s own channel coordinates.

basissolprop.modes.guide.ChannelBasis

The declared change of basis.

Returns:
solprop.modes.vector.ModalSample

The sample in the declared coordinates, with the composed gauge identity.

Raises:
ModeError

If the transform’s width disagrees with the sample, the generator is not already diagonal, the evaluated product of a split block shows gain, or the transform changes the basis’s measured departure from orthonormal, which a unitary one cannot.

solprop.translate.modal_tables.channel_area(sample, nodes, channel)[source]#

Return the scalar effective area of one channel of a vector sample.

The scalar definition A_eff = (int |E|^2 dA)^2 / int |E|^4 dA with |E|^2 = E . E* summed over the three Cartesian components, integrated on the caller’s rule. It is a surrogate: it is what the legacy mode-averaged interfaces mean by an area, and it is not the nonlinear coefficient of a vector channel, which is the full contraction of solprop.nonlinear.normalisation.modal_kerr_coefficient() over the material regions that respond.

Parameters:
sampleModalSample

Internal-convention sample whose fields are normalised to unit power.

nodesModalNodeRule

The rule the sample was evaluated on; supplies the area weights in m².

channelint

Channel index on the mode axis.

Returns:
float

Effective area in m².

solprop.translate.modal_tables.composed_gauge(gauge_id, basis)[source]#

Return the gauge a declared basis leaves on top of a tracking gauge.

The order is fixed and it is documented here. A knot is first put in the tracking gauge – the permutation, the phase and the degenerate-block SVD alignment align_modal_sample() applies – and the declared basis is applied after it, so the composed transform is T_track U and the composed identity is "<tracking gauge>+<basis gauge>". That order is what a resume has to reproduce, and it is the one that leaves a run with no declared basis bitwise what it was: the tracking is untouched and the declaration is a second, independent step on top of it.

Parameters:
gauge_idstr

The tracking gauge’s own identity, the provider’s.

basissolprop.modes.guide.ChannelBasis or None

The declared change of basis, or None.

Returns:
str

The composed identity, unchanged when nothing is declared.

Examples

>>> from solprop.translate.modal_tables import composed_gauge
>>> composed_gauge("step-index-exact", None)
'step-index-exact'
solprop.translate.modal_tables.generator_is_coupled(table)[source]#

Return whether a tracked table’s generator couples its channels anywhere.

The one predicate both callers of the split-block route ask: the guide constructor, to know whether its per-channel index tables are surrogates rather than propagation constants, and the route, to know whether to build linear.coupled_flow (solprop.translate.general.coupled_block()). It is judged at the rounding of the propagation constants themselves, 64 eps max|beta|, the same floor apply_channel_basis() gates its declared blocks at, so that a generator diagonal to the bit is diagonal here too.

Parameters:
tableDenseModalTable

The tracked reference, in whatever basis was declared on it.

Returns:
bool

True when some knot’s generator has an off-diagonal above that floor.

Examples

>>> import numpy as np
>>> from solprop.modes.graded_index import (
...     GradedIndexProvider, PowerLawPermittivity,
... )
>>> from solprop.modes.material import IndexMaterial
>>> from solprop.translate.modal_tables import (
...     generator_is_coupled, sample_modal_table,
... )
>>> guide = GradedIndexProvider(
...     permittivity=PowerLawPermittivity(IndexMaterial(1.45), 0.005),
...     exterior=IndexMaterial(1.45 * np.sqrt(1 - 2 * 0.005)),
...     radius=25e-6, family="LP", count=2, elements=16, extent=2.0,
... )
>>> omega = np.linspace(1.80e15, 1.86e15, 4)
>>> table = sample_modal_table(
...     guide, omega, np.array([0.0]), guide.node_rule(1.80e15, n_r=2)
... )
>>> generator_is_coupled(table)
False
solprop.translate.modal_tables.modal_transform_tables(table, omega, *, profile_error, omega_fine=None, z=None, policy_id='dense-knots-cubic-frequency')[source]#

Build the flat seam record from a dense reference table.

The one constructor of ModalTransformTables. It evaluates the table on the grid a run will use – which need not be the table’s own knots, and in general is not – and stores the fields, the reciprocal partners, the coefficient dual, the two execution maps and the node geometry as arrays.

Outside the table’s band the record is explicitly zero, and the fine grid’s valid mask is false there. A canonical field grid starts at zero and runs past any guide’s support, so a leaf tabulated on it necessarily leaves the band; what must not happen is an evaluated profile there, which extrapolates a mode past the frequencies it was solved at. A zero reference is a zero source and a zero synthesised field: a run whose active band leaves the guide’s support is refused where the problem is assembled, not silently propagated against zeros.

The execution maps are built for a snapshot table only. S and Q are stored as one Chebyshev coefficient per interval, which is exact when the map does not vary along z and an average when it does, so a table sampled over a path of positions gets synthesis and projection of None and the general route refuses it: a moving basis needs sections with their own node rules, which solprop.translate.modal_path builds. A snapshot is broadcast across every requested position, which is the frozen-rule case ModalTransformTables.frozen_rule names.

Parameters:
tableDenseModalTable

Tracked reference knots. Requested frequencies outside its band are stored as zeros; requested positions outside a path table’s own are refused, and a snapshot is broadcast to all of them.

omeganumpy.ndarray

Coarse angular frequencies in rad/s, shape (n_omega,), increasing.

profile_errorarray_like or float

Declared non-negative profile error of the representation, per interval and coarse frequency or one value for all of them. It is a budget the caller has measured, for example with validate_modal_table(); nothing here infers it.

omega_finearray_like or None, optional

Fine angular frequencies in rad/s; None uses the coarse grid, which is what a run with no oversampling declares.

zarray_like or None, optional

Positions in m to store; None uses the table’s own.

policy_idstr, optional

Identity of the numeric policy this record was built under.

Returns:
ModalTransformTables

The validated flat record, with factors left None and the coordinate_dual filled.

Raises:
ModeError

If a position leaves a path table’s domain, or the record does not validate.

solprop.translate.modal_tables.project_transverse_field(tables, electric)[source]#

Project a declared transverse electric field onto a guide’s channels.

The launch a caller declares as a field rather than as a channel: the amplitude of each channel in a declared E(x, y, omega) sampled at the guide’s own quadrature nodes, with the power captured and the remainder named.

The map is the coordinate dual, not the source projection. The amplitude of a field in a flux-normalised basis is a = W^-1 B with B_m = int(E x h_m* + e_m* x H)_z dA / 4, the same Hermitian Poynting pairing the flux metric is built from, and that is what tables.coordinate_dual carries. Q, the record’s other map, contracts a polarisation with the conjugated electric profile and carries the reciprocal normalisation for a source term; it is a different operation and gives a different number.

Only E is declared, so the H half is closed by the forward-field assumption. For any superposition of these channels travelling forward, the two halves of B_m are equal – the Poynting cross matrix of a lossless bound basis is Hermitian – so B_m = 2 int(E x h_m*)_z dA / 4 and the declared E alone determines the amplitudes. Measured on the step-index parity pair, a field synthesised from known amplitudes comes back through this map to 1.412e-15 of them, which is rounding and not the basis’s Gram defect (1.3e-6), so the closure is exact for this package’s providers rather than approximate. (Its last digits move with the BLAS thread count, as a 288-node reduction’s rounding does; the quoted number is one thread everywhere, and the bound the test asserts is 1e-13.) That exactness is a statement about a lossless basis: the Hermitian symmetry of the Poynting cross matrix is what makes the two halves equal, and a complex PermittivityLaw breaks it, so on such a basis the closure would be wrong by an amount only a growing residual share would show. No such basis can reach here: both providers this package ships refuse a complex permittivity when they solve, by name ("provider material differs from its lossless step-index law" and "radial provider requires positive lossless permittivity"), so the condition is enforced upstream and a future lossy provider must re-derive the closure rather than inherit it. A field that is not forward-propagating – a declared backward or evanescent component – is not what this map returns either, and nothing here can see the difference; declare the amplitudes directly if that is wanted.

What is not captured is named. The bound channels span what they span: a declared field with power in radiation modes, in a channel the guide did not solve for, or simply outside the node rule’s reach leaves a residual, and ProjectedLaunch.residual reports it as a share of the declared field in the guide’s own quadrature norm. Reprojecting a field creates a new initial-value problem; it is not a resume path.

Parameters:
tablesModalTransformTables

The guide’s reference, which must carry its coordinate dual.

electricnumpy.ndarray

The declared transverse field at the guide’s nodes, real or complex (a real profile is the common case and is taken as it is), shape (n_nodes, n_components, n_omega), in V/m, on the record’s own coarse frequency axis. A profile that does not depend on frequency is broadcast to it by the caller.

Returns:
ProjectedLaunch

The amplitudes, the captured flux and the residual.

Raises:
ModeError

If the record carries no coordinate dual, or the declared field does not lie on its nodes, components and frequencies.

solprop.translate.modal_tables.sample_modal_table(provider, omega, z, nodes, *, degenerate_blocks=(), basis=None, gram_atol=0.0001)[source]#

Build dense knots with frequency and longitudinal physical-overlap tracking.

Parameters:
providerModalProvider

Internal-convention bound-mode physics provider.

omeganumpy.ndarray

At least four increasing positive frequencies in rad/s, shape (n_omega,).

znumpy.ndarray

Increasing longitudinal positions in m, shape (n_z,); one position, or at least MIN_Z_KNOTS of them.

nodesModalNodeRule

Common physical nodes; changing material ownership requires separate tables.

degenerate_blockssequence of sequence of int, optional

Declared blocks passed to the physical-overlap alignment, and retained on the table so that held-out validation aligns a fresh solve the same way.

basissolprop.modes.guide.ChannelBasis or None, optional

A declared unitary change of channel coordinates, applied to every knot after the tracking gauge and gated on the exact degeneracy of each of its blocks (apply_channel_basis(), composed_gauge()). Every block it acts inside must be one of degenerate_blocks: tracking rotates freely inside a declared block, so a transform of a block the tracking did not know about would be applied on top of an ordering the eigenvalues cannot resolve. None keeps the provider’s own coordinates, and then this function is bitwise what it was.

gram_atolfloat, optional

Budget on each knot’s sampled Gram defect, default DEFAULT_GRAM_ATOL; the table refuses a basis above it.

Returns:
DenseModalTable

Full E/H knots, with frequency tracking after each z-row entrance alignment.

Raises:
ModeError

If the provider has an unsupported convention or crosses a physical kink, if basis acts on a block the tracking was not given or on one whose propagation constants differ, or if the sampled basis is further from orthonormal than gram_atol.

solprop.translate.modal_tables.sample_provider_table(provider, channel, omega, *, z=0.0, nodes=None, include_area=True)[source]#

Sample one channel of a modal provider into a dense index snapshot.

The sibling of solprop.modes.tabulated.sample_index_table() for a provider rather than a mode. beta is the provider’s own generator at the knots, read off the tracked diagonal, so the channel is followed by physical overlap and not by eigenvalue order; beta_1 and beta_2 are the interpolant’s, the dense table’s cubic in frequency differentiated, because a provider declares no analytic frequency derivative.

Nothing here certifies those derivatives, and validate_modal_table() does not either: it measures a profile, a generator and a flux defect, and no frequency derivative at all. Measured held-out against fresh-solve five-point differences on the reference parabolic sector of tests/modes/test_modal_tables.py, over a +-2 % band, beta_1 is 4.4e-13 relative at nine knots while beta_2 is 2.8e-2 – eleven orders apart, because a table’s cubic reproduces a nearly constant group delay exactly and still has almost no curvature. Treat beta_1 as the interpolant’s, and beta_2 as a diagnostic (measured by a development study of the provider-table derivatives; solprop.modes.guide.ProviderChannelMode).

Parameters:
providerModalProvider

Internal-convention bound-mode provider that names its channels.

channelint

Channel index on the provider’s mode axis.

omeganumpy.ndarray

At least four increasing positive frequencies in rad/s, shape (n_omega,).

zfloat, optional

Snapshot position in m inside the provider’s z domain.

nodesModalNodeRule or None, optional

Common physical rule; None asks the provider for one at the lowest frequency of the band, where the power a rule discards is largest, so that its certified tail is a bound over the band.

include_areabool, optional

Sample the surrogate channel_area() at every frequency.

Returns:
IndexTable

Sorted, immutable index samples with the interpolant’s slopes, carrying derivative_values and a source_identity that names the provider, the channel, the snapshot position, the area model and the derivative model.

Raises:
ModeError

If the frequencies, the channel, the rule or the sampled basis is invalid.

solprop.translate.modal_tables.solved_in_table_basis(table, provider, omega, z)[source]#

Solve a fresh sample and put it in the table’s declared channel basis.

A held-out comparison asks the provider again and aligns the answer against the table’s own knots. The provider always answers in its coordinates, so a table carrying a declared basis must apply the same declaration before the alignment; otherwise the degenerate-block SVD would be asked to discover the declared transform, which it can only do up to the same block rotation the declaration exists to fix.

Parameters:
tableDenseModalTable

The table whose basis the solve is put in.

providersolprop.modes.vector.ModalProvider

The physics, asked again at one point.

omegafloat

Positive angular frequency in rad/s.

zfloat

Position in m.

Returns:
solprop.modes.vector.ModalSample

The fresh solve, in the table’s coordinates but not yet aligned to it.

solprop.translate.modal_tables.transformed_sample(sample, transform, *, gauge_id)[source]#

Put a sample in new channel coordinates: e' = e U, with every partner.

The one place a channel transform is applied to a ModalSample, shared by the tracking of align_modal_sample() – whose transform is a permutation, a phase and a degenerate-block rotation – and by the declared change of basis of solprop.modes.guide.ChannelBasis. The rules follow from e' = e U alone: the Hermitian objects conjugate as U.H X U and the bilinear reciprocal partners, which pair without a conjugate, carry U.conj().

The boundary residual of a mixed channel is reported as the block’s largest. A combination sum_n c_n e_n of unit-norm coefficients has a residual bounded by sum_n |c_n| times the largest, which for a unitary column is at most sqrt(n_modes) times it, so this is a diagnostic and not a certificate; it is the rule the tracking has always used and one number for the set is what the validation of ModalSample compares.

Parameters:
samplesolprop.modes.vector.ModalSample

The sample to re-express.

transformnumpy.ndarray

The matrix U, shape (n_modes, n_modes), whose columns are the new basis vectors written in the old one.

gauge_idstr

Non-empty phase and coordinate identity of the result.

Returns:
solprop.modes.vector.ModalSample

The same physics in the new channel coordinates.

solprop.translate.modal_tables.validate_modal_table(table, provider, points, *, profile_rtol, generator_atol, flux_atol)[source]#

Compare dense interpolation with held-out physical provider solves.

The third defect is the flux defect norm(W - I, 2) of the interpolated basis: an interpolated field is no longer an exact mode, and the spectral norm of its sampled Gram against the identity says by how much the basis has stopped being orthonormal in true power. It falls with the frequency knots like the profile error – measured 2.25e-8, 1.13e-9 and 5.96e-13 for 5, 9 and 17 knots on the 250 nm strand – but a held-out position also carries a provider effect that refinement in frequency does not remove: the node rule is frozen at the entrance radius, so on a taper the panel edge no longer sits on the material interface and the defect grows linearly with the displacement (4.9e-7, 9.8e-7 and 1.95e-6 at z = 0.25, 0.5 and 1.0 m for a 1e-5 relative radius change, 7.22e-7 at the report’s held-out z). Budget it accordingly, and split the table where an interface crosses a node.

Parameters:
tableDenseModalTable

Reference table on fixed physical nodes. Its declared degenerate blocks are reused here, so a table tracked through a rotating degenerate pair is compared in the same subspaces it was built in.

providerModalProvider

Independent fresh mode solves, in the same physical model.

pointsnumpy.ndarray

Held-out (omega, z) pairs in rad/s and m, shape (n_points, 2).

profile_rtolfloat

Positive relative E/H weighted norm budget, fixed before evaluation.

generator_atolfloat

Positive absolute generator error budget in 1/m, fixed before evaluation.

flux_atolfloat

Positive dimensionless budget on norm(W - I, 2), fixed before evaluation.

Returns:
numpy.ndarray

Profile, generator and flux defects, shape (n_points, 3).

Raises:
ModeError

If a budget is invalid or a held-out error exceeds it.