# -*- coding: utf-8 -*-
"""
Pulses of light in the time and frequency domains.
Notes
-----
The public facing routines and properties of the defined class have inputs and
outputs that are arranged such that the coordinate arrays are monotonically
ordered. Many of the associated private methods and properties, those prefixed
by ``_``, are arranged in standard fft order (using `ifftshift`).
"""
__all__ = ["Pulse"]
# %% Imports
import collections
import numpy as np
from scipy.constants import pi
from pynlo.utility import TFGrid, fft, resample_v, resample_t
from pynlo.utility.misc import ndproperty, replace
# %% Collections
PowerSpectralWidth = collections.namedtuple("PowerSpectralWidth", ["fwhm", "rms", "eqv"])
PowerEnvelopeWidth = collections.namedtuple("PowerEnvelopeWidth", ["fwhm", "rms", "eqv"])
Autocorrelation = collections.namedtuple("Autocorrelation", ["t_grid", "ac_t", "fwhm", "rms", "eqv"])
Spectrogram = collections.namedtuple("Spectrogram", ["v_grid", "t_grid", "spg", "extent"])
# %% Pulse
[docs]
class Pulse(TFGrid):
"""
An optical pulse.
A set of complementary time and frequency grids are generated to represent
the pulse in both the time and frequency domains.
Parameters
----------
n : int
The number of grid points.
v_ref : float
The target central frequency of the grid.
dv : float
The frequency step size. This is equal to the reciprocal of the time
window.
v0 : float, optional
The comoving-frame reference frequency. The default value is the center
frequency of the resulting grid.
a_v : array_like of complex, optional
The root-power spectrum. The default value is an empty spectrum.
alias : float, optional
The number of harmonics supported by the real-valued time domain grid
without aliasing. The default is 1, which only generates enough points
for one alias-free Nyquist zone. A higher number may be useful when
simulating nonlinear interactions.
See Also
--------
pynlo.utility.TFGrid :
Documentation of the methods and attributes related to this class's
time and frequency grids.
Notes
-----
The power spectrum and temporal envelope are normalized to the pulse energy
`e_p`::
e_p == np.sum(p_v*dv) == np.sum(p_t*dt) == np.sum(rp_t*rdt)
The amplitude of the analytic root-power spectrum is a factor of ``2**0.5``
larger than the double-sided root-power spectrum of the real-valued time
domain. When transforming between the two representations use the following
normalization::
a_v = 2**0.5 * ra_v[rn_slice]
ra_v[rn_slice] = 2**-0.5 * a_v
The comoving-frame reference frequency `v0` is only used to adjust the
group delay of the time window during pulse propagation simulations, it
does not otherwise affect the properties of the pulse.
"""
def __init__(self, n, v_ref, dv, v0=None, a_v=None, alias=1):
#---- Initialize Grids
super().__init__(n, v_ref, dv, alias=alias)
self.__a_v = np.zeros_like(self.v_grid, dtype=complex)
if v0 is None:
self.v0 = self.v_grid[self.n//2] # same as v_ref
else:
self.v0 = v0
#---- Set Spectrum
if a_v is not None:
a_v = np.asarray(a_v, astype=complex)
if a_v.size > 1:
assert (len(a_v) == n), (
"The length of `a_v` must match the number of grid points.")
self.a_v = a_v
#---- Class Methods
[docs]
@classmethod
def FromPowerSpectrum(cls, p_v, n, v_min, v_max, v0=None, e_p=None, phi_v=None, **kwargs):
"""
Initialize a pulse using existing spectral data.
Parameters
----------
p_v : callable -> array_like of float
The power spectrum.
n : int, optional
The number of grid points.
v_min : float, optional
The target minimum frequency.
v_max : float, optional
The target maximum frequency.
v0 : float, optional
The comoving-frame reference frequency. The default value is the
center of the resulting frequency grid.
e_p : float, optional
The pulse energy. The default inherits the pulse energy of the
input spectrum.
phi_v : callable -> array_like of float, optional
The phase of the power spectrum. The default initializes a
transform limited pulse.
"""
assert callable(p_v), "The power spectrum must be a callable function."
#---- Initialize Grids
self = super().FromFreqRange(n, v_min, v_max, **kwargs)
self.__a_v = np.zeros_like(self.v_grid, dtype=complex)
if v0 is None:
self.v0 = self.v_grid[self.n//2] # same as v_ref
else:
self.v0 = v0
#---- Evaluate Input
p_v = np.asarray(p_v(self.v_grid), dtype=float)
p_v[p_v < 0] = 0
if phi_v is not None:
assert callable(phi_v), "The phase must be a callable function."
phi_v = np.asarray(phi_v(self.v_grid), dtype=float)
else:
phi_v = np.zeros_like(self.v_grid)
#---- Set spectrum
self.a_v = p_v**0.5 * np.exp(1j*phi_v)
#---- Set Pulse Energy
if e_p is not None:
self.e_p = e_p
return self
[docs]
@classmethod
def Gaussian(cls, n, v_min, v_max, v0, e_p, t_fwhm, m=1, **kwargs):
"""
Initialize a Gaussian or super-Gaussian pulse.
Parameters
----------
n : int
The number of grid points.
v_min : float
The target minimum frequency.
v_max : float
The target maximum frequency.
v0 : float
The pulse's center frequency. Also taken as the reference frequency
for the comoving frame.
e_p : float
The pulse energy.
t_fwhm : float
The full width at half maximum of the pulse's power envelope.
m : float, optional
The super-Gaussian order. Default is 1.
"""
assert (t_fwhm > 0), "The pulse width must be greater than 0."
#---- Initialize Grids
self = super().FromFreqRange(n, v_min, v_max, **kwargs)
self.__a_v = np.zeros_like(self.v_grid, dtype=complex)
self.v0 = v0
#---- Set Spectrum
p_t = 2**(-((2*self.t_grid/t_fwhm)**2)**m)
phi_t = 2*pi*(v0-self.v_ref)*self.t_grid
self.a_t = p_t**0.5 * np.exp(1j*phi_t)
#---- Set Pulse Energy
self.e_p = e_p
return self
[docs]
@classmethod
def Sech(cls, n, v_min, v_max, v0, e_p, t_fwhm, **kwargs):
"""
Initialize a squared hyperbolic secant pulse.
Parameters
----------
n : int
The number of grid points.
v_min : float
The target minimum frequency.
v_max : float
The target maximum frequency.
v0 : float
The pulse's center frequency. Also taken as the reference frequency
for the comoving frame.
e_p : float
The pulse energy.
t_fwhm : float
The full width at half maximum of the pulse's power envelope.
"""
assert (t_fwhm > 0), "The pulse width must be greater than 0."
#---- Initialize Grids
self = super().FromFreqRange(n, v_min, v_max, **kwargs)
self.__a_v = np.zeros_like(self.v_grid, dtype=complex)
self.v0 = v0
#---- Set Spectrum
t0 = t_fwhm / (2 * np.arccosh(2**0.5))
p_t = (1/np.cosh(self.t_grid/t0))**2
phi_t = 2*pi*(v0-self.v_ref)*self.t_grid
self.a_t = p_t**0.5 * np.exp(1j*phi_t)
#---- Set Pulse Energy
self.e_p = e_p
return self
[docs]
@classmethod
def Parabolic(cls, n, v_min, v_max, v0, e_p, t_fwhm, **kwargs):
"""
Initialize a parabolic pulse.
Parameters
----------
n : int
The number of grid points.
v_min : float
The target minimum frequency.
v_max : float
The target maximum frequency.
v0 : float
The pulse's center frequency. Also taken as the reference frequency
for the comoving frame.
e_p : float
The pulse energy.
t_fwhm : float
The full width at half maximum of the pulse's power envelope.
"""
assert (t_fwhm > 0), "The pulse width must be greater than 0."
#---- Initialize Grids
self = super().FromFreqRange(n, v_min, v_max, **kwargs)
self.__a_v = np.zeros_like(self.v_grid, dtype=complex)
self.v0 = v0
#---- Set Spectrum
p_t = 1-2*(self.t_grid/t_fwhm)**2
p_t[p_t < 0] = 0
phi_t = 2*pi*(v0-self.v_ref)*self.t_grid
self.a_t = p_t**0.5 * np.exp(1j*phi_t)
#---- Set Pulse Energy
self.e_p = e_p
return self
[docs]
@classmethod
def Lorentzian(cls, n, v_min, v_max, v0, e_p, t_fwhm, **kwargs):
"""
Initialize a squared Lorentzian pulse.
Parameters
----------
n : int
The number of grid points.
v_min : float
The target minimum frequency.
v_max : float
The target maximum frequency.
v0 : float
The pulse's center frequency. Also taken as the reference frequency
for the comoving frame.
e_p : float
The pulse energy.
t_fwhm : float
The full width at half maximum of the pulse's power envelope.
"""
assert (t_fwhm > 0), "The pulse width must be greater than 0."
#---- Initialize Grids
self = super().FromFreqRange(n, v_min, v_max, **kwargs)
self.__a_v = np.zeros_like(self.v_grid, dtype=complex)
self.v0 = v0
#---- Set Spectrum
p_t = 1/(1+4*(2**0.5-1)*(self.t_grid/t_fwhm)**2)**2
phi_t = 2*pi*(v0-self.v_ref)*self.t_grid
self.a_t = p_t**0.5 * np.exp(1j*phi_t)
#---- Set Pulse Energy
self.e_p = e_p
return self
[docs]
@classmethod
def CW(cls, n, v_min, v_max, v0, p_avg, **kwargs):
"""
Initialize a continuous wave.
The target frequency will be offset so that it directly aligns with one
of the `v_grid` coordinates.
Parameters
----------
n : int
The number of grid points.
v_min : float
The target minimum frequency.
v_max : float
The target maximum frequency.
v0 : float
The target CW frequency. Also taken as the reference frequency for
the comoving frame.
p_avg : float
The average power of the CW light.
"""
#---- Initialize Grids
self = super().FromFreqRange(n, v_min, v_max, **kwargs)
self.__a_v = np.zeros_like(self.v_grid, dtype=complex)
self.v0 = v0
#---- Set Spectrum
p_t = np.ones_like(self.t_grid)
phi_t = 2*pi*(self.v0-self.v_ref)*self.t_grid
self.a_t = p_t**0.5 * np.exp(1j*phi_t)
#---- Set Pulse Energy
e_p = p_avg*self.t_window
self.e_p = e_p
return self
#---- Frequency Domain Properties
@property
def a_v(self):
"""
The root-power spectrum, with units of ``(J/Hz)**0.5``.
Returns
-------
ndarray of complex
"""
return self.__a_v
@a_v.setter
def a_v(self, a_v):
self.__a_v[...] = a_v
@ndproperty
def _a_v(self, key=...):
"""
The root-power spectrum arranged in standard fft order.
Returns
-------
ndarray of complex
"""
return fft.ifftshift(self.a_v)[key]
@_a_v.setter
def _a_v(self, _a_v, key=...):
if key is not ...:
_a_v = replace(self._a_v, _a_v, key)
self.a_v = fft.fftshift(_a_v)
@ndproperty
def p_v(self, key=...):
"""
The power spectrum, with units of ``J/Hz``.
Returns
-------
ndarray of float
"""
return self.a_v[key].real**2 + self.a_v[key].imag**2
@p_v.setter
def p_v(self, p_v, key=...):
self.a_v[key] = p_v**0.5 * np.exp(1j*self.phi_v[key])
@ndproperty
def _p_v(self, key=...):
"""
The power spectrum arranged in standard fft order.
Returns
-------
ndarray of float
"""
return fft.ifftshift(self.p_v)[key]
@_p_v.setter
def _p_v(self, _p_v, key=...):
if key is not ...:
_p_v = replace(self._p_v, _p_v, key)
self.p_v = fft.fftshift(_p_v)
@ndproperty
def phi_v(self, key=...):
"""
The spectral phase, in ``rad``.
Returns
-------
ndarray of float
"""
return np.angle(self.a_v[key])
@phi_v.setter
def phi_v(self, phi_v, key=...):
self.a_v[key] = self.p_v[key]**0.5 * np.exp(1j*phi_v)
@ndproperty
def _phi_v(self, key=...):
"""
The spectral phase arranged in standard fft order.
Returns
-------
ndarray of float
"""
return fft.ifftshift(self.phi_v)[key]
@_phi_v.setter
def _phi_v(self, _phi_v, key=...):
if key is not ...:
_phi_v = replace(self._phi_v, _phi_v, key)
self.phi_v = fft.fftshift(_phi_v)
@ndproperty
def tg_v(self, key=...):
"""
The spectral group delay, with units of ``s``.
Returns
-------
ndarray of float
"""
return self.t_ref - np.gradient(np.unwrap(self.phi_v)/(2*pi), self.v_grid, edge_order=2)[key]
[docs]
def v_width(self, m=None):
"""
Calculate the width of the pulse in the frequency domain.
Set `m` to optionally resample the number of points and change the
frequency resolution.
Parameters
----------
m : float, optional
The multiplicative number of points at which to resample the power
spectrum. The default is to not resample.
Returns
-------
fwhm : float
The full width at half maximum of the power spectrum.
rms : float
The full root-mean-square width of the power spectrum.
eqv : float
The equivalent width of the power spectrum.
"""
#---- Power
p_v = self.p_v
#---- Resample
if m is None:
n = self.n
v_grid = self.v_grid
dv = self.dv
else:
assert m > 0, "The point multiplier must be greater than 0."
n = round(m * self.n)
resampled = resample_v(self.v_grid, p_v, n)
p_v = resampled.f_v
v_grid = resampled.v_grid
dv = resampled.dv
#---- FWHM
p_max = p_v.max()
v_selector = v_grid[p_v >= 0.5*p_max]
v_fwhm = dv + (v_selector.max() - v_selector.min())
#---- RMS
p_norm = np.sum(p_v*dv)
v_avg = np.sum(v_grid*p_v*dv)/p_norm
v_var = np.sum((v_grid - v_avg)**2 * p_v*dv)/p_norm
v_rms = 2 * v_var**0.5
#---- Equivalent
v_eqv = 1/np.sum((p_v/p_norm)**2 * dv)
#---- Construct PowerSpectralWidth
v_widths = PowerSpectralWidth(fwhm=v_fwhm, rms=v_rms, eqv=v_eqv)
return v_widths
#---- Time Domain Properties
@ndproperty
def a_t(self, key=...):
"""
The root-power complex envelope, with units of ``(J/s)**0.5``.
Returns
-------
ndarray of complex
"""
return fft.fftshift(self._a_t)[key]
@a_t.setter
def a_t(self, a_t, key=...):
if key is not ...:
a_t = replace(self.a_t, a_t, key)
self._a_t = fft.ifftshift(a_t)
@ndproperty
def _a_t(self, key=...):
"""
The root-power complex envelope arranged in standard fft order.
Returns
-------
ndarray of complex
"""
return fft.ifft(self._a_v, fsc=self.dt)[key]
@_a_t.setter
def _a_t(self, _a_t, key=...):
if key is not ...:
_a_t = replace(self._a_t, _a_t, key)
self._a_v = fft.fft(_a_t, fsc=self.dt)
@ndproperty
def p_t(self, key=...):
"""
The power envelope, with units of ``J/s``.
This gives the average or rms power of the complex envelope. The
envelope of the instantaneous power, which tracks the peak power of
each optical cycle, is a factor of 2 larger.
Returns
-------
ndarray of float
See Also
--------
rp_t : The instantaneous power.
"""
return fft.fftshift(self._p_t)[key]
@p_t.setter
def p_t(self, p_t, key=...):
if key is not ...:
p_t = replace(self.p_t, p_t, key)
self._p_t = fft.ifftshift(p_t)
@ndproperty
def _p_t(self, key=...):
"""
The power envelope arranged in standard fft order.
Returns
-------
ndarray of float
"""
_a_t = self._a_t[key]
return _a_t.real**2 + _a_t.imag**2
@_p_t.setter
def _p_t(self, _p_t, key=...):
self._a_t[key] = _p_t**0.5 * np.exp(1j*self._phi_t[key])
@ndproperty
def phi_t(self, key=...):
"""
The phase of the complex envelope, in ``rad``.
Returns
-------
ndarray of float
"""
return fft.fftshift(self._phi_t)[key]
@phi_t.setter
def phi_t(self, phi_t, key=...):
if key is not ...:
phi_t = replace(self.phi_t, phi_t, key)
self._phi_t = fft.ifftshift(phi_t)
@ndproperty
def _phi_t(self, key=...):
"""
The phase of the complex envelope arranged in standard fft order.
Returns
-------
ndarray of float
"""
return np.angle(self._a_t[key])
@_phi_t.setter
def _phi_t(self, _phi_t, key=...):
self._a_t[key] = self._p_t[key]**0.5 * np.exp(1j*_phi_t)
@ndproperty
def vg_t(self, key=...):
"""
The instantaneous frequency of the complex envelope, with units of
``Hz``.
Returns
-------
ndarray of float
"""
return self.v_ref + np.gradient(np.unwrap(self.phi_t)/(2*pi), self.t_grid, edge_order=2)[key]
@ndproperty
def ra_t(self, key=...):
"""
The real-valued instantaneous root-power amplitude, with units of
``(J/s)**0.5``.
Returns
-------
ndarray of float
"""
return fft.fftshift(self._ra_t)[key]
@ra_t.setter
def ra_t(self, ra_t, key=...):
if key is not ...:
ra_t = replace(self.ra_t, ra_t, key)
self._ra_t = fft.ifftshift(ra_t)
@ndproperty
def _ra_t(self, key=...):
"""
The real-valued instantaneous root-power amplitude arranged in standard
fft order.
Returns
-------
ndarray of float
"""
ra_v = np.zeros_like(self.rv_grid, dtype=complex)
ra_v[self.rn_slice] = 2**-0.5 * self.a_v
ra_t = fft.irfft(ra_v, fsc=self.rdt, n=self.rn)
return ra_t[key]
@_ra_t.setter
def _ra_t(self, _ra_t, key=...):
if key is not ...:
_ra_t = replace(self._ra_t, _ra_t, key)
ra_v = fft.rfft(_ra_t, fsc=self.rdt)
self.a_v = 2**0.5 * ra_v[self.rn_slice]
@ndproperty
def rp_t(self, key=...):
"""
The instantaneous power, with units of ``J/s``.
Returns
-------
ndarray of float
"""
return fft.fftshift(self._rp_t)[key]
@ndproperty
def _rp_t(self, key=...):
"""
The instantaneous power arranged in standard fft order.
Returns
-------
ndarray of float
"""
return self._ra_t[key]**2
[docs]
def t_width(self, m=None):
"""
Calculate the width of the pulse in the time domain.
Set `m` to optionally resample the number of points and change the
time resolution.
Parameters
----------
m : float, optional
The multiplicative number of points at which to resample the power
envelope. The default is to not resample.
Returns
-------
fwhm : float
The full width at half maximum of the power envelope.
rms : float
The full root-mean-square width of the power envelope.
eqv : float
The equivalent width of the power envelope.
"""
#---- Power
p_t = self.p_t
#---- Resample
if m is None:
n = self.n
t_grid = self.t_grid
dt = self.dt
else:
assert m > 0, "The point multiplier must be greater than 0."
n = round(m * self.n)
resampled = resample_t(self.t_grid, p_t, n)
p_t = resampled.f_t
t_grid = resampled.t_grid
dt = resampled.dt
#---- FWHM
p_max = p_t.max()
t_selector = t_grid[p_t >= 0.5*p_max]
t_fwhm = dt + (t_selector.max() - t_selector.min())
#---- RMS
p_norm = np.sum(p_t*dt)
t_avg = np.sum(t_grid*p_t*dt)/p_norm
t_var = np.sum((t_grid - t_avg)**2 * p_t*dt)/p_norm
t_rms = 2 * t_var**0.5
#---- Equivalent
t_eqv = 1/np.sum((p_t/p_norm)**2 * dt)
#---- Construct PowerEnvelopeWidth
t_widths = PowerEnvelopeWidth(fwhm=t_fwhm, rms=t_rms, eqv=t_eqv)
return t_widths
#---- Energy Properties
@property
def e_p(self):
"""
The pulse energy, with units of ``J``.
Returns
-------
float
"""
return np.sum(self.p_v*self.dv)
@e_p.setter
def e_p(self, e_p):
assert (e_p > 0), "The pulse energy must be greater than 0."
self.a_v *= (e_p/self.e_p)**0.5
#---- Grid Properties
@property
def v0(self):
"""
The comoving-frame reference frequency, with units of ``Hz``.
Returns
-------
float
"""
return self._v0
@v0.setter
def v0(self, v0):
assert (v0 > 0), "The comoving-frame reference frequency must be greater than 0."
self._v0_idx = np.argmin(np.abs(self.v_grid - v0))
self._v0 = self.v_grid[self.v0_idx]
@property
def v0_idx(self):
"""
The array index of the comoving frame’s reference frequency.
Returns
-------
int
"""
return self._v0_idx
#---- Indirect Measures
[docs]
def autocorrelation(self, m=None):
"""
Calculate the intensity autocorrelation and related diagnostic
information.
Set `m` to optionally resample the number of points and change the
time resolution. The intensity autocorrelation is normalized to a max
amplitude of 1.
Parameters
----------
m : int, optional
The multiplicative number of points at which to resample the
intensity autocorrelation. The default is to not resample.
Returns
-------
t_grid : ndarray of float
The time grid.
ac_t : ndarray of float
The amplitude of the intensity autocorrelation.
fwhm : float
The full width at half maximum of the intensity autocorrelation.
rms : float
The full root-mean-square width of the intensity autocorrelation.
eqv : float
The equivalent width of the intensity autocorrelation.
"""
#---- Intensity Autocorrelation
ac_v = np.abs(fft.fftshift(fft.fft(self._p_t, fsc=self.dt)))**2
ac_t = fft.fftshift(fft.ifft(fft.ifftshift(ac_v), fsc=self.dt, overwrite_x=True).real)
#---- Resample
if m is None:
n = self.n
t_grid = self.t_grid
dt = self.dt
else:
assert m > 0, "The point multiplier must be greater than 0."
n = round(m * self.n)
resampled = resample_t(self.t_grid, ac_t, n)
ac_t = resampled.f_t
t_grid = resampled.t_grid
dt = resampled.dt
ac_t /= ac_t.max()
#---- FWHM
ac_max = ac_t.max()
t_selector = t_grid[ac_t >= 0.5*ac_max]
t_fwhm = dt + (t_selector.max() - t_selector.min())
#---- RMS
ac_norm = np.sum(ac_t*dt)
t_avg = np.sum(t_grid*ac_t*dt)/ac_norm
t_var = np.sum((t_grid - t_avg)**2 * ac_t*dt)/ac_norm
t_rms = 2 * t_var**0.5
#---- Equivalent
t_eqv = 1/np.sum((ac_t/ac_norm)**2 * dt)
#---- Construct Autocorrelation
ac = Autocorrelation(t_grid=t_grid, ac_t=ac_t, fwhm=t_fwhm, rms=t_rms, eqv=t_eqv)
return ac
[docs]
def spectrogram(self, t_fwhm=None, v_range=None, n_t=None, t_range=None):
"""
Calculate the spectrogram of the pulse through convolution with a
Gaussian window.
Parameters
----------
t_fwhm : float, optional
The full width at half maximum of the Gaussian window. The default
derives a fwhm from the bandwidth of the power spectrum.
v_range : array_like of float, optional
The target range of frequencies to sample. This should be given as
(min, max) values. The default takes the full range of `v_grid`.
n_t : int or str, optional
The number of sampled delays. Setting to "equal" gives the same
number of delays as points in `v_grid`. The default samples 4
points per fwhm of the Gaussian window.
t_range : array_like of float, optional
The range of delays to sample. This should be given as (min, max)
values. The default takes the full range of the `t_grid`.
Returns
-------
v_grid : ndarray of float
The frequency grid.
t_grid : ndarray of float
The time grid.
spg : ndarray of float
The amplitude of the spectrogram. The first axis corresponds to
frequency and the second axis to time.
extent : tuple of float
A bounding box suitable for use with `matplotlib`'s `imshow`
function with the `origin` keyword set to "lower". This
reliably centers the pixels on the `v_grid` and `t_grid`
coordinates.
Notes
-----
The resolution in both the time and frequency domains is limited by the
time-bandwidth product of the Gaussian window. The full width at half
maximum of the Gaussian window should be similar to the full width at
half maximum of the pulse in order to evenly distribute resolution
bandwidth between the time and frequency domains.
"""
#---- Resample
if v_range is None:
n = self.n
v_grid = self.v_grid
a_t = self.a_t
t_grid = self.t_grid
dt = self.dt
else:
v_range = np.asarray(v_range)
assert (v_range.min() >= self.v_grid.min()), (
"The minimum frequency cannot be less than the minimum possible frequency.")
assert (v_range.max() <= self.v_grid.max()), (
"The maximum frequency cannot be greater than the maximum possible frequency.")
v_min_selector = np.argmin(np.abs(self.v_grid - v_range.min()))
v_max_selector = np.argmin(np.abs(self.v_grid - v_range.max()))
v_grid = self.v_grid[v_min_selector:v_max_selector+1]
n = len(v_grid)
dt = 1/(n*self.dv)
a_v = self.a_v[v_min_selector:v_max_selector+1]
a_t = fft.fftshift(fft.ifft(fft.ifftshift(a_v), fsc=dt, overwrite_x=True))
t_grid = dt*(np.arange(n) - (n//2))
#---- Set Gate
if t_fwhm is None:
v_sigma = 0.5 * self.v_width().rms
t_fwhm = np.log(4)**0.5 / (2*pi*v_sigma)
g_t = (2**(-(2*t_grid/t_fwhm)**2))**0.5
g_t /= np.sum(np.abs(g_t)**2 * dt)**0.5
g_v = fft.fftshift(fft.fft(fft.ifftshift(g_t), fsc=dt))
#---- Set Delays
if t_range is None:
t_min, t_max = t_grid.min(), t_grid.max()
else:
t_range = np.asarray(t_range)
assert (t_range.min() >= t_grid.min()), (
"The minimum delay cannot be less than the minimum possible delay.")
assert (t_range.max() <= t_grid.max()), (
"The maximum delay cannot be greater than the maximum possible delay.")
t_min, t_max = t_range
if n_t is None:
n_t = int(4*round((t_max - t_min)/t_fwhm))
elif isinstance(n_t, str):
assert (n_t in ["equal"]), (
"'{:}' is not a valid string argument for n_t".format(n_t))
n_t = n
else:
assert isinstance(n_t, (int, np.integer)), "The number of points must be an integer."
assert (n_t > 1), "The number of points must be greater than 1."
delay_t_grid = np.linspace(t_min, t_max, n_t)
delay_dt = (t_max - t_min)/(n_t - 1)
gate_pulses_v = (g_v[:, np.newaxis]
* np.exp(-1j*2*pi*delay_t_grid[np.newaxis, :]*v_grid[:, np.newaxis]))
gate_pulses_t = fft.fftshift(fft.ifft(
fft.ifftshift(gate_pulses_v, axis=0), fsc=dt, axis=0, overwrite_x=True), axis=0)
#---- Spectrogram
spg_t = a_t[:, np.newaxis] * gate_pulses_t
spg_v = fft.fftshift(fft.fft(
fft.ifftshift(spg_t, axis=0), fsc=dt, axis=0, overwrite_x=True), axis=0)
p_spg = spg_v.real**2 + spg_v.imag**2
#---- Extent
extent = (delay_t_grid.min()-0.5*delay_dt, delay_t_grid.max()+0.5*delay_dt,
v_grid.min()-0.5*self.dv, v_grid.max()+0.5*self.dv)
#---- Construct Spectrogram
spg = Spectrogram(v_grid=v_grid, t_grid=delay_t_grid, spg=p_spg, extent=extent)
return spg
#---- Misc
[docs]
def copy(self):
"""Copy the pulse."""
return super().copy()