Channel labels and modal tables#
Channel labels identify physical modes as frequency or geometry changes. A modal table samples their fields and propagation constants so a run can reconstruct a frequency-dependent basis. Resolution, overlap tracking, cutoff and omitted exterior power all affect whether that basis remains accurate.
Channel labels and dense reference tables#
Every mode carries a ChannelLabel: its family, azimuthal and radial order,
parity, handedness and a stable display label. It is a member of the Mode
protocol, so the label belongs to the mode rather than to its place in a set, and
ModeCollection.channel_labels is simply its members’ labels in the order of the
mode axis. A GradedIndexProvider produces one label per requested radial
channel, and both providers publish them in identity["channels"], which is what
ModalGuide checks its mode collection against. A label a family does not have is
None, never a string standing for one: an axially symmetric TE0m or TM0m
channel has no parity partner, so its parity is None. handedness is None
on every real basis, and is filled where a guide declares a complex one: the
circular partners of solprop.modes.guide.circular_pair() carry left and
right in place of even and odd, by the convention
General modal propagation derives and Polarisation and azimuthal modes fixes the sign of.
Parity means one thing across every family, and ChannelLabel’s docstring is
where it is defined: "even" is the member whose transverse pattern is the
\(\cos((n-1)\theta)\) one about the x axis — for \(n=1\) that is the x-polarised
member — and "odd" is its quarter-turn partner. Each family reaches it its own
way. An exact step-index \(HE_{nm}\) builds the pair from \(\cos(n\theta)\) and
\(\sin(n\theta)\) on the radial component, and its “even” member is x-polarised on
the axis. A Marcatili \(HE_{nm}\) is a rotation \(\phi\) of one pattern, and its “even”
member is the unrotated \(\phi = 0\) — so MarcatiliMode(125e-6, "He", 1.0), which is
x-polarised, is HE1,1:even, and its orthogonal_polarisation, the quarter turn
\(\phi = \pi/(2n)\), is HE1,1:odd. Any other rotation is a gauge and not
a parity: its parity is None and the angle goes in the display label. Note that
the label’s azimuthal_order is the family index \(n\) — unlike the mode property of the
same name, which is the field’s highest harmonic — while the parity names the
\((n-1)\)-fold pattern the transverse vector field has.
Repeating a step-index parity or adding a second TE/TM parity is refused. A common physical step-index guide may have different mode-specific cladding tail radii; its collection uses a common enclosing domain.
sample_index_table(mode, omega, z=..., include_area=True) is the dense dispersion
bridge beside IndexTable. It retains the source’s frequency derivative in a
Hermite interpolant and records the snapshot position and source identity.
Step-index areas are sampled at each frequency. Other modes retain their declared
frozen-profile area. The resulting table does not supply electric or magnetic
fields, and never extrapolates beyond its knots. It is an IndexTable subclass,
so it also carries derivative_values and source_identity, and it is built
with loss_interpolation="linear" rather than the table default "log": a
lossless source has an imaginary index of exactly zero, which has no logarithm
and which the logarithmic policy refuses outright.
solprop.translate.modal_tables.sample_modal_table retains full E/H samples on
common physical nodes. It assigns channels by physical overlap, aligns their
phases and optionally rotates declared degenerate subspaces. It rotates the
complete generator and metric with the fields, including a near-degenerate block.
The blocks a table was tracked with are retained on it, so a held-out comparison
aligns a fresh solve in the same declared subspaces. A table interpolates over one
z snapshot or at least four z knots, because SciPy’s not-a-knot cubic degrades
silently to a parabola on three positions and to a straight line on two. An
interpolated sample carries the largest boundary_residual of its bracketing
knots. No compression is performed.
Construction gates the sampled Gram. A provider normalises over its whole
physical domain and deliberately does not renormalise a caller’s rule, so a rule
that truncates or under-resolves the fields returns a basis whose sampled
\(W\) is not the identity while every sample is still valid. A table refuses a
knot whose \(\lVert W-I\rVert_2\) exceeds gram_atol, whose default
DEFAULT_GRAM_ATOL is 1e-4 – the loosest defect a provider here can
legitimately produce, since the radial provider admits up to
MAX_EXTERIOR_TAIL_FRACTION of a channel’s power beyond its matching radius and
the sampled Gram is short by exactly the power the rule discards. The largest
defect over the knots stays on the record as gram_defect. On the 250 nm strand
it is 0.31, 2.41e-4, 1.24e-5, 2.92e-8 and 2.03e-14 at radial orders 3, 12, 16, 24
and 48; the six-channel 1.5 um guide is at rounding (1.33e-15) from order 24, and
a parabolic GRIN sector is at 4.2e-14 on its own element rule.
The cutoff policy of a table is the providers’ own, applied when the table is built: a band in which a channel is below cutoff, or too near it for the frozen profile to be resolved, is refused by name at construction, and a table never extrapolates past its knots into a band it does not carry. Nothing is masked and no channel is silently dropped.
Tracking follows the physical channel through a crossing: the assignment is by the overlap of the fields, so two channels whose propagation constants exchange order across the band keep their identities, where sorting by eigenvalue would swap them.
modal_transform_tables(table, omega, profile_error=...) builds
ModalTransformTables, the flat record a run’s kernel problem carries as its
transform.modal_tables leaf. It evaluates the table on the frequency 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 (the
synthesis and projection of general modal
propagation)
and the node geometry as arrays, with frequency last and contiguous. Outside the
table’s band the record is explicitly zero rather than extrapolated, and a run whose
active band leaves the guide’s support is refused when the problem is assembled. The
execution maps are built for a snapshot table only; a moving guide’s maps come from the
path sampling described above.
coordinate_dual recovers amplitudes from a physical field: contracted against
the six components (E, H) at every node it returns the amplitude vector that
synthesised them, because it is W^-1 times the same Hermitian Poynting pairing
the flux metric is built from. Its first three columns are in sqrt(W) m/V and its
last three in sqrt(W) m/A, and its two longitudinal columns are zero, since only
the transverse components carry longitudinal flux. It is for amplitude recovery,
not for nonlinear projection, which carries the separate -i omega of
source_frequency_factor.
profile_error is declared by the caller, per interval and coarse frequency:
nothing in the constructor infers the accuracy of its own representation.
A snapshot declares one interval; a path declares one per pair of stored
positions.
validate_modal_table compares held-out solves
against budgets supplied before the comparison; frequency derivatives can be
checked through generator_derivative. Physical kinks require separate sections.
Besides the weighted field and generator defects it reports the flux defect
\(\lVert W-I\rVert_2\) of the interpolated basis, which is what says how far the
interpolant has stopped being an orthonormal set in true power. That column falls
with the frequency knots like the field defect (2.25e-8, 1.13e-9 and 5.96e-13 for
5, 9 and 17 knots on the 250 nm strand), but at a held-out position it is a
property of the provider rather than of the interpolation: the node rule is
frozen at the entrance radius, so along a taper the panel edge no longer sits on
the material interface and the defect grows linearly with the displacement
(4.84e-7, 9.68e-7 and 1.94e-6 at z = 0.25, 0.5 and 1.0 m for a 1e-5 relative
radius change). Budget that floor, or rebuild the rule along the path, as
ModalGuide.from_provider does for a moving guide.
A guide built by ModalGuide.from_provider is something to run, not only to
inspect: Simulation.from_waveguide(..., provider=guide) and
prop_waveguide(..., provider=guide) propagate its three-component fields on
numpy_ref, numba_cpu and native (on the CPU and on the qualified GPU builds), and
the general modal page is the whole description. The guide carries
its own ModalSampling
– the knots, the position, the node rule, the declared degenerate blocks and the
measured profile interpolation error – and a run rebuilds its dense reference
from that; a guide built by from_step_index carries none, because nothing there
declares a transverse resolution, and a run refuses it by name. A region’s
nonlinear response is declared on the region, with
MaterialKerr, by chi3 or by n2. A
GradedIndexProvider builds its own two regions
and so takes its core’s responses as core_responses=(...); its exterior is always
linear, because a bound radial sector’s exterior is the exact Bessel continuation and
holds at most MAX_EXTERIOR_TAIL_FRACTION of the
power, at nodes the rule barely resolves.