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#

[1]

Y. Chen and F. W. Wise, “A simple accurate way to model noise-seeded ultrafast nonlinear processes”, arXiv:2410.20567 (2024).

[2]

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

NoiseModel

Which of the two shot-noise models a realisation belongs to.

Functions

input_noise(grid, rng, *[, n_modes, n_pol])

Draw the traditional input-noise realisation, added to the field at z = 0.

modified_noise(grid, rng, *[, n_modes, n_pol])

Draw the modified-model realisation, injected as a source at every step.

photons_per_bin(grid, spectrum)

Return the photon number in each frequency bin of a stored spectrum.

spatial_noise(flux, rng, *[, model, n_pol])

Draw one photon per admitted transverse column, polarisation and bin.

transformed_noise(realisation, transform)

Carry one draw into a declared change of channel basis, covariantly.

Classes

NoiseRealisation(spectrum, seed, model)

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: object

One drawn shot-noise field, with the seed that produced it.

Equality is by identity (eq=False) because spectrum is 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 None when 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)
model: Literal['input', 'modified']#
seed: int | None#
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 kind selects 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_modes or n_pol is 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 (see docs/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_modes or n_pol is 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) with S the spectral energy density of solprop.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.PlanarGrid is 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 / 2 times the temporal spectral energy functional weighted by the real admittance c Re(k_z)/omega of 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 = 0 column 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 U the declared transform, whose columns are the new basis vectors written in the old ones (solprop.modes.guide.ChannelBasis), a physical field with reference-basis amplitudes c has declared-basis amplitudes d = U.H c: the fields carry U and the amplitudes carry its inverse, which for a unitary U is U.H. So the realisation is multiplied on its channel axis by U.H.

What is conserved and what is not. sum_m |d_m|^2 = c.H U U.H c = sum_n |c_n|^2 for a unitary U, 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)