solprop.nonlinear.normalisation#
Prefactors that turn a nonlinear polarisation into a rate of change of the field.
The nonlinear term of the propagation equation is the polarisation, transformed to the frequency domain, multiplied by
-i omega / (2 eps0 c n_eff(omega))
(docs/dev/architecture.md section 2). That one expression covers both grids: on
a field grid it multiplies the transform of the real polarisation, on an envelope grid
the transform of its envelope, and in both omega is the absolute frequency the grid
stores. Its omega dependence is the optical shock term, so turning the shock off
is not a different equation but the same prefactor with omega frozen at the
carrier.
Mode-averaged propagation adds one more step: the propagated amplitude is in the square
root of watts, and a response needs volts per metre. sqrtw_to_vm() is that
conversion and mode_average_prefactor() is the two composed.
What is not here is the transform normalisation: the crop and pad scale factors belong
to the grid and are folded in by the translation layer (docs/dev/kernel-contract.md
section 5).
Functions
|
Return the self-phase-modulation coefficient of one vector channel. |
|
Return the prefactor a mode-averaged run applies, amplitude to amplitude. |
|
Return the factor converting a mode-averaged amplitude to a field strength. |
|
Return the UPPE prefactor on the coarse frequency grid. |
- solprop.nonlinear.normalisation.modal_kerr_coefficient(electric, weights, chi3, omega, *, metric=None, channel=None)[source]#
Return the self-phase-modulation coefficient of one vector channel.
\[\gamma = \frac{\omega\varepsilon_0}{16 W}\int\chi^{(3)} \left(2|\mathbf E|^4 + |\mathbf E\cdot\mathbf E|^2\right)\,dA\]with the three-component field of a channel normalised to unit power (
docs/guide/waveguides/vector-fields.md) andWits sampled longitudinal flux, one by construction and by default. The instantaneous isotropic Kerr polarisation of an analytic signal isP = eps0 chi3 (2 |E|^2 E + (E.E) E*) / 4, and the modal source functional of a bound reciprocal basis isda/dz = -i omega W^-1 integral(E* . P) dA / 4(docs/guide/waveguides/general-modal.md). Putting the first into the second at unit amplitude leaves-i gamma, which is this expression: both the longitudinal field and the|E.E|^2term of a vector channel are in it, and neither is in the scalarn_2 omega / (c A_eff).The metric is the caller’s to supply, and the identity is the default. Nothing here measures it: the field is taken to be a channel normalised to unit longitudinal Poynting flux over its whole physical domain, and the integral is evaluated on the caller’s weights. A rule that truncates or under-resolves the channel carries less than unit flux where it is sampled, and a general modal run divides by that sampled flux – the
W^-1inside the route’s projection map – rather than by one. Passing metric makes this coefficient the same statement as that run’s, to rounding, instead of one that differs by the basis’s departure from orthonormal. A table built bysolprop.translate.modal_tables.sample_modal_table()gates that departure atDEFAULT_GRAM_ATOLand reports it, and a caller assembling a rule by hand checks it itself.It is the metric the route’s own contraction shares, not a scalar flux. The route projects with
W^-1, so the coefficient of one channelmcarries[W^-1]_mm– which is1/W_mmonly whenWis diagonal. Give the whole Hermitian metric of a degenerate block, with channel saying which row this field is, and the inverse is taken once here; give a positive float and it is read as that channel’s own diagonal entryW_mm, which is the diagonal statement. Only the channel’s own entry enters either way, because only one channel’s field is supplied: one channel’s polarisation also drives its partners, no single coefficient describes that, and the route’s own projection map is then the only complete statement.- Parameters:
- electricnumpy.ndarray
One channel’s Cartesian E in V/m per sqrt(W), shape (n_nodes, 3), in the internal convention and normalised over the whole physical domain. Its two axes carry independent names, so that the agreement with weights and the three components are checked here, in production, and not only under the test session’s runtime hook.
- weightsnumpy.ndarray
Area weights in m², shape (n_nodes,), of the rule the field was sampled on.
- chi3array_like or float
Third-order susceptibility in m²/V² at each node, shape (n_nodes,), or one value for every node. It is zero where a region does not respond, which is how a linear cladding is excluded; the caller owns that assignment.
- omegafloat
Positive angular frequency in rad/s.
- metricarray_like, float or None, optional
The sampled Hermitian flux metric of this channel’s degenerate block (
solprop.modes.vector.flux_product()), dimensionless: a square(n_modes, n_modes)array, of which[W^-1]at channel is used, or a positive float read as this channel’s own diagonal entryW_mm.None, the default, assumes the identity the normalisation declares.- channelint or None, optional
Which row of a square metric this field is, required when the metric has more than one row and refused when metric is not a matrix.
- Returns:
- float
gammain 1/(W m), non-negative for a non-negative susceptibility.
- Raises:
- ModeError
If the shapes disagree, the frequency is not positive, the metric is not a positive scalar or a Hermitian positive-definite matrix, channel is missing, out of range or given without a matrix, or the field, the weights or the susceptibility is not finite.
Examples
>>> import numpy as np >>> from solprop.nonlinear.normalisation import modal_kerr_coefficient >>> field = np.array([[1.0e6 + 0j, 0j, 0j]]) >>> f"{modal_kerr_coefficient(field, np.array([1e-12]), 1e-22, 2.0e15):.6e}" '3.320320e-07' >>> half = modal_kerr_coefficient( ... field, np.array([1e-12]), 1e-22, 2.0e15, metric=2.0 ... ) >>> f"{half:.6e}" '1.660160e-07' >>> dense = modal_kerr_coefficient( ... field, np.array([1e-12]), 1e-22, 2.0e15, ... metric=np.array([[2.0, 0.0], [0.0, 1.0]]), channel=0, ... ) >>> f"{dense:.6e}" '1.660160e-07'
- solprop.nonlinear.normalisation.mode_average_prefactor(mode, grid, z=0.0, *, shock=True, index=None)[source]#
Return the prefactor a mode-averaged run applies, amplitude to amplitude.
The composition of
uppe_prefactor()withsqrtw_to_vm(): the response is evaluated on a field in V/m obtained by multiplying the propagated amplitude bysqrtw_to_vm, and its polarisation is brought back by dividing by the same factor, somode_average_prefactor = uppe_prefactor / sqrtw_to_vm(A_eff).Written out this is
-i omega sqrt(eps0 c A_eff / 2) / (2 eps0 c n_eff).- Parameters:
- modesolprop.modes.protocol.Mode
The mode; supplies the effective index and the effective area.
- gridsolprop.grid.TimeGrid
The grid.
- zfloat, optional
Position along the waveguide, m.
- shockbool, optional
Keep the
omegadependence of the numerator. DefaultTrue.- indexfloat or None, optional
Replace the effective index by this constant; the GNLSE passes 1.0.
- Returns:
- numpy.ndarray
The prefactor, complex, shape
(n_omega,).
- Raises:
- ModeError
If the effective index or the effective area is invalid.
Examples
>>> import numpy as np >>> from solprop.grid import EnvelopeGrid >>> from solprop.nonlinear.gnlse import SpecifiedMode >>> from solprop.nonlinear.normalisation import ( ... mode_average_prefactor, ... sqrtw_to_vm, ... uppe_prefactor, ... ) >>> grid = EnvelopeGrid.from_limits(1.0, 1030e-9, (700e-9, 2e-6), 2e-12) >>> mode = SpecifiedMode.from_taylor([1.0], omega_ref=grid.omega0) >>> composed = mode_average_prefactor(mode, grid, index=1.0) >>> plain = uppe_prefactor(mode, grid, index=1.0) / sqrtw_to_vm(mode.aeff()) >>> bool(np.array_equal(composed, plain)) True
- solprop.nonlinear.normalisation.sqrtw_to_vm(aeff)[source]#
Return the factor converting a mode-averaged amplitude to a field strength.
E[V/m] = a[sqrt(W)] * sqrtw_to_vm(A_eff)withsqrtw_to_vm = 1 / sqrt(eps0 c A_eff / 2), applied to the internal vacuum-normalised amplitudea, so thateps0 c |E_a|^2 / 2is the vacuum-admittance intensity|a|^2 / A_eff– the true powerP = Re(n_eff) |a|^2divided byRe(n_eff) A_eff(docs/dev/conventions.mdsection 4 anddocs/guide/results/power-and-intensity.md).- Parameters:
- aefffloat
Effective area in m^2, positive and finite.
- Returns:
- float
The conversion factor in V/(m sqrt(W)).
- Raises:
- ModeError
If
aeffis not positive and finite.
Examples
>>> from solprop.nonlinear.normalisation import sqrtw_to_vm >>> f"{sqrtw_to_vm(1e-10):.6e}" '2.744924e+06'
- solprop.nonlinear.normalisation.uppe_prefactor(mode, grid, z=0.0, *, shock=True, index=None)[source]#
Return the UPPE prefactor on the coarse frequency grid.
-i omega / (2 eps0 c n_eff(omega, z)), evaluated on the grid’s active band and zero elsewhere. Zero rather than one out of band: the state there is removed by the hard band mask, and leaving raw, unnormalised polarisation in those bins would be a defect that every Runge-Kutta stage and error estimate sees.With
shock=Falsetheomegaof the numerator is frozen at the grid’s carrier, which removes self-steepening; then_eff(omega)of the denominator is part of the projection, not of the shock, and stays frequency dependent.Only the real part of the effective index appears. The loss belongs in the linear operator; carrying it here as well would give the nonlinear term a small real prefactor, that is a spurious gain, and the prefactor would no longer be purely imaginary.
- Parameters:
- modesolprop.modes.protocol.Mode
The mode whose effective index appears in the denominator. Unused when index is given.
- gridsolprop.grid.TimeGrid
The grid; supplies the frequency axis, the active band and the carrier.
- zfloat, optional
Position along the waveguide, m.
- shockbool, optional
Keep the
omegadependence of the numerator. DefaultTrue.- indexfloat or None, optional
Replace the effective index by this constant. The GNLSE passes 1.0: its
betais a fitted dispersion rather than a physical effective index, soc beta / omeganeed not be anywhere near one.None, the default, takes the index from the mode.
- Returns:
- numpy.ndarray
The prefactor in m/(V s) per unit polarisation, purely imaginary, shape
(n_omega,).
- Raises:
- ModeError
If the real part of the effective index is not finite, or vanishes, anywhere in the active band; it is not lower-bounded to keep the run going.
Examples
>>> import numpy as np >>> from solprop.constants import C, EPS0 >>> from solprop.grid import EnvelopeGrid >>> from solprop.nonlinear.gnlse import SpecifiedMode >>> from solprop.nonlinear.normalisation import uppe_prefactor >>> grid = EnvelopeGrid.from_limits(1.0, 1030e-9, (700e-9, 2e-6), 2e-12) >>> mode = SpecifiedMode.from_taylor([1.0], omega_ref=grid.omega0) >>> prefactor = uppe_prefactor(mode, grid, index=1.0) >>> centre = int(np.argmin(abs(grid.omega - grid.omega0))) >>> expected = -1j * grid.omega0 / (2 * EPS0 * C) >>> bool(np.isclose(prefactor[centre], expected)) True