solprop.noise#
Shot-noise realisations: one photon per mode per frequency bin.
Two noise models share one draw. The input model of the supercontinuum literature
[2] adds a realisation to the initial field once, at z = 0, and lets the
propagation carry it; the modified model of Chen and Wise [1] keeps it as a source
term injected into the nonlinear operator at every step, which seeds spontaneous
processes without the artificial four-wave-mixing phase matching that a propagated
background produces. The modified field is advanced by the unitary part of the linear
operator rather than held constant in z: holding it constant is exact only in the
dispersion-free gain model it came from (see docs/dev/decisions.md).
The amplitude is fixed by docs/dev/conventions.md §4 and pairs with the spectral
density of solprop.fourier.spectral_density(), so that every bin of the active
band carries exactly hbar omega: one photon. The phase is uniform on [0, 2 pi)
and is the only random quantity. Both grids are masked to the active band, so no noise
sits in the zero-frequency bin or above the window edge of a field grid.
Randomness is explicit: the caller passes a numpy.random.Generator, and the
entropy it was created with is recorded on the realisation so that a run can be
reproduced from its output file.
The manual page is docs/guide/media/noise.md.
References#
Y. Chen and F. W. Wise, “A simple accurate way to model noise-seeded ultrafast nonlinear processes”, arXiv:2410.20567 (2024).
J. M. Dudley, G. Genty and S. Coen, “Supercontinuum generation in photonic crystal fiber”, Rev. Mod. Phys. 78, 1135 (2006). doi:10.1103/RevModPhys.78.1135
Module Attributes
Which of the two shot-noise models a realisation belongs to. |
Functions
|
Draw the traditional input-noise realisation, added to the field at |
|
Draw the modified-model realisation, injected as a source at every step. |
|
Return the photon number in each frequency bin of a stored spectrum. |
|
Draw one photon per admitted transverse column, polarisation and bin. |
|
Carry one draw into a declared change of channel basis, covariantly. |
Classes
|
One drawn shot-noise field, with the seed that produced it. |
- solprop.noise.NoiseModel#
Which of the two shot-noise models a realisation belongs to.
alias of
Literal[‘input’, ‘modified’]
- class solprop.noise.NoiseRealisation(spectrum, seed, model)[source]#
Bases:
objectOne drawn shot-noise field, with the seed that produced it.
Equality is by identity (
eq=False) becausespectrumis an array; compare the seeds instead, which is what reproducibility actually means.- Attributes:
- spectrumnumpy.ndarray
Stored noise spectrum, shape
(n_modes, n_pol, n_omega), complex, in the square root of watts for modes, or the spatial transform’s field units for a spatial run, in the internal FFT convention. Zero outside the active band.- seedint or None
Entropy of the generator the realisation was drawn from, or
Nonewhen it could not be recovered. Recorded so that a run is reproducible and resumable.- model{‘input’, ‘modified’}
Which model the realisation is for. The draw is the same; the difference is entirely in how the driver consumes it.
Examples
>>> import numpy as np >>> from solprop.grid import EnvelopeGrid >>> from solprop.noise import input_noise >>> grid = EnvelopeGrid.from_limits(1.0, 835e-9, (400e-9, 2e-6), 2e-12) >>> realisation = input_noise(grid, np.random.default_rng(11)) >>> realisation.model, realisation.seed ('input', 11)
- spectrum: Complex[ndarray, 'n_modes n_pol n_omega']#
- solprop.noise.input_noise(grid, rng, *, n_modes=1, n_pol=1)[source]#
Draw the traditional input-noise realisation, added to the field at
z = 0.This is the model of the supercontinuum literature: one photon per mode per bin with a random phase, superposed on the input pulse and then propagated like any other field. It seeds modulation instability and spontaneous processes correctly wherever the seeding happens near the input, and it costs nothing during the run; its weakness is that dispersion phase-matches the propagated background into four-wave mixing that should not be there.
- Parameters:
- gridsolprop.grid.TimeGrid
Grid to draw on; its
kindselects the amplitude and its active mask decides which bins are populated.- rngnumpy.random.Generator
Source of the random phase. Build it with
numpy.random.default_rng()so that its seed is recorded on the realisation.- n_modesint, optional
Number of modes, at least 1, each with an independent realisation.
- n_polint, optional
Number of polarisation components, 1 or 2, each with an independent realisation.
- Returns:
- NoiseRealisation
The realisation, with
model = 'input'.
- Raises:
- NoiseError
If
n_modesorn_polis smaller than one.
Examples
>>> import numpy as np >>> from solprop.grid import FieldGrid >>> from solprop.noise import input_noise, photons_per_bin >>> grid = FieldGrid.from_limits(1.0, 800e-9, (200e-9, 2e-6), 400e-15) >>> realisation = input_noise(grid, np.random.default_rng(3)) >>> photons = photons_per_bin(grid, realisation.spectrum)[0, 0] >>> bool(np.allclose(photons[grid.active_mask], 1.0, rtol=1e-12, atol=0.0)) True
- solprop.noise.modified_noise(grid, rng, *, n_modes=1, n_pol=1)[source]#
Draw the modified-model realisation, injected as a source at every step.
The draw is identical to
input_noise()— the same amplitudes, the same mask, the same stream for the same seed — and the two differ only in how the driver uses the result. Here the realisation is a source term added inside the nonlinear operator at every stage, advanced by the unitary part of the cumulative linear operator so that it disperses but neither grows nor decays (seedocs/dev/decisions.md); holding it constant in z would be exact only in the dispersion-free gain model it came from. Expect roughly eight times as many integrator steps as the input model, because the source is broadband and never decays.- Parameters:
- gridsolprop.grid.TimeGrid
Grid to draw on.
- rngnumpy.random.Generator
Source of the random phase.
- n_modesint, optional
Number of modes, at least 1, each with an independent realisation.
- n_polint, optional
Number of polarisation components, 1 or 2, each with an independent realisation.
- Returns:
- NoiseRealisation
The realisation, with
model = 'modified'.
- Raises:
- NoiseError
If
n_modesorn_polis smaller than one.
Examples
>>> import numpy as np >>> from solprop.grid import EnvelopeGrid >>> from solprop.noise import input_noise, modified_noise >>> grid = EnvelopeGrid.from_limits(1.0, 835e-9, (400e-9, 2e-6), 2e-12) >>> one = input_noise(grid, np.random.default_rng(5)) >>> other = modified_noise(grid, np.random.default_rng(5)) >>> bool(np.array_equal(one.spectrum, other.spectrum)), other.model (True, 'modified')
- solprop.noise.photons_per_bin(grid, spectrum)[source]#
Return the photon number in each frequency bin of a stored spectrum.
n_k = S(omega_k) d_omega / (hbar omega_k)withSthe spectral energy density ofsolprop.fourier.spectral_density(). For a shot-noise realisation this is 1 in every active bin, which is the definition the amplitudes come from; for a pulse it says how far above the quantum floor the field sits, bin by bin.Bins at or below zero frequency have no photon number and are reported as zero.
- Parameters:
- gridsolprop.grid.TimeGrid
Grid the spectrum belongs to.
- spectrumnumpy.ndarray
Stored spectrum, shape
(..., n_omega), in the square root of watts.
- Returns:
- numpy.ndarray
Photon number per bin, dimensionless, of the same shape as
spectrum.
Examples
>>> import numpy as np >>> from solprop.grid import EnvelopeGrid >>> from solprop.noise import input_noise, photons_per_bin >>> grid = EnvelopeGrid.from_limits(1.0, 835e-9, (400e-9, 2e-6), 2e-12) >>> realisation = input_noise(grid, np.random.default_rng(2)) >>> photons = photons_per_bin(grid, realisation.spectrum) >>> float(round(photons[0, 0][grid.active_mask].mean(), 12)) 1.0
- solprop.noise.spatial_noise(flux, rng, *, model='input', n_pol=1)[source]#
Draw one photon per admitted transverse column, polarisation and bin.
- Parameters:
- fluxsolprop.spatial.SpatialFlux
The entrance plane’s measure, on a radial or Cartesian grid; a
solprop.modes.slab.PlanarGridis refused by name.- rngnumpy.random.Generator
Explicit source of random phases.
- model{‘input’, ‘modified’}, optional
How the driver consumes the draw; both use the same normalization.
- n_polint, optional
Independent polarisation components, 1 or 2.
- Returns:
- NoiseRealisation
Internal radial spectrum in V m or Cartesian spectrum in V/m, retaining the original draw’s seed and model.
- Raises:
- NoiseError
If the model or polarisation count is invalid, or the grid is not a radial or Cartesian one (a planar grid has no noise draw yet).
Notes
A column’s energy is
eps0 c w_j / 2times the temporal spectral energy functional weighted by the real admittancec Re(k_z)/omegaof the medium at the entrance (SEP 34 §4.3). Dividing the existing modal draw by the square root of that weight therefore leaves exactly one photon in every admitted (propagating, active, positive-frequency) degree of freedom, in both models. A bin with no forward admittance is not a power port and carries no photon; the random stream is drawn as before, so seeds keep their meaning.On a line uniform in y the same draw is one photon per column and bin per metre of y: the line’s one
k_y = 0column stands for a unit period in y, so a column’s energy is in J/m. This is a declared convention, not a derivation of the vacuum noise of a beam of finite height.
- solprop.noise.transformed_noise(realisation, transform)[source]#
Carry one draw into a declared change of channel basis, covariantly.
The shot-noise draw is one photon per channel per bin with a fixed modulus and a random phase, and that is a statement about an orthonormal true-power basis: it is where “one photon in this channel” means an energy of
hbar omega. A run in a declared unitary basis therefore draws once, in the reference basis, with the same seed and the same stream, and this function carries that one realisation into the run’s coordinates. It is not a second draw and does not touch the generator.The rule, and why it is the inverse. With
Uthe declared transform, whose columns are the new basis vectors written in the old ones (solprop.modes.guide.ChannelBasis), a physical field with reference-basis amplitudeschas declared-basis amplitudesd = U.H c: the fields carryUand the amplitudes carry its inverse, which for a unitaryUisU.H. So the realisation is multiplied on its channel axis byU.H.What is conserved and what is not.
sum_m |d_m|^2 = c.H U U.H c = sum_n |c_n|^2for a unitaryU, so the total photon number in each bin is exactly what it was; the per-channel numbers are redistributed, and the moduli are no longer all equal. That is the physics the A2 companion names – “fixed modulus random phases are not invariant in distribution under arbitrary unitary mixing” – and it is why the draw is made once, in the basis it is defined in, rather than again in each basis a run chooses.- Parameters:
- realisationNoiseRealisation
A draw made in the orthonormal true-power reference basis, whose spectrum has one row per reference channel.
- transformnumpy.ndarray
The declared unitary
U, shape(n_modes, n_modes), dimensionless.
- Returns:
- NoiseRealisation
The same realisation in the declared coordinates, with its seed and model unchanged, so that reproducibility and the resume key are untouched.
- Raises:
- NoiseError
If the transform is not a square matrix of the realisation’s own channel count.
Examples
>>> import numpy as np >>> from solprop.grid import FieldGrid >>> from solprop.noise import input_noise, photons_per_bin, transformed_noise >>> grid = FieldGrid.from_limits(1.0, 800e-9, (200e-9, 2e-6), 400e-15) >>> drawn = input_noise(grid, np.random.default_rng(3), n_modes=2) >>> turned = transformed_noise(drawn, np.array([[1, 1], [-1j, 1j]]) / np.sqrt(2)) >>> before = photons_per_bin(grid, drawn.spectrum).sum(axis=(0, 1)) >>> after = photons_per_bin(grid, turned.spectrum).sum(axis=(0, 1)) >>> bool(np.allclose(before, after, rtol=1e-12, atol=0.0)), turned.seed (True, 3)