solprop.translate.raman#
Sampling the Raman response kernel onto the doubled grid a backend convolves on.
The physics layer (solprop.nonlinear.raman) says what the response is; this
module turns it into the arrays a kernel consumes. Three things happen here.
The grid is doubled. The convolution is evaluated as a product of transforms, and a
transform product is a circular convolution: without padding, the tail of the response
wraps back onto the head of the drive. A rotational coherence in hydrogen lives for
nanoseconds against a window of tens of picoseconds, so that wrapped tail is not a small
error. Writing the response and the drive into the first half of an array of twice the
length makes the product the linear convolution exactly. It costs one transform of
2 n_to in place of one of n_to.
The tail is tapered, by the grid’s own collar. Whatever the window, a coherence that outlives it has to be cut somewhere, and cutting it with a step puts a sinc ringing across the whole kernel spectrum. The taper’s falling edge has the width of the grid’s temporal collar, so the response is cut with the same profile, and over the same fraction of the window, as everything else the grid absorbs. A coherence that outlives the window is noted in the debug log. Padding prevents wraparound, but a pump-probe delay entering the taper collar samples an attenuated response even when both pulses lie inside the window. Extend the window until every relevant delay precedes the collar, and check window convergence separately from sample spacing.
The dephasing is applied per density. This is the design decision the kernel
contract needs, and it is set out in docs/guide/media/raman-numerics.md: a single
raman_kernel
array cannot be right, because the dephasing depends on the density, which varies
along a pressure gradient, and a molecular gas has two dephasing times – its
rotational and vibrational coherences decay 3.5 times (H2) to 17 times (N2) apart at one
bar. So the expensive part, the undamped model responses, is sampled once per grid as
an (n_terms, 2 n_to) array (the contract’s raman_kernel_undamped), the cheap
part is one exponential per term at a decay rate that is a scalar path along z
(raman_decay_rates, from decay_rate_profiles(), so that the contract carries
paths and arrays and no physics object), and the damped kernel spectrum is recomputed
and memoised whenever the rate changes: one real transform of length 2 n_to per
change, against n_modes n_pol transforms of length n_to per stage.
Module Attributes
Amplitude a coherence may still have where the taper starts, before it is noted. |
|
Largest |
Functions
|
Sample a Raman response onto a grid, ready for a backend to convolve with. |
|
Return the decay rate |
|
Return the kernel spectrum with the convolution's normalisation folded in. |
|
Return the taper applied to the tail of the sampled response. |
|
Return the causal, tapered, damped response kernel at one density. |
|
Return each model's undamped response, causal and tapered, on the doubled grid. |
Classes
|
The Raman kernel of one response on one grid, with its spectrum cached on rho. |
- solprop.translate.raman.MAX_DECAY_EXPONENT: Final = 708.0#
Largest
rate * dt_finea decay-rate path carries.exp(-708)is the last power of e above the smallest normal double, so a rate at this cap damps a coherence to nothing within one sample. It is the finite stand-in for the infinite rate of a Dicke-narrowed line at zero density, where the polarisation is zero anyway; the same device as the limit ofsolprop.linear.LossPolicy.
- class solprop.translate.raman.RamanKernels(undamped, response, grid)[source]#
Bases:
objectThe Raman kernel of one response on one grid, with its spectrum cached on rho.
The two arrays the kernel contract needs, kept together with the transform that joins them.
undampedis sampled once;spectrum()applies the dephasings, transforms, and remembers the result, so that a propagation at constant density transforms the kernel once and one along a pressure gradient transforms it once per density the stepper asks for.Only the most recent density is remembered, which is the right behaviour here: a gradient presents a new density at every stage, so a growing cache would hold thousands of spectra and hit none of them.
Build one with
build_raman_kernels().- Attributes:
- undampednumpy.ndarray
Each model’s undamped, causal, tapered response, shape
(n_terms, 2 n_to), m^5 V^-2 s^-1. The contract’sraman_kernel_undamped.- responseRamanResponse
The response the kernels came from; supplies the dephasings and the fraction.
- gridTimeGrid
The grid they were sampled on.
- Raises:
- TranslationError
If the kernel array does not match the response and the grid.
Examples
>>> from solprop.grid import FieldGrid >>> from solprop.nonlinear.raman import response_for >>> from solprop.translate.raman import build_raman_kernels >>> grid = FieldGrid.from_limits(0.1, 800e-9, (200e-9, 4e-6), 2e-12) >>> kernels = build_raman_kernels(response_for("SiO2"), grid) >>> kernels.n_terms 1 >>> kernels.spectrum(1.0).shape == (grid.n_to + 1,) True >>> kernels.recomputations 1
- decay_rates(density)[source]#
Return the decay rate of each term along
z, for a density path.- Parameters:
- densitysolprop.profiles.ZProfile
The number density along the waveguide, m^-3.
- Returns:
- tuple of solprop.profiles.ZProfile
The contract’s
raman_decay_rates, one path per row of the kernel; seedecay_rate_profiles().
Examples
>>> from solprop.grid import FieldGrid >>> from solprop.nonlinear.raman import response_for >>> from solprop.physdata import Gas, MaterialSelection >>> from solprop.profiles import Constant >>> from solprop.translate.raman import build_raman_kernels >>> grid = FieldGrid.from_limits(0.1, 800e-9, (200e-9, 4e-6), 2e-12) >>> selection = MaterialSelection(carrier_wavelength=800e-9) >>> response = response_for(Gas.N2, selection=selection) >>> kernels = build_raman_kernels(response, grid) >>> len(kernels.decay_rates(Constant(2.5e25))) 72
- property dephasings: tuple[Dephasing, ...][source]#
Return the dephasing of each term, in the order of
undamped.- Returns:
- tuple of Dephasing
One entry per row of the kernel;
decay_rates()turns them into the paths the contract carries.
- property n_terms: int[source]#
Return the number of independently damped terms.
- Returns:
- int
One for a glass or a vibration-only gas, two for a gas with both a rotational and a vibrational coherence.
- property recomputations: int[source]#
Return how many times the spectrum has been transformed.
- Returns:
- int
One per distinct density asked for in succession; repeated calls at the same density do not add to it.
- response: RamanResponse#
- spectrum(density)[source]#
Return the damped kernel spectrum at a density, transforming if it changed.
- Parameters:
- densityfloat
Number density in m^-3, non-negative.
- Returns:
- numpy.ndarray
The one-sided spectrum with the
dtscaling folded in, shape(n_to + 1,), complex. Read-only, because it is the cached array itself.
- Raises:
- MaterialError
If the density is negative.
Examples
>>> from solprop.grid import FieldGrid >>> from solprop.nonlinear.raman import response_for >>> from solprop.translate.raman import build_raman_kernels >>> grid = FieldGrid.from_limits(0.1, 800e-9, (200e-9, 4e-6), 2e-12) >>> kernels = build_raman_kernels(response_for("SiO2"), grid) >>> first = kernels.spectrum(1.0) >>> second = kernels.spectrum(1.0) >>> (first is second, kernels.recomputations) (True, 1)
- undamped: Float[ndarray, 'n_terms n_to_doubled']#
- solprop.translate.raman.WINDOW_RESIDUAL_TOLERANCE: Final = 0.001#
Amplitude a coherence may still have where the taper starts, before it is noted.
The taper multiplies whatever is left at that delay down to zero over the collar’s width. A coherence damped to a thousandth of its initial amplitude by then is cut with a small absolute change; one that is still at a tenth outlives the window. The taper changes responses at delays within its collar, and the kernel spectrum resolves that line only to the window’s own width. The debug log reports the residual once per density; it does not certify window convergence.
- solprop.translate.raman.build_raman_kernels(response, grid)[source]#
Sample a Raman response onto a grid, ready for a backend to convolve with.
- Parameters:
- responseRamanResponse
The response to sample.
- gridTimeGrid
The grid supplying the fine time axis and the temporal collar.
- Returns:
- RamanKernels
The undamped rows, the dephasings and the density-cached spectrum.
- Raises:
- TranslationError
If the collar leaves no untapered region.
Examples
>>> from solprop.grid import EnvelopeGrid >>> from solprop.nonlinear.raman import response_for >>> from solprop.physdata import Gas, MaterialSelection >>> from solprop.translate.raman import build_raman_kernels >>> grid = EnvelopeGrid.from_limits(0.1, 800e-9, (400e-9, 2e-6), 2e-12) >>> selection = MaterialSelection(carrier_wavelength=800e-9) >>> response = response_for(Gas.N2, selection=selection) >>> build_raman_kernels(response, grid).n_terms 72
- solprop.translate.raman.decay_rate_profiles(response, density, grid)[source]#
Return the decay rate
1/tau2of each term as a path alongz.The contract’s
raman_decay_rates: the translation layer composes each term’ssolprop.nonlinear.raman.Dephasingwith the density path, so that what reaches a backend is a scalar path and an array per term, and no physics object. A backend damps rowkoframan_kernel_undampedbyexp(-rate_k(z) t).The paths are finite everywhere. A Dicke-narrowed line has an infinite rate at zero density; the path carries
MAX_DECAY_EXPONENTdivided by the fine sample spacing instead, which damps the term to nothing within one sample – the same answersolprop.nonlinear.raman.Dephasing.damping()gives, and one the density multiplies by zero in any case. The breakpoints and the span are the density path’s own: the rate can kink only where the density does.- Parameters:
- responseRamanResponse
The response whose terms are damped.
- densitysolprop.profiles.ZProfile
The number density along the waveguide, m^-3.
- gridTimeGrid
The grid whose fine sample spacing sets the cap.
- Returns:
- tuple of solprop.profiles.ZProfile
One path per term, in the order of the response’s models, in s^-1.
Examples
>>> import numpy as np >>> from solprop.grid import FieldGrid >>> from solprop.nonlinear.raman import response_for >>> from solprop.profiles import Constant >>> from solprop.translate.raman import decay_rate_profiles >>> grid = FieldGrid.from_limits(0.1, 800e-9, (200e-9, 4e-6), 2e-12) >>> (path,) = decay_rate_profiles(response_for("SiO2"), Constant(1.0), grid) >>> path(np.array([0.0, 0.05])).tolist() [0.0, 0.0]
- solprop.translate.raman.kernel_spectrum(kernel, dt)[source]#
Return the kernel spectrum with the convolution’s normalisation folded in.
A backend then computes the convolved drive as
irfft(kernel_spectrum * rfft(drive_padded)): the sample spacing that turns the discrete sum into an integral is already here, and the inverse transform’s1/Nsupplies the rest. Folding it into the kernel rather than multiplying bydtafter the product means it is applied once per density change instead of once per column.The spectrum is in the internal FFT convention (
docs/dev/conventions.mdsection 2) and must not be passed throughsolprop.fourier.to_optics(): the sign of its imaginary part is what distinguishes Raman gain from Raman loss, and conjugating it would silently swap the Stokes and anti-Stokes sides.- Parameters:
- kernelnumpy.ndarray
The sampled kernel on the doubled grid, shape
(2 n_to,), real.- dtfloat
Sample spacing of the fine time grid, s, positive.
- Returns:
- numpy.ndarray
The one-sided spectrum, shape
(n_to + 1,), complex.
- Raises:
- TranslationError
If the sample spacing is not positive and finite, or the kernel is not real, or its length is odd so that it is not a doubled grid.
Examples
>>> import numpy as np >>> from solprop.translate.raman import kernel_spectrum >>> kernel = np.zeros(8) >>> kernel[1] = 1.0 >>> kernel_spectrum(kernel, 2.0).shape (5,)
- solprop.translate.raman.kernel_taper(grid)[source]#
Return the taper applied to the tail of the sampled response.
A Planck taper (
solprop.grid.planck_taper()) that is one from zero delay until the lastt_collar.width_highof the response’s span, then falls smoothly to zero at the final sample. The rising edge is placed below zero delay, so the taper is exactly one att = 0: the response must not be attenuated where it is strongest.Using the grid’s own collar width rather than a fixed fraction of the window keeps one statement of “how much of the window is boundary” instead of two that can disagree.
- Parameters:
- gridTimeGrid
The grid whose fine time axis and temporal collar the kernel follows.
- Returns:
- numpy.ndarray
Taper values in
[0, 1], shape(n_to,), one at the start and zero at the end.
- Raises:
- TranslationError
If the collar’s falling edge is not narrower than the response’s own span, so that there is no untapered region left.
Examples
>>> from solprop.grid import FieldGrid >>> from solprop.translate.raman import kernel_taper >>> grid = FieldGrid.from_limits(0.1, 800e-9, (200e-9, 4e-6), 2e-12) >>> taper = kernel_taper(grid) >>> float(taper[0]), float(taper[-1]) (1.0, 0.0)
- solprop.translate.raman.sample_kernel(response, grid, density)[source]#
Return the causal, tapered, damped response kernel at one density.
On the untapered part this is bit for bit
solprop.nonlinear.raman.RamanResponse.impulse(), because the taper is exactly one there and the terms are summed in the same order.- Parameters:
- responseRamanResponse
The response to sample.
- gridTimeGrid
The grid supplying the fine time axis and the temporal collar.
- densityfloat
Number density in m^-3, non-negative.
- Returns:
- numpy.ndarray
Shape
(2 n_to,), m^5 V^-2 s^-1, zero in its second half.
- Raises:
- TranslationError
If the collar leaves no untapered region.
Examples
>>> import numpy as np >>> from solprop.grid import FieldGrid >>> from solprop.nonlinear.raman import response_for >>> from solprop.translate.raman import sample_kernel >>> grid = FieldGrid.from_limits(0.1, 800e-9, (200e-9, 4e-6), 2e-12) >>> kernel = sample_kernel(response_for("SiO2"), grid, 1.0) >>> bool(np.all(kernel[grid.n_to :] == 0.0)) True
- solprop.translate.raman.undamped_kernels(response, grid)[source]#
Return each model’s undamped response, causal and tapered, on the doubled grid.
This is the expensive half of the kernel and the half that does not move with the density, so it is sampled once per grid and reused. The rows are the contract’s
raman_kernel_undamped;decay_rate_profiles()gives the matchingraman_decay_rates.The overall fraction is not folded in here: it is applied once to the sum in
sample_kernel(), so that the arithmetic matchessolprop.nonlinear.raman.RamanResponse.impulse()operation for operation.- Parameters:
- responseRamanResponse
The response whose models are sampled.
- gridTimeGrid
The grid supplying the fine time axis and the temporal collar.
- Returns:
- numpy.ndarray
Shape
(n_terms, 2 n_to), m^5 V^-2 s^-1. The firstn_tocolumns hold the response from zero delay; the rest is the zero padding the linear convolution needs.
- Raises:
- TranslationError
If the collar leaves no untapered region.
Examples
>>> from solprop.grid import FieldGrid >>> from solprop.nonlinear.raman import response_for >>> from solprop.translate.raman import undamped_kernels >>> grid = FieldGrid.from_limits(0.1, 800e-9, (200e-9, 4e-6), 2e-12) >>> kernels = undamped_kernels(response_for("SiO2"), grid) >>> kernels.shape == (1, 2 * grid.n_to) True