solprop.nonlinear.gnlse#

The GNLSE as a mode-averaged envelope problem.

The generalised nonlinear Schroedinger equation is not a separate solver path in solprop: the field is primary and the GNLSE is an interface mapping. It is the mode-averaged envelope problem with a specified mode (SpecifiedMode) — a mode whose propagation constant is given directly, as a Taylor series, a table, or a declared path of either along z, rather than derived from a waveguide geometry — plus a mapping from the usual GNLSE coefficient gamma onto the third-order susceptibility the Kerr response takes. The shock term is not a separate operator either: it is the omega dependence of the normalisation prefactor (solprop.nonlinear.normalisation).

When Raman is off and no fraction is given the whole of gamma acts as Kerr, so an “N = 6” soliton is N = 6. A fraction given explicitly is always held back from the Kerr term, with Raman on or off, so that switching Raman alone adds the delayed response without changing the instantaneous one.

The derivation and a worked example are in docs/guide/waveguides/gnlse.md.

Module Attributes

DispersionFunction

A propagation constant, or a derivative of one, as a function of frequency.

LossFunction

A power attenuation coefficient in 1/m as a function of angular frequency.

Functions

as_dispersion_path(value)

Coerce a frequency-only dispersion to a path, and pass a path through.

attenuation_from_db(loss_db_per_m)

Convert a power loss in dB/m to a power attenuation coefficient in 1/m.

kerr_susceptibility(n2[, raman_fraction])

Return the Kerr susceptibility of the GNLSE mapping.

problem_from(betas, *, omega_ref[, gamma, ...])

Map GNLSE parameters onto a mode-averaged envelope problem.

raman_susceptibility(n2)

Return the Raman strength of the GNLSE mapping, as an integrated response.

Classes

ConstantDispersion(function)

Adapt a frequency-only dispersion or loss to the path interface.

DispersionPath(*args, **kwargs)

A spectral quantity that follows a declared path along z.

GnlseProblem(mode, kerr, omega0, gamma, n2, ...)

A GNLSE fibre expressed as a mode-averaged envelope problem.

InterpolatedDispersion(positions, members)

beta(omega, z) from declared sets at declared positions, linear in z.

SpecifiedMode(dispersion, ...)

A mode defined by its propagation constant rather than by a waveguide.

TabulatedDispersion(omega, beta[, derivative])

beta(omega), or a derivative of it, from a table, by cubic spline.

TaylorDispersion(betas, omega_ref[, derivative])

beta(omega), or a derivative of it, as a Taylor series about a frequency.

class solprop.nonlinear.gnlse.ConstantDispersion(function)[source]#

Bases: object

Adapt a frequency-only dispersion or loss to the path interface.

The counterpart of solprop.profiles.Constant for a spectral quantity: it ignores z, declares no breakpoints and reports itself constant, so a fibre whose dispersion does not move stays on the exact constant-operator path and its arithmetic is unchanged – the wrapped function is called with exactly the argument it was called with before.

Attributes:
functioncallable

f(omega) returning an array of the same shape.

Examples

>>> import numpy as np
>>> from solprop.nonlinear.gnlse import ConstantDispersion, TaylorDispersion
>>> path = ConstantDispersion(TaylorDispersion([1.0e7], 2.0e15))
>>> path.is_constant, path.breakpoints.size
(True, 0)
>>> float(path(np.array([2.0e15]), 3.0)[0])
10000000.0
property breakpoints: Float[ndarray, 'n_breakpoints'][source]#

Return the empty set of breakpoints.

Returns:
numpy.ndarray

An empty float array: a constant path kinks nowhere.

function: Callable[[Float[ndarray, 'n_omega']], Float[ndarray, 'n_omega']]#
property is_constant: bool[source]#

Return whether the path varies along z.

Returns:
bool

Always True.

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

Return the interval the path covers.

Returns:
tuple of float

(0, inf): a constant path covers any run.

solprop.nonlinear.gnlse.DispersionFunction#

A propagation constant, or a derivative of one, as a function of frequency.

alias of Callable[[Float[ndarray, 'n_omega']], Float[ndarray, 'n_omega']]

class solprop.nonlinear.gnlse.DispersionPath(*args, **kwargs)[source]#

Bases: Protocol

A spectral quantity that follows a declared path along z.

The spectral sibling of solprop.profiles.ZProfile: where a ZProfile carries one scalar along the fibre, this carries a whole frequency axis – beta(omega, z), d beta / d omega (omega, z) or alpha(omega, z). The three members below are the ones the translation layer reads: the breakpoints become the hard kinks the schedule lands on, the span is what a run is checked against, and is_constant is what keeps a fibre that does not move on the exact constant-operator path.

A mode built on one is z-varying by construction, and solprop.translate.linop.build_phi() resolves it along z exactly as it resolves a gas gradient: the operator is sampled at positions, the declared breakpoints are mandatory interval edges, and the Chebyshev integral is exact in between (docs/guide/numerics/linear-operator.md).

Implementations here are ConstantDispersion, which adapts a DispersionFunction of frequency alone, and InterpolatedDispersion, which interpolates declared sets. Anything with these four members is accepted: the protocol is runtime-checkable and as_dispersion_path() is the one coercion.

property breakpoints: Float[ndarray, 'n_breakpoints'][source]#

Return the positions in m where the path kinks; may be empty.

property is_constant: bool[source]#

Return whether the path takes the same value at every position.

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

Return the interval in m the path is declared over, (start, end).

class solprop.nonlinear.gnlse.GnlseProblem(mode, kerr, omega0, gamma, n2, chi3, aeff, raman_fraction, shock, prefactor_index=1.0, raman=None, bundle=None, fraction_origin='', n2_origin='user')[source]#

Bases: object

A GNLSE fibre expressed as a mode-averaged envelope problem.

Everything the translation layer needs to build a kernel problem for the GNLSE, and nothing else: the mode carries the dispersion and the loss, the response carries the nonlinearity, and the two scalars say how the shock term and any future Raman response are configured. The coefficients that went into the mapping are kept so that a run records what it was asked for.

Attributes:
modesolprop.modes.protocol.Mode

The fibre: beta(omega), the loss and the effective area.

kerrsolprop.nonlinear.responses.KerrResponse

The Kerr response, with thg=False: an envelope GNLSE grid carries no third harmonic.

ramansolprop.nonlinear.raman.RamanResponse or None

The delayed response taking raman_fraction of the nonlinearity, or None when Raman is off: the optmat silica bundle’s sampled shape, normalised to raman_susceptibility() of n2 and scaled by its own fraction.

bundlesolprop.physdata.glasses.SilicaBundle or None

The optmat silica bundle the fraction, the shape or n2 came from; None when none was used. Recorded in the run’s material manifest, not in its configuration.

fraction_originstr

Where raman_fraction came from: "user" (given), "bundle" (the bundle’s f_R, Raman on) or "raman-off" (0, Raman off and none given).

n2_originstr

"user" for a given gamma or n2, "bundle" for n2="recommended", the bundle’s total n2 at the carrier.

omega0float

Reference angular frequency the mapping used, rad/s.

gammafloat

Nonlinear coefficient in 1/(W m), n2 omega0 / (c A_eff).

n2float

Nonlinear index in m^2/W.

chi3float

Third-order susceptibility in m^2/V^2, already reduced by the Raman fraction.

aefffloat

Effective area in m^2.

raman_fractionfloat

Share of gamma held back from the Kerr term, between 0 and 1. The Raman response carries it when there is one; with Raman off it is zero unless a fraction was given, and then that share of gamma is not propagated.

shockbool

Whether the normalisation prefactor keeps its omega dependence.

prefactor_indexfloat

Effective index the normalisation prefactor uses, always 1.0. The GNLSE’s beta is a fitted dispersion rather than a physical effective index, so c beta / omega need not be near one and must not be used here. Pass it to solprop.nonlinear.normalisation.uppe_prefactor() as index.

Examples

>>> from solprop.nonlinear.gnlse import problem_from
>>> problem = problem_from([0.0, 5.0e-9, -2.0e-26], omega_ref=1.8e15, gamma=0.1)
>>> f"{problem.gamma:.6f}", problem.raman_fraction, problem.kerr.thg
('0.100000', 0.0, False)
>>> problem.responses
(KerrResponse(chi3=..., thg=False, species=None),)
aeff: float#
bundle: SilicaBundle | None = None#
chi3: float#
fraction_origin: str = ''#
gamma: float#
kerr: KerrResponse#
mode: Mode#
n2: float#
n2_origin: str = 'user'#
omega0: float#
prefactor_index: float = 1.0#
raman: RamanResponse | None = None#
raman_fraction: float#
property responses: tuple[object, ...][source]#

Return the declared responses, in the order the compiler is given them.

Returns:
tuple

The Kerr response, and the Raman response beside it when one was asked for.

shock: bool#
class solprop.nonlinear.gnlse.InterpolatedDispersion(positions, members)[source]#

Bases: object

beta(omega, z) from declared sets at declared positions, linear in z.

The one mechanism a z-varying specified fibre runs through. Each member is an ordinary frequency-only dispersion – a TaylorDispersion expansion set, a TabulatedDispersion column of an n_eff(omega, z) table, or any callable – and between two positions the value is the straight line

beta(omega, z) = (1 - t) beta_i(omega) + t beta_{i+1}(omega), t = (z - z_i) / (z_{i+1} - z_i),

which is exact at every declared position and reproduces the members there bit for bit. Outside the declared range the value is held at the nearest end, as a solprop.profiles.PiecewiseLinear scalar path is: that covers the few per cent of margin the cumulative operator tabulates beyond z_max, while a run longer than the declared range is refused by name by solprop.translate.linop.build_phi() rather than silently extrapolated.

A kink is a declared position and nothing else, which is why the positions are the breakpoints: the linear interpolant is smooth in between, so the adaptive Chebyshev split resolves each section at degree one and the integral of the operator over it is exact.

Equality is by identity (eq=False) because the positions are an array.

Attributes:
positionsnumpy.ndarray

Strictly increasing positions in m, shape (n_positions,), at least two and all finite. Adopted as a read-only float64 array, so that two runs specified the same way record the same positions and serialise alike.

memberstuple of callable

One dispersion per position, in the same order. A sequence is adopted as a tuple.

Raises:
ModeError

If fewer than two positions are given, they are not strictly increasing or finite, or the number of members does not match.

Examples

>>> import numpy as np
>>> from solprop.nonlinear.gnlse import InterpolatedDispersion, TaylorDispersion
>>> entrance = TaylorDispersion([0.0, 5.0e-9], 2.0e15)
>>> exit_set = TaylorDispersion([0.0, 7.0e-9], 2.0e15)
>>> path = InterpolatedDispersion([0.0, 1.0], (entrance, exit_set))
>>> float(path(np.array([3.0e15]), 0.5)[0])
6000000.0
property breakpoints: Float[ndarray, 'n_breakpoints'][source]#

Return the declared positions, every one of which is a kink.

Returns:
numpy.ndarray

The positions in m, shape (n_positions,).

property is_constant: bool[source]#

Return whether every position declares the same set.

Returns:
bool

True only when one object was declared at every position, which is the conservative test an arbitrary callable admits.

members: tuple[Callable[[Float[ndarray, 'n_omega']], Float[ndarray, 'n_omega']], ...] | Sequence[Callable[[Float[ndarray, 'n_omega']], Float[ndarray, 'n_omega']]]#
positions: Float[ndarray, 'n_positions'] | Sequence[float]#
property z_span: tuple[float, float][source]#

Return the interval the declared sets cover.

Returns:
tuple of float

(first position, last position) in m.

solprop.nonlinear.gnlse.LossFunction#

A power attenuation coefficient in 1/m as a function of angular frequency.

alias of Callable[[Float[ndarray, 'n_omega']], Float[ndarray, 'n_omega']]

class solprop.nonlinear.gnlse.SpecifiedMode(dispersion, dispersion_derivative, loss, effective_area)[source]#

Bases: object

A mode defined by its propagation constant rather than by a waveguide.

This is the GNLSE’s “fibre”: beta(omega) given as data, a power loss, and an effective area. It implements solprop.modes.protocol.Mode in full, so the linear operator, the normalisation and the mode-averaged transform treat it exactly like a capillary mode and the GNLSE needs no special path (see docs/dev/decisions.md).

Build one with from_taylor() or from_table() for a fibre that does not move, and with from_taylor_path(), from_index_path() or from_mode() for one that does; the constructor takes the resolved functions and is the escape hatch for an arbitrary analytic dispersion.

Two paths along z, declared together. The dispersion, its derivative and the loss are each either a function of frequency alone or a DispersionPath; the effective area is either a number or a solprop.profiles.ZProfile. A fibre whose dispersion moves must declare an area that moves with it, at the same declared positions – no real waveguide changes one without the other – and the area-only case, a taper whose dispersion is held fixed, stays as the documented constant-dispersion special case. The z dependence then goes through the one mechanism a gas gradient goes through: the declared positions are the hard kinks, the cumulative operator resolves D(omega, z) between them and integrates it exactly (docs/guide/numerics/linear-operator.md).

The transverse profile is a uniform disc of area effective_area, which is the profile whose effective area is exactly that area, so the single-node mode-averaged transform is well defined and agrees with aeff().

Attributes:
dispersioncallable or DispersionPath

beta(omega) in 1/m, taking and returning an array of shape (n_omega,), or a declared path beta(omega, z).

dispersion_derivativecallable or DispersionPath

d beta / d omega in s/m, analytic, same shapes; a path when dispersion is one, declared at the same positions.

lossfloat or callable or DispersionPath

Power attenuation in 1/m: a constant, a function of omega with the same shapes, or a declared path. Convert a figure in dB/m with attenuation_from_db().

effective_areafloat or ZProfile

A_eff in m^2, positive: a constant, or a declared path along the fibre. A path makes the mode z-varying, and the area then enters exactly once, as the shape of the nonlinear coefficient: the mode-averaged transform tabulates sqrt(A_eff(0) / A_eff(z)) and the response prefactor keeps the entrance area, so gamma(z) = gamma A_eff(0) / A_eff(z) with the supplied gamma as the entrance coefficient.

Raises:
ModeError

If the effective area is not positive and finite anywhere it is declared, if the dispersion moves without the area, if the value and the derivative disagree about whether they move, or if the dispersion and the area are declared at different positions.

Examples

>>> import numpy as np
>>> from solprop.nonlinear.gnlse import SpecifiedMode
>>> mode = SpecifiedMode.from_taylor(
...     [1.0e7, 5.0e-9, -2.0e-26], omega_ref=2.0e15, aeff=1e-10
... )
>>> f"{float(mode.beta(np.array([2.0e15]))[0]):.6e}"
'1.000000e+07'
>>> mode.aeff()
1e-10
aeff(z=0.0)[source]#

Return the effective area at a position.

Parameters:
zfloat, optional

Position along the waveguide, m; it is the declared area path that is evaluated, and a constant area ignores it.

Returns:
float

A_eff(z) in m^2.

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

Return the power attenuation coefficient.

Parameters:
omeganumpy.ndarray

Angular frequencies, rad/s, shape (n_omega,).

zfloat, optional

Position along the waveguide, m. It reaches the declared loss path; a constant or a function of frequency alone ignores it.

Returns:
numpy.ndarray

alpha(omega, z) in 1/m, shape (n_omega,), so that the power falls as exp(-alpha z).

Examples

>>> import numpy as np
>>> from solprop.nonlinear.gnlse import (
...     SpecifiedMode,
...     attenuation_from_db,
... )
>>> mode = SpecifiedMode.from_taylor(
...     [1.0e7], omega_ref=2.0e15, loss=attenuation_from_db(10.0)
... )
>>> f"{mode.alpha(np.array([2.0e15]))[0]:.6f}"
'2.302585'
property area_path: ZProfile[source]#

Return the declared effective-area path in m^2.

Returns:
solprop.profiles.ZProfile

The path, a Constant when the area is a number. It is the path the transform’s kink walk and solprop.translate.linop.declared_paths() take their breakpoints from, so that the tables split there and the schedule lands on them.

at_parameters(values)[source]#

Return the same fibre with its area frozen at a value.

Parameters:
valuescollections.abc.Mapping

Any subset of {"effective_area"}, in m^2 and positive. A name left out keeps its path; a name this mode does not declare is an error.

Returns:
SpecifiedMode

The same dispersion and loss with a constant effective area.

Raises:
ModeError

If a name is not declared, the value is not a positive area, or the dispersion follows a path along z – there is then no parameter value that reproduces this mode, which is what parameters_own_z says in advance.

property azimuthal_order: int[source]#

Highest azimuthal order of the profile.

Returns:
int

Always 0: the disc is azimuthally uniform.

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

Return the propagation constant.

Unlike neff() this is defined at zero frequency, so it is the right entry point for anything that works over a whole field grid.

Parameters:
omeganumpy.ndarray

Angular frequencies, rad/s, shape (n_omega,).

zfloat, optional

Position along the waveguide, m. It reaches the declared dispersion path; a dispersion given as a function of frequency alone ignores it.

Returns:
numpy.ndarray

beta(omega, z) in 1/m, shape (n_omega,).

Examples

>>> import numpy as np
>>> from solprop.nonlinear.gnlse import SpecifiedMode
>>> mode = SpecifiedMode.from_taylor([1.0e7], omega_ref=2.0e15)
>>> mode.beta(np.array([0.0, 3.0e15])).tolist()
[10000000.0, 10000000.0]
property channel_label: ChannelLabel[source]#

Name this channel: a dispersion, not an eigenmode of a guide.

Returns:
ChannelLabel

Family specified, with no azimuthal or radial order, no parity and no handedness: a mode given by beta(omega) has no quantum numbers.

Examples

>>> from solprop.nonlinear.gnlse import SpecifiedMode
>>> mode = SpecifiedMode.from_taylor([1.0e7], omega_ref=2.0e15)
>>> mode.channel_label.display_label
'specified'
property derivative_path: DispersionPath[source]#

Return d beta / d omega as a path along z.

Returns:
DispersionPath

The declared path, a ConstantDispersion when the derivative is a function of frequency alone.

dimlimits(z=0.0)[source]#

Return the cross-section parametrisation and the integration limits.

Parameters:
zfloat, optional

Position along the waveguide, m; the surrogate disc follows the area.

Returns:
tuple

("polar", (0.0, 0.0), (radius(z), 2 pi)) in m and rad.

dispersion: Callable[[Float[ndarray, 'n_omega']], Float[ndarray, 'n_omega']] | DispersionPath#
dispersion_derivative: Callable[[Float[ndarray, 'n_omega']], Float[ndarray, 'n_omega']] | DispersionPath#
property dispersion_is_constant: bool[source]#

Whether the spectral quantities are functions of frequency alone.

Returns:
bool

True when the dispersion, its derivative and the loss all report themselves constant along z.

Examples

>>> from solprop.nonlinear.gnlse import SpecifiedMode
>>> SpecifiedMode.from_taylor([1.0e7], omega_ref=2.0e15).dispersion_is_constant
True
property dispersion_path: DispersionPath[source]#

Return the propagation constant as a path along z.

Returns:
DispersionPath

The declared path, a ConstantDispersion when the dispersion is a function of frequency alone. Its breakpoints are kinks of the cumulative operator exactly as the area path’s are.

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

Return the analytic frequency derivative of the effective index.

Differentiating n_eff = c (beta + i alpha / 2) / omega gives (c beta' - n_eff) / omega plus i c alpha' / (2 omega). The second term is zero for a constant loss and is dropped for a callable one, which is the only approximation in this class: the co-moving frame quantities take the real part, where the loss never appears, so beta_0 and beta_1 are unaffected either way.

Parameters:
omeganumpy.ndarray

Angular frequencies, rad/s, shape (n_omega,), strictly positive.

zfloat, optional

Position along the waveguide, m. It reaches the declared dispersion and loss paths; quantities given as functions of frequency alone ignore it.

Returns:
numpy.ndarray

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

Raises:
ModeError

If any frequency is not strictly positive.

Examples

>>> import numpy as np
>>> from solprop.constants import C
>>> from solprop.nonlinear.gnlse import SpecifiedMode
>>> flat = SpecifiedMode.from_taylor(
...     [1.5 * 2.0e15 / C, 1.5 / C], omega_ref=2.0e15
... )
>>> bool(np.allclose(flat.dneff_domega(np.array([2.0e15])), 0.0, atol=1e-28))
True
effective_area: float | ZProfile#
field(points, z=0.0)[source]#

Return the transverse field components at points of the cross-section.

A uniform disc polarised along x: one inside the radius, zero outside. Its effective area (int |e|^2 dA)^2 / int |e|^4 dA is the disc’s area, which is aeff() by construction, so the single-node mode-averaged transform is consistent with the normalisation.

Parameters:
pointsnumpy.ndarray

(r, theta) in m and rad, shape (n_points, 2).

zfloat, optional

Position along the waveguide, m; the disc follows the declared area.

Returns:
numpy.ndarray

(E_x, E_y) of the unnormalised profile, shape (n_points, 2), real.

Examples

>>> import numpy as np
>>> from solprop.nonlinear.gnlse import SpecifiedMode
>>> mode = SpecifiedMode.from_taylor([1.0], omega_ref=1.0, aeff=np.pi)
>>> points = np.array([[0.5, 0.0], [1.5, 0.0]])
>>> mode.field(points).tolist()
[[1.0, 0.0], [0.0, 0.0]]
classmethod from_index_path(omega, neff, positions, *, aeff)[source]#

Build a fibre from an n_eff(omega, z) table and its area path.

The table’s real part is the propagation constant beta = omega Re n_eff / c and its imaginary part the power attenuation alpha = 2 omega Im n_eff / c (docs/dev/conventions.md section 2, Im n_eff > 0 is loss). Each row is one position’s spectrum, a cubic spline in frequency differentiated exactly in the spline basis, and the rows are interpolated linearly in z.

The frequency axis is shared by every position, and it must span the run’s active band: a spline evaluated outside the table it was built from is refused by name rather than extrapolated, which is the classic way to get a plausible, wrong answer.

Parameters:
omeganumpy.ndarray

Strictly increasing angular frequencies, rad/s, shape (n_nodes,), at least solprop.modes.tabulated.MIN_TABLE_POINTS of them.

neffnumpy.ndarray

Complex effective index at each position and frequency, shape (n_positions, n_nodes).

positionsarray_like

Strictly increasing positions in m, shape (n_positions,), at least two.

aeffarray_like or ZProfile

Effective area in m^2: one positive value per position, or a declared path whose breakpoints are exactly positions.

Returns:
SpecifiedMode

The mode.

Raises:
ModeError

If the table’s shape does not match the axes, fewer than two positions are given, or the two paths are declared at different positions.

Examples

>>> import numpy as np
>>> from solprop.constants import C
>>> from solprop.nonlinear.gnlse import SpecifiedMode
>>> omega = np.linspace(1.0e15, 3.0e15, 8)
>>> index = np.stack([np.full(8, 1.45 + 0j), np.full(8, 1.46 + 0j)])
>>> mode = SpecifiedMode.from_index_path(
...     omega, index, [0.0, 1.0], aeff=[1e-10, 2e-10]
... )
>>> f"{float(mode.neff(np.array([2.0e15]), 0.5).real[0]):.6f}"
'1.455000'
classmethod from_index_table(table, *, aeff=1.0)[source]#

Build a dispersion-specified mode from a complex-index table.

Parameters:
tablesolprop.modes.tabulated.IndexTable

Whole effective index and loss, with finite spectral support.

aefffloat or ZProfile, optional

Positive propagation area in m^2, or a declared path along the fibre. The driver selects the table’s area at its reference wavelength; default 1.0 is the gamma convention.

Returns:
SpecifiedMode

Mode with analytic real-index derivative and reproducible callables.

Raises:
ModeError

If the propagation area is not positive.

classmethod from_mode(mode, *, aeff=None, wavelength=None)[source]#

Use a mode’s dispersion, loss and area for a GNLSE fibre.

Its fill contributes to the supplied dispersion and loss, but is ignored as a nonlinear material. The GNLSE caller supplies gamma or n2 instead. Frozen callables retain the original mode for reproducible records.

A source whose geometry moves hands over both paths. The frozen callables forward the position to the source mode, so its dispersion, its derivative and its loss are evaluated where they are asked for rather than frozen at the entrance, and the effective area is sampled from the same mode over the same declared positions – the two come together, so a moving source is accepted rather than refused. A source that does not move keeps the ordinary constant path, bit for bit.

A provider channel’s area belongs to a frequency. A ProviderChannelMode’s own area is the surrogate at the median knot of the band its guide was sampled on, which is not in general the pump: on worked example 05’s graded-index taper it is 35 % above the pump’s. With wavelength, the area is solved again at that wavelength instead (area_at()) – once for a snapshot, and at every sampled position of a moving guide’s path, so that a moving channel hands over an area path at the pump. With neither aeff nor wavelength, the median-knot surrogate is used and a SurrogateAreaWarning says so. solprop.driver.interface.prop_gnlse() passes its lambda0.

Parameters:
modesolprop.modes.protocol.Mode

Source mode.

aefffloat or ZProfile or None, optional

Positive finite effective area in m^2, or a declared path. None takes the source’s declared area path if it has one, samples the source’s own aeff along z when its geometry moves, and otherwise mode.aeff() – or, for a provider channel given wavelength, the channel’s area solved at that wavelength.

wavelengthfloat or None, optional

Positive wavelength in m the area of a provider channel is taken at, normally the pump’s; read only when aeff is None. Every other mode’s area is not a function of frequency in the Mode protocol – it is declared once, as TabulatedMode’s is at its own reference_wavelength – so there it is accepted and ignored, which is what lets solprop.driver.interface.prop_gnlse() pass its lambda0 for any fibre.

Returns:
SpecifiedMode

GNLSE mode with an analytic first dispersion derivative.

Raises:
ModeError

If the source’s geometry moves and a constant area is asked for, if a moving source declares no path saying where it moves, if the area is not positive and finite, if wavelength is not positive and finite, or if a provider channel asked for its area at wavelength carries no provider to solve.

Warns:
SurrogateAreaWarning

For a provider channel given neither aeff nor wavelength.

classmethod from_table(omega, beta, *, loss=0.0, aeff=1.0)[source]#

Build a mode from a tabulated propagation constant.

The table is interpolated by a cubic spline and differentiated exactly in the spline basis, so dneff_domega stays analytic, as the co-moving frame requires. Evaluation outside the tabulated range raises rather than extrapolating.

Parameters:
omeganumpy.ndarray

Strictly increasing angular frequencies, rad/s, shape (n_nodes,), at least four.

betanumpy.ndarray

Propagation constant at those frequencies, 1/m, shape (n_nodes,).

lossfloat or callable, optional

Power attenuation in 1/m; a constant or a function of omega. Default 0.

aefffloat or ZProfile, optional

Effective area in m^2, or a declared path along the fibre. Default 1.0.

Returns:
SpecifiedMode

The mode.

Raises:
ModeError

If the two arrays disagree in shape, the table is shorter than four points, the frequencies are not strictly increasing, or aeff is not positive.

Examples

>>> import numpy as np
>>> from solprop.nonlinear.gnlse import SpecifiedMode
>>> omega = np.linspace(1.0e15, 3.0e15, 8)
>>> mode = SpecifiedMode.from_table(omega, 5.0e-9 * omega)
>>> f"{float(mode.beta(np.array([2.0e15]))[0]):.6f}"
'10000000.000000'
classmethod from_taylor(betas, omega_ref, *, loss=0.0, aeff=1.0)[source]#

Build a mode from Taylor coefficients of beta about a frequency.

beta(omega) = sum_n betas[n] (omega - omega_ref)**n / n!. There is no constructor that centres the series at zero frequency, because a high-order Taylor polynomial extrapolated from DC to the optical band is meaningless.

Parameters:
betasarray_like

Taylor coefficients beta_0, beta_1, ... in s^n/m, at least one.

omega_reffloat

Frequency the series is expanded about, rad/s, positive.

lossfloat or callable, optional

Power attenuation in 1/m; a constant or a function of omega. Default 0.

aefffloat or ZProfile, optional

Effective area in m^2, or a declared path along the fibre. Default 1.0, which is the GNLSE convention in which the area has been folded into n2 already.

Returns:
SpecifiedMode

The mode.

Raises:
ModeError

If no coefficients are given, omega_ref is not positive, or aeff is not positive.

Examples

>>> import numpy as np
>>> from solprop.nonlinear.gnlse import SpecifiedMode
>>> mode = SpecifiedMode.from_taylor([0.0, 5.0e-9], omega_ref=2.0e15)
>>> f"{float(mode.beta(np.array([3.0e15]))[0]):.6f}"
'5000000.000000'
classmethod from_taylor_path(betas, positions, omega_ref, *, aeff, loss=0.0)[source]#

Build a fibre whose Taylor dispersion and area are declared along z.

One expansion set and one effective area per declared position, both interpolated linearly in between: the two are sampled together, which is what makes the model consistent. Between two positions the propagation constant is the straight line between the two sets, so the cumulative operator resolves each section at degree one in z and integrates it exactly, and the declared positions are hard kinks the schedule lands on.

Parameters:
betasarray_like

Taylor coefficients beta_0, beta_1, ... in s^n/m at each position, shape (n_positions, n_deg). Every row carries the same number of coefficients.

positionsarray_like

Strictly increasing positions in m, shape (n_positions,), at least two. They must cover the run, which solprop.translate.linop.build_phi() checks by name.

omega_reffloat

Frequency every series is expanded about, rad/s, positive.

aeffarray_like or ZProfile

Effective area in m^2: one positive value per position, or a declared path whose breakpoints are exactly positions.

lossfloat or callable or DispersionPath, optional

Power attenuation in 1/m. Default 0.

Returns:
SpecifiedMode

The mode, with an analytic first dispersion derivative at every position.

Raises:
ModeError

If the rows and the positions disagree, fewer than two positions are given, the areas are not positive, or the two paths are declared at different positions.

Examples

>>> import numpy as np
>>> from solprop.nonlinear.gnlse import SpecifiedMode
>>> mode = SpecifiedMode.from_taylor_path(
...     [[0.0, 5.0e-9], [0.0, 7.0e-9]],
...     [0.0, 1.0],
...     omega_ref=2.0e15,
...     aeff=[1e-10, 2e-10],
... )
>>> f"{float(mode.beta(np.array([3.0e15]), 0.5)[0]):.6f}"
'6000000.000000'
>>> mode.z_constant
False
loss: float | Callable[[Float[ndarray, 'n_omega']], Float[ndarray, 'n_omega']] | DispersionPath#
property loss_path: DispersionPath[source]#

Return the power attenuation as a path along z.

Returns:
DispersionPath

The declared path; a constant number becomes a uniform attenuation at every frequency and every position.

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

Return the complex effective index.

n_eff = c (beta + i alpha / 2) / omega, the inverse of the beta and alpha definitions in docs/dev/conventions.md section 2, so that Im n_eff > 0 is loss.

Parameters:
omeganumpy.ndarray

Angular frequencies, rad/s, shape (n_omega,), strictly positive.

zfloat, optional

Position along the waveguide, m. It reaches the declared dispersion and loss paths; quantities given as functions of frequency alone ignore it.

Returns:
numpy.ndarray

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

Raises:
ModeError

If any frequency is not strictly positive. A specified mode has no effective index at zero frequency. Evaluate on the grid’s active band instead.

Examples

>>> import numpy as np
>>> from solprop.nonlinear.gnlse import SpecifiedMode
>>> mode = SpecifiedMode.from_taylor(
...     [1.0e7, 5.0e-9], omega_ref=2.0e15
... )
>>> f"{mode.neff(np.array([2.0e15]))[0].real:.6f}"
'1.498962'
neff_minus_one(omega, z=0.0)[source]#

Return n_eff - 1 formed without cancellation.

(c (beta + i alpha / 2) - omega) / omega: the subtraction happens on quantities of the same size rather than after a division that lands near one.

Parameters:
omeganumpy.ndarray

Angular frequencies, rad/s, shape (n_omega,), strictly positive.

zfloat, optional

Position along the waveguide, m. It reaches the declared dispersion and loss paths; quantities given as functions of frequency alone ignore it.

Returns:
numpy.ndarray

n_eff - 1, complex, shape (n_omega,).

Raises:
ModeError

If any frequency is not strictly positive.

Examples

>>> import numpy as np
>>> from solprop.constants import C
>>> from solprop.nonlinear.gnlse import SpecifiedMode
>>> mode = SpecifiedMode.from_taylor(
...     [1.45 * 2.0e15 / C], omega_ref=2.0e15
... )
>>> f"{mode.neff_minus_one(np.array([2.0e15]))[0].real:.6f}"
'0.450000'
property parameter_paths: Mapping[str, ZProfile][source]#

Return the scalar path this mode’s z dependence runs through.

Returns:
collections.abc.Mapping

{"effective_area": path} in m^2.

Notes

When the dispersion is a function of frequency alone – the documented special case, a taper whose dispersion is held fixed – the declared area is the whole of the z dependence and solprop.modes.protocol.ParameterisedMode holds exactly: the linear operator is expanded in a parameter it does not depend on, at degree zero, and the path’s breakpoints become the hard kinks the schedule lands on.

When the dispersion follows a path the area is no longer the whole of it, and at_parameters() cannot return a z-constant mode. The declaration is still the mode’s kinks, which is what solprop.translate.linop.declared_paths() reads it for; the expansion is not taken, because solprop.translate.linop.parameter_paths() checks that freezing a mode’s parameters leaves it z-constant and falls back to the z table when it does not.

Examples

>>> from solprop.nonlinear.gnlse import SpecifiedMode
>>> from solprop.profiles import Linear
>>> mode = SpecifiedMode.from_taylor(
...     [0.0], omega_ref=2.0e15, aeff=Linear(1e-10, 2e-10, 1.0)
... )
>>> sorted(mode.parameter_paths)
['effective_area']
property parameters_own_z: bool[source]#

Whether the declared parameters are the whole of this mode’s z dependence.

The trait the translation layer reads before it expands the cumulative operator in a mode’s parameters instead of tabulating it along z. It is True for the constant-dispersion special case, where the declared area is everything, and False once the dispersion follows a path, which no scalar parameter can be frozen at.

Returns:
bool

dispersion_is_constant.

Examples

>>> from solprop.nonlinear.gnlse import SpecifiedMode
>>> SpecifiedMode.from_taylor([0.0], omega_ref=2.0e15).parameters_own_z
True
principal_component(z=0.0)[source]#

Return the polarisation of the uniform surrogate profile.

Parameters:
zfloat, optional

Position along the fibre, m; ignored by the uniform profile.

Returns:
{‘x’}

The sole nonzero Cartesian component.

radius(z=0.0)[source]#

Return the radius of the equivalent uniform disc.

Parameters:
zfloat, optional

Position along the waveguide, m.

Returns:
float

sqrt(A_eff(z) / pi) in m, the radius of the disc whose effective area is A_eff(z).

property scale_invariant: bool[source]#

Whether the profile scales exactly with a core radius along z.

Returns:
bool

Always True: the surrogate profile is a disc of radius sqrt(A_eff(z) / pi), which is exactly f(r / a(z)).

property z_constant: bool[source]#

Whether the mode is independent of z.

Returns:
bool

True when the effective area, the dispersion, its derivative and the loss are all constant along z.

class solprop.nonlinear.gnlse.TabulatedDispersion(omega, beta, derivative=0)[source]#

Bases: object

beta(omega), or a derivative of it, from a table, by cubic spline.

A cubic spline through the table, differentiated exactly in the spline basis, as the co-moving frame requires. Extrapolation is refused rather than silently produced: a spline dispersion evaluated outside the data it was built from is the classic way to get a plausible, wrong answer.

Callable and frozen for the reason TaylorDispersion gives: a mode built from it records its table, not a closure’s memory address, so a run can be resumed from its own file in another process.

Equality is by identity (eq=False) because the table is an array.

Attributes:
omeganumpy.ndarray

Strictly increasing angular frequencies, rad/s, shape (n_nodes,), at least solprop.modes.tabulated.MIN_TABLE_POINTS of them.

betanumpy.ndarray

Propagation constant at those frequencies, 1/m, the same shape. The two arrays carry independent axis names on purpose: their agreement is what __post_init__ checks, and that check must stay reachable in production, where the test session’s runtime type checker is absent.

derivativeint

How many times to differentiate before evaluating: 0 for beta itself, 1 for d beta / d omega. Non-negative.

splinescipy.interpolate.CubicSpline

The differentiated spline, built once in __post_init__. Derived, so it is neither a constructor argument nor part of what a run records.

Raises:
ModeError

If the two arrays disagree in shape, the table is too short, its frequencies are not strictly increasing, or derivative is negative.

Examples

>>> import numpy as np
>>> from solprop.nonlinear.gnlse import TabulatedDispersion
>>> omega = np.linspace(1.0e15, 3.0e15, 8)
>>> beta = TabulatedDispersion(omega, 5.0e-9 * omega)
>>> f"{float(beta(np.array([2.0e15]))[0]):.6f}"
'10000000.000000'
beta: Float[ndarray, 'n_beta_nodes']#
derivative: int = 0#
omega: Float[ndarray, 'n_nodes']#
spline: CubicSpline#
class solprop.nonlinear.gnlse.TaylorDispersion(betas, omega_ref, derivative=0)[source]#

Bases: object

beta(omega), or a derivative of it, as a Taylor series about a frequency.

beta(omega) = sum_n betas[n] (omega - omega_ref)**n / n!, so betas[n] is the n-th derivative of beta at the reference frequency and betas[1] is the inverse group velocity there. Differentiation is exact, in the polynomial basis, never by differencing, as the co-moving frame requires.

The instance is callable and stands where a closure would. That is what makes a mode built from it reproducible in a run’s metadata: solprop.output.schema.serialise() records a closure as its repr, which carries the function’s memory address, so a configuration holding one hashes differently in every process and the run could never be resumed from its own file. A frozen dataclass records its fields instead.

Equality is by identity (eq=False) because betas is an array.

Attributes:
betasnumpy.ndarray

Taylor coefficients beta_0, beta_1, ... in s^n/m, shape (n_deg,), at least one and all finite. A sequence is adopted as a read-only float64 array, so that two runs specified the same way record the same coefficients whichever type they passed and therefore hash alike.

omega_reffloat

Frequency the series is expanded about, rad/s, positive.

derivativeint

How many times to differentiate before evaluating: 0 for beta itself, 1 for d beta / d omega. Non-negative.

polynomialnumpy.polynomial.Polynomial

The differentiated series in the shifted variable omega - omega_ref, built once in __post_init__. Derived, so it is neither a constructor argument nor part of what a run records.

Raises:
ModeError

If no coefficients are given, one is not finite, omega_ref is not positive, or derivative is negative.

Examples

>>> import numpy as np
>>> from solprop.nonlinear.gnlse import TaylorDispersion
>>> beta = TaylorDispersion([1.0e7, 5.0e-9, -2.0e-26], 2.0e15)
>>> f"{float(beta(np.array([2.0e15]))[0]):.6e}"
'1.000000e+07'
>>> gradient = TaylorDispersion([1.0e7, 5.0e-9], 2.0e15, 1)
>>> f"{float(gradient(np.array([3.0e15]))[0]):.1e}"
'5.0e-09'
betas: Float[ndarray, 'n_deg'] | Sequence[float]#
derivative: int = 0#
omega_ref: float#
polynomial: Polynomial#
solprop.nonlinear.gnlse.as_dispersion_path(value)[source]#

Coerce a frequency-only dispersion to a path, and pass a path through.

The spectral sibling of solprop.profiles.as_profile(), and the one coercion: a SpecifiedMode asks for its dispersion, its derivative and its loss through this, so that every one of them answers the same four questions whether it moves along z or not.

Parameters:
valuecallable or DispersionPath

A function of frequency alone, or a declared path already.

Returns:
DispersionPath

The path, a ConstantDispersion when the argument is a plain function.

Raises:
ModeError

If a solprop.profiles.ZProfile is given. Its members are a superset of this protocol’s, so it passes the structural test and would then fail inside SpecifiedMode.beta() with a raw TypeError; the two are different things and the refusal belongs at the boundary.

Examples

>>> from solprop.nonlinear.gnlse import TaylorDispersion, as_dispersion_path
>>> as_dispersion_path(TaylorDispersion([1.0e7], 2.0e15)).is_constant
True
solprop.nonlinear.gnlse.attenuation_from_db(loss_db_per_m)[source]#

Convert a power loss in dB/m to a power attenuation coefficient in 1/m.

alpha = ln(10)/10 * loss, so that the power falls as exp(-alpha z) and the field amplitude as exp(-alpha z / 2), which is the -alpha/2 of the linear operator (docs/dev/conventions.md section 2).

Parameters:
loss_db_per_mfloat

Power loss in dB/m. Negative values describe gain and are permitted.

Returns:
float

Power attenuation coefficient in 1/m.

Examples

>>> from solprop.nonlinear.gnlse import attenuation_from_db
>>> f"{attenuation_from_db(10.0):.6f}"
'2.302585'
solprop.nonlinear.gnlse.kerr_susceptibility(n2, raman_fraction=0.0)[source]#

Return the Kerr susceptibility of the GNLSE mapping.

chi3 = (4/3) (1 - f_R) n2 eps0 c: the 4/3 undoes the 3/4 of the envelope Kerr response, and the linear index is taken as one, which is solprop.physdata.nonlinear_data.chi3_from_n2() with a unit index. raman_fraction is the share of the nonlinearity a Raman response carries; problem_from() passes zero when Raman is off, and the share it does pass goes to a response of integral raman_susceptibility() times that fraction, so that the two together carry the whole of gamma.

Parameters:
n2float

Nonlinear index in m^2/W.

raman_fractionfloat, optional

Share of the nonlinearity reserved for Raman, in [0, 1]. Default 0.

Returns:
float

Third-order susceptibility in m^2/V^2.

Raises:
ModeError

If raman_fraction lies outside [0, 1].

Examples

>>> from solprop.nonlinear.gnlse import kerr_susceptibility
>>> f"{kerr_susceptibility(2.6e-20):.6e}"
'9.201985e-23'
>>> f"{kerr_susceptibility(2.6e-20, 0.18) / kerr_susceptibility(2.6e-20):.2f}"
'0.82'
solprop.nonlinear.gnlse.problem_from(betas, *, omega_ref, gamma=None, n2=None, aeff=1.0, loss_db_per_m=0.0, raman=False, raman_fraction=None, shock=True, selection=None)[source]#

Map GNLSE parameters onto a mode-averaged envelope problem.

The mapping:

  • k0 = omega_ref / c and n2 = gamma A_eff / k0, with A_eff = 1 unless one is given, so that gamma = n2 omega_ref / (c A_eff) either way;

  • chi3 = (4/3) (1 - f_R) n2 eps0 c, the 4/3 undoing the 3/4 of the envelope Kerr response and the linear index taken as one;

  • with raman=True, a fused-silica solprop.nonlinear.raman.RamanResponse of integral raman_susceptibility() and fraction f_R beside it, so that the two together carry the whole of gamma;

  • the linear operator sees beta(omega) directly and the prefactor sees n_eff = 1, which follows from n_eff = c beta / omega only in the sense that the GNLSE’s beta already contains the whole dispersion;

  • the shock term is the omega / omega_ref dependence of that prefactor rather than a time derivative, so shock=False simply freezes it.

With raman=False and no raman_fraction the (1 - f_R) factor is not applied, so none of the Kerr nonlinearity is discarded: the textbook “no Raman”, f_R = 0 at fixed gamma. With raman=False and an explicit raman_fraction the factor is applied and nothing carries the rest, so a pair of runs that gives the same fraction and switches only raman differs by the delayed response alone.

Parameters:
betasarray_like

Taylor coefficients of beta about omega_ref, beta_0, beta_1, ... in s^n/m. beta_0 and beta_1 only shift the frame and do not change the physics.

omega_reffloat

Reference angular frequency, rad/s, positive. Both the Taylor expansion and the gamma mapping refer to it.

gammafloat or None, optional

Nonlinear coefficient in 1/(W m). Give this or n2, not both.

n2float, str or None, optional

Nonlinear index in m^2/W. Give this or gamma, not both. "recommended" takes the silica bundle’s total long-pulse n2 (optmat’s kerr/SiO2) at the carrier 2 pi c / omega_ref.

aefffloat, optional

Effective area in m^2. Default 1.0, the GNLSE convention in which the area is already folded into gamma.

loss_db_per_mfloat, optional

Power loss in dB/m, converted by attenuation_from_db(). Default 0.

ramanbool, optional

Whether a Raman response takes the share raman_fraction of gamma. Default False.

raman_fractionfloat or None, optional

Share of gamma held back from the Kerr term, between 0 and 1. None, the default, is the silica bundle’s own f_R with raman true – 0.2188 for optmat’s default bundle (SEP 3, D4) – and 0 with it false, which gives the whole of gamma to the Kerr term. A value given is always held back, with raman true or false, and overrides the bundle’s.

shockbool, optional

Keep the omega dependence of the normalisation prefactor. Default True.

selectionsolprop.physdata.selection.MaterialSelection or None, optional

None is MaterialSelection(). Fused silica’s nonlinear response is optmat’s silica bundle (SEP 3, section 4.5): the bundle named by selection.raman ("recommended" is the "default" bundle, Hollenbeck and Cantrell’s shape with the Kerr fit’s f_R; another name is an optmat bundle name, an object is taken as it is), its f_R for an omitted fraction with Raman on, its normalised h_R sampled as the Raman response, and its n2 for n2="recommended".

Returns:
GnlseProblem

The mode, the Kerr response and the coefficients they came from.

Raises:
ModeError

If neither or both of gamma and n2 are given, if raman_fraction is outside [0, 1], if omega_ref or aeff is not positive, if raman is true with a zero fraction, or if n2 is a string other than "recommended".

Notes

raman=True with raman_fraction=0 raises rather than returning a problem whose Raman response carries none of the nonlinearity: the response itself refuses a fraction outside (0, 1], and a run that declared a term contributing nothing would pay for a convolution at every stage and be indistinguishable from one that did not declare it.

Examples

>>> from solprop.nonlinear.gnlse import problem_from
>>> problem = problem_from([0.0, 5.0e-9], omega_ref=1.8e15, gamma=0.1, aeff=1.0)
>>> f"{problem.n2:.6e}", f"{problem.chi3:.6e}"
('1.665514e-08', '5.894628e-11')
>>> raman = problem_from([0.0, 5.0e-9], omega_ref=1.8e15, gamma=0.1, raman=True)
>>> f"{raman.chi3 / problem.chi3:.2f}", f"{raman.raman_fraction:.4f}"
('0.78', '0.2188')
>>> held = problem_from(
...     [0.0, 5.0e-9], omega_ref=1.8e15, gamma=0.1, raman_fraction=0.18
... )
>>> f"{held.chi3 / problem.chi3:.2f}", held.raman is None
('0.82', True)
solprop.nonlinear.gnlse.raman_susceptibility(n2)[source]#

Return the Raman strength of the GNLSE mapping, as an integrated response.

A Raman response is stated by its integral: integral h dt carries the units of solprop.nonlinear.responses.KerrResponse.chi3 per unit density (docs/guide/media/raman-numerics.md), so it is directly comparable with the Kerr susceptibility. The two are not equal for the same share of the nonlinearity, because the envelope forms differ by their numerical factors:

\[P_\text{Kerr} = \tfrac34\,\varepsilon_0\chi^{(3)}\lvert A\rvert^2 A, \qquad P_\text{Raman} \to \tfrac12\,\varepsilon_0 \left(\textstyle\int h\,\mathrm{d}t\right)\lvert A\rvert^2 A\]

in the slowly varying limit, so a Raman term of integral X acts like a Kerr term of (2/3) X and carrying the whole nonlinearity needs X = (3/2) chi3_total = 2 n2 eps0 c. A response built with this amplitude and a fraction of f_R therefore takes exactly the share kerr_susceptibility() leaves behind, and

(2/3) f_R raman_susceptibility(n2) + kerr_susceptibility(n2, f_R) == kerr_susceptibility(n2)

holds by construction, which is a test. Because the response function here is linear in eps0, no further eps0 appears; a convention with a response quadratic in eps0 writes the same number as chi3R = 2 f_R n2 eps0 c.

Parameters:
n2float

Nonlinear index in m^2/W.

Returns:
float

The value integral h dt takes when the whole nonlinearity is Raman, in m^2/V^2; a solprop.nonlinear.raman.RamanResponse scales it by its own fraction.

Examples

>>> from solprop.nonlinear.gnlse import kerr_susceptibility, raman_susceptibility
>>> f"{raman_susceptibility(2.6e-20):.6e}"
'1.380298e-22'
>>> total = kerr_susceptibility(2.6e-20)
>>> share = (2 / 3) * 0.18 * raman_susceptibility(2.6e-20)
>>> bool(abs(share + kerr_susceptibility(2.6e-20, 0.18) - total) < 1e-36)
True