Circular and coupled modal bases#

A change of basis can express the same guided field using different channels, such as circular instead of linear polarisation. It must transform the fields, power metric and linear propagation together. A rotated pair is independently propagating only when its propagation constants are exactly degenerate.

Declaring a circular basis#

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 — diagonalises the isotropic Kerr tensor’s polarisation dependence: for a field that is circular at every node \(\mathbf E\cdot\mathbf E\) vanishes, so \((\mathbf E\cdot\mathbf E)\mathbf E^*\) drops out of the cubic and the self coefficient falls to two thirds of the linear one.

circular_pair() declares it, and from_provider() takes it as basis=:

from solprop.modes.guide import ModalGuide, circular_pair, provider_channels

# provider: a field provider whose channels 0 and 1 are an exactly degenerate parity
# pair; omega: the knots; rule: its node rule, all as in the first example.
basis = circular_pair(provider_channels(provider), (0, 1))
guide = ModalGuide.from_provider(
    provider, omega, nodes=rule, degenerate_blocks=((0, 1),), basis=basis
)
# The channels are now HE1,1:left and HE1,1:right, with handedness and no parity.

The transform is applied to every knot after the tracking gauge, in that fixed order, and the composed identity is <tracking gauge>+circular(0,1). Everything downstream follows from the one rule \(\mathbf e' = \mathbf e\,U\): the maps become \(S' = S\,U\) and \(Q' = U^{\mathsf H} Q\), the metric \(W' = U^{\mathsf H} W U\), and a physical field’s amplitudes \(\mathbf d = U^{\mathsf H}\mathbf c\).

The convention. The even partner is the \(\cos((n-1)\theta)\) pattern about \(x\) — for \(n = 1\) the x-polarised member — and the odd one its quarter-turn partner. A provider’s fields are in the internal convention, whose plane wave is \(e^{i(\omega t - \beta z)}\), so the channel \((\mathbf e_{\rm even} - i\,\mathbf e_{\rm odd})/\sqrt2\) has the physical transverse field \((\cos\omega t, \sin\omega t)\): it turns from \(x\) towards \(y\), which an observer looking towards the source sees turn anticlockwise. That is positive helicity, which optics calls left circular, and its optics-convention Jones vector \((1, i)/\sqrt2\) is the one Polarisation and azimuthal modes records as positive Stokes \(V\). So 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.

Three gates, each refusing by name. The declaration itself must be unitary to BASIS_UNITARY_ATOL (1e-12); each block it acts inside must be one the guide declared degenerate; and the transform must not change the basis’s measured departure from orthonormal, which the spectral norm’s unitary invariance says a unitary one cannot — to the declaration’s own BASIS_UNITARY_ATOL, because a transform unitary only to that budget moves the measured norm by its own unitary defect, so a tighter gate here would refuse what the declaration has just admitted. The route and the backend then gate the stored metric against MAX_DENSE_METRIC_DEPARTURE (1e-3), which is the loosest of the three and the one a backend can apply, since it sees the block and not the declaration.

The metric is then genuinely dense. \(W' = I + U^{\mathsf H}\delta U\) with \(\delta\) the sampled Gram defect: the exact identity is invariant but the measured object is not, and the off-diagonal of \(U^{\mathsf H}\delta U\) is not \(\delta\)’s. So linear.modal_metric becomes kind="dense", built from the table’s own sampled flux metric per frequency rather than from a literal, and the identity outside the guide’s band, where nothing lives and a singular block would certify nothing.

The declaration is opt-in: a guide built without basis= stays in the provider’s own coordinates, bit for bit.

A declared basis of a block that is only nearly degenerate#

\(U^{\mathsf H}\operatorname{diag}(\beta)U\) is diagonal only where the block’s propagation constants are equal. Declare a basis over a block whose members are split — and every “degenerate” group of a real guide is split by something: the vector correction, a stress, a bend — and the rotated channels genuinely beat. That is not an error, it is the physics of the coordinates asked for, and the run carries it in linear.coupled_flow.

from solprop.modes.guide import ChannelBasis, ModalGuide

root = 1.0 / np.sqrt(2.0)
basis = ChannelBasis(
    np.array([[root, -root], [root, root]], dtype=complex),  # a quarter turn
    ((0, 1),),  # the block it acts in
    rotated_labels,  # a ChannelLabel for each channel after the rotation
    "lp11(0,1)",
)
guide = ModalGuide.from_provider(
    provider, omega, nodes=rule, degenerate_blocks=((0, 1),), basis=basis
)

What the route does with it. The transformed generator is evaluated rather than assumed diagonal — for an exactly degenerate block it is carried over bit for bit and no coupled flow is built — and coupled_block() reads it at the run’s own bins relative to one reference channel, the first. The assembly then builds the cumulative \(\Phi\) from that channel alone, replicated, so that \(\Phi\) is a multiple of the identity in the channel axis and commutes with the block, and puts every channel’s departure from it — its own dispersion included — in the leaf beside it. The leaf is one "constant_schur" section, because the geometry does not move: only the coordinates are rotated, so the block is the same matrix at every position.

What it costs. The whole split is in the leaf’s off-diagonal, and over a length \(L\) a launch into one rotated channel arrives with \(\sin^2(|G_{01}|L)\) of its power in the other — 3.8e-3 over a millimetre on the TE_01/TM_01 pair of a strand of 6 µm core radius, whose vector splitting is 120 1/m at 835 nm, 1.1e-5 of its own propagation constants (on a 3 µm core it is 744 1/m and 6.9e-5). The same launch in the provider’s own coordinates, carried through \(U\), reproduces it: measured 8.4e-6 in each channel’s modulus and 2.1e-5 rad in the phase between them, which is what says the flow is the same physics in other coordinates and not a second model. Both are measured inside the declared window, where the linear operator is the guide’s own; outside it the grid carries the absorbing collar, which two guides with different index tables carry differently, and the same comparison reads 1e-3.

Where it runs, and what it refuses by name. Every backend executes the leaf — numpy_ref, numba_cpu and native, on the CPU and on a GPU — except a CF4 section, which runs on numpy_ref alone and which the entry point refuses elsewhere before anything is translated. A noise model beside it is refused, because the shot-noise draw (Shot noise) is transported by the unitary phase of a diagonal flow and its transport through a coupled one has not been measured. The lockstep batch refuses a member carrying one, because it evaluates its own stage arithmetic and applies neither the flow action nor its residual. And a block whose exponent over the largest declared step exceeds MAX_GENERAL_FLOW_EXPONENT (1e3) is refused: the block carries every channel’s departure from the reference, so a basis declared over channels that were never a degenerate group would put their whole dispersion difference into a matrix exponential taken by scaling and squaring.

The run records what it propagated. modal.flow joins the schema-7 identity with the section methods, the degree, the reference channel, the three certificate rows and the largest coupling, so a resume against a changed block is refused by the block that moved. The key is written only where a flow exists; a run without one has no modal.flow entry at all. Its model reads "declared channel basis" and its gauge_id ends in the declaration’s own name (+lp11(0,1) above); the entries it shares with the capillary’s coherent wall mean the same things there, and are tabulated under reading the record.

What a rotated channel reports, and what it withholds. A channel of a split block has no propagation constant of its own — it beats — so the per-channel index the guide publishes for it is the block’s mean, read off the diagonal of the coupled generator: both rotated TE_01/TM_01 channels above report \(n_{\rm eff} = 1.447776101449\) at 835 nm, where the provider’s own channels report 1.447784077518 and 1.447768125380. That mean is what the surrogate area, the dimension limits and the labels need, and the run itself reads only the reference channel’s. ProviderChannelMode.neff on such a channel returns this mean: do not read it as a propagation constant, because the channel has none. The one statistic built from per-channel propagation constants, zdw, is withheld on a coupled block — it is left out of the record’s list of admitted accessors, and asked for by name it is refused at the driver, the backend and the file — because a zero-dispersion wavelength of a mean propagates nothing.