solprop.pulses#

Initial pulses on either grid: shape, exact scaling and the user’s spectral phase.

Every builder here is a frozen dataclass with one method, build(grid), which returns the initial spectrum in the canonical (n_modes, n_pol, n_omega) layout of docs/dev/conventions.md §6 with both leading axes equal to one. The spectrum is in the internal FFT convention, ready to be handed to the translation layer.

Signs. Spectral phases, Taylor coefficients and delays are given in the optics convention (docs/dev/conventions.md §2): a positive group-delay dispersion chirps the pulse positively, and a positive t0 makes it arrive later. They are converted to the internal convention exactly once, when the phase factor is formed, with solprop.fourier.to_internal(); a measured absolute spectrum additionally needs the half-window origin shift and goes through solprop.fourier.spectrum_to_internal(). This module is the first of the three boundary places where the optics sign convention meets the internal FFT convention (see docs/dev/decisions.md), and nothing else in it conjugates anything.

Scaling. A pulse is specified by its energy or by its peak_power, never both. Both are realised exactly on the grid, to the round-off of one rescaling, and both are applied after the spectral phase, so the peak power of a chirped pulse is the peak power the caller asked for.

The manual page is docs/guide/setup/pulses.md.

Functions

load_spectrum(path, *, energy[, axis, ...])

Read a measured spectral power density and phase from a text file.

Classes

CWPulse(*, wavelength, average_power[, ...])

A continuous-wave field, monochromatic or with a sech**2 power spectrum.

CustomPulse(*, wavelength[, energy, ...])

A pulse whose power envelope is an arbitrary callable of time.

DataPulse(*, omega, spectrum, energy[, ...])

A pulse from a measured complex spectrum, interpolated onto the grid.

GaussianPulse(*, wavelength[, energy, ...])

A (super-)Gaussian pulse of given intensity FWHM.

IncidentField(field, energy)

An incident beam, normalised over its whole plane, sampled at given points.

PlanarPulse(temporal, waist[, order, propz, ...])

Launch an astigmatic Gaussian beam into the hybrid planar grid.

Pulse(*args, **kwargs)

The one method every input pulse provides.

PulseSum(pulses)

Several pulses launched together: the input is the sum of their fields.

SechPulse(*, wavelength[, energy, ...])

A sech**2 pulse, specified by its FWHM or by its natural width.

ShapedPulse(*, wavelength[, energy, ...])

Common parameters and build path of a pulse defined by its power envelope.

SpatialPulse(temporal, waist[, order, ...])

Wrap a temporal pulse in an energy-normalised transverse beam.

TimeDataPulse(*, t, field, energy[, phase, ...])

A pulse from a measured field in time, transformed exactly onto the grid.

class solprop.pulses.CWPulse(*, wavelength, average_power, bandwidth=0.0, seed=None)[source]#

Bases: Displayed

A continuous-wave field, monochromatic or with a sech**2 power spectrum.

The spectrum is written straight onto the grid’s own bins, so the field is periodic in the time window by construction and needs no apodisation; the average power is then realised in one pass over the flat part of the temporal window, rather than by alternating between a fixed spectral magnitude and a windowed field until the two agree. The single pass gives the requested spectral shape and the requested power at a bounded cost.

Attributes:
wavelengthfloat

Centre wavelength, m. Must fall inside the grid’s active band.

average_powerfloat

Average power, W, over the samples where the temporal collar is exactly one.

bandwidthfloat

Full width at half maximum of the power spectrum, in wavelength, m. Zero, the default, gives a monochromatic field on the single nearest bin. It is mapped to frequency at first order, Δω = 2πcΔλ/λ² at the centre wavelength: the sech**2 spectrum has that frequency FWHM, the frequency equivalent of the requested width, and its wavelength FWHM is bandwidth only in the narrowband limit.

seedint or None

Seed of the uniform random spectral phase used when bandwidth is positive, so that a realisation is reproducible. Unused for a monochromatic field.

Raises:
PulseError

If the wavelength, power or bandwidth is invalid.

Examples

>>> import numpy as np
>>> from solprop.grid import EnvelopeGrid
>>> from solprop.pulses import CWPulse
>>> grid = EnvelopeGrid.from_limits(1.0, 1030e-9, (900e-9, 1.2e-6), 20e-12)
>>> spectrum = CWPulse(wavelength=1030e-9, average_power=5.0).build(grid)
>>> spectrum.shape[:2]
(1, 1)
average_power: float#
bandwidth: float = 0.0#
build(grid)[source]#

Return the continuous-wave spectrum on the given grid.

Parameters:
gridsolprop.grid.TimeGrid

Grid to build on.

Returns:
numpy.ndarray

Stored spectrum, shape (1, 1, n_omega), in the square root of watts.

Raises:
PulseError

If the centre wavelength falls outside the grid’s active band.

Examples

>>> import numpy as np
>>> from solprop import fourier
>>> from solprop.grid import EnvelopeGrid
>>> from solprop.pulses import CWPulse
>>> grid = EnvelopeGrid.from_limits(1.0, 1030e-9, (900e-9, 1.2e-6), 20e-12)
>>> spectrum = CWPulse(wavelength=1030e-9, average_power=5.0).build(grid)
>>> samples = fourier.inverse(spectrum[0, 0], kind=grid.kind)
>>> flat = grid.t_window == 1.0
>>> bool(np.isclose(np.mean(np.abs(samples[flat]) ** 2), 5.0, rtol=1e-12))
True
seed: int | None = None#
wavelength: float#
class solprop.pulses.CustomPulse(*, wavelength, energy=None, peak_power=None, phase=(), t0=0.0, shape_fn)[source]#

Bases: ShapedPulse

A pulse whose power envelope is an arbitrary callable of time.

Attributes:
wavelengthfloat

Centre wavelength of the pulse, m, and the reference of the Taylor phase.

shape_fncallable

I(t) returning the power envelope for an array of times in seconds. It must be finite and non-negative on the grid; its normalisation is irrelevant.

energyfloat or None

Pulse energy, J. Exactly one of energy and peak_power is given.

peak_powerfloat or None

Peak power, W, realised after the spectral phase.

phasearray_like

Optics-convention Taylor coefficients about wavelength: radians at order 0 and s**n at order n thereafter.

t0float

Delay, s, positive for later arrival.

Examples

>>> import numpy as np
>>> from solprop.pulses import CustomPulse
>>> triangle = CustomPulse(
...     wavelength=800e-9,
...     shape_fn=lambda t: np.clip(1.0 - np.abs(t) / 20e-15, 0.0, None),
...     energy=1e-6,
... )
>>> float(triangle.intensity(np.array([10e-15]))[0])
0.5
intensity(t)[source]#

Return the caller’s power envelope evaluated on the time samples.

Parameters:
tnumpy.ndarray

Time samples, s, shape (n_t,).

Returns:
numpy.ndarray

The value of shape_fn(t), shape (n_t,).

Examples

>>> import numpy as np
>>> from solprop.pulses import CustomPulse
>>> flat = CustomPulse(
...     wavelength=800e-9, shape_fn=np.ones_like, energy=1e-6
... )
>>> flat.intensity(np.zeros(3)).tolist()
[1.0, 1.0, 1.0]
shape_fn: Callable[[Float[ndarray, 'n_t']], Float[ndarray, 'n_t']]#
class solprop.pulses.DataPulse(*, omega, spectrum, energy, spectral_phase=None, phase=(), t0=0.0, wavelength=None)[source]#

Bases: Displayed

A pulse from a measured complex spectrum, interpolated onto the grid.

The stored form is the spectrum because that is what a measurement gives: a spectral power density and a retrieved phase. The phase is absolute and referenced to t = 0 in the optics convention, so it fixes where in the window the pulse sits; it therefore goes through solprop.fourier.spectrum_to_internal(), which carries the half-window origin shift as well as the conjugation.

Magnitude and phase are interpolated separately with a cubic B-spline, never the complex spectrum itself, whose real and imaginary parts oscillate. Outside the data’s own frequency range the spectrum is zero, and the grid’s spectral collar is applied so that nothing is left sitting on the window edge.

The phase interpolated is spectral_phase when the pulse holds one, as from_spectral_density() makes it, however fast that phase varies. Otherwise it is numpy.unwrap(numpy.angle(spectrum)), which cannot tell a phase change of more than pi between neighbouring samples from a smaller one of the other sign: such a spectrum, of a strongly chirped pulse or one far from t = 0 on a coarse axis, is built with the wrong phase and without any warning. Give that phase through from_spectral_density(), or sample it more finely.

A measured field in time is a TimeDataPulse, which needs no interpolation.

Attributes:
omeganumpy.ndarray

Angular frequency axis of the data, rad/s, shape (n_data,), strictly increasing and positive. It need not be uniform.

spectrumnumpy.ndarray

Measured complex spectrum on omega, shape (n_data,), optics convention, arbitrary units.

spectral_phasenumpy.ndarray or None

Optics-convention phase of spectrum, unwrapped, rad, shape (n_data,), interpolated as it stands. It must agree with spectrum modulo 2 pi. None, the default, recovers the phase from spectrum by unwrapping it.

energyfloat

Pulse energy to scale to, J.

phasearray_like

Further optics-convention Taylor coefficients, applied on top of the phase already in the data, about wavelength; radians at order 0 and s**n thereafter.

t0float

Further delay, s, on top of the arrival time the data’s own phase implies.

wavelengthfloat or None

Reference wavelength of the Taylor phase, m. Defaults to the spectral centre of mass of the data.

Raises:
PulseError

If the arrays disagree in length, are too short to interpolate, are not finite, omega is not strictly increasing and positive, or spectral_phase is not the phase of spectrum.

Examples

>>> import numpy as np
>>> from solprop.pulses import DataPulse
>>> omega = np.linspace(1.5e15, 3.5e15, 256)
>>> intensity = np.exp(-(((omega - 2.35e15) / 1e14) ** 2))
>>> pulse = DataPulse.from_spectral_density(
...     omega, intensity, np.zeros_like(omega), energy=1e-6
... )
>>> float(round(pulse.reference_wavelength * 1e9))
802.0
build(grid)[source]#

Return the initial spectrum of the tabulated pulse on the given grid.

Parameters:
gridsolprop.grid.TimeGrid

Grid to build on.

Returns:
numpy.ndarray

Stored spectrum, shape (1, 1, n_omega), in the square root of watts.

Raises:
PulseError

If the delay leaves the window, or the data does not reach the grid’s active band at all.

Examples

>>> import numpy as np
>>> from solprop.grid import FieldGrid
>>> from solprop.pulses import DataPulse
>>> omega = np.linspace(4e14, 1.1e16, 4096)  # covers the grid's active band
>>> intensity = np.exp(-(((omega - 2.35e15) / 1e14) ** 2))
>>> pulse = DataPulse.from_spectral_density(
...     omega, intensity, np.zeros_like(omega), energy=1e-6
... )
>>> grid = FieldGrid.from_limits(1.0, 800e-9, (200e-9, 2e-6), 400e-15)
>>> pulse.build(grid).shape[:2]
(1, 1)
energy: float#
classmethod from_spectral_density(omega, intensity, spectral_phase, *, energy, phase=(), t0=0.0, wavelength=None)[source]#

Build a pulse from a spectral power density and an unwrapped spectral phase.

This is the form a spectrometer and a phase retrieval give. Negative densities, which fits and baseline subtraction can produce, are clipped to zero. The pulse keeps the phase as given and build() interpolates it directly, so it may change by any amount between samples, provided a cubic spline resolves it.

Parameters:
omeganumpy.ndarray

Angular frequency axis, rad/s, shape (n_data,), strictly increasing.

intensitynumpy.ndarray

Spectral power density on omega, arbitrary units, shape (n_data,).

spectral_phasenumpy.ndarray

Continuous (unwrapped) optics-convention spectral phase on omega, rad, shape (n_data,).

energyfloat

Pulse energy to scale to, J.

phasearray_like, optional

Further Taylor coefficients about wavelength, s**n at order n.

t0float, optional

Further delay, s.

wavelengthfloat or None, optional

Reference wavelength of the Taylor phase, m.

Returns:
DataPulse

The pulse, with spectrum = sqrt(intensity) exp(i spectral_phase) and the phase held as spectral_phase.

Raises:
PulseError

If the three arrays disagree in length, or the data fails validation.

Examples

>>> import numpy as np
>>> from solprop.pulses import DataPulse
>>> omega = np.linspace(2.0e15, 2.7e15, 64)
>>> pulse = DataPulse.from_spectral_density(
...     omega, np.ones_like(omega), np.zeros_like(omega), energy=1e-6
... )
>>> pulse.spectrum.shape
(64,)
omega: Float[ndarray, 'n_data']#
phase: Sequence[float] | ndarray = ()#
property reference_wavelength: float[source]#

Return the wavelength the Taylor phase is expanded about, m.

Returns:
float

wavelength if it was given, otherwise the wavelength of the spectral centre of mass of the data.

spectral_phase: Float[ndarray, 'n_data_phase'] | None = None#
spectrum: Complex[ndarray, 'n_data_spectrum']#
t0: float = 0.0#
wavelength: float | None = None#
class solprop.pulses.GaussianPulse(*, wavelength, energy=None, peak_power=None, phase=(), t0=0.0, fwhm, m=1)[source]#

Bases: ShapedPulse

A (super-)Gaussian pulse of given intensity FWHM.

The envelope is exp(-(t/sigma)**(2m) / 2) with sigma = fwhm / (2 (2 ln 2)**(1/2m)), which realises the requested FWHM for every order m.

Attributes:
wavelengthfloat

Centre wavelength of the pulse, m, and the reference of the Taylor phase.

fwhmfloat

Full width at half maximum of the power envelope, s.

mint

Super-Gaussian order, at least 1. The exponent of the envelope is 2m, so m = 1 is an ordinary Gaussian and large m approaches a flat top.

energyfloat or None

Pulse energy, J. Exactly one of energy and peak_power is given.

peak_powerfloat or None

Peak power, W, realised after the spectral phase.

phasearray_like

Optics-convention Taylor coefficients about wavelength: radians at order 0 and s**n at order n thereafter.

t0float

Delay, s, positive for later arrival.

Raises:
PulseError

If the duration or the order is invalid, or the scaling request is ambiguous.

Examples

>>> import numpy as np
>>> from solprop.pulses import GaussianPulse
>>> pulse = GaussianPulse(wavelength=800e-9, fwhm=30e-15, energy=1e-6)
>>> float(pulse.intensity(np.array([0.0, 15e-15]))[1])
0.5
fwhm: float#
intensity(t)[source]#

Return the (super-)Gaussian power envelope, normalised to a peak of one.

Parameters:
tnumpy.ndarray

Time samples, s, shape (n_t,).

Returns:
numpy.ndarray

Envelope values in [0, 1], shape (n_t,).

Examples

>>> import numpy as np
>>> from solprop.pulses import GaussianPulse
>>> pulse = GaussianPulse(wavelength=800e-9, fwhm=2.0, energy=1.0, m=3)
>>> float(round(pulse.intensity(np.array([1.0]))[0], 12))
0.5
m: int = 1#
class solprop.pulses.IncidentField(field, energy)[source]#

Bases: object

An incident beam, normalised over its whole plane, sampled at given points.

The result of SpatialPulse.incident().

Attributes:
fieldnumpy.ndarray

The beam’s x and y components at the points, complex128, shape (n_nodes, 2, n_omega), in the internal convention: the electric field in V/m per square root of watt times a stored spectrum in the square root of watts, the units in which a guide’s node profiles project to channel amplitudes.

energynumpy.ndarray

The energy each bin carries through the whole plane, float64, shape (n_omega,), J; its sum is the beam’s incident energy.

energy: Float[ndarray, 'n_omega']#
field: Complex[ndarray, 'n_nodes n_pol n_omega']#
class solprop.pulses.PlanarPulse(temporal, waist, order=1, propz=0.0, jones=(1 + 0j,), displacement=(0.0, 0.0), tilt=(0.0, 0.0), *, waist_x, offset_x=0.0)[source]#

Bases: SpatialPulse

Launch an astigmatic Gaussian beam into the hybrid planar grid.

The beam is exp(-((x - offset_x)/waist_x)**2) exp(-(|y|/waist)**(2 order)) in field, carrying the temporal pulse, polarised along the slab modes’ own component (y for TE, x for TM). In x it is projected onto the slab modes (solprop.modes.slab.gaussian_projection(), with signs); in y it is sampled on the grid and transformed. The temporal pulse’s energy or peak power is the incident beam’s, over the whole beam in x and the sampled y axis: what falls on the plates is lost at the entrance, so the modes carry the coupled share of it (98.8 % into p = 1 at waist_x = 0.735 a, Arnold et al. 2010), not a beam renormalised over the gap. The geometry integrates over y, so the energy is in J and the peak power in W.

Attributes:
temporalPulse

Temporal pulse shape; its energy (J) or peak power (W) is the incident beam’s.

waistfloat

The 1/e field radius in y (the 1/e^2 intensity radius), m, positive.

orderint

Super-Gaussian order in y, at least 1.

propzfloat

Must be 0: the launch is at the entrance plane; a free-space pre-propagation before a guide is refused.

jonestuple of complex

One component, a phase; the component itself is the modes’. Two components are refused, the planar state being scalar.

waist_xfloat

The 1/e field radius in x, m, positive (keyword).

offset_xfloat

The beam centre’s displacement from the mid-plane in x, m (keyword); an offset couples to even orders.

Raises:
PulseError

If a waist, the order, offset_x, propz or jones is invalid.

Examples

>>> import numpy as np
>>> from solprop.grid import EnvelopeGrid
>>> from solprop.modes import PlanarGrid, SlabMode
>>> from solprop.pulses import GaussianPulse, PlanarPulse
>>> from solprop.spatial import SpatialMedium
>>> grid = EnvelopeGrid.from_limits(0.1, 800e-9, (650e-9, 1000e-9), 400e-15)
>>> space = PlanarGrid([SlabMode(63.5e-6, order=p) for p in (1, 3)], 1e-3, 32)
>>> pulse = PlanarPulse(
...     GaussianPulse(wavelength=800e-9, fwhm=100e-15, energy=1e-3),
...     250e-6, waist_x=0.735 * 63.5e-6,
... )
>>> pulse.build(grid, space, medium=SpatialMedium()).shape
(64, 1, 256)
build(grid, space, *, medium, z=0.0, side=None, frame=None)[source]#

Build the launched (p, k_y) spectrum on a planar grid.

The requested energy or peak power is the incident beam’s physical axial flux in the fill medium, over the whole beam in x and the sampled y axis (SEP 34): the x integral of the continuous Gaussian’s angular spectrum is taken by Gauss-Hermite quadrature of its admittance. The slab modes then carry the coupled share of that beam.

Parameters:
gridsolprop.grid.TimeGrid

Field or envelope temporal grid.

spacesolprop.modes.slab.PlanarGrid

The slab modes in x and the y axis; any other grid is refused.

mediumsolprop.spatial.SpatialMedium

The fill medium at the entrance.

zfloat, optional

Position of the entrance plane in m.

side{‘before’, ‘after’} or None, optional

Side of a declared index jump at z; required there.

framesolprop.linear.Frame or None, optional

Unused: the launch is at the entrance, with no pre-propagation.

Returns:
numpy.ndarray

Modal amplitudes in sqrt(W/m), raw-DFT transformed in y, times the temporal spectrum, shape (n_p*n_y, 1, n_omega), the mode index fastest, in the internal convention.

Raises:
PulseError

If the grid is not a planar one, the temporal pulse is not scalar, or the beam has no propagating content on the y axis.

offset_x: float = 0.0#
waist_x: float#
class solprop.pulses.Pulse(*args, **kwargs)[source]#

Bases: Protocol

The one method every input pulse provides.

A pulse is a specification, not an array: it is built against a particular grid, and the same specification may be built against a field grid and an envelope grid to give the same physical pulse in the two representations.

Examples

>>> from solprop.grid import EnvelopeGrid
>>> from solprop.pulses import GaussianPulse, Pulse
>>> pulse: Pulse = GaussianPulse(wavelength=800e-9, fwhm=30e-15, energy=1e-6)
>>> grid = EnvelopeGrid.from_limits(1.0, 800e-9, (400e-9, 2e-6), 2e-12)
>>> pulse.build(grid).shape[:2]
(1, 1)
build(grid)[source]#

Return the initial spectrum of this pulse on the given grid.

Parameters:
gridsolprop.grid.TimeGrid

Grid to build on; its kind selects the field or envelope layout.

Returns:
numpy.ndarray

Stored spectrum, shape (n_modes, n_pol, n_omega), complex, in the square root of watts and in the internal FFT convention.

class solprop.pulses.PulseSum(pulses)[source]#

Bases: Displayed

Several pulses launched together: the input is the sum of their fields.

Two-colour and multi-colour inputs – a pump and a seed for four-wave mixing, a strong pump and a weak probe for cross-phase modulation, a pulse and its delayed replica – are the sum of independently specified pulses. Each member keeps its own centre wavelength, duration, energy or peak power, spectral phase and delay, and is built and scaled on the grid exactly as it would be alone; the spectra are then added. The energies of well-separated members therefore add, and members that overlap in time and frequency interfere, as the fields do.

Attributes:
pulsestuple of Pulse

The members, at least one; any object with a build(grid) method whose spectra share a shape, typically GaussianPulse, SechPulse, CustomPulse, DataPulse or TimeDataPulse.

Examples

>>> import numpy as np
>>> from solprop import fourier
>>> from solprop.grid import FieldGrid
>>> from solprop.pulses import GaussianPulse, PulseSum
>>> grid = FieldGrid.from_limits(1.0, 800e-9, (150e-9, 3e-6), 1e-12)
>>> pump = GaussianPulse(wavelength=267e-9, fwhm=35e-15, energy=95e-6)
>>> seed = GaussianPulse(wavelength=800e-9, fwhm=35e-15, energy=45e-6)
>>> spectrum = PulseSum((pump, seed)).build(grid)
>>> energy = fourier.energy_omega(spectrum[0, 0], grid.dt, kind=grid.kind)
>>> bool(np.isclose(energy, 140e-6, rtol=1e-6))
True
build(grid)[source]#

Return the sum of the members’ spectra on the given grid.

Parameters:
gridsolprop.grid.TimeGrid

Grid to build on; every member is built on it independently.

Returns:
numpy.ndarray

Stored spectrum, the members’ common shape, in the square root of watts.

Raises:
PulseError

If the members build to spectra of different shapes.

pulses: tuple[Pulse, ...]#
class solprop.pulses.SechPulse(*, wavelength, energy=None, peak_power=None, phase=(), t0=0.0, fwhm=None, width=None)[source]#

Bases: ShapedPulse

A sech**2 pulse, specified by its FWHM or by its natural width.

Attributes:
wavelengthfloat

Centre wavelength of the pulse, m, and the reference of the Taylor phase.

fwhmfloat or None

Full width at half maximum of the power envelope, s. Exactly one of fwhm and width is given.

widthfloat or None

Natural width of sech**2(t/width), s, which is fwhm / (2 ln(1 + sqrt(2))).

energyfloat or None

Pulse energy, J. Exactly one of energy and peak_power is given.

peak_powerfloat or None

Peak power, W, realised after the spectral phase.

phasearray_like

Optics-convention Taylor coefficients about wavelength: radians at order 0 and s**n at order n thereafter.

t0float

Delay, s, positive for later arrival.

Raises:
PulseError

If both or neither duration is given, or the scaling request is ambiguous.

Examples

>>> import numpy as np
>>> from solprop.pulses import SechPulse
>>> pulse = SechPulse(wavelength=1030e-9, fwhm=100e-15, energy=10e-6)
>>> float(round(pulse.intensity(np.array([50e-15]))[0], 12))
0.5
fwhm: float | None = None#
intensity(t)[source]#

Return the sech**2 power envelope, normalised to a peak of one.

Parameters:
tnumpy.ndarray

Time samples, s, shape (n_t,).

Returns:
numpy.ndarray

Envelope values in (0, 1], shape (n_t,).

Examples

>>> import numpy as np
>>> from solprop.pulses import SechPulse
>>> pulse = SechPulse(wavelength=1030e-9, width=1.0, energy=1.0)
>>> float(pulse.intensity(np.array([0.0]))[0])
1.0
width: float | None = None#
class solprop.pulses.ShapedPulse(*, wavelength, energy=None, peak_power=None, phase=(), t0=0.0)[source]#

Bases: Displayed

Common parameters and build path of a pulse defined by its power envelope.

GaussianPulse, SechPulse and CustomPulse derive from this class: it holds the parameters they share and the whole of build() (the carrier, the spectral phase, the exact scaling and the canonical layout), so that the three differ only in one line of arithmetic. Subclass it and supply intensity() to add a shape of your own; it is abstract here.

Attributes:
wavelengthfloat

Centre wavelength of the pulse, m. Also the frequency the Taylor spectral phase is expanded about, which need not be the grid’s reference wavelength.

energyfloat or None

Pulse energy, J. Exactly one of energy and peak_power is given.

peak_powerfloat or None

Peak power, W, realised after the spectral phase is applied.

phasearray_like

Optics-convention spectral phase as Taylor coefficients about wavelength: [CEP (rad), group delay (s), GDD (s**2), TOD (s**3), ...]. Empty by default.

t0float

Delay, s, positive for later arrival. Must lie strictly inside the window.

build(grid)[source]#

Return the initial spectrum of this pulse on the given grid.

On a field grid the samples are sqrt(I(t)) cos(omega_p t), whose analytic signal is the pulse; on an envelope grid they are sqrt(I(t)) exp(i (omega_p - omega0) t), which is that analytic signal with the grid’s carrier removed (conventions.md §4). The spectral phase is applied to the stored spectrum and the scaling comes last.

Parameters:
gridsolprop.grid.TimeGrid

Grid to build on.

Returns:
numpy.ndarray

Stored spectrum, shape (1, 1, n_omega), in the square root of watts.

Raises:
PulseError

If the delay leaves the window, the envelope is not finite and non-negative, or the pulse carries nothing to scale on this grid.

Examples

>>> import numpy as np
>>> from solprop import fourier
>>> from solprop.grid import EnvelopeGrid
>>> from solprop.pulses import GaussianPulse
>>> grid = EnvelopeGrid.from_limits(1.0, 800e-9, (400e-9, 2e-6), 2e-12)
>>> spectrum = GaussianPulse(
...     wavelength=800e-9, fwhm=30e-15, energy=1e-6
... ).build(grid)
>>> energy = fourier.energy_omega(spectrum[0, 0], grid.dt, kind=grid.kind)
>>> bool(np.isclose(energy, 1e-6, rtol=1e-12))
True
energy: float | None = None#
intensity(t)[source]#

Return the power envelope of the pulse, in arbitrary units.

The normalisation is irrelevant: the spectrum is rescaled to the requested energy or peak power afterwards.

Parameters:
tnumpy.ndarray

Time samples, s, shape (n_t,).

Returns:
numpy.ndarray

Non-negative power envelope, shape (n_t,).

Raises:
NotImplementedError

Always; each shaped pulse supplies its own envelope.

peak_power: float | None = None#
phase: Sequence[float] | ndarray = ()#
t0: float = 0.0#
wavelength: float#
class solprop.pulses.SpatialPulse(temporal, waist, order=1, propz=0.0, jones=(1 + 0j,), displacement=(0.0, 0.0), tilt=(0.0, 0.0))[source]#

Bases: Displayed

Wrap a temporal pulse in an energy-normalised transverse beam.

Attributes:
temporalPulse

Existing temporal pulse shape. Its energy or peak power sets the whole beam’s physical energy or peak axial power through the launch plane, in the medium there (SEP 34). On a solprop.grid.LineGrid the beam is uniform in y and these are per metre of y: the energy is in J/m and the peak power in W/m (solprop.modes.slab.gaussian_to_per_unit_width() converts a beam of finite height).

waistfloat

Positive finite 1/e^2 intensity radius in m; on a line, the 1/e^2 intensity half-width in x.

orderint

Super-Gaussian order, at least 1. The field profile is exp(-(r/waist)**(2*order)), with r = |x| on a line.

propzfloat

Signed launch pre-propagation distance in m, finite. Applied by the exact longitudinal operator with the launch plane’s material frozen. Zero launches at the waist; negative values launch before it. Normalisation follows the pre-propagation, so the launch plane carries the requested quantity.

jonestuple of complex

One scalar component or two optics-convention Cartesian components (x, y). The vector is normalised to unit squared magnitude. Default is scalar.

displacementtuple of float

(d_x, d_y) in m, finite: the beam’s centre at the launch plane. A positive value moves it towards +x (+y). Default none.

tilttuple of float

(theta_x, theta_y) in rad, each below pi/2 in magnitude: a positive tilt makes the beam propagate towards +x (+y). Default none.

Raises:
PulseError

If the waist, order, distance, Jones vector, displacement or tilt is invalid.

Notes

Displacement and tilt are those of solprop.coupling.elements.Alignment, applied at the launch plane after propz: the tilt multiplies the field by exp(i k(omega) (x sin(theta_x) + y sin(theta_y))), k the launch medium’s real wave number, so every frequency leaves at the same angle, and the displacement shifts the field exactly in the periodic Cartesian representation. The energy or peak power is set after both. They are applied on a FreeGrid only: a radial grid has no direction to move along, a line grid no y, and a PlanarPulse has its own offset_x. A tilted beam’s transverse wave numbers must lie inside the grid’s band, the moved beam must be negligible at the window’s edges, and its pulse-front delay across the beam must fit in half the time window; otherwise build() refuses it.

property aligned: bool[source]#

Return whether the beam is displaced or tilted.

Returns:
bool

True when either has a nonzero component.

build(grid, space, *, medium, z=0.0, side=None, frame=None)[source]#

Build the beam’s transverse and temporal spectrum on a spatial grid.

The requested energy or peak power is the physical axial Poynting flux through the launch plane, in the medium there (SEP 34): what an energy meter in that medium reads. Each member of a PulseSum carries its own requested quantity. The inactive band and zero frequency are hard-masked as everywhere; a beam with more than rounding-level content in nonpropagating columns is refused, since no energy can be assigned to it.

Parameters:
gridsolprop.grid.TimeGrid

Field or envelope temporal grid.

spacesolprop.grid.RadialGrid or solprop.grid.FreeGrid

Transverse samples and their spectral integration measure. A solprop.modes.slab.PlanarGrid is refused: its launch is a PlanarPulse.

mediumsolprop.spatial.SpatialMedium

The medium at the launch plane; there is no vacuum default.

zfloat, optional

Position of the launch plane in m, at which the medium is evaluated.

side{‘before’, ‘after’} or None, optional

Side of a declared index jump at z; required there.

framesolprop.linear.Frame or None, optional

Launch frame, normally the run’s frame. None uses the grid’s co-moving frame without envelope THG.

Returns:
numpy.ndarray

Hankel/temporal spectrum in V m or Cartesian DFT/temporal spectrum in V/m, shape (n_modes, n_pol, n_omega), in the internal convention.

Raises:
PulseError

If the grid is a planar one, the temporal pulse is not scalar, the beam has no propagating content or more than rounding-level nonpropagating content, launch pre-propagation would overflow the field, or a displacement or tilt is refused (see the class notes).

displacement: tuple[float, float] = (0.0, 0.0)#
incident(grid, xy, *, index)[source]#

Sample the beam, normalised over its whole incident plane, at given points.

The incident-field boundary of a guide launch (SEP 31, phase 1): the beam is given its requested energy or peak power over the whole transverse plane and only then sampled where a receiving guide wants it, so light that misses the guide still counts as incident and is lost, not restored. The measure is the paraxial local one, \(M_0 = Y_0\,dA/2\) with \(Y_0 = n_{\rm ref}(\omega)\varepsilon_0 c\): the axial Poynting flux of a paraxial beam in a medium of phase index \(n_{\rm ref}\), which is what an energy meter in that medium reads (SEP 34). The plane integral of the super-Gaussian profile \(u = \exp(-(r/w)^{2N})\) is exact, \(\int|u|^2 dA = \pi w^2 2^{-1/N}\Gamma(1 + 1/N)\), so no transverse window or quadrature enters the incident energy.

A one-component Jones vector is polarised along x; two components are the x and y components. The beam is centred on the origin of xy.

Parameters:
gridsolprop.grid.TimeGrid

Temporal grid.

xynumpy.ndarray

Transverse sample positions in m, shape (n_nodes, 2).

indexnumpy.ndarray

Real phase index of the launch medium at each of the grid’s frequencies, shape (n_omega,), positive on every bin that carries flux; a zero bin carries none and is masked.

Returns:
IncidentField

The normalised field at the points and the energy each bin carries through the whole plane.

Raises:
PulseError

If the beam is a PlanarPulse or is launched away from its waist, the index is not finite and non-negative on the grid, the temporal pulse is not scalar, or the beam carries no forward flux.

Examples

>>> import numpy as np
>>> from solprop.grid import FieldGrid
>>> from solprop.pulses import GaussianPulse, SpatialPulse
>>> grid = FieldGrid.from_limits(1e-3, 800e-9, (700e-9, 900e-9), 200e-15)
>>> beam = SpatialPulse(
...     GaussianPulse(wavelength=800e-9, fwhm=30e-15, energy=1e-6), 50e-6
... )
>>> index = np.where(grid.omega > 0, 1.45, 0.0)
>>> sampled = beam.incident(grid, np.zeros((1, 2)), index=index)
>>> f"{np.sum(sampled.energy):.6e} J"  # the whole plane, wherever it is sampled
'1.000000e-06 J'
jones: tuple[complex, ...] = ((1+0j),)#
order: int = 1#
propz: float = 0.0#
temporal: Pulse#
tilt: tuple[float, float] = (0.0, 0.0)#
waist: float#
class solprop.pulses.TimeDataPulse(*, t, field, energy, phase=(), t0=0.0, wavelength=None)[source]#

Bases: Displayed

A pulse from a measured field in time, transformed exactly onto the grid.

The stored form is the record, and build() takes out its mean E_mean and evaluates the transform in the optics convention, sum_n (E_n - E_mean) exp(+i omega t_n), at the grid’s own frequencies. Nothing is interpolated, so the record may hold any number of pulses at any separation, however deep the fringes between them, and each lands where it sat for any t[0]. That is the difference from a DataPulse: a spline through the magnitude and the phase of a spectrum cannot follow a fringe to its null.

The mean is taken out because a baseline is an offset of the instrument, not part of a field that propagates, and left in it would be a step at each end of the record, whose spectrum reaches the band. A background that varies across the record stays, and is such a step where it does not vanish at the ends. So is the mean of a pulse with a net area, such as a half-cycle pulse, which this changes.

The spectrum is zero at frequencies that are not positive and above the record’s Nyquist frequency pi / dt, and the grid’s spectral collar is applied. The record’s ends are its ends: a pulse that reaches them is cut there. A record longer than the grid’s window folds back into it, as any transform sampled at the grid’s spacing must, and the build warns when that takes more than a millionth of the energy.

A real array is the electric field. A complex array is taken as the analytic signal in the optics convention, so that a positive frequency means a positive frequency, as everywhere else at the interface.

Attributes:
tnumpy.ndarray

Time samples, s, shape (n_data,), uniformly spaced and increasing.

fieldnumpy.ndarray

Measured field on t, arbitrary units, shape (n_data,), real or complex.

energyfloat

Pulse energy to scale to, J.

phasearray_like

Further optics-convention Taylor coefficients, applied on top of the phase already in the record, about wavelength; radians at order 0 and s**n thereafter.

t0float

Further delay, s, on top of the arrival time the record’s own axis implies.

wavelengthfloat or None

Reference wavelength of the Taylor phase, m. Defaults to the spectral centre of mass of the record.

Raises:
PulseError

If the arrays disagree in length, the record is too short or not finite, the axis is not uniform and increasing, or the record has nothing at positive frequencies.

Examples

>>> import numpy as np
>>> from solprop.pulses import TimeDataPulse
>>> t = np.linspace(-100e-15, 100e-15, 1024)
>>> field = np.exp(-((t / 20e-15) ** 2)) * np.cos(2.35e15 * t)
>>> pulse = TimeDataPulse(t=t, field=field, energy=1e-6)
>>> float(round(pulse.reference_wavelength * 1e9))
802.0
build(grid)[source]#

Return the initial spectrum of the recorded pulse on the given grid.

Parameters:
gridsolprop.grid.TimeGrid

Grid to build on.

Returns:
numpy.ndarray

Stored spectrum, shape (1, 1, n_omega), in the square root of watts.

Raises:
PulseError

If the delay leaves the window, or the record’s band does not reach the grid’s active band at all.

Examples

>>> import numpy as np
>>> from solprop.grid import FieldGrid
>>> from solprop.pulses import TimeDataPulse
>>> t = np.linspace(-100e-15, 100e-15, 1024)
>>> field = np.exp(-((t / 20e-15) ** 2)) * np.cos(2.35e15 * t)
>>> pulse = TimeDataPulse(t=t, field=field, energy=1e-6)
>>> grid = FieldGrid.from_limits(1.0, 800e-9, (200e-9, 2e-6), 400e-15)
>>> pulse.build(grid).shape[:2]
(1, 1)
energy: float#
field: Inexact[ndarray, 'n_data_field']#
phase: Sequence[float] | ndarray = ()#
property reference_wavelength: float[source]#

Return the wavelength the Taylor phase is expanded about, m.

Returns:
float

wavelength if it was given, otherwise the wavelength of the spectral centre of mass of the record.

t: Float[ndarray, 'n_data']#
t0: float = 0.0#
wavelength: float | None = None#
solprop.pulses.load_spectrum(path, *, energy, axis='frequency', axis_unit=None, columns=None, phase_unit='rad', phase_convention='optics', wrapped=None, floor=0.0001)[source]#

Read a measured spectral power density and phase from a text file.

The default layout is the usual output of a spectrometer and a phase retrieval, in three columns: frequency in Hz, spectral power density in arbitrary units (energy sets the scale) and spectral phase in radians. A file with two columns has a flat phase, and further columns are ignored. The pulse is built through DataPulse.from_spectral_density(), so the phase is interpolated as read, never re-derived from a complex spectrum.

Format. Columns are separated by commas on a line that holds one, otherwise by whitespace. Everything after a # is a comment, blank lines are skipped, and the first line that is not a comment may be a header of column names, which is skipped when none of its fields is a number. The rows may come in either order along the axis: they are sorted.

Axis. With axis="wavelength" the first column is a wavelength, and the power density per unit wavelength is converted to one per unit angular frequency, P_omega = P_lambda lambda**2 / (2 pi c), so that the energy in any band is the same on either axis.

Phase. The phase is in the optics convention (docs/dev/conventions.md section 2), so a delay tau is +omega tau and a positive group-delay dispersion is a positive chirp. phase_convention="fft" reads the opposite sign, which is that of a file written for Luna.jl. A column is taken as wrapped and unwrapped when wrapped says so, or, by default, when all its values lie within 2 pi (with a milliradian for the rounding of the file’s last digit); any other column is taken as it stands, so a large chirp given unwrapped survives at any sampling a spline resolves. A wrapped column is unwrapped only over the samples whose power is above floor times the peak of the power column: between them the phase is interpolated linearly across any gap, and beyond the first and last it is held at their value, so the random phase of the noise floor cannot add a turn to the band.

Power. Negative values, which a background subtraction leaves where there is no signal, are clipped to zero; one more negative than a tenth of the peak is refused, because that is not such noise.

Parameters:
pathstr, os.PathLike or io.TextIOBase

The file, UTF-8 text, or an open text stream such as an io.StringIO, which is read from where it stands.

energyfloat

Pulse energy to scale to, J.

axis{“frequency”, “wavelength”}, optional

What the axis column holds.

axis_unitstr or None, optional

Unit of the axis column. None is the axis’s SI unit: Hz for a frequency, m for a wavelength. A frequency may be in "Hz", "GHz", "THz", "PHz" or, as an angular frequency, "rad/s" or "rad/fs"; a wavelength in "m", "um" (or "µm") or "nm".

columnssequence of int or None, optional

Zero-based indices of the axis, power and, optionally, phase columns, as (axis, power) or (axis, power, phase); two indices give a flat phase. None is (0, 1, 2), or (0, 1) for a two-column file.

phase_unit{“rad”, “deg”}, optional

Unit of the phase column. A wrapped phase in degrees spans 360 and would otherwise be taken as unwrapped radians.

phase_convention{“optics”, “fft”}, optional

Sign convention of the phase column.

wrappedbool or None, optional

Whether the phase column is wrapped; None decides from its range.

floorfloat, optional

Share of the peak power, in [0, 1), below which a sample is noise for the unwrapping of a wrapped phase.

Returns:
DataPulse

The pulse, holding the read phase as its spectral_phase.

Raises:
PulseError

If there is no file at path; a line after the header is not a row of numbers, or the rows differ in length; the file has fewer than two columns or columns asks for one it does not have; a value read is not finite; there are fewer than four rows; an axis value is not positive or repeats; the power column has no positive value or is more negative than a tenth of its peak; or a keyword is not one of its documented values.

Warns:
SolpropWarning

If a wrapped phase looks undersampled: some step between neighbouring samples above the floor lies within a tenth of pi of +-pi, and the samples beyond the innermost such step, outward from the peak, hold more than a thousandth of the power. Such a phase cannot be recovered.

See also

DataPulse.from_spectral_density

The same pulse from arrays.

Examples

A Gaussian spectrum written over wavelength in nanometres, with a header line:

>>> import io
>>> import numpy as np
>>> from solprop.pulses import load_spectrum
>>> wavelength = np.linspace(700.0, 900.0, 201)
>>> power = np.exp(-(((wavelength - 800.0) / 20.0) ** 2))
>>> file = io.StringIO()
>>> np.savetxt(file, np.column_stack([wavelength, power]), header="nm power")
>>> _ = file.seek(0)
>>> pulse = load_spectrum(file, energy=1e-6, axis="wavelength", axis_unit="nm")
>>> pulse.spectrum.shape
(201,)
>>> float(np.ptp(pulse.spectral_phase))
0.0