solprop.modes.guide#

Build Poynting-normalised bound-mode providers with explicit material ownership.

Module Attributes

BASIS_UNITARY_ATOL

Largest norm(U.H U - I, 2) a declared change of basis is still called unitary.

MATERIAL_AGREEMENT_RTOL

Largest relative disagreement a step-index provider admits between a region's declared permittivity law and the index its channels were solved with.

PROVIDER_BETA_RTOL

Largest relative disagreement a guide admits between its tables and its provider.

Functions

circular_pair(labels, block)

Declare the circular partners of one exactly degenerate parity pair.

provider_channels(provider)

Return a provider's declared channels as labels, in its own order.

radial_node_rule(interfaces, outer_radius, ...)

Build interface-split Cartesian nodes from the existing polar panel rule.

Classes

ChannelBasis(transform, blocks, labels, gauge_id)

Declare the unitary coordinates a guide's degenerate channels are given in.

ChannelSource(provider, index, nodes, z[, basis])

What a provider channel is solved again from: its provider, index and rule.

ModalGuide(field_provider, mode_collection, ...)

Associate a vector provider with the existing ordered mode collection.

ModalPath(z_edges, degree, sections, rtol, ...)

Record how a moving guide was sampled along z, and what that measured.

ModalSampling(omega, z, nodes, ...[, basis, ...])

Record what a guide's channels were solved on, and what that measured.

ProviderChannelMode(table, label, ...[, ...])

One labelled channel of a modal provider, carrying its sampled dispersion.

StepIndexProvider(modes, regions)

Evaluate exact bound step-index E/H using independent full-domain power norms.

solprop.modes.guide.BASIS_UNITARY_ATOL = 1e-12#

Largest norm(U.H U - I, 2) a declared change of basis is still called unitary.

Dimensionless. A declared transform is admitted only where it is a unitary change of coordinates of an exactly degenerate block: the flux metric then transforms as W' = U.H W U, which is the identity wherever W was, and the generator U.H diag(beta) U stays diagonal. A nonunitary transform does neither – it is a leaky or skewed basis whose metric is genuinely off the identity and whose flow is a matrix exponential – and nothing in this package has measured one, so it is refused here rather than approximated.

The number. circular_pair()’s matrix has entries +-1/sqrt(2) and +-i/sqrt(2); 1/sqrt(2) is exact to a half ulp and the products that form U.H U cancel to the last bit, so its measured defect is at the 1e-16 level. A budget of 1e-12 is four orders above that – room for a matrix assembled from measured numbers rather than from a literal – and twelve orders below any departure a reader would call a different basis.

The room is real, because every later gate honours this budget. A transform that is unitary only to E moves a knot’s measured norm(W - I, 2) by norm(E + E.H, 2), which is this defect and not the metric’s, so the per-knot invariance check of solprop.translate.modal_tables.apply_channel_basis() takes this constant as its floor rather than the rounding of its own product (2.8e-14 for two channels, which would refuse a declaration that had just passed here). The four orders are therefore the room a declaration actually has, not the room it has until the next gate.

class solprop.modes.guide.ChannelBasis(transform, blocks, labels, gauge_id)[source]#

Bases: Displayed

Declare the unitary coordinates a guide’s degenerate channels are given in.

A guide’s channels arrive in whatever basis its provider solved them in, which for an azimuthally degenerate family is the real parity pair: the cos and sin patterns ChannelLabel defines. Any unitary combination of an exactly degenerate block is as good a basis, and one of them – the circular pair of circular_pair() – diagonalises the isotropic Kerr tensor’s polarisation dependence. This record is the declaration of that choice: the matrix, the blocks it acts inside, the labels the transformed channels carry and the identity a file records and a resume compares.

The transform acts on the reference, not on the state. With e the provider’s nodal field rows and U this matrix, the declared basis is e' = e U, so a physical field whose reference-basis amplitudes are c has declared-basis amplitudes d = U.H c: the columns of U are the new basis vectors written in the old one. Every derived object follows from that one rule – W' = U.H W U, G' = U.H G U, Q' = U.H Q, S' = S U – and the noise draw made in the reference basis reaches the declared one as U.H times itself.

Whether a block is degenerate is decided where the table is built, not here (solprop.translate.modal_tables.apply_channel_basis()): U.H diag(beta) U is diagonal when the block’s propagation constants are equal and is a coupled matrix flow, linear.coupled_flow, when they are not.

Attributes:
transformnumpy.ndarray

The unitary U, shape (n_modes, n_modes), dimensionless. It is the identity outside blocks, and unitary to BASIS_UNITARY_ATOL.

blockstuple of tuple of int

Disjoint channel-index blocks U acts inside, each with at least two members. They must be blocks the guide declared degenerate.

labelstuple of ChannelLabel

One label per channel of the declared basis, in row order: the provider’s own outside the blocks and the transformed ones inside them.

gauge_idstr

Non-empty identity of this declaration, composed into the tables’ and the metric’s gauge_id so that a run in one basis cannot resume in another.

Raises:
solprop.exceptions.ModeError

If the matrix is not square, finite, unitary or block-diagonal on blocks, if the blocks are not disjoint valid sets of at least two channels, if the label count disagrees with the matrix, or if the identity is empty.

Examples

>>> from solprop.modes.guide import circular_pair
>>> from solprop.modes.protocol import ChannelLabel
>>> pair = (
...     ChannelLabel("HE", 1, 1, "even", None, "HE1,1:even"),
...     ChannelLabel("HE", 1, 1, "odd", None, "HE1,1:odd"),
... )
>>> basis = circular_pair(pair, (0, 1))
>>> basis.gauge_id
'circular(0,1)'
>>> [label.display_label for label in basis.labels]
['HE1,1:left', 'HE1,1:right']
blocks: tuple[tuple[int, ...], ...]#
gauge_id: str#
property identity: Mapping[str, JsonValue][source]#

Return what a run records of this declaration, and a resume compares.

The matrix enters by value: two different unitary transforms of the same block are two different bases, and the block list alone does not tell them apart. Its entries are declared numbers, not measurements, so they enter exactly, as a declared chi3 or a knot does (solprop.modes.guide.ModalSampling.identity()).

Returns:
collections.abc.Mapping

JSON-serialisable scalars and lists of scalars, stable across processes.

labels: tuple[ChannelLabel, ...]#
transform: Complex[ndarray, 'n_modes_basis_out n_modes_basis_in']#
property unitary_defect: float[source]#

Return norm(U.H U - I, 2), how far the declaration is from unitary.

Returns:
float

Non-negative, dimensionless.

class solprop.modes.guide.ChannelSource(provider, index, nodes, z, basis=None)[source]#

Bases: object

What a provider channel is solved again from: its provider, index and rule.

A ProviderChannelMode is a sampled snapshot, and some questions need the fields again at a frequency or a position the snapshot did not keep – a scalar model’s effective area at its pump, above all. ModalGuide.from_provider() gives every channel one, so that the channel can ask the provider itself rather than its caller reaching into the translation layer.

Attributes:
providerModalProvider

The guide’s full-vector provider.

indexint

The channel’s index on the provider’s mode axis, after basis, from zero.

nodesModalNodeRule

The rule the guide was sampled on: the entrance rule of a moving guide, which the provider is asked for again at every position (solprop.translate.modal_path.path_rule()).

zfloat

The position in m the snapshot, or a moving guide’s entrance, was solved at.

basisChannelBasis or None

The guide’s declared change of channel basis, applied to every fresh solve before the channel is read, or None for the provider’s own.

basis: ChannelBasis | None = None#
index: int#
nodes: ModalNodeRule#
provider: ModalProvider#
z: float#
solprop.modes.guide.MATERIAL_AGREEMENT_RTOL = 1e-06#

Largest relative disagreement a step-index provider admits between a region’s declared permittivity law and the index its channels were solved with.

The modes own the eigenproblem’s indices (StepIndexMode.core and cladding) and the regions own the material laws; the two describe the same material and nothing else made them agree. The regions’ laws are what a declared n2 is converted against (solprop.modes.material.MaterialKerr.susceptibility()) and what a guide’s identity records, so a core region declared at 2.0 beside channels solved at 1.45 would propagate the 1.45 fields with a chi3 90 % too large and nothing downstream would say so.

The budget is on the permittivity, and 1e-6 is chosen to admit a law evaluated by two paths – a glass law asked for its index and the same law asked for its permittivity – while refusing any disagreement a reader would call physical: the smallest index step this package’s own weakly guiding fixtures use is 0.001 in the index, which is 1.4e-3 in the permittivity, three orders above this.

class solprop.modes.guide.ModalGuide(field_provider, mode_collection, regions, sampling=None)[source]#

Bases: Displayed

Associate a vector provider with the existing ordered mode collection.

Attributes:
field_providerModalProvider

Internal-convention full-vector field provider.

mode_collectionModeCollection

Physical modes and their channel labels in matching order.

regionstuple of MaterialRegion

Ordered material regions owned by the guide, each declaring its own response (MaterialKerr) or none, which is how a linear cladding is said to be linear.

samplingModalSampling or None

What the guide’s channels were solved on, filled by from_provider() and None for a guide built from constructed modes. A guide without it can be inspected and compared against, and cannot be propagated: solprop.driver.simulation.Simulation.from_waveguide() refuses it by name, because the resolution its dense reference would be sampled at is nowhere declared.

field_provider: ModalProvider#
classmethod from_provider(provider, omega, *, z=0.0, nodes=None, degenerate_blocks=(), basis=None, beta_rtol=1e-06, sections=1, path_rtol=None)[source]#

Build a guide from a provider that solves its own fields.

The route for a GradedIndexProvider, and for any provider that publishes its channels: the provider owns its sector, channel count, elements, extent and radius path, exactly as from_step_index() takes constructed modes, and nothing here invents a resolution. Dispersion comes through the table bridge, so each channel becomes a ProviderChannelMode whose propagation constant is the provider’s own generator at the knots and whose derivatives are the dense table’s interpolant.

The propagation constants are then checked back against the provider at the entrance frequency, where the table interpolates nothing, and midway across the first, the middle and the last gap between knots, where it interpolates as it will in a run; the ends are where a not-a-knot cubic is worst.

A provider whose geometry moves – a declared radius path that is not constant, or any longitudinal breakpoint – is sampled along its path as well: its z domain is cut into sections at every declared kink, each divided sections times, and every section is solved at the Chebyshev-Lobatto points of the degree its own accuracy check demands, on the provider’s own rule at each position and in its own gauge (solprop.translate.modal_path.sample_modal_path()). The checks below are then taken at the entrance, which is the provider’s own z domain start, and each channel carries its index and area along the path (ModalPath, ProviderChannelMode.path). A guide whose geometry does not move is a snapshot at one position and is bitwise what it was.

Parameters:
providerModalProvider

Internal-convention provider naming its channels in identity.

omeganumpy.ndarray

At least four increasing positive frequencies in rad/s covering the band the guide will be used over, 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 a bound field reaches furthest and the power a rule discards is largest, so that the rule’s tail_power_bound is a bound over the whole band and not only at the frequency it was taken.

degenerate_blockssequence of sequence of int, optional

Channel indices the provider’s own order declares degenerate, as solprop.translate.modal_tables.sample_modal_table() takes them. Tracking rotates freely inside a declared block and matches one-to-one outside it, and the generator is rotated with the fields, so a degenerate pair – the two parities of an HE_1m channel – stays physically equivalent instead of being matched by an ordering the eigenvalues cannot resolve. The blocks are retained on the table, so the propagation-constant checks below align a fresh solve the same way. Empty by default: an arbitrary provider declares no degeneracy.

basisChannelBasis or None, optional

A declared unitary change of the channel coordinates, applied to every knot after the tracking gauge (circular_pair() builds the circular partners of a parity pair). It must act only inside blocks named in degenerate_blocks and only on blocks whose propagation constants are equal to the rounding of the largest; both are refused by name. The guide’s channels then carry the declaration’s labels – handedness rather than parity – and its ProviderChannelMode rows carry the transformed channels’ surrogate areas. None keeps the provider’s own coordinates, and the guide is then bitwise what it was.

beta_rtolfloat, optional

Budget on the two propagation-constant checks, default PROVIDER_BETA_RTOL.

sectionsint, optional

Sections each kink interval of a moving guide is divided into, positive; one by default. Ignored by nothing: a snapshot refuses any other value, because it has no path to divide.

path_rtolfloat or None, optional

Budget on a moving guide’s held-out departure along z; None takes the frequency interpolation’s own measured profile error, floored at solprop.translate.modal_path.PATH_RTOL_FLOOR, so that the sampling in z is never the coarser of the two. A snapshot refuses one.

Returns:
ModalGuide

The provider, one channel mode per declared channel in the provider’s own order, and the provider’s material regions.

Raises:
ModeError

If the provider names no channels, the budget is invalid, a declared basis is not admitted, a channel’s tabulated propagation constant disagrees with a fresh solve by more than beta_rtol; for a moving guide, if z is not its entrance, nodes is not a rule its provider builds, its gauge is not continuous or its path does not resolve; for a snapshot, if sections or path_rtol is declared.

classmethod from_step_index(modes, *, core, cladding, regions=())[source]#

Build a bound provider from existing exact step-index modes.

Parameters:
modessequence of StepIndexMode

Ordered unique bound channels of one guide.

corePermittivityLaw

Core law, matching the modes’ core index.

claddingPermittivityLaw

Explicit exterior law, matching the modes’ cladding index.

regionssequence of MaterialRegion, optional

Core/exterior response ownership; empty builds two linear regions.

Returns:
ModalGuide

Guide with a separate full-vector physics provider.

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

Return the provider physical identity.

Returns:
collections.abc.Mapping

Serializable physical model and construction metadata.

mode_collection: ModeCollection#
property omega_domain: tuple[float, float][source]#

Return the provider frequency interval in rad/s.

Returns:
tuple of float

Lower and upper angular frequency in rad/s.

regions: tuple[MaterialRegion, ...]#
sampling: ModalSampling | None = None#
solve(omega, z, *, nodes)[source]#

Delegate a solve to the guide’s full-vector provider.

Parameters:
omegafloat

Positive angular frequency in rad/s.

zfloat

Position in m.

nodesModalNodeRule

Common physical nodes and material indices.

Returns:
ModalSample

The provider’s validated E/H sample.

property z_breakpoints: tuple[float, ...][source]#

Return the provider physical breakpoints in m.

Returns:
tuple of float

Positions of physical path kinks in m.

property z_domain: tuple[float, float][source]#

Return the provider longitudinal interval in m.

Returns:
tuple of float

Lower and upper position in m.

class solprop.modes.guide.ModalPath(z_edges, degree, sections, rtol, z_error, gram_departure)[source]#

Bases: Displayed

Record how a moving guide was sampled along z, and what that measured.

The z half of a guide’s sampling (ModalSampling), filled by ModalGuide.from_provider() for a provider whose geometry moves. The path is cut into sections – the provider’s declared kink intervals, each divided sections times – and every section is solved at the Chebyshev-Lobatto points of one degree along z, the degree the accuracy check demanded: doubled until the expansion reproduced a fresh solve at the points a doubling adds to within rtol (solprop.translate.modal_path.sample_modal_path()). A run rebuilds the same positions from this record rather than searching again.

Attributes:
z_edgesnumpy.ndarray

Section edges in m, strictly increasing, shape (n_edges_path,): the provider’s z domain split at its interior breakpoints and subdivided.

degreeint

The Chebyshev degree along z every section is sampled at, positive; each section holds degree + 1 positions, its two ends included.

sectionsint

How many sections each kink interval was divided into, positive.

rtolfloat

The budget the degree was searched against, positive: by default the frequency interpolation’s own measured profile error, so that the sampling in z is never the coarser of the two.

z_errorfloat

The measured held-out departure at the degree kept, dimensionless and non-negative: the largest relative maximum-norm departure of E, H or the generator from a fresh solve between the sampled positions.

gram_departurefloat

The largest norm(W - I, 2) of the expanded basis at those same held-out positions, dimensionless and non-negative.

Raises:
ModeError

Naming the field that is out of range.

Examples

>>> import numpy as np
>>> from solprop.modes.guide import ModalPath
>>> path = ModalPath(np.array([0.0, 1e-3]), 2, 1, 1e-6, 3e-7, 1e-8)
>>> path.positions.round(6).tolist()
[[0.0, 0.0005, 0.001]]
>>> round(float(path.interpolate(np.array([0.0, 1.0, 2.0]), 2.5e-4)), 12)
0.5
degree: int#
gram_departure: float#
property identity: Mapping[str, JsonValue][source]#

Return what a run records of the path, and a resume compares.

The declared edges, degree, subdivision and budget exactly; the two measured departures at MEASURED_DIGITS, as every other measured entry of a guide’s identity is.

Returns:
collections.abc.Mapping

JSON-serialisable scalars and lists of scalars.

interpolate(values, z)[source]#

Evaluate values sampled at the path’s positions at any position.

The barycentric form of the interpolating polynomial through one section’s Chebyshev-Lobatto samples – weights (-1)^j, halved at the two ends – which is the same polynomial the translation layer’s Chebyshev coefficients carry, evaluated stably. A position outside the path holds the nearest end’s value, as a ZProfile holds its own beyond its span: a run’s tables reach a few per cent past its end, and the geometry is not declared there.

Parameters:
valuesnumpy.ndarray

One value per position, in positions order flattened, position axis first, shape (n_intervals * (degree + 1), *value_shape).

zfloat

Position in m.

Returns:
numpy.ndarray

The value at z, shape value_shape.

Raises:
ModeError

If values does not carry one row per position.

property n_intervals: int[source]#

Return how many sections the path is cut into.

Returns:
int

The section count.

property positions: Float[ndarray, 'n_intervals_path n_deg_path'][source]#

Return every sampled position, section by section.

Returns:
numpy.ndarray

Positions in m, shape (n_intervals, degree + 1), increasing inside a section, so a position on an interior edge appears twice.

rtol: float#
section(z)[source]#

Return the section a position lies in, clamped to the path’s ends.

Parameters:
zfloat

Position in m.

Returns:
int

The section index; a position on an interior edge takes the section starting there, and one outside the path the nearest end’s.

sections: int#
z_edges: Float[ndarray, 'n_edges_path']#
z_error: float#
class solprop.modes.guide.ModalSampling(omega, z, nodes, degenerate_blocks, profile_error, basis=None, path=None)[source]#

Bases: Displayed

Record what a guide’s channels were solved on, and what that measured.

Everything a dense reference table of this guide is rebuilt from – the knots, the position, the node rule and the declared degenerate blocks – together with the profile interpolation error those knots were measured to give. A guide built by ModalGuide.from_provider() carries one; a guide built from constructed modes does not, and cannot be propagated, because nothing declares the resolution its reference would be sampled at.

Attributes:
omeganumpy.ndarray

The knots the provider was solved at, increasing positive angular frequencies in rad/s, shape (n_omega_sampling,), at least four.

zfloat

The one position in m the snapshot was taken at.

nodesModalNodeRule

The common Cartesian rule every knot was solved on, with its own certified tail bound and material ownership.

degenerate_blockstuple of tuple of int

Channel indices the provider’s own order declares degenerate, as the tracking took them.

basisChannelBasis or None

The declared unitary change of channel coordinates the reference carries on top of the tracking gauge, or None for the provider’s own. It is here because it is part of what the reference is, exactly as the knots and the node rule are: a table rebuilt from this sampling without it is a different basis.

profile_errorfloat

Largest relative departure, in the maximum norm over nodes, components and channels, between the table’s frequency interpolation of E and H and a fresh provider solve, measured at the entrance knot, where the table interpolates nothing, and at the midpoints of the first, the middle and the last knot gap, where a not-a-knot cubic is worst. Non-negative and dimensionless. It is the budget solprop.translate.modal_tables.modal_transform_tables() records on the run’s own record, and it is a measurement at four frequencies, not a proof over the band.

pathModalPath or None

How a guide whose geometry moves was sampled along z – its sections, the degree its accuracy check demanded and what that measured – or None for a snapshot. With it z is the path’s entrance and nodes the entrance rule, which the provider is asked for again at every sampled position.

Raises:
ModeError

If the knots are not at least four increasing positive finite values, the position is not finite, or the profile error is negative or not finite.

basis: ChannelBasis | None = None#
degenerate_blocks: tuple[tuple[int, ...], ...]#
property identity: Mapping[str, JsonValue][source]#

Return what a run records of this sampling, and a resume compares.

Everything a reader needs to say what resolution the reference was built at – the knots themselves, the snapshot position, the node rule’s own construction and tail certificate, the declared degenerate blocks and the measured profile error – and no array the rule owns by value: a rule’s node coordinates and area weights are the megabytes ModalTransformTables already describes by extent, and the rule’s own identity is a mapping of scalars (output schema 7).

The profile error is measured, by a dense solve and a reduction, so it enters at MEASURED_DIGITS significant digits: the same guide re-solved on another BLAS differs in the last bits of it and in nothing else, and a resume key and a reconstruction gate must not refuse a run that did not change. The knots, the position, the blocks and a declared basis are declared and enter exactly.

Returns:
collections.abc.Mapping

JSON-serialisable scalars and lists of scalars, stable across processes.

Examples

>>> import numpy as np
>>> from solprop.modes.guide import ModalSampling
>>> from solprop.modes.vector import ModalNodeRule
>>> rule = ModalNodeRule(
...     np.zeros((1, 2)), np.ones(1), np.zeros(1, np.int32), {"rule": "demo"}
... )
>>> sampling = ModalSampling(
...     np.array([1.0, 2.0, 3.0, 4.0]), 0.0, rule, ((0,),), 1e-5
... )
>>> sampling.identity["n_knots"], sampling.identity["n_nodes"]
(4, 1)
nodes: ModalNodeRule#
omega: Float[ndarray, 'n_omega_sampling']#
property omega_span: tuple[float, float][source]#

Return the band the guide was solved over.

Returns:
tuple of float

Lowest and highest knot in rad/s.

path: ModalPath | None = None#
profile_error: float#
z: float#
solprop.modes.guide.PROVIDER_BETA_RTOL = 1e-06#

Largest relative disagreement a guide admits between its tables and its provider.

ModalGuide.from_provider() reads each channel’s propagation constant out of a tracked dense table and then asks the provider again, at the entrance frequency and at three frequencies the table did not sample – midway across the first, the middle and the last gap between knots. The held-out ones are what means something: they measure the frequency interpolation the table performs, which is the model the guide’s linear operator will use, and the ends of a band are where a not-a-knot cubic is worst.

It certifies ``beta`` alone. beta_1 is the interpolant’s and is not checked against anything here; beta_2 is uncertified and diagnostic (ProviderChannelMode).

The margin is measured, with this guard’s own quantity on the reference parabolic sector of tests/modes/test_modal_tables.py, by a development study of the provider-table derivatives. At the frequency the guard tests: 1.11e-12 over a +-2 % band with five knots, 7.03e-14 with nine, 1.96e-8 over +-20 % with four knots and 2.08e-7 over +-40 % with four. So 1e-6 has orders of margin on any band a run would use and a factor of five on the widest band measured at the middle gap. Taken over every knot midpoint, that widest case reaches 3.10e-6 at an end gap, above this budget, which is why the guard now samples the first and the last gap as well: a four-knot table over +-40 % is refused, and needs more knots.

class solprop.modes.guide.ProviderChannelMode(table, label, outer_radius, area, path=None, path_tables=(), path_areas=(), source=None)[source]#

Bases: Displayed

One labelled channel of a modal provider, carrying its sampled dispersion.

The dispersion carrier the ordered modes of a ModalGuide need for a provider that solves its own fields: its index comes from the table bridge (solprop.translate.modal_tables.sample_provider_table()), and its identity is the provider’s own channel label. It is not a second mode solver behind a Mode interface, and it has no transverse profile of its own: field() refuses, because the physical fields of this channel are the provider’s three-component E and H, not two real transverse components.

What is certified and what is not. ModalGuide.from_provider() certifies beta and nothing else: it compares the table’s propagation constant with a fresh provider solve at the entrance and at one held-out frequency, against PROVIDER_BETA_RTOL. beta_1 is the interpolant’s own derivative, and nothing checks it at construction; measured held-out against fresh-solve five-point differences on the reference parabolic sector it is 4.4e-13 relative at nine knots over a +-2 % band. beta_2 – what solprop.modes.base.beta2() differences out of that derivative, and so what solprop.modes.base.zdw(), solprop.tools.dispersion_length() and solprop.tools.soliton_order() are given for this channel – is diagnostic only and uncertified: on the same sector and the same held-out frequencies it is 2.8e-2 relative at nine knots, 3.2e-3 at seventeen and 2.1e-3 at thirty-three over +-2 %, and worse than unity over a +-20 % band at any knot count measured. A table’s cubic reproduces a nearly constant group delay to twelve digits and still has no useful curvature; read beta_2 here as an order of magnitude, and take a dispersion length or a soliton order from it as the same (measured by a development study of the provider-table derivatives).

Attributes:
tablesolprop.modes.tabulated.IndexTable

Snapshot of this channel’s complex index, with the interpolant’s slopes.

labelChannelLabel

The provider’s own label for this channel.

outer_radiusfloat

Positive radius in m of the provider’s declared physical domain, shared by every channel of one guide, so that a collection has one cross-section.

areafloat

Positive surrogate effective area in m² (solprop.translate.modal_tables.channel_area()) at the median knot of the band the guide was sampled on, the middle of that band. The table’s source_identity["channel_area_omega"] names that frequency. The frequency is part of the number: on the reference parabolic sector the lowest knot’s area is 2.0 %, 25 % and 66 % above the median knot’s over +-2 %, +-20 % and +-40 % bands. On a moving guide, the entrance’s.

pathModalPath or None

How a guide whose geometry moves was sampled along z, or None for a snapshot. With it the channel is not z-constant: its index and its surrogate area are the path’s, interpolated between the sampled positions exactly as the run’s maps are.

path_tablestuple of IndexTable

The channel’s index table at every position of path, in ModalPath.positions order flattened; empty for a snapshot.

path_areastuple of float

Its surrogate area at every position of path, in m², same order; empty for a snapshot.

sourceChannelSource or None

The provider, index and rule the channel can be solved again from, set by ModalGuide.from_provider(); None for a channel built by hand, which then cannot answer solved_area(). A live handle, not configuration: it is left out of the channel’s serialised record, since the run records the provider’s identity beside its channels.

aeff(z=0.0)[source]#

Return the declared surrogate effective area.

Parameters:
zfloat, optional

Position in m; ignored for a snapshot, interpolated along a moving guide’s path.

Returns:
float

A_eff in m², the scalar surrogate of solprop.translate.modal_tables.channel_area() at the median knot of the sampled band (source_identity["channel_area_omega"]), not the nonlinear coefficient of this vector channel and not a function of frequency: a scalar route has one area and this is the one declared.

area: float#
area_at(omega)[source]#

Return the surrogate area at one frequency, along z for a moving guide.

solved_area() at every position the channel’s own area is sampled at: once for a snapshot, and at each position of a moving guide’s path – the positions path_areas holds – interpolated between them exactly as area_path interpolates the median-knot areas, with the same span and the same section edges. It is what solprop.nonlinear.gnlse.SpecifiedMode.from_mode() hands a scalar model given the pump’s wavelength.

Parameters:
omegafloat

Positive angular frequency in rad/s, inside the provider’s domain.

Returns:
float or solprop.profiles.ZProfile

The area in m², a number for a snapshot and a path along z for a moving guide.

Raises:
ModeError

As solved_area().

property area_path: ZProfile | None[source]#

Return the surrogate area along a moving guide’s path, or None.

Declared under the cross-section name a moving mode declares its area by (solprop.modes.protocol.GEOMETRY_PATH_NAMES), so that the tables of a run split on the path’s section edges, where the interpolant’s slope may jump.

Returns:
solprop.profiles.ZProfile or None

The area in m² along z, or None for a snapshot.

property azimuthal_order: int[source]#

Return the label’s azimuthal order, or zero where it has none.

Returns:
int

Azimuthal order, dimensionless.

property channel_label: ChannelLabel[source]#

Return the provider’s own label for this channel.

Returns:
ChannelLabel

The label the provider published in its identity.

dimlimits(z=0.0)[source]#

Return the provider’s declared physical domain.

Parameters:
zfloat, optional

Position in m; ignored, the snapshot is one position.

Returns:
tuple

("polar", (0, 0), (outer_radius, 2 pi)) in m and rad, the same for every channel of one guide.

dneff_domega(omega, z=0.0)[source]#

Return the frequency derivative of the effective index.

It is the interpolant’s, not an analytic model derivative (ADR-006): a provider declares no closed form, so the table carries the slopes of the dense generator interpolant and this differentiates them. Nothing certifies it: solprop.translate.modal_tables.validate_modal_table() measures no frequency derivative, and ModalGuide.from_provider() checks beta alone. Measured held-out against fresh-solve five-point differences on the reference parabolic sector, the beta_1 it gives is 4.4e-13 relative at nine knots over a +-2 % band; the second derivative taken from it is a diagnostic (ProviderChannelMode).

Parameters:
omeganumpy.ndarray

Positive angular frequencies in rad/s inside the table, shape (n_omega,).

zfloat, optional

Position in m; ignored for a snapshot, interpolated along a moving guide’s path.

Returns:
numpy.ndarray

d n_eff / d omega in s/rad, complex, shape (n_omega,).

entrance()[source]#

Return this channel’s snapshot at the entrance, with no path.

What a moving guide’s cumulative Phi is carried by: the run puts one reference channel’s entrance dispersion in Phi and every channel’s departure from it, the motion included, in the coupled flow (solprop.translate.problem._general_assembly).

Returns:
ProviderChannelMode

The same channel, z-constant, with its entrance table and area.

field(points, z=0.0)[source]#

Refuse a two-component transverse profile this channel does not have.

Parameters:
pointsnumpy.ndarray

Cross-section positions, shape (n_points, 2).

zfloat, optional

Position in m.

Returns:
numpy.ndarray

Never returns.

Raises:
ModeError

Always: the physical fields of a provider channel are the provider’s three-component E and H on its node rule, and squeezing them into two real transverse components would be a different, unqualified mode.

label: ChannelLabel#
neff(omega, z=0.0)[source]#

Return the tabulated complex effective index.

Parameters:
omeganumpy.ndarray

Positive angular frequencies in rad/s inside the table, shape (n_omega,).

zfloat, optional

Position in m; ignored for a snapshot, interpolated along a moving guide’s path.

Returns:
numpy.ndarray

n_eff(omega), complex, shape (n_omega,).

neff_minus_one(omega, z=0.0)[source]#

Return n_eff - 1 formed without cancellation.

Parameters:
omeganumpy.ndarray

Positive angular frequencies in rad/s inside the table, shape (n_omega,).

zfloat, optional

Position in m; ignored for a snapshot, interpolated along a moving guide’s path.

Returns:
numpy.ndarray

n_eff - 1, complex, shape (n_omega,), the first of the table’s refractivity pair; its second is the derivative dneff_domega() returns.

outer_radius: float#
path: ModalPath | None = None#
path_areas: tuple[float, ...] = ()#
path_tables: tuple[IndexTable, ...] = ()#
principal_component(z=0.0)[source]#

Declare that a vector channel needs both transverse components.

Parameters:
zfloat, optional

Position in m; ignored, the snapshot is one position.

Returns:
None

Always None.

property scale_invariant: bool[source]#

Whether the profile scales exactly with a core radius.

Returns:
bool

Always False: a provider channel has no scaling law, and its normalised frequency changes with the geometry.

solved_area(omega, z=0.0)[source]#

Return the surrogate effective area from a fresh provider solve.

The same definition as area and aeff() – solprop.translate.modal_tables.channel_area(), (int |E|^2 dA)^2 / int |E|^4 dA over the three Cartesian components, the longitudinal one included – but at the frequency asked for rather than at the median knot of the sampled band, which is where a scalar model’s pump usually is not. The provider is solved at omega on the guide’s own rule: the sampling rule for a snapshot, and for a moving guide the rule the provider builds at z on the same request (solprop.translate.modal_path.path_rule()), which is the rule the path’s own samples were taken on. The declared channel basis is applied first; no tracking gauge is, so a channel inside a degenerate block that no basis declares has the provider’s own member of that block.

Parameters:
omegafloat

Positive angular frequency in rad/s, inside the provider’s domain.

zfloat, optional

Position in m; ignored for a snapshot, which is solved where it was sampled.

Returns:
float

A_eff in m².

Raises:
ModeError

If the channel carries no ChannelSource, or the provider refuses the frequency or the position.

source: ChannelSource | None = None#
table: IndexTable#
property z_constant: bool[source]#

Whether the channel is independent of position.

Returns:
bool

True for a snapshot at one declared position; False on a guide whose geometry moves, whose channel carries a sampled path.

class solprop.modes.guide.StepIndexProvider(modes, regions)[source]#

Bases: Displayed

Evaluate exact bound step-index E/H using independent full-domain power norms.

A returned ModalSample is always fully valid: an unsupported channel is refused by name rather than masked out, so valid is all True in every sample this provider returns.

Attributes:
modestuple of StepIndexMode

Unique physical channels of one core/cladding geometry, in state order.

regionstuple of MaterialRegion

Core and unbounded exterior, with material laws matching the modes.

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

Return internal convention, bound-model and channel identities.

Returns:
collections.abc.Mapping

Serializable physical model and construction metadata.

modes: tuple[StepIndexMode, ...]#
node_rule(omega, z=0.0, *, n_r=None, n_theta=None)[source]#

Build a common rule and certify the discarded cladding power.

Parameters:
omegafloat

Positive frequency in rad/s in the guided band.

zfloat, optional

Position in m within the radius path.

n_rint or None, optional

Gauss nodes per radial panel, at least two; None uses the 64 this provider’s exponential cladding tail needs.

n_thetaint or None, optional

Angular node count; None uses 4*(maximum n+1)+1 for quartic products.

Returns:
ModalNodeRule

Material-split rule with independently integrated tail bounds.

Raises:
ModeError

If the bound field or tail cannot be certified.

property omega_domain: tuple[float, float][source]#

Return material frequency support; solve checks each mode cutoff.

Returns:
tuple of float

Lower and upper angular frequency in rad/s.

regions: tuple[MaterialRegion, ...]#
solve(omega, z, *, nodes)[source]#

Solve bound channels on explicitly owned physical nodes.

Parameters:
omegafloat

Positive angular frequency in rad/s within material and guided support.

zfloat

Position in m in the radius path.

nodesModalNodeRule

Common Cartesian nodes with correct core/cladding ownership.

Returns:
ModalSample

Unit full-domain power E/H and their sampled physical Gram matrix. Its valid mask is all True: an unsupported channel is refused here, not masked.

Raises:
ModeError

If domains, material ownership, norm or a boundary residual is invalid.

property z_breakpoints: tuple[float, ...][source]#

Return radius-path breakpoints in m.

Returns:
tuple of float

Positions of physical path kinks in m.

property z_domain: tuple[float, float][source]#

Return the radius-path interval in m.

Returns:
tuple of float

Lower and upper position in m.

solprop.modes.guide.circular_pair(labels, block)[source]#

Declare the circular partners of one exactly degenerate parity pair.

The change of basis that turns the real cos/sin pair of an azimuthally degenerate family into its two circular partners, which is the basis an isotropic Kerr tensor is diagonal in: for a field that is circular at every node E . E vanishes, so (E . E) E* drops out of P = eps0 chi3 (2 |E|^2 E + (E . E) E*) / 4 and the self coefficient falls to two thirds of the linear one.

The convention, derived here. The even partner is the cos((n-1) theta) pattern about x, which for n = 1 is the x-polarised member, and the odd one is its quarter-turn partner about y (ChannelLabel). A provider’s fields are in the internal convention of docs/dev/conventions.md section 2, whose plane wave is exp(i(omega t - beta z)), so the physical transverse field of a channel with nodal rows (e_even, e_odd) and amplitude one is Re[(e_even + a e_odd) exp(i omega t)]. With a = -i that is (cos(omega t), sin(omega t)) in (x, y): the vector turns from x towards y, which an observer looking towards the source sees turn anticlockwise. That is positive helicity, which optics calls left circular (Jackson, Classical Electrodynamics, section 7.2), and its optics-convention Jones vector is the conjugate (1, i)/sqrt(2), which solprop.polarisation records as positive Stokes V. So

e_left  = (e_even - i e_odd)/sqrt(2), e_right = (e_even + i e_odd)/sqrt(2)

and the two columns of the returned matrix are exactly those, in that order: block[0] becomes the left channel and block[1] the right one. The order is not free – block[0] must be the even member and block[1] the odd one, and a block given the other way round is refused by name. The names left and right are a convention, fixed by the derivation above, not a derived fact.

Parameters:
labelscollections.abc.Sequence

Every channel’s label, in the guide’s own row order.

blockcollections.abc.Sequence

Exactly two channel indices, one labelled "even" and one "odd", of the same family and quantum numbers.

Returns:
ChannelBasis

The declaration, with gauge_id "circular(i,j)" for the block’s own two indices in ascending order and the two labels relabelled by handedness.

Raises:
solprop.exceptions.ModeError

If the block is not two distinct valid indices, or the two labels are not a parity pair of one family.

Examples

>>> import numpy as np
>>> from solprop.modes.guide import circular_pair
>>> from solprop.modes.protocol import ChannelLabel
>>> pair = (
...     ChannelLabel("HE", 1, 1, "even", None, "HE1,1:even"),
...     ChannelLabel("HE", 1, 1, "odd", None, "HE1,1:odd"),
... )
>>> basis = circular_pair(pair, (0, 1))
>>> bool(np.allclose(basis.transform[:, 0], [1, -1j] / np.sqrt(2)))
True
>>> basis.labels[1].handedness
'right'
solprop.modes.guide.provider_channels(provider)[source]#

Return a provider’s declared channels as labels, in its own order.

A provider publishes its channels as mappings inside identity, which is what survives JSON; this is the one place they are turned back into ChannelLabel objects for a caller that has to name one – to declare a change of basis over a pair, most of all.

Parameters:
providersolprop.modes.vector.ModalProvider

A provider that names its channels.

Returns:
tuple

One label per channel.

Raises:
ModeError

If the provider names no channels.

solprop.modes.guide.radial_node_rule(interfaces, outer_radius, *, n_r, n_theta)[source]#

Build interface-split Cartesian nodes from the existing polar panel rule.

Parameters:
interfacessequence of float

Strictly increasing positive material radii in m, inside outer_radius.

outer_radiusfloat

Positive finite truncation radius in m; tail certification is separate.

n_rint

At least two Gauss nodes in each radial panel.

n_thetaint

At least two angular nodes; caller certifies angular resolution.

Returns:
ModalNodeRule

Positive area weights and one int32 material index per node.

Raises:
ModeError

If the radius or interface arrangement is invalid.