solprop.linear#
Assembly of the linear operator D(omega, z).
The linear operator carries the dispersion and the loss of one mode in a co-moving frame, plus any spectral absorber expressed as a rate:
D = -i (beta - beta_0 - beta_1 (omega - omega_ref)) - alpha/2 - alpha_abs/2
with beta = (omega/c) Re n_eff and alpha = 2 (omega/c) Im n_eff, in the internal
FFT convention of docs/dev/conventions.md section 2. It is diagonal in omega,
which is the property the whole solver rests on; this module only evaluates it at one
z, and the translation layer turns those samples into the cumulative operator
Phi(omega, z), piecewise Chebyshev in z (see docs/dev/decisions.md).
The frame’s beta_1 comes from the mode’s analytic dneff_domega rather than
from a finite-difference stencil, and the loss limit is a stated policy derived from the
fibre length rather than empirical constants. The narrative, including which
reference each term uses on each grid, is in docs/guide/numerics/linear-operator.md.
Module Attributes
Largest |
|
The length-derived loss limit, used when a caller states no policy. |
Functions
|
Return the linear operator on the coarse frequency grid at one position. |
|
Return where the frame's delay term |
|
Return the |
|
Return the logarithm of the modal power weight. |
|
Return the modal power weight |
Classes
|
The reference the linear operator is written in. |
|
What to do with a loss too large to represent. |
- solprop.linear.DEFAULT_LOSS_POLICY = LossPolicy(max_alpha=None)#
The length-derived loss limit, used when a caller states no policy.
- class solprop.linear.Frame(omega0, subtract_beta0, beta0=None, beta1=None)[source]#
Bases:
objectThe reference the linear operator is written in.
The frame removes from
beta(omega)a constantbeta_0and a linearbeta_1 (omega - omega_ref). The linear term is a change of time origin — it keeps the pulse in the window on a gradient fibre — and the constant term is a change of carrier phase.Which terms apply depends on the grid, because the two grids store different things:
grid
operator
field
-i (beta - beta_1 omega) - alpha/2envelope, no THG
-i (beta - beta_0 - beta_1 (omega - omega_ref)) - alpha/2envelope, with THG
-i (beta - beta_1 omega) - alpha/2A field grid stores the real field, so subtracting
beta_0would rotate its carrier-envelope phase, which is physical rather than a global phase; and its frame term is measured from zero frequency, because a pure delay is a phase linear inomegathrough the origin. An envelope grid has the carrier removed already, sobeta_0may be taken out — except when a third-harmonic term is present, whoseexp(2 i omega_ref t)factor is tied to the true carrier phase. Such a frame keeps the carrier phase whole, so its delay is also measured from zero frequency:beta_1 (omega - omega_ref)would still remove a constant-beta_1 omega_ref, a carrier phase ofbeta_1 omega_ref zthat the cubic term does not share (seeframe_origin()).conventions.mdsection 2 writes the envelope form for both grids.comoving()chooses it for you.- Attributes:
- omega0float
Reference angular frequency, rad/s, positive: where
beta_0andbeta_1are evaluated and, on an envelope grid that removesbeta_0, the origin of the frame term.- subtract_beta0bool
Whether the constant phase is removed.
Falsekeeps the whole carrier phase, so the frame term is then measured from zero frequency on either grid.- beta0float or None, optional
Fixed
beta_0in 1/m.None, the default, recomputes it from the mode at eachz, which is what makes the frame co-moving on a gradient fibre.- beta1float or None, optional
Fixed
beta_1in s/m.None, the default, recomputes it from the mode.
Notes
A frame belongs to a problem, not to a mode: a multimode operator is written in one frame for the whole set, because
beta_1 omegais a change of the time variable and modes given their own would no longer be simultaneous. Handed a set,solprop.translate.linop.build_phi()therefore fixes both quantities on the first mode at each position and gives that frame to every column.Examples
>>> from solprop.grid import EnvelopeGrid, FieldGrid >>> from solprop.linear import Frame >>> limits = (400e-9, 2e-6) >>> field = FieldGrid.from_limits(1.0, 835e-9, limits, 20e-12) >>> Frame.comoving(field).subtract_beta0 False >>> envelope = EnvelopeGrid.from_limits(1.0, 835e-9, limits, 20e-12) >>> Frame.comoving(envelope).subtract_beta0 True >>> Frame.comoving(envelope, thg=True).subtract_beta0 False
- classmethod comoving(grid, *, thg=False)[source]#
Build the frame a grid asks for, with both quantities taken from the mode.
- Parameters:
- gridsolprop.grid.TimeGrid
The grid the operator will be evaluated on; its
omega0becomes the reference and itskinddecides whetherbeta_0is removed.- thgbool, optional
Whether a third-harmonic term is present. On an envelope grid this keeps
beta_0; on a field grid it changes nothing. DefaultFalse.
- Returns:
- Frame
A co-moving frame: both
beta_0andbeta_1follow the mode along z.
Examples
>>> from solprop.grid import EnvelopeGrid >>> from solprop.linear import Frame >>> grid = EnvelopeGrid.from_limits(1.0, 835e-9, (400e-9, 2e-6), 20e-12) >>> frame = Frame.comoving(grid) >>> frame.omega0 == grid.omega0, frame.beta1 is None (True, True)
- class solprop.linear.LossPolicy(max_alpha=None)[source]#
Bases:
objectWhat to do with a loss too large to represent.
An effective index can produce an arbitrarily large
alpha— a capillary bin below cutoff, a table evaluated where it should not be — and two things then go wrong:exp(-alpha z / 2)underflows, and the adaptive Chebyshev builder spends intervals resolving a bin whose field is zero. The answer here is a limit derived from the fibre itself rather than empirical constants.The default limit is
2 * MAX_ATTENUATION_EXPONENT / length: the loss at which the field has already underflowed over the whole fibre, so clamping there cannot change a result. There is no lower limit, so a negativealpha— gain — passes through rather than being clamped to zero.Whatever the limit, it is applied to
DbeforePhiis integrated, never to an accumulated exponent afterwards (docs/guide/numerics/linear-operator.md).- Attributes:
- max_alphafloat or None, optional
Largest power attenuation in 1/m, positive and finite.
None, the default, derives it from the fibre length. Pass, say, 3000.0 for a fixed limit independent of the length.
Examples
>>> from solprop.linear import LossPolicy >>> f"{LossPolicy().limit(1.5):.3f}" '944.000' >>> LossPolicy(max_alpha=3000.0).limit(1.5) 3000.0
- limit(length)[source]#
Return the largest attenuation this policy allows over a fibre.
- Parameters:
- lengthfloat
Propagation distance the operator will be integrated over, m, positive.
- Returns:
- float
The limit in 1/m.
- Raises:
- GridError
If
lengthis not positive and finite.
Examples
>>> from solprop.linear import LossPolicy >>> f"{LossPolicy().limit(0.1):.1f}" '14160.0'
- solprop.linear.MAX_ATTENUATION_EXPONENT = 708.0#
Largest
alpha z / 2worth representing:exp(-708)is still a normal double.Beyond it the transmitted amplitude has underflowed, so no clamp placed at or above this exponent can change any answer (see
LossPolicy).
- solprop.linear.dispersion_operator(mode, grid, frame, z=0.0, *, loss=LossPolicy(max_alpha=None), absorber_rate=None)[source]#
Return the linear operator on the coarse frequency grid at one position.
The mode is evaluated on the grid’s active band only (
omega_min/2 < omega < 1.1 omega_max). Outside it the operator is zero, because an effective index is not defined at zero frequency and the state there is removed by the hard band mask anyway (seedocs/dev/decisions.md). The absorber rate, in contrast, is added over the whole axis: with the collar expressed as a rate, out-of-band bins must carry it rather than propagate loss-free.- Parameters:
- modesolprop.modes.protocol.Mode
The mode. Only
neffanddneff_domegaare used.- gridsolprop.grid.TimeGrid
The grid; supplies
omega, the active band, the layout and the length the loss policy is derived from.- frameFrame
The co-moving frame, usually from
Frame.comoving().- zfloat, optional
Position along the waveguide, m.
- lossLossPolicy, optional
What to do with an attenuation too large to represent. Defaults to the length-derived limit.
- absorber_ratenumpy.ndarray or None, optional
Spectral absorber rate
alpha_abs(omega)in 1/m, shape(n_omega,), non-negative.solprop.boundariesproduces one from the grid’s collar;Nonemeans no absorber.
- Returns:
- numpy.ndarray
D(omega, z)in 1/m, complex, shape(n_omega,).
- Raises:
- GridError
If the frame’s reference lies outside the grid, or the absorber rate is invalid.
- ModeError
If the mode returns a non-finite effective index anywhere in the active band, rather than letting a NaN through.
Examples
>>> import numpy as np >>> from solprop.constants import C >>> from solprop.grid import EnvelopeGrid >>> from solprop.linear import Frame, dispersion_operator >>> from solprop.nonlinear.gnlse import SpecifiedMode >>> grid = EnvelopeGrid.from_limits(1.0, 1030e-9, (700e-9, 2e-6), 2e-12) >>> mode = SpecifiedMode.from_taylor( ... [1.45 * grid.omega0 / C, 1.47 / C, 2e-26], omega_ref=grid.omega0 ... ) >>> operator = dispersion_operator(mode, grid, Frame.comoving(grid)) >>> operator.shape == (grid.n_omega,) True >>> bool(np.allclose(operator[np.argmin(abs(grid.omega - grid.omega0))], 0.0)) True
- solprop.linear.frame_origin(frame, kind)[source]#
Return where the frame’s delay term
beta_1 (omega - origin)starts.A frame either removes the carrier phase or keeps it whole. One that removes it — an envelope grid without a third harmonic — measures the delay from its carrier,
omega0, alongsidebeta_0. One that keeps it — a field grid, or an envelope grid withsubtract_beta0=False— measures the delay from zero frequency, so the frame is a pure change of time origin. Measured fromomega0instead, the delay term would also remove the constant-beta_1 omega0: the envelope would turn atbeta_1 omega0per metre, about 1e7 rad/m in a gas at 1 um, whileA**3 exp(2 i omega0 t)turns at three times that, so the third harmonic would be driven with a spurious mismatch of2 beta_1 omega0.- Parameters:
- frameFrame
The frame.
- kind{‘field’, ‘envelope’}
The spectral layout of the grid the operator is evaluated on.
- Returns:
- float
frame.omega0on an envelope grid whose frame removesbeta_0, else 0.0.
Examples
>>> from solprop.linear import Frame, frame_origin >>> frame_origin(Frame(1.8e15, subtract_beta0=True), "envelope") 1800000000000000.0 >>> frame_origin(Frame(1.8e15, subtract_beta0=False), "envelope") 0.0
- solprop.linear.frame_quantities(mode, frame, z=0.0)[source]#
Return the
(beta_0, beta_1)the frame removes from the operator.beta_0 = omega_ref Re n_eff / cand, differentiatingbeta = omega n_eff / c,beta_1 = (Re n_eff + omega_ref Re dn_eff/domega) / c. The derivative is the mode’s analytic one: a differencedbeta_1has a roughness floor near 5e-8 that would cap the whole cumulative operator at 1e-8, and costs about sevenneffevaluations per stage.Only the real part of the index enters, so the loss never affects the frame.
- Parameters:
- modesolprop.modes.protocol.Mode
The mode whose dispersion the frame follows.
- frameFrame
The frame; its fixed values, where given, override the mode’s.
- zfloat, optional
Position along the waveguide, m.
- Returns:
- tuple of float
(beta_0, beta_1)in 1/m and s/m, as they enterD.beta_0is zero when the frame does not subtract it, so that these are the quantities the kernel contract records as already being insidePhi.
Examples
>>> from solprop.constants import C >>> from solprop.linear import Frame, frame_quantities >>> from solprop.nonlinear.gnlse import SpecifiedMode >>> mode = SpecifiedMode.from_taylor( ... [1.45 * 1.8e15 / C, 1.47 / C], omega_ref=1.8e15 ... ) >>> beta0, beta1 = frame_quantities(mode, Frame(1.8e15, subtract_beta0=True)) >>> f"{beta0:.6e}", f"{beta1 * C:.6f}" ('8.706023e+06', '1.470000')
- solprop.linear.log_power_weight(mode, omega, z=0.0, *, index=None)[source]#
Return the logarithm of the modal power weight.
- Parameters:
- modesolprop.modes.protocol.Mode
Physical mode; unused when the index override is supplied.
- omeganumpy.ndarray
Positive angular frequencies in rad/s, shape
(n_omega,). The caller selects the active band, as for the nonlinear prefactor.- zfloat, optional
Position along the waveguide in m.
- indexfloat or None, optional
Positive finite index override. The GNLSE mapping supplies 1.0.
- Returns:
- numpy.ndarray
Dimensionless
ln(Re n_eff)of shape(n_omega,).
- Raises:
- ModeError
If the power weight is not positive and finite.
- solprop.linear.power_weight(mode, omega, z=0.0, *, index=None)[source]#
Return the modal power weight
w = Re n_eff.The one place the weight is evaluated and validated: the interface amplitude is
b = sqrt(w) a, so a weight that is zero, negative or non-finite would make the conversion meaningless rather than merely inaccurate, and the refusal therefore belongs to the weight itself rather than to each of its callers.- Parameters:
- modesolprop.modes.protocol.Mode
Physical mode; unused when the index override is supplied.
- omeganumpy.ndarray
Positive angular frequencies in rad/s, shape
(n_omega,). The caller selects the active band, as for the nonlinear prefactor.- zfloat, optional
Position along the waveguide in m.
- indexfloat or None, optional
Positive finite index override. The GNLSE mapping supplies 1.0.
- Returns:
- numpy.ndarray
Dimensionless
Re n_effof shape(n_omega,), positive and finite.
- Raises:
- ModeError
If the power weight is not positive and finite. A mode specified by its dispersion alone has no physical effective index – a Taylor
betagivesRe n_eff = beta c / omega, which vanishes at the reference whenbeta(omega_ref) = 0– so the message names the override.
Examples
>>> import numpy as np >>> from solprop.linear import power_weight >>> from solprop.nonlinear.gnlse import SpecifiedMode >>> from solprop.constants import C >>> mode = SpecifiedMode.from_taylor( ... [1.45 * 1.8e15 / C], omega_ref=1.8e15 ... ) >>> float(power_weight(mode, np.array([1.8e15]), index=1.0)[0]) 1.0