solprop.nonlinear.raman#

Raman response models and the reference evaluation of the Raman polarisation.

A Raman response is a medium’s delayed answer to the field: the molecular vibration or rotation the pulse drives keeps ringing after the pulse has gone, and the polarisation at time t depends on the whole history of the drive. It is therefore a convolution, where the Kerr response of solprop.nonlinear.responses is pointwise, and that difference is the whole of why this module exists separately.

The models here are a damped oscillator, the vibrational mode of a molecular gas, the non-rigid-rotor rotational ladder with its Boltzmann populations, the intermediate-broadening line set of an amorphous solid, and a shape sampled from an optmat silica bundle. Each is a frozen dataclass returning h(t) for t >= 0, and each declares how its coherence dephases with density. The molecular and line-set models take a caller’s own constants (solprop.physdata.raman_types); every built-in gas’s response is optmat’s (solprop.nonlinear.optmat_raman, SEP 3) and fused silica’s is its bundle.

Beside them is SilicaTensorResponse: the delayed tensor response of fused silica declared by a material region of a general modal guide, whose two channels are a share of the region’s own chi3 rather than a per-density response, and whose kernels are therefore in 1/s and not in the units below.

Units. Every model of a gas or a scalar route returns h(t) in m^5 V^-2 s^-1, the units that make

\[P(t) = \varepsilon_0\, \rho\, E(t) \int h(t - t')\, E^2(t')\, dt'\]

hold with P in C/m^2, the number density rho in m^-3 and the field E in V/m. So integral h dt carries the units of solprop.nonlinear.responses.KerrResponse.chi3 per unit density, m^5/V^2, and the Raman and Kerr responses of the same medium are directly comparable. The eps0 stands outside the integral, so h here is linear in eps0; a convention whose outer expression has no eps0 hides it inside every K as (4 pi eps0)^2 and has an h quadratic in eps0, larger by that factor. The scalar vibrational factor also includes the independent amplitude correction described in vibrational().

The manual page is docs/guide/media/raman.md; the kernel’s view of the response is docs/dev/kernel-contract.md section 5, and the sampling onto the doubled grid is solprop.translate.raman.

Module Attributes

MIN_ROTATIONAL_LEVELS

Levels a rotational ladder needs before one J -> J + 2 pair exists.

SILICA_TENSOR_INTEGRALS

What the two normalised shapes of SilicaTensorResponse integrate to.

SILICA_TENSOR_AMPLITUDE

The field-domain amplitude factor of SilicaTensorResponse's kernels.

SILICA_NAMES

Spellings response_for() accepts for fused silica.

Functions

bundle_response(bundle, *[, amplitude, ...])

Build fused silica's Raman response from an optmat bundle.

normalised_damped_oscillator(amplitude, ...)

Build a damped oscillator whose response integrates to a given value.

raman_drive(field_t, *, kind, thg)

Return the real quantity the Raman response is driven by.

response_for(material[, options, amplitude, ...])

Build the Raman response of a named material.

rotational_nonrigid(parameters[, ...])

Build the rotational Raman spectrum of a linear molecule.

undamped_dephasing()

Return the dephasing of a coherence that does not decay with density.

vibrational(parameters)

Build the Raman response of a molecular vibration.

Classes

DampedOscillator(amplitude, frequency, dephasing)

A single Raman line as a damped harmonic oscillator.

IntermediateBroadening(lines[, amplitude])

The Raman response of an amorphous solid, as intermediately broadened lines.

RamanModel(*args, **kwargs)

One term of a Raman response: an undamped shape and a density-dependent decay.

RamanResponse([models, fraction, thg, ...])

The delayed third-order polarisation of a Raman-active medium.

RamanTensorTerm(model, contraction)

Declare how one scalar linear-polarisation kernel contracts a vector field.

RotationalLines(quantum_numbers, energies, ...)

The rotational Raman spectrum of a linear molecule, line by line.

SampledShape(bundle[, channel, amplitude])

A density-independent Raman term sampled from optmat's silica bundle.

SilicaTensorResponse([parameters])

The delayed tensor Raman response of fused silica (Lin--Agrawal).

class solprop.nonlinear.raman.DampedOscillator(amplitude, frequency, dephasing)[source]#

Bases: object

A single Raman line as a damped harmonic oscillator.

h(t) = amplitude * sin(frequency * t) * exp(-t / tau2) for t > 0 and zero otherwise: the impulse response of a driven oscillator in the weak-damping limit, and the model behind every molecular vibration and every rotational line.

Attributes:
amplitudefloat

Scale factor K, in m^5 V^-2 s^-1, the units of h itself. It may be negative: a rotational line’s weight changes sign with the population difference it comes from.

frequencyfloat

Angular frequency of the transition, rad/s, positive.

dephasingDephasing

How the coherence decays with the local number density.

Raises:
MaterialError

If the frequency is not positive and finite.

Examples

>>> import numpy as np
>>> from solprop.nonlinear.raman import DampedOscillator, undamped_dephasing
>>> line = DampedOscillator(2.0, np.pi / 2.0, undamped_dephasing())
>>> line.undamped(np.array([-1.0, 0.0, 1.0])).tolist()
[0.0, 0.0, 2.0]
amplitude: float#
dephasing: Dephasing#
frequency: float#
undamped(t)[source]#

Return the undamped response on a time axis, in m^5 V^-2 s^-1.

Parameters:
tnumpy.ndarray

Delays, s, shape (n_to,).

Returns:
numpy.ndarray

amplitude * sin(frequency * t) where t > 0 and zero elsewhere, shape (n_to,).

Examples

>>> import numpy as np
>>> from solprop.nonlinear.raman import DampedOscillator, undamped_dephasing
>>> line = DampedOscillator(1.0, 1.0, undamped_dephasing())
>>> float(line.undamped(np.array([np.pi / 2.0]))[0])
1.0
class solprop.nonlinear.raman.Dephasing(linewidth)#

Bases: object

The decay of a Raman coherence, as a function of the local number density.

A wrapper over solprop.physdata.raman_types.Linewidth that turns a full width at half maximum in hertz into the decay factor exp(-t / tau2) a response is damped by, and that is defined at every density including zero.

The zero-density limit has three cases, and which one applies is fixed by the linewidth’s own coefficients:

  • a gas with Dicke narrowing (inverse_density > 0) has a diverging linewidth as the density falls, so tau2 -> 0: no coherence at all;

  • a gas with only collisional broadening (constant and inverse_density both zero) has a vanishing linewidth, so tau2 -> inf: an undamped coherence;

  • a gas with a density-independent term keeps tau2 = 1 / (pi * constant).

All three are finite answers, for the vibrational model as for the rotational one, so a fill that starts from vacuum gives no NaN polarisation. The density multiplies the polarisation anyway, so the choice is invisible in the answer and visible only in whether the answer is a number.

Attributes:
linewidthLinewidth

The dephasing rate of the transition, in Hz per its own density terms.

Examples

>>> from solprop.constants import AMAGAT
>>> from solprop.nonlinear.raman import Dephasing
>>> from solprop.physdata import Linewidth
>>> dephasing = Dephasing(Linewidth(density=3.3e9))
>>> f"{dephasing.dephasing_time(AMAGAT):.4e}"
'9.6458e-11'
>>> dephasing.dephasing_time(0.0)
inf
damping(t, density)#

Return the decay factor exp(-t / tau2) on a time axis.

Parameters:
tnumpy.ndarray

Times since the drive, s, shape (n_to,), non-negative.

densityfloat

Number density in m^-3, non-negative.

Returns:
numpy.ndarray

The factor in [0, 1], shape (n_to,). All ones for an undamped coherence and all zeros where the coherence has collapsed, rather than an exp(-0/0) NaN.

Raises:
MaterialError

If the density is negative.

Examples

>>> import numpy as np
>>> from solprop.nonlinear.raman import Dephasing
>>> from solprop.physdata import Linewidth
>>> dephasing = Dephasing(Linewidth(inverse_density=6.15e6))
>>> dephasing.damping(np.array([0.0, 1e-12]), 0.0).tolist()
[0.0, 0.0]
decay_rate(density)#

Return the coherence decay rate at a number density.

The reciprocal of dephasing_time(), and the form the translation layer prefers: zero is the undamped limit and is an ordinary float, where the dephasing time it corresponds to is infinite.

Parameters:
densityfloat

Number density in m^-3, non-negative.

Returns:
float

1 / tau2 in s^-1. Zero for an undamped coherence and inf where the Dicke term diverges at zero density.

Raises:
MaterialError

If the density is negative.

Examples

>>> from solprop.nonlinear.raman import Dephasing
>>> from solprop.physdata import Linewidth
>>> Dephasing(Linewidth(inverse_density=6.15e6)).decay_rate(0.0)
inf
dephasing_time(density)#

Return the coherence dephasing time at a number density.

Parameters:
densityfloat

Number density in m^-3, non-negative.

Returns:
float

tau2 in s. inf for an undamped coherence and 0.0 where the Dicke term diverges at zero density.

Raises:
MaterialError

If the density is negative.

Examples

>>> from solprop.nonlinear.raman import Dephasing
>>> from solprop.physdata import Linewidth
>>> Dephasing(Linewidth(constant=8220e6)).dephasing_time(0.0)
3.872...e-11
linewidth: Linewidth#
class solprop.nonlinear.raman.IntermediateBroadening(lines, amplitude=1.0)[source]#

Bases: object

The Raman response of an amorphous solid, as intermediately broadened lines.

Each line of the set contributes a damped sinusoid with both a homogeneous (Lorentzian) and an inhomogeneous (Gaussian) width,

\[h(t) \propto \sum_i A_i \mathrm{e}^{-\gamma_i t} \mathrm{e}^{-\Gamma_i^2 t^2/4}\sin(\omega_i t),\]

the Hollenbeck-Cantrell model of fused silica. The widths are the dephasing, so the response does not depend on density and dephasing is the trivial one; a glass is a fixed medium and its density path is one.

The amplitudes of the line set are relative, so the model is normalised: its integral is exactly amplitude, which is therefore the Raman contribution to the medium’s third-order susceptibility in m^5/V^2 per unit of the density argument.

The normalisation is analytic. A numerical quadrature of the oscillatory sum would need to reach out to about 1 ns over an integrand supported in its first 0.1 % and crossing zero some thousands of times, at the mercy of the quadrature’s tolerance. Here

\[\int_0^\infty \mathrm{e}^{-\gamma t}\mathrm{e}^{-\Gamma^2t^2/4}\sin(\omega t) \,dt = \mathrm{Im}\left[\frac{\sqrt{\pi}}{\Gamma}\, w\!\left(\frac{\omega + \mathrm{i}\gamma}{\Gamma}\right)\right],\]

with w the Faddeeva function scipy.special.wofz, which is the stable spelling of exp(z^2) erfc(z) in the half plane these arguments occupy.

Attributes:
linesIntermediateBroadeningRaman

The line set, the caller’s own; fused silica’s built-in response is its optmat bundle (bundle_response()).

amplitudefloat, optional

The value integral h dt is to take, m^5/V^2 per unit density. Default 1.0, which makes the model a pure shape.

Raises:
MaterialError

If the line set integrates to zero, so that it cannot be normalised.

References

D. Hollenbeck and C. D. Cantrell, “Multiple-vibrational-mode model for fiber-optic Raman gain spectrum and response function”, J. Opt. Soc. Am. B 19, 2886 (2002), doi:10.1364/JOSAB.19.002886.

Examples

>>> from solprop.nonlinear.raman import IntermediateBroadening
>>> from solprop.physdata import IntermediateBroadeningRaman
>>> glass = IntermediateBroadening(
...     IntermediateBroadeningRaman(
...         frequencies=(8.2e13,),
...         amplitudes=(1.0,),
...         gaussian_widths=(9.6e12,),
...         lorentzian_widths=(3.0e12,),
...         citation="one illustrative line",
...     )
... )
>>> f"{glass.scale:.9e}"
'8.154299442e+13'
amplitude: float = 1.0#
property dephasing: Dephasing[source]#

Return the density-dependent dephasing, which for a glass is none.

Returns:
Dephasing

A dephasing whose factor is one everywhere; the Lorentzian widths of lines already carry the decay.

lines: IntermediateBroadeningRaman#
property raw_integral: float[source]#

Return the integral of the unnormalised line sum over the positive axis.

Returns:
float

sum_i A_i Im[(sqrt(pi)/Gamma_i) w((omega_i + i gamma_i)/Gamma_i)], in the units of the tabulated amplitudes times seconds.

Examples

>>> from solprop.nonlinear.raman import IntermediateBroadening
>>> from solprop.physdata import IntermediateBroadeningRaman
>>> glass = IntermediateBroadening(
...     IntermediateBroadeningRaman(
...         frequencies=(8.2e13,),
...         amplitudes=(1.0,),
...         gaussian_widths=(9.6e12,),
...         lorentzian_widths=(3.0e12,),
...         citation="one illustrative line",
...     )
... )
>>> f"{glass.raw_integral:.9e}"
'1.226346919e-14'
property scale: float[source]#

Return the factor the raw line sum is multiplied by.

Returns:
float

amplitude divided by raw_integral, so that the response integrates to amplitude.

Raises:
MaterialError

If the line set integrates to zero.

undamped(t)[source]#

Return the normalised response on a time axis, in m^5 V^-2 s^-1.

Named undamped for the RamanModel protocol, but the Lorentzian and Gaussian widths of the line set are already in it: what the name excludes is the density-dependent decay, and a glass has none.

Parameters:
tnumpy.ndarray

Delays, s, shape (n_to,).

Returns:
numpy.ndarray

The response where t > 0 and zero elsewhere, shape (n_to,).

Examples

>>> import numpy as np
>>> from solprop.nonlinear.raman import IntermediateBroadening
>>> from solprop.physdata import IntermediateBroadeningRaman
>>> glass = IntermediateBroadening(
...     IntermediateBroadeningRaman(
...         frequencies=(8.2e13,),
...         amplitudes=(1.0,),
...         gaussian_widths=(9.6e12,),
...         lorentzian_widths=(3.0e12,),
...         citation="one illustrative line",
...     )
... )
>>> bool(np.all(glass.undamped(np.array([-1e-15, 0.0])) == 0.0))
True
solprop.nonlinear.raman.MIN_ROTATIONAL_LEVELS: Final = 3#

Levels a rotational ladder needs before one J -> J + 2 pair exists.

class solprop.nonlinear.raman.MolecularOptions(temperature=None, rotation=None, vibration=None, min_j=0, max_j=50)#

Bases: object

Which parts of a molecular gas’s Raman spectrum to build, and at what state.

Grouped because solprop.nonlinear.raman.response_for() would otherwise take more arguments than the project allows loose (AGENTS.md, “Functions and modules”).

Each term flag has three states: None (the default) takes the term when the species has a model for it and silently goes without otherwise, True requires it and is refused by species name when no model exists, False leaves it out. So MolecularOptions() means every term the species has, and only a term the caller named can fail. The temperature likewise defaults to the run’s or the mixture’s own, and to solprop.constants.ROOM_TEMPERATURE for a bare solprop.nonlinear.raman.response_for() call.

Attributes:
temperaturefloat or None, optional

Gas temperature in K, positive; sets the rotational populations. None takes the temperature of the run or mixture the options are used in.

rotationbool or None, optional

Whether to include the rotational ladder. None takes it when available.

vibrationbool or None, optional

Whether to include the vibrational mode. None takes it when available.

min_jint, optional

Lowest rotational quantum number, non-negative. Default 0.

max_jint, optional

Highest rotational quantum number before truncation. Default 50.

Examples

>>> from solprop.nonlinear.raman import MolecularOptions
>>> MolecularOptions(rotation=False).vibration is None
True
>>> MolecularOptions().resolved(300.0).temperature
300.0
max_j: int = 50#
min_j: int = 0#
resolved(temperature)#

Return the options with an unset temperature taken from the caller.

Parameters:
temperaturefloat

The run’s or mixture’s temperature in K.

Returns:
MolecularOptions

The same options, with the temperature filled in when it was None.

rotation: bool | None = None#
temperature: float | None = None#
vibration: bool | None = None#
class solprop.nonlinear.raman.RamanModel(*args, **kwargs)[source]#

Bases: Protocol

One term of a Raman response: an undamped shape and a density-dependent decay.

The split is what lets the translation layer sample the expensive part once per grid and recompute only the cheap exponential when the density changes along a pressure gradient. A model is responsible for the part of h(t) that does not move with density; dephasing supplies the rest.

property dephasing: Dephasing[source]#

How this term’s coherence decays with the local number density.

undamped(t)[source]#

Return h(t) without the density-dependent decay, in m^5 V^-2 s^-1.

Zero before t = 0, so that the response is causal. At t = 0 it is zero for a response that rises continuously from zero; a response that jumps there takes half its right-hand limit, the trapezoid endpoint of the causal convolution.

Parameters:
tnumpy.ndarray

Delays since the drive, s, shape (n_to,).

Returns:
numpy.ndarray

The undamped response, m^5 V^-2 s^-1, shape (n_to,).

class solprop.nonlinear.raman.RamanResponse(models=None, fraction=None, thg=True, species=None, *, tensor_terms=None)[source]#

Bases: object

The delayed third-order polarisation of a Raman-active medium.

The scalar reduction is P(t) = eps0 rho E(t) integral h(t - t') E^2(t') dt', with h the sum of the RamanModel terms and the drive chosen per grid by raman_drive(). The convolution is what makes this response different in kind from the Kerr one: it has memory, so a backend evaluates it with a transform pair rather than sample by sample, and solprop.translate.raman prepares the kernel for that.

Build one with response_for() rather than by hand, unless the models are yours. A tensor declaration couples the local Cartesian components through the isotropic trace and three-dimensional traceless molecular responses. Envelope drives use Re(A A†)/2; beat-removed fields use their analytic signals for that drive and the real field for the final multiplication.

Parameters:
modelstuple of RamanModel or None, optional

Legacy scalar-only kernels, in summation order. Supply exactly one nonempty route: models or keyword-only tensor_terms.

fractionfloat or None, optional

Overall Raman share in (0, 1], or None for absolute kernels.

thgbool, optional

Retain the real-field harmonic drive. Default True; ignored for envelopes.

speciesstr or None, optional

Label selecting the amplitude density in a mixture.

tensor_termstuple of RamanTensorTerm or None, optional

Explicit labelled kernels, preserving each term’s own dephasing.

Attributes:
tensor_termstuple of RamanTensorTerm

The canonical immutable declaration. Supply this keyword or legacy models, exactly one nonempty route. Legacy models become scalar-only terms. Each term retains its own dephasing and explicit tensor assignment.

modelstuple of RamanModel

Return the scalar models in declaration order.

fractionfloat or None, optional

The Raman fraction f_R, in (0, 1], multiplying the whole response, or None when the models are already absolute – which every molecular model here is. It exists for a medium whose Raman strength is stated as a share of its total nonlinearity, as a fibre GNLSE does. Default None.

thgbool, optional

Whether a field grid keeps the third harmonic of the drive. Ignored on an envelope grid. Default True.

speciesstr or None, optional

Component label whose density sets this response’s amplitude.

Raises:
MaterialError

If there are no models, or the fraction is outside (0, 1].

Examples

>>> import numpy as np
>>> from solprop.nonlinear.raman import response_for
>>> from solprop.physdata import Gas, MaterialSelection
>>> selection = MaterialSelection(carrier_wavelength=800e-9)
>>> response = response_for(Gas.N2, selection=selection)
>>> response.traits.domain
'convolution'
>>> len(response.models)
72
evaluate(field_t, density, *, kind, dt)[source]#

Return the Raman polarisation on the fine time grid.

The reference implementation, and therefore the oracle for every backend. It allocates, transforms twice on a grid of twice the length, and rebuilds the kernel on every call; solprop.translate.raman does none of those things.

The convolution runs on a doubled, zero-padded grid. A circular convolution on the fine grid alone would wrap the tail of the response back onto the head of the drive, and for a gas whose rotational coherence outlives the window – nanoseconds against tens of picoseconds – that wrapped tail is not small. The doubled grid makes the transform product the linear convolution exactly. The dt scaling is the sample spacing the discrete sum needs to become the integral; the inverse transform’s 1/N supplies the rest.

Parameters:
field_tnumpy.ndarray

Finite local Cartesian field, shape (n_pol, n_to), with one or two components: real E(t) in V/m on a field grid, complex A(t) in V/m on an envelope grid. Two components require tensor-capable terms.

densityfloat

Number density in m^-3, non-negative. It both scales the polarisation and sets the dephasing.

kind{‘field’, ‘envelope’}

The spectral layout the samples belong to.

dtfloat

Sample spacing of the fine time grid, s, positive.

Returns:
numpy.ndarray

The polarisation in C/m^2, shape (n_pol, n_to), real on a field grid and complex on an envelope grid.

Raises:
GridError

If the layout is unknown, the shape or dtype does not match it, the sample spacing is not positive, samples are non-finite, or a scalar-only term is evaluated on a vector field.

MaterialError

If the density is negative or non-finite.

Examples

>>> import numpy as np
>>> from solprop.nonlinear.raman import response_for
>>> from solprop.physdata import Gas, MaterialSelection
>>> selection = MaterialSelection(carrier_wavelength=800e-9)
>>> t = (np.arange(64) - 32) * 1e-15
>>> field = (1e9 * np.exp(-((t / 5e-15) ** 2)))[None, :]
>>> response = response_for(Gas.N2, selection=selection)
>>> out = response.evaluate(field, 1e25, kind="field", dt=1e-15)
>>> out.shape, out.dtype
((1, 64), dtype('float64'))
fraction: float | None = None#
impulse(t, density)[source]#

Return the response function h(t) at one density, in m^5 V^-2 s^-1.

Each model is damped by its own dephasing before the sum, which is what a gas with both a rotational and a vibrational coherence requires: at one bar those two decay 3.5 times (H2) to 17 times (N2) apart.

Parameters:
tnumpy.ndarray

Delays, s, shape (n_to,). Zero and negative delays give zero.

densityfloat

Number density in m^-3, non-negative.

Returns:
numpy.ndarray

h(t), m^5 V^-2 s^-1, shape (n_to,).

Raises:
MaterialError

If the density is negative.

Examples

>>> import numpy as np
>>> from solprop.nonlinear.raman import response_for
>>> from solprop.physdata import Gas, MaterialSelection
>>> selection = MaterialSelection(carrier_wavelength=800e-9)
>>> response = response_for(Gas.N2, selection=selection)
>>> bool(np.all(response.impulse(np.array([-1e-15, 0.0]), 1e25) == 0.0))
True
property models: tuple[RamanModel, ...][source]#

Return the scalar models in declaration order.

Returns:
tuple of RamanModel

A derived compatibility view; replace tensor_terms to update models.

property scale: float[source]#

Return the overall multiplier the response carries.

Returns:
float

fraction, or 1.0 when it is None.

species: str | None = None#
property tensor_capable: bool[source]#

Return whether every term has an established vector contraction.

Returns:
bool

False if any term is declared scalar_only.

tensor_impulse(t, density)[source]#

Return the isotropic and rank-two kernels in that order.

These are the scalar linear-polarisation kernels h_v and h_r, before multiplication by eps0 * density. Damping and the declared fraction are included. Their sum equals impulse() up to rounding.

Parameters:
tnumpy.ndarray

Finite real delays in s, shape (n_to,); nonpositive delays give zero.

densityfloat

Finite non-negative number density in m^-3, setting the damping.

Returns:
numpy.ndarray

Shape (2, n_to), in m^5 V^-2 s^-1. Row zero is isotropic; row one is rank two. A channel with no declared terms is zero.

Raises:
MaterialError

If any term is scalar-only or the density is invalid.

GridError

If delays are not a finite real one-dimensional array.

tensor_terms: tuple[RamanTensorTerm, ...]#
thg: bool = True#
property traits: ResponseTraits[source]#

Return the declared traits of the Raman response.

Returns:
ResponseTraits

A convolution on both grids consuming the density path. Tensor-capable declarations couple Cartesian components; other declarations are scalar.

Examples

>>> from solprop.nonlinear.raman import response_for
>>> response_for("SiO2").traits.polarisation
'scalar'
class solprop.nonlinear.raman.RamanTensorTerm(model, contraction)[source]#

Bases: object

Declare how one scalar linear-polarisation kernel contracts a vector field.

A model’s class does not determine its tensor character. In particular, a custom oscillator needs an explicit physical assignment before vector use.

Attributes:
modelRamanModel

Scalar kernel in m^5 V^-2 s^-1, with its own dephasing law.

contraction{‘scalar_only’, ‘isotropic’, ‘rank2’}

No established vector contraction, the isotropic trace channel, or the three-dimensional traceless rank-two channel, respectively. Each channel reduces to model for a fixed real linear polarisation.

Raises:
MaterialError

If the model does not implement the Raman protocol or the label is unknown.

contraction: Literal['scalar_only', 'isotropic', 'rank2']#
model: RamanModel#
class solprop.nonlinear.raman.RotationalLines(quantum_numbers, energies, populations, frequencies, amplitudes, dephasing, temperature)[source]#

Bases: object

The rotational Raman spectrum of a linear molecule, line by line.

A non-rigid rotor’s levels are E_J = 2 pi hbar c (B J(J+1) - D (J(J+1))^2) and the Raman-active transitions are the S branch, J -> J + 2. Each pair contributes a damped oscillator whose weight is set by the population difference between its two levels, so a line can have either sign, and the whole ladder shares one dephasing: the J dependence of the linewidth is dropped.

Build one with rotational_nonrigid(); the arrays are stored so that a test or a manual page can look at the ladder itself rather than only at its sum.

Attributes:
quantum_numbersnumpy.ndarray

The rotational quantum numbers J retained, shape (n_levels,), integer.

energiesnumpy.ndarray

Level energies in J, shape (n_levels,).

populationsnumpy.ndarray

Fractional level populations, shape (n_levels,), summing to one.

frequenciesnumpy.ndarray

Transition angular frequencies of the J -> J + 2 pairs, rad/s, shape (n_lines,) with n_lines = n_levels - 2.

amplitudesnumpy.ndarray

Line scale factors in m^5 V^-2 s^-1, shape (n_lines,). Signed.

dephasingDephasing

The dephasing shared by every line.

temperaturefloat

The temperature the populations were computed at, K.

Raises:
MaterialError

If the array lengths do not agree, or there are fewer than three levels.

Examples

>>> from solprop.nonlinear.raman import rotational_nonrigid
>>> from solprop.physdata import Linewidth, RotationalRaman
>>> nitrogen = RotationalRaman(
...     b=199.0,
...     d=5.74e-4,
...     degeneracy_odd=1,
...     degeneracy_even=2,
...     polarisability_anisotropy=6.7e-31,
...     linewidth=Linewidth(density=3.3e9),
...     citation="a caller's N2 constants",
... )
>>> lines = rotational_nonrigid(nitrogen)
>>> lines.frequencies.size == lines.quantum_numbers.size - 2
True
>>> f"{float(lines.populations.sum()):.15f}"
'1.000000000000000'
amplitudes: Float[ndarray, 'n_amplitudes']#
dephasing: Dephasing#
energies: Float[ndarray, 'n_energies']#
frequencies: Float[ndarray, 'n_lines']#
property n_lines: int[source]#

Return the number of J -> J + 2 transitions.

Returns:
int

Two fewer than the number of levels.

populations: Float[ndarray, 'n_populations']#
quantum_numbers: Int[ndarray, 'n_levels']#
temperature: float#
undamped(t)[source]#

Return the undamped response of the whole ladder, in m^5 V^-2 s^-1.

Parameters:
tnumpy.ndarray

Delays, s, shape (n_to,).

Returns:
numpy.ndarray

sum_i amplitude_i sin(frequency_i t) where t > 0 and zero elsewhere, shape (n_to,).

Examples

>>> import numpy as np
>>> from solprop.nonlinear.raman import rotational_nonrigid
>>> from solprop.physdata import Linewidth, RotationalRaman
>>> hydrogen = RotationalRaman(
...     b=5890.0,
...     d=5.0,
...     degeneracy_odd=3,
...     degeneracy_even=1,
...     polarisability_anisotropy=3e-31,
...     linewidth=Linewidth(inverse_density=6.15e6, density=114e6),
...     citation="a caller's H2 constants",
... )
>>> lines = rotational_nonrigid(hydrogen)
>>> bool(np.all(lines.undamped(np.array([-1e-12, 0.0])) == 0.0))
True
solprop.nonlinear.raman.SILICA_NAMES: Final = ('sio2', 'silica', 'fused_silica', 'fused silica')#

Spellings response_for() accepts for fused silica.

solprop.nonlinear.raman.SILICA_TENSOR_AMPLITUDE: Final = 1.5#

The field-domain amplitude factor of SilicaTensorResponse’s kernels.

f_R and n2 are envelope (Agrawal) quantities: f_R is the share of the effective nonlinearity on the fundamental that the delayed term carries. The kernels here act in the field domain, where the delayed polarisation eps0 chi3 E int h(tau) E^2(t - tau) dtau is driven by the cycle average |A|^2/2 and so contributes only (1/2) (int h) |A|^2 A on the fundamental, against the instantaneous cubic’s (3/4) chi3 |A|^2 A. A field-domain kernel of integral X therefore acts like a Kerr term of (2/3) X, and carrying the share f_R of one chi3 needs

int h = (3/2) f_R chi3 .

This is exactly solprop.nonlinear.gnlse.raman_susceptibility()’s 2 n2 eps0 c = (3/2) chi3, the identity (2/3) f_R X + kerr_susceptibility(n2, f_R) == kerr_susceptibility(n2) it is built to satisfy: the (3/2) chi3_total amplitude mapping, with the fraction applied exactly once. Without it the declared n2 comes out f_R/3 – 8.2 % for silica – low.

solprop.nonlinear.raman.SILICA_TENSOR_INTEGRALS: Final = (0.8333333333333334, 0.16666666666666666)#

What the two normalised shapes of SilicaTensorResponse integrate to.

These are the shape integrals, not the kernel integrals: the kernels carry the amplitude SILICA_TENSOR_AMPLITUDE times f_R on top of them, so they integrate to (3/2) f_R times the pair below. The pair itself sums to one, which is what makes it a shape.

Lin and Agrawal write the delayed polarisation as an isotropic term in \(a(\tau)\) and a dyadic term in \(b(\tau)\), with \(a = w_a h_a\) and \(b = w_b h_b + w_{ab} h_a\) (Opt. Lett. 31, 3086 (2006), Eq. (1); the vector theory is J. Opt. Soc. Am. B 20, 1616 (2003)). The record the seam carries instead splits the same polarisation into a trace channel and the three-dimensional traceless rank-two channel T(Q) = (3Q - tr(Q) I)/2, which is the contraction the existing vector Raman route already uses. Matching

a tr(Q) E + b Q E  =  r_trace tr(Q) E + r_rank T(Q) E

gives r_trace = a + b/3 and r_rank = 2 b/3. Because h_a and h_b are each normalised, the two integrate to

r_trace = w_a + (w_b + w_ab)/3 and r_rank = 2 (w_b + w_ab)/3 ,

whose sum is w_a + w_b + w_ab = 1. The values stored here, 5/6 and 1/6, are what those expressions give for Lin and Agrawal’s (2006) weights – w_a = 0.75 and w_b + w_ab = 0.25 – and for no others: a different w_a moves both numbers. They are not the paper’s own int a = 3/4 and int b = 1/4, which belong to a different pair of channels.

class solprop.nonlinear.raman.SampledShape(bundle, channel='parallel', amplitude=1.0)[source]#

Bases: object

A density-independent Raman term sampled from optmat’s silica bundle.

The undamped row is the bundle’s normalised shape times amplitude, and the dephasing is none: a glass’s widths are already in its shape, and it has no density to scale (SEP 3, section 4.5). The channels:

  • "parallel": the copolarised \(h_R = a + b\), unit integral, for a scalar run (a scalar_only term);

  • "isotropic": \(h_0 = a + b/3\), the trace channel;

  • "rank2": \(h_2 = 2b/3\), the traceless rank-two channel,

with Hellwarth’s \(a\) and \(b\) from h_r(t, "a") and h_r(t, "b"). With the seam’s weights, (1, 0) for the isotropic term and (1/4, 3/4) for the rank-two one, a copolarised probe then sees \(a + b\) and a cross-polarised one \(a\).

At zero delay the sample is half the shape’s right-hand limit, the trapezoid rule’s endpoint weight, and zero below it. That is exactly zero for a shape that opens with a sine (Hollenbeck and Cantrell’s, and \(a\)); for the boson peak in \(b\), whose limit is \(2 f_b/\tau_b\), it keeps the convolution second order where a zero sample would leave it first order, at no cost to causality, as SilicaTensorResponse.kernels() explains.

Attributes:
bundleSilicaBundle

The optmat silica bundle.

channel{‘parallel’, ‘isotropic’, ‘rank2’}, optional

The part sampled. Default "parallel".

amplitudefloat, optional

The value integral h dt takes for the "parallel" channel, m^5/V^2 per unit density. Default 1.0, a pure shape.

Raises:
MaterialError

If the channel is unknown, the amplitude is not finite, or a tensor channel is asked of a copolarised-only bundle, naming it.

Examples

>>> import numpy as np
>>> from solprop.nonlinear.raman import SampledShape
>>> from solprop.physdata.glasses import SilicaBundle
>>> shape = SampledShape(SilicaBundle.resolve())
>>> t = np.arange(40000) * 5e-17
>>> f"{shape.undamped(t).sum() * 5e-17:.4f}"
'1.0000'
amplitude: float = 1.0#
bundle: SilicaBundle#
channel: Literal['parallel', 'isotropic', 'rank2'] = 'parallel'#
property dephasing: Dephasing[source]#

Return the density-dependent dephasing, which for a glass is none.

Returns:
Dephasing

A dephasing whose factor is one everywhere.

property material_records: tuple[MaterialRecord, ...][source]#

Return the records of the bundle the term samples.

Returns:
tuple of MaterialRecord

The bundle’s, its Kerr fit’s and its shape’s.

property name: str[source]#

Return the term’s name: the bundle’s and the channel.

Returns:
str

For example "silica_bundle/default:parallel".

shape(t)[source]#

Return the channel’s normalised shape, in 1/s, zero at and below zero delay.

Parameters:
tnumpy.ndarray

Delays in s, shape (n_to,).

Returns:
numpy.ndarray

a + b, a + b/3 or 2 b/3, shape (n_to,).

undamped(t)[source]#

Return the sampled term, in m^5 V^-2 s^-1.

Parameters:
tnumpy.ndarray

Delays in s, shape (n_to,).

Returns:
numpy.ndarray

amplitude times the shape where t > 0, half its right-hand limit at t = 0 and zero below, shape (n_to,).

Examples

>>> import numpy as np
>>> from solprop.nonlinear.raman import SampledShape
>>> from solprop.physdata.glasses import SilicaBundle
>>> shape = SampledShape(SilicaBundle.resolve())
>>> shape.undamped(np.array([-1e-15, 0.0])).tolist()
[0.0, 0.0]
class solprop.nonlinear.raman.SilicaTensorResponse(parameters=<factory>)[source]#

Bases: Displayed

The delayed tensor Raman response of fused silica (Lin–Agrawal).

A material region’s declared delayed response, beside solprop.modes.material.MaterialKerr: the two share one \(\chi^{(3)}\), of which this one takes the fraction \(f_R\) its parameters declare and the instantaneous Kerr term keeps \(1 - f_R\). It is a separately named model and changes nothing about the existing scalar silica response of response_for(), which is a different fit (Hollenbeck–Cantrell) reached by a different route (a gas-filled or mode-averaged run’s response list).

The polarisation. For the real three-component field \(\mathbf E(t)\), Lin and Agrawal’s delayed terms are

\[P_i^{\rm R}(t) = \varepsilon_0\chi^{(3)}\Big[ E_i(t)\!\int_0^\infty\! a(\tau)\, \mathbf E(t-\tau)\!\cdot\!\mathbf E(t-\tau)\,\mathrm d\tau + \sum_j E_j(t)\!\int_0^\infty\! b(\tau)\, E_i(t-\tau)E_j(t-\tau)\,\mathrm d\tau\Big],\]

and this class carries them as the two channels SILICA_TENSOR_INTEGRALS names, over the six real symmetric pair histories \(Q_{kl} = E_kE_l\) of three components:

P_i = eps0 chi3 sum_term sum_jkl D[term]_ijkl E_j (h[term] * Q)_kl ,

with D[trace]_ijkl = d_ij d_kl and D[rank]_ijkl = (3 (d_ik d_jl + d_il d_jk) - 2 d_ij d_kl)/4, the second being T(Q) written as a contraction and symmetrised in its last two indices because a pair history is symmetric. The scalar limit is exact: for one fixed linear polarisation the two channels add to E(t) int (3/2) f_R h_R(tau) E^2(t - tau) dtau with h_R = a + b integrating to one – the standard fibre model this package’s scalar routes carry, at the field-domain amplitude SILICA_TENSOR_AMPLITUDE explains, which is what makes the declared n2 come out right on the fundamental.

On an envelope grid, and on a field grid with the analytic-signal cubic, the drive is the beat-free Re(E_a,k conj(E_a,l))/2 instead, exactly as raman_drive() chooses it for the scalar response.

From optmat. With parameters an optmat tensor bundle (SilicaBundle, SilicaBundle.resolve( tensor=True)), Hellwarth’s \(a\) and \(b\) are optmat’s own h_r(t, "a") and h_r(t, "b"), normalised so that \(\int(a+b)=1\), and the fraction is the bundle’s (0.245 for Lin and Agrawal’s shape): the same two channels, \(a + b/3\) and \(2b/3\), and the same endpoint rule, with the right-hand limits at zero delay taken from optmat. A copolarised-only bundle (Hollenbeck and Cantrell’s, the scalar default) has no \(a\) and \(b\) and is refused by name.

Attributes:
parameterssolprop.physdata.raman_types.LinAgrawalRaman or SilicaBundle

An optmat silica bundle with a tensor shape, by default optmat’s recommended tensor bundle (SilicaBundle.resolve(tensor=True), Lin and Agrawal’s shape, SEP 3, D11), or a caller’s own time constants, weights and Raman fraction.

Raises:
MaterialError

If an optmat bundle has no tensor parts.

Examples

>>> import numpy as np
>>> from solprop.nonlinear.raman import SilicaTensorResponse
>>> response = SilicaTensorResponse()
>>> response.fraction
0.245
>>> t = np.arange(20000) * 1e-16
>>> integrals = response.kernels(t).sum(axis=1) * 1e-16 / response.fraction
>>> [float(round(one, 4)) for one in integrals]
[1.25, 0.25]
evaluate(field_t, chi3, *, kind, dt, thg=True)[source]#

Return the delayed polarisation on the fine time grid, in C/m^2.

The reference implementation, and therefore the oracle a compiled route is measured against. Like RamanResponse.evaluate() it convolves on a doubled, zero-padded grid, so the transform product is the linear convolution exactly and the response’s tail cannot wrap onto the head of the drive.

Parameters:
field_tnumpy.ndarray

The local Cartesian field on the fine time grid, shape (n_pol, n_to) with one, two or three components: real E(t) in V/m on a field grid, complex A(t) in V/m on an envelope grid.

chi3float

The region’s absolute third-order susceptibility in m^2/V^2, non-negative. The Raman fraction is already in the kernels.

kind{‘field’, ‘envelope’}

The temporal representation the samples belong to.

dtfloat

Sample spacing of the fine time grid, s, positive.

thgbool, optional

On a field grid, whether the drive keeps the 2 omega beat – E_kE_l, which radiates the third harmonic through the later multiplication by E – or is the beat-free Re(E_a,k conj(E_a,l))/2. Ignored on an envelope grid, which cannot carry the beat.

Returns:
numpy.ndarray

The polarisation in C/m^2, shape (n_pol, n_to), real on a field grid and complex on an envelope grid.

Raises:
GridError

If the layout is unknown, the shape or dtype does not match it, the spacing is not positive, or samples are non-finite.

MaterialError

If the susceptibility is negative or not finite.

Examples

>>> import numpy as np
>>> from solprop.nonlinear.raman import SilicaTensorResponse
>>> t = (np.arange(64) - 32) * 1e-15
>>> field = np.stack([1e9 * np.exp(-((t / 5e-15) ** 2)), np.zeros(64),
...                   np.zeros(64)])
>>> out = SilicaTensorResponse().evaluate(
...     field, 2.2e-22, kind="field", dt=1e-15
... )
>>> out.shape, out.dtype
((3, 64), dtype('float64'))
property fraction: float[source]#

Return the Raman fraction f_R the parameters declare.

Returns:
float

A share of the medium’s third-order susceptibility, in (0, 1]: an optmat bundle’s own f_R.

hellwarth(t)[source]#

Return Hellwarth’s isotropic a and anisotropic b, in 1/s.

a + b is the copolarised response \(h_R\) and integrates to one; both are zero at and below zero delay. From the declared parameters, a = w_a h_a and b = w_b h_b + w_ab h_a; from an optmat bundle, optmat’s h_r(t, "a") and h_r(t, "b").

Parameters:
tnumpy.ndarray

Delays in s, shape (n_to,), finite and real.

Returns:
numpy.ndarray

Shape (2, n_to) in 1/s: row zero a, row one b.

Raises:
GridError

If the delays are not a finite real one-dimensional array.

Examples

>>> import numpy as np
>>> from solprop.nonlinear.raman import SilicaTensorResponse
>>> parts = SilicaTensorResponse().hellwarth(np.arange(20000) * 1e-16)
>>> float(round(parts.sum() * 1e-16, 3))
1.0
property instantaneous_share: float[source]#

Return 1 - f_R, the share the instantaneous Kerr term keeps.

Returns:
float

A dimensionless share in [0, 1).

kernels(t)[source]#

Return the trace and rank-two seam kernels, in 1/s.

(3/2) f_R (a + b/3) and (3/2) f_R (2 b/3) of the class docstring, which integrate to SILICA_TENSOR_AMPLITUDE times f_R times SILICA_TENSOR_INTEGRALS. Both the amplitude factor and the Raman fraction are included here and nowhere else, so that a compiled record multiplies by chi3 alone and the fraction is applied exactly once; this is also the one place the sampled kernel is built, so the endpoint rule below is seen by every caller.

The endpoint quadrature is the trapezoid, and the sample at zero delay carries half the kernel’s right-hand limit there. h_b(0^+) = 2/\tau_b is not zero, so a causal kernel sampled on arange(n) dt and integrated by the rectangle-rule transform product misses dt h(0^+)/2 and converges only to first order: sampling the jump at its midpoint – the mean of the two one-sided limits, one of which is zero – turns that sum into the trapezoid rule and restores second order. The half-weighted sample multiplies Q(t) itself, so it costs no causality. The late lobe of h_b, where t > 2\tau_b, is genuinely negative and is left unclipped: it is part of the response, not a gain.

Parameters:
tnumpy.ndarray

Delays in s, shape (n_to,), finite and real. The endpoint rule assumes a uniform axis starting at zero, which is what both callers pass.

Returns:
numpy.ndarray

Shape (2, n_to) in 1/s: row zero the trace channel, row one the traceless rank-two channel.

Raises:
GridError

If the delays are not a finite real one-dimensional array.

Examples

>>> import numpy as np
>>> from solprop.nonlinear.raman import SilicaTensorResponse
>>> kernels = SilicaTensorResponse().kernels(np.arange(8) * 1e-15)
>>> kernels.shape
(2, 8)
parameters: LinAgrawalRaman | SilicaBundle#
shapes(t)[source]#

Return the two normalised response functions h_a and h_b.

Each integrates to one over the positive axis; both are zero at and below zero delay, which is what makes the later convolution causal.

Parameters:
tnumpy.ndarray

Delays in s, shape (n_to,), finite and real.

Returns:
numpy.ndarray

Shape (2, n_to) in 1/s: row zero h_a, row one h_b.

Raises:
GridError

If the delays are not a finite real one-dimensional array.

MaterialError

For an optmat bundle, which gives Hellwarth’s a and b rather than h_a and h_b (hellwarth()).

Examples

>>> import numpy as np
>>> from solprop.nonlinear.raman import SilicaTensorResponse
>>> from solprop.physdata import LinAgrawalRaman
>>> declared = LinAgrawalRaman(
...     tau1=12.2e-15,
...     tau2=32.0e-15,
...     tau_b=96.0e-15,
...     isotropic_weight=0.75,
...     boson_weight=0.21,
...     anisotropic_weight=0.04,
...     fraction=0.245,
...     citation="Lin and Agrawal, Opt. Lett. 31, 3086 (2006)",
... )
>>> response = SilicaTensorResponse(declared)
>>> shapes = response.shapes(np.array([-1e-15, 0.0, 1e-14]))
>>> [float(round(one, 4)) for one in shapes[:, 0]]
[0.0, 0.0]
tensors()[source]#

Return the two dimensionless rank-four channel tensors.

D[trace]_ijkl = d_ij d_kl and D[rank]_ijkl = (3 (d_ik d_jl + d_il d_jk) - 2 d_ij d_kl)/4, contracted as P_i = ... D_ijkl E_j H_kl with H the convolved pair history. Both are symmetric in their last two indices, because a pair history is, and that symmetry is what makes the six stored histories enough.

Returns:
numpy.ndarray

Shape (2, 3, 3, 3, 3), dimensionless.

Examples

>>> import numpy as np
>>> from solprop.nonlinear.raman import SilicaTensorResponse
>>> tensors = SilicaTensorResponse().tensors()
>>> [float(one[0, 0, 0, 0]) for one in tensors]
[1.0, 1.0]
property traits: ResponseTraits[source]#

Return the declared traits of this response.

Returns:
ResponseTraits

A convolution on a field grid, coupling Cartesian components, with no density path at all: a glass has none, and the magnitude comes from the region’s own chi3.

Examples

>>> from solprop.nonlinear.raman import SilicaTensorResponse
>>> SilicaTensorResponse().traits.polarisation
'tensor'
solprop.nonlinear.raman.bundle_response(bundle, *, amplitude=1.0, fraction=None, thg=True, tensor=False)[source]#

Build fused silica’s Raman response from an optmat bundle.

The scalar response is one scalar_only SampledShape of the bundle’s copolarised shape. The tensor response is the bundle’s isotropic channel a + b/3 and rank-two channel 2 b/3 as two RamanTensorTerm declarations, which the seam weights (1, 0) and (1/4, 3/4).

Parameters:
bundleSilicaBundle

The optmat silica bundle; a tensor response needs one with tensor parts.

amplitudefloat, optional

The value integral h dt of the copolarised response, m^5/V^2 per unit density, before fraction. Default 1.0.

fractionfloat or None, optional

The Raman fraction in (0, 1] the response carries, or None.

thgbool, optional

Whether a field grid keeps the third harmonic of the drive. Default True.

tensorbool, optional

Build the tensor declaration, for a vector run. Default False.

Returns:
RamanResponse

One scalar-only term, or an isotropic and a rank-two term.

Raises:
MaterialError

If a tensor response is asked of a copolarised-only bundle, naming it, or the fraction is outside (0, 1].

Examples

>>> from solprop.nonlinear.raman import bundle_response
>>> from solprop.physdata.glasses import SilicaBundle
>>> bundle_response(SilicaBundle.resolve()).tensor_capable
False
>>> response = bundle_response(SilicaBundle.resolve(tensor=True), tensor=True)
>>> [term.contraction for term in response.tensor_terms]
['isotropic', 'rank2']
solprop.nonlinear.raman.normalised_damped_oscillator(amplitude, frequency, dephasing_time)[source]#

Build a damped oscillator whose response integrates to a given value.

The integral of K sin(Omega t) exp(-t / tau2) over the positive axis is K Omega / (Omega^2 + 1/tau2^2), so scaling K by the reciprocal of that factor makes the integral exactly amplitude. Useful wherever the Raman strength is known as a fraction of the total nonlinearity rather than from molecular constants, which is how a fibre GNLSE states it.

A module-level function rather than a classmethod, for the reason given in solprop.physdata.raman_types.linewidth_from_dephasing_time().

Parameters:
amplitudefloat

The value integral h dt is to take, m^5/V^2 per unit density.

frequencyfloat

Angular frequency of the transition, rad/s, positive.

dephasing_timefloat

Density-independent coherence time tau2 in s, positive.

Returns:
DampedOscillator

The line, with a density-independent dephasing.

Raises:
MaterialError

If the frequency or the dephasing time is not positive.

Examples

>>> import numpy as np
>>> from scipy.integrate import quad
>>> from solprop.nonlinear.raman import normalised_damped_oscillator
>>> line = normalised_damped_oscillator(1.0, 2 * np.pi * 100e12, 100e-15)
>>> integral = quad(
...     lambda t: float(line.undamped(np.array([t]))[0])
...     * float(line.dephasing.damping(np.array([t]), 1.0)[0]),
...     0.0,
...     2e-12,
...     limit=400,
... )[0]
>>> bool(abs(integral - 1.0) < 1e-8)
True
solprop.nonlinear.raman.raman_drive(field_t, *, kind, thg)[source]#

Return the real quantity the Raman response is driven by.

The Raman coherence is driven by the intensity of the field, and which real quadratic form that is depends on the grid, exactly as the Kerr cubic does (docs/guide/media/nonlinear.md):

grid

thg

drive

field

True

E^2, which carries the 2 omega beat and so radiates the third harmonic through the later multiplication by E

field

False

|E_a|^2 / 2, the cycle-averaged intensity, with the beat removed

envelope

–

|A|^2 / 2, the same quantity written on the envelope

The factor of one half is the cycle average of cos^2: E^2 and |E_a|^2 / 2 agree in their slowly varying part, so the two field-grid forms drive the coherence identically and differ only in the harmonic.

Parameters:
field_tnumpy.ndarray

The field on the fine time grid, shape (1, n_to): real E(t) in V/m on a field grid, complex A(t) in V/m on an envelope grid.

kind{‘field’, ‘envelope’}

The spectral layout the samples belong to.

thgbool

Whether the third harmonic is kept. Ignored on an envelope grid, which cannot carry it in a quadratic drive.

Returns:
numpy.ndarray

The drive in V^2/m^2, real, shape (n_to,).

Examples

>>> import numpy as np
>>> from solprop.nonlinear.raman import raman_drive
>>> raman_drive(np.array([[3.0, -2.0]]), kind="field", thg=True).tolist()
[9.0, 4.0]
>>> raman_drive(np.array([[2.0 + 0j]]), kind="envelope", thg=True).tolist()
[2.0]
solprop.nonlinear.raman.response_for(material, options=None, *, amplitude=1.0, fraction=None, thg=True, selection=None, density=None)[source]#

Build the Raman response of a named material.

A built-in gas’s response is optmat’s (SEP 3), absolute, so its fraction must be left None; a caller’s species with its own molecular constants (MolecularRaman) gives an absolute response built from them; fused silica gives optmat’s bundle shape, normalised, whose magnitude the caller supplies, either as amplitude or, downstream, as fraction times a susceptibility.

No material selects a normalised damped oscillator; build one with normalised_damped_oscillator() if you want it.

Parameters:
materialGas, str or Species

A built-in gas, a caller’s species with Raman constants, or one of SILICA_NAMES for fused silica.

optionsMolecularOptions or None, optional

Which parts of a molecular spectrum to build, and at what temperature. Ignored for silica. None and the default options include every available term at solprop.constants.ROOM_TEMPERATURE; a term set to True is refused by species name when it has no model.

amplitudefloat, optional

For silica only: the value integral h dt is to take, m^5/V^2 per unit density. Default 1.0, a pure shape.

fractionfloat or None, optional

The Raman fraction f_R in (0, 1], or None. Refused for a molecular gas, whose models are absolute and would be scaled twice.

thgbool, optional

Whether a field grid keeps the third harmonic of the drive. Default True.

selectionMaterialSelection or None, optional

None is MaterialSelection(). A built-in gas’s response is optmat’s under it, dispersed to its carrier_wavelength, which must then be set (solprop.nonlinear.optmat_raman.gas_raman()); fused silica’s is the bundle its raman names. Refused beside a caller’s species.

densityfloat or None, optional

For a built-in gas, the constant number density in m^-3 of the fill: checked against the response’s validated range, and the density at which H2’s shifted lines are built.

Returns:
RamanResponse

The response: for silica one term; for a built-in gas an OptmatRamanResponse with one term per damping group and tensor family; for a caller’s species one term per part of its spectrum.

Raises:
MaterialError

If the material is unknown, has no Raman data, has no part left after the options, is a gas given a fraction, or is a built-in gas without a carrier.

Examples

>>> from solprop.nonlinear.raman import response_for
>>> from solprop.physdata import Gas, MaterialSelection
>>> selection = MaterialSelection(carrier_wavelength=800e-9)
>>> type(response_for(Gas.N2, selection=selection)).__name__
'OptmatRamanResponse'
>>> len(response_for("SiO2", fraction=0.18).models)
1
solprop.nonlinear.raman.rotational_nonrigid(parameters, temperature=293.15, *, min_j=0, max_j=50)[source]#

Build the rotational Raman spectrum of a linear molecule.

The ladder is truncated wherever the non-rigid-rotor energy stops increasing with J: beyond that point the expansion B J(J+1) - D (J(J+1))^2 has left its range of validity and the “levels” it predicts are an artefact. For H2, whose centrifugal distortion is large, that happens at J = 24.

Populations are Boltzmann with the nuclear-spin statistical weights, normalised over the retained levels. The line weight of the J -> J + 2 pair is

\[K \frac{(J+1)(J+2)}{2J+3} \left(\frac{p_{J+2}}{2J+5} - \frac{p_J}{2J+1}\right), \qquad K = -\frac{(4\pi\varepsilon_0)^2}{\varepsilon_0} \frac{2}{15}\frac{\Delta\alpha^2}{\hbar},\]

with p_J the fractional populations. K is negative and so is the bracket, because the population per magnetic sublevel falls with J, so the lines come out positive: the same sign as a vibration, and the sign that amplifies the Stokes side.

Parameters:
parametersRotationalRaman

The molecule’s rotational constants, the caller’s own.

temperaturefloat, optional

Gas temperature in K, positive. Default solprop.constants.ROOM_TEMPERATURE.

min_jint, optional

Lowest rotational quantum number to include, non-negative. Default 0.

max_jint, optional

Highest rotational quantum number to consider before truncation. Default 50.

Returns:
RotationalLines

The retained levels, their populations and the resulting lines.

Raises:
MaterialError

If the temperature is not positive, the J range is inverted or shorter than three levels, or the truncation leaves too few levels to form a pair.

References

Laser Photonics Rev. 16, 2100426 (2022), doi:10.1002/lpor.202100426, the SI form of the model of Wahlstrand et al., Phys. Rev. A 92, 063828 (2015), doi:10.1103/PhysRevA.92.063828.

Examples

>>> from solprop.nonlinear.raman import rotational_nonrigid
>>> from solprop.physdata import Linewidth, RotationalRaman
>>> hydrogen = RotationalRaman(
...     b=5890.0,
...     d=5.0,
...     degeneracy_odd=3,
...     degeneracy_even=1,
...     polarisability_anisotropy=3e-31,
...     linewidth=Linewidth(inverse_density=6.15e6, density=114e6),
...     citation="a caller's H2 constants",
... )
>>> lines = rotational_nonrigid(hydrogen, 293.15)
>>> int(lines.quantum_numbers[-1])
24
>>> lines.n_lines
23
solprop.nonlinear.raman.undamped_dephasing()[source]#

Return the dephasing of a coherence that does not decay with density.

The right choice for a model that carries its own damping, such as the intermediate-broadening line set of a glass, whose Lorentzian widths are the dephasing and do not depend on how much glass there is.

Returns:
Dephasing

A dephasing whose factor is one at every density and every delay.

Examples

>>> from solprop.nonlinear.raman import undamped_dephasing
>>> undamped_dephasing().decay_rate(1e25)
0.0
solprop.nonlinear.raman.vibrational(parameters)[source]#

Build the Raman response of a molecular vibration.

A vibrational mode is one damped oscillator whose strength comes from the volume polarisability derivative along a displacement coordinate,

\[K = \frac{(4\pi\varepsilon_0\, d\alpha/dQ)^2} {2\mu\Omega_v\varepsilon_0},\]

the (4 pi eps0) converting the tabulated volume polarisability derivative to an SI one and the final eps0 being the one this package’s h carries outside the integral (see the module docstring). The induced-dipole energy -alpha_SI(Q) E_real^2 / 2 gives force alpha_SI'(Q) E_real^2 / 2. Its retarded displacement response gives the denominator two above. The cycle average is already in raman_drive(); applying another half here would count it twice, which is why the denominator is two and not four. For a diatomic bond coordinate use its reduced mass; do not insert a mass-weighted normal-coordinate derivative while retaining that same mass.

Parameters:
parametersVibrationalRaman

The mode’s constants, the caller’s own (a built-in gas’s response is optmat’s, solprop.nonlinear.optmat_raman.gas_raman()).

Returns:
DampedOscillator

The vibrational line, with the mode’s own density-dependent dephasing.

References

S.-F. Gao, Y.-Y. Wang, F. Belli, C. Brahms, P. Wang and J. C. Travers, “From Raman frequency combs to supercontinuum generation in nitrogen-filled hollow-core anti-resonant fiber”, Laser Photonics Rev. 16, 2100426 (2022), doi:10.1002/lpor.202100426, and Wahlstrand et al., Phys. Rev. A 92, 063828 (2015), doi:10.1103/PhysRevA.92.063828, are the inherited model lineage. The prefactor here is independently derived from the induced-dipole Hamiltonian under this module’s field convention, rather than copied from that lineage. N2 bond-coordinate data are from Murphy, Holzer and Bernstein, Appl. Spectrosc. 23, 211 (1969), Table I and Eq. (4), doi:10.1366/000370269774380824. The prefactor’s common amplitude is verified by an independent quantum-oscillator impulse calculation; each material’s tabulated derivative is not independently recalibrated, and the transfer of N2’s derivative from 435.8 nm to the near infrared and the omission of the anisotropic vibrational response remain assumptions.

Examples

>>> from solprop.constants import ATOMIC_MASS
>>> from solprop.nonlinear.raman import vibrational
>>> from solprop.physdata import Linewidth, VibrationalRaman
>>> mode = VibrationalRaman(
...     frequency=7.826769e14,
...     polarisability_derivative=1.3e-20,
...     reduced_mass=1.00784 * ATOMIC_MASS / 2.0,
...     linewidth=Linewidth(inverse_density=309e6, density=52.2e6),
...     citation="a caller's H2 constants, Phys. Rev. A 92, 063828 (2015)",
... )
>>> line = vibrational(mode)
>>> line.frequency == mode.frequency
True
>>> f"{line.amplitude:.6e}"
'1.803981e-37'