From c76890f10464193ef6fe864c1d02441eb39a3220 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81ngel=20Ferran=20Pousa?= Date: Thu, 17 Aug 2023 21:17:56 +0200 Subject: [PATCH 01/30] Implement `get_spectrum` --- lasy/utils/laser_utils.py | 97 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/lasy/utils/laser_utils.py b/lasy/utils/laser_utils.py index 28c182e0..0ffcc93d 100644 --- a/lasy/utils/laser_utils.py +++ b/lasy/utils/laser_utils.py @@ -211,6 +211,103 @@ def get_full_field(laser, theta=0, slice=0, slice_axis="x", Nt=None): return env, ext +def get_spectrum( + grid, + dim, + bins=20, + range=None, + is_envelope=True, + is_hilbert=False, + omega0=None, + phase_unwrap_1d=None +): + """ + Get the the frequency spectrum of an electric field or envelope. + + Parameters + ---------- + grid : a Grid object. + It contains a ndarrays with the field data from which the + spectrum is computed, and the associated metadata. The last axis must + be the longitudinal dimension. + Can be the full electric field or the envelope. + + dim : string (optional) + Dimensionality of the array. Only used if is_envelope is False. + Options are: + + - 'xyt': The laser pulse is represented on a 3D grid: + Cartesian (x,y) transversely, and temporal (t) longitudinally. + - 'rt' : The laser pulse is represented on a 2D grid: + Cylindrical (r) transversely, and temporal (t) longitudinally. + + bins : int (optional) + Number of bins of the spectrum. + + range : list of float (optional) + List of two values indicating the minimum and maximum frequency of the + spectrum. + + is_envelope : bool (optional) + Whether the field provided uses the envelope representation, as used + internally in lasy. If False, field is assumed to represent the + the electric field. + + is_hilbert : boolean (optional) + If True, the field argument is assumed to be a Hilbert transform, and + is used through the computation. Otherwise, the Hilbert transform is + calculated in the function. + + omega0 : scalar + Angular frequency at which the envelope is defined. + Required if an only if is_envelope is True. + + phase_unwrap_1d : boolean (optional) + Whether the phase unwrapping is done in 1D. + This is not recommended, as the unwrapping will not be accurate, + but it might be the only practical solution when dim is 'xyt'. + + Returns + ------- + spectrum : ndarray of doubles + Array with the spectrum amplitudes. + + omega_spectrum : scalar + Array with the frequencies of the spectrum. + """ + # Get frequency array. + omega, central_omega = get_frequency( + grid=grid, + dim=dim, + is_envelope=is_envelope, + is_hilbert=is_hilbert, + omega0=omega0, + phase_unwrap_1d=phase_unwrap_1d + ) + # Calculate weights of each frequency (amplitude of the field). + if dim == "xyt": + dV = grid.dx[0] * grid.dx[1] * dz + weights = np.abs(grid.field) * dV + elif dim == "rt": + r = grid.axes[0] + dr = grid.dx[0] + dz = grid.dx[-1] * c + # 1D array that computes the volume of radial cells + dV = np.pi * ((r + 0.5 * dr) ** 2 - (r - 0.5 * dr) ** 2) * dz + weights = np.abs(grid.field) * dV[np.newaxis, :, np.newaxis] + # Get weighted spectrum. + # Neglects the 2 first and last time slices, whose values seems to be + # slightly off (maybe due to lower order derivative at the edges). + spectrum, edges = np.histogram( + a=np.squeeze(omega)[..., 2:-2], + weights=np.squeeze(weights[..., 2:-2]), + bins=bins, + range=range + ) + omega_spectrum = edges[1:] - (edges[1] - edges[0]) / 2 + return spectrum, omega_spectrum + + def get_frequency( grid, dim=None, From 551aee916d97966ad8fa9ad7b6a7e9f06a9747fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81ngel=20Ferran=20Pousa?= Date: Thu, 17 Aug 2023 22:07:07 +0200 Subject: [PATCH 02/30] Implement `get_duration` --- lasy/utils/laser_utils.py | 104 ++++++++++++++++++++++++++++---------- 1 file changed, 77 insertions(+), 27 deletions(-) diff --git a/lasy/utils/laser_utils.py b/lasy/utils/laser_utils.py index 0ffcc93d..4ce5c6ab 100644 --- a/lasy/utils/laser_utils.py +++ b/lasy/utils/laser_utils.py @@ -40,16 +40,11 @@ def compute_laser_energy(dim, grid): envelope = grid.field - dz = grid.dx[-1] * c + dV = get_grid_cell_volume(grid, dim) if dim == "xyt": - dV = grid.dx[0] * grid.dx[1] * dz energy = ((dV * epsilon_0 * 0.5) * abs(envelope) ** 2).sum() elif dim == "rt": - r = grid.axes[0] - dr = grid.dx[0] - # 1D array that computes the volume of radial cells - dV = np.pi * ((r + 0.5 * dr) ** 2 - (r - 0.5 * dr) ** 2) * dz energy = ( dV[np.newaxis, :, np.newaxis] * epsilon_0 @@ -216,13 +211,11 @@ def get_spectrum( dim, bins=20, range=None, - is_envelope=True, - is_hilbert=False, omega0=None, phase_unwrap_1d=None ): """ - Get the the frequency spectrum of an electric field or envelope. + Get the the frequency spectrum of an envelope. Parameters ---------- @@ -248,16 +241,6 @@ def get_spectrum( List of two values indicating the minimum and maximum frequency of the spectrum. - is_envelope : bool (optional) - Whether the field provided uses the envelope representation, as used - internally in lasy. If False, field is assumed to represent the - the electric field. - - is_hilbert : boolean (optional) - If True, the field argument is assumed to be a Hilbert transform, and - is used through the computation. Otherwise, the Hilbert transform is - calculated in the function. - omega0 : scalar Angular frequency at which the envelope is defined. Required if an only if is_envelope is True. @@ -279,21 +262,15 @@ def get_spectrum( omega, central_omega = get_frequency( grid=grid, dim=dim, - is_envelope=is_envelope, - is_hilbert=is_hilbert, + is_envelope=True, omega0=omega0, phase_unwrap_1d=phase_unwrap_1d ) # Calculate weights of each frequency (amplitude of the field). + dV = get_grid_cell_volume(grid, dim) if dim == "xyt": - dV = grid.dx[0] * grid.dx[1] * dz weights = np.abs(grid.field) * dV elif dim == "rt": - r = grid.axes[0] - dr = grid.dx[0] - dz = grid.dx[-1] * c - # 1D array that computes the volume of radial cells - dV = np.pi * ((r + 0.5 * dr) ** 2 - (r - 0.5 * dr) ** 2) * dz weights = np.abs(grid.field) * dV[np.newaxis, :, np.newaxis] # Get weighted spectrum. # Neglects the 2 first and last time slices, whose values seems to be @@ -410,6 +387,32 @@ def get_frequency( return omega, central_omega +def get_duration(grid, dim): + """Get envelope duration, measured as RMS. + + Parameters + ---------- + grid : Grid + The grid with the envelope to analyze. + dim : str + Dimensionality of the grid. + + Returns + ------- + float + RMS duration of the envelope in seconds. + """ + # Calculate weights of each frequency (amplitude of the field). + dV = get_grid_cell_volume(grid, dim) + if dim == "xyt": + weights = np.abs(grid.field) * dV + elif dim == "rt": + weights = np.abs(grid.field) * dV[np.newaxis, :, np.newaxis] + # project weights to longitudinal axes + weights = np.sum(weights, axis=(0, 1)) + return weighted_std(grid.axes[-1], weights) + + def field_to_vector_potential(grid, omega0): """ Convert envelope from electric field (V/m) to normalized vector potential. @@ -518,6 +521,53 @@ def hilbert_transform(grid): return hilbert(grid.field[:, :, ::-1])[:, :, ::-1] +def get_grid_cell_volume(grid, dim): + """Get the volume of the grid cells. + + Parameters + ---------- + grid : Grid + The grid form which to compute the cell volume + dim : str + Dimensionality of the grid. + + Returns + ------- + float or ndarray + A float with the cell volume (if dim=='xyt') or a numpy array with the + radial distribution of cell volumes (if dim=='rt'). + """ + if dim == "xyt": + dV = grid.dx[0] * grid.dx[1] * dz + elif dim == "rt": + r = grid.axes[0] + dr = grid.dx[0] + dz = grid.dx[-1] * c + # 1D array that computes the volume of radial cells + dV = np.pi * ((r + 0.5 * dr) ** 2 - (r - 0.5 * dr) ** 2) * dz + return dV + + +def weighted_std(values, weights=None): + """Calculates the weighted standard deviation of the given values + + Parameters + ---------- + values: array + Contains the values to be analyzed + + weights : array + Contains the weights of the values to analyze + + Returns + ------- + A float with the value of the standard deviation + """ + mean_val = np.average(values, weights=weights) + std = np.sqrt(np.average((values-mean_val)**2, weights=weights)) + return std + + def create_grid(array, axes, dim): """Create a lasy grid from a numpy array. From 57598cbfff3c63c568ea40d4100f3eca64ffa03a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 18 Aug 2023 07:28:35 +0000 Subject: [PATCH 03/30] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- lasy/utils/laser_utils.py | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/lasy/utils/laser_utils.py b/lasy/utils/laser_utils.py index 4ce5c6ab..2ac6eb16 100644 --- a/lasy/utils/laser_utils.py +++ b/lasy/utils/laser_utils.py @@ -206,14 +206,7 @@ def get_full_field(laser, theta=0, slice=0, slice_axis="x", Nt=None): return env, ext -def get_spectrum( - grid, - dim, - bins=20, - range=None, - omega0=None, - phase_unwrap_1d=None -): +def get_spectrum(grid, dim, bins=20, range=None, omega0=None, phase_unwrap_1d=None): """ Get the the frequency spectrum of an envelope. @@ -264,7 +257,7 @@ def get_spectrum( dim=dim, is_envelope=True, omega0=omega0, - phase_unwrap_1d=phase_unwrap_1d + phase_unwrap_1d=phase_unwrap_1d, ) # Calculate weights of each frequency (amplitude of the field). dV = get_grid_cell_volume(grid, dim) @@ -279,7 +272,7 @@ def get_spectrum( a=np.squeeze(omega)[..., 2:-2], weights=np.squeeze(weights[..., 2:-2]), bins=bins, - range=range + range=range, ) omega_spectrum = edges[1:] - (edges[1] - edges[0]) / 2 return spectrum, omega_spectrum @@ -564,7 +557,7 @@ def weighted_std(values, weights=None): A float with the value of the standard deviation """ mean_val = np.average(values, weights=weights) - std = np.sqrt(np.average((values-mean_val)**2, weights=weights)) + std = np.sqrt(np.average((values - mean_val) ** 2, weights=weights)) return std From 4bdd0f5750d5be3fc9283fbbea57b4be2bc997c4 Mon Sep 17 00:00:00 2001 From: Angel Ferran Pousa Date: Fri, 18 Aug 2023 09:29:50 +0200 Subject: [PATCH 04/30] Fix docstring --- lasy/utils/laser_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lasy/utils/laser_utils.py b/lasy/utils/laser_utils.py index 2ac6eb16..3452a9ab 100644 --- a/lasy/utils/laser_utils.py +++ b/lasy/utils/laser_utils.py @@ -542,7 +542,7 @@ def get_grid_cell_volume(grid, dim): def weighted_std(values, weights=None): - """Calculates the weighted standard deviation of the given values + """Calculate the weighted standard deviation of the given values. Parameters ---------- From 07673b377add311ea886e5b52a10c4a4ce3efcbb Mon Sep 17 00:00:00 2001 From: Angel Ferran Pousa Date: Fri, 18 Aug 2023 09:32:14 +0200 Subject: [PATCH 05/30] Fix bug --- lasy/utils/laser_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lasy/utils/laser_utils.py b/lasy/utils/laser_utils.py index 3452a9ab..009b939b 100644 --- a/lasy/utils/laser_utils.py +++ b/lasy/utils/laser_utils.py @@ -530,12 +530,12 @@ def get_grid_cell_volume(grid, dim): A float with the cell volume (if dim=='xyt') or a numpy array with the radial distribution of cell volumes (if dim=='rt'). """ + dz = grid.dx[-1] * c if dim == "xyt": dV = grid.dx[0] * grid.dx[1] * dz elif dim == "rt": r = grid.axes[0] dr = grid.dx[0] - dz = grid.dx[-1] * c # 1D array that computes the volume of radial cells dV = np.pi * ((r + 0.5 * dr) ** 2 - (r - 0.5 * dr) ** 2) * dz return dV From 96c43f39760f147007f5f537bf77cbe1277ef81d Mon Sep 17 00:00:00 2001 From: Angel Ferran Pousa Date: Fri, 18 Aug 2023 09:45:19 +0200 Subject: [PATCH 06/30] Low-effort ifs --- lasy/utils/laser_utils.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lasy/utils/laser_utils.py b/lasy/utils/laser_utils.py index 009b939b..1562b002 100644 --- a/lasy/utils/laser_utils.py +++ b/lasy/utils/laser_utils.py @@ -44,7 +44,7 @@ def compute_laser_energy(dim, grid): if dim == "xyt": energy = ((dV * epsilon_0 * 0.5) * abs(envelope) ** 2).sum() - elif dim == "rt": + else: # dim == "rt": energy = ( dV[np.newaxis, :, np.newaxis] * epsilon_0 @@ -263,7 +263,7 @@ def get_spectrum(grid, dim, bins=20, range=None, omega0=None, phase_unwrap_1d=No dV = get_grid_cell_volume(grid, dim) if dim == "xyt": weights = np.abs(grid.field) * dV - elif dim == "rt": + else: # dim == "rt": weights = np.abs(grid.field) * dV[np.newaxis, :, np.newaxis] # Get weighted spectrum. # Neglects the 2 first and last time slices, whose values seems to be @@ -399,7 +399,7 @@ def get_duration(grid, dim): dV = get_grid_cell_volume(grid, dim) if dim == "xyt": weights = np.abs(grid.field) * dV - elif dim == "rt": + else: # dim == "rt": weights = np.abs(grid.field) * dV[np.newaxis, :, np.newaxis] # project weights to longitudinal axes weights = np.sum(weights, axis=(0, 1)) @@ -533,7 +533,7 @@ def get_grid_cell_volume(grid, dim): dz = grid.dx[-1] * c if dim == "xyt": dV = grid.dx[0] * grid.dx[1] * dz - elif dim == "rt": + else: # dim == "rt": r = grid.axes[0] dr = grid.dx[0] # 1D array that computes the volume of radial cells From 686ffc8f7427e47d9812dc49fd7c083f7809ed16 Mon Sep 17 00:00:00 2001 From: Angel Ferran Pousa Date: Fri, 18 Aug 2023 18:02:30 +0200 Subject: [PATCH 07/30] Improve comments --- lasy/utils/laser_utils.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lasy/utils/laser_utils.py b/lasy/utils/laser_utils.py index 1562b002..38eee0d1 100644 --- a/lasy/utils/laser_utils.py +++ b/lasy/utils/laser_utils.py @@ -216,7 +216,6 @@ def get_spectrum(grid, dim, bins=20, range=None, omega0=None, phase_unwrap_1d=No It contains a ndarrays with the field data from which the spectrum is computed, and the associated metadata. The last axis must be the longitudinal dimension. - Can be the full electric field or the envelope. dim : string (optional) Dimensionality of the array. Only used if is_envelope is False. @@ -236,7 +235,6 @@ def get_spectrum(grid, dim, bins=20, range=None, omega0=None, phase_unwrap_1d=No omega0 : scalar Angular frequency at which the envelope is defined. - Required if an only if is_envelope is True. phase_unwrap_1d : boolean (optional) Whether the phase unwrapping is done in 1D. @@ -251,7 +249,7 @@ def get_spectrum(grid, dim, bins=20, range=None, omega0=None, phase_unwrap_1d=No omega_spectrum : scalar Array with the frequencies of the spectrum. """ - # Get frequency array. + # Get the array of angular frequency. omega, central_omega = get_frequency( grid=grid, dim=dim, @@ -267,7 +265,7 @@ def get_spectrum(grid, dim, bins=20, range=None, omega0=None, phase_unwrap_1d=No weights = np.abs(grid.field) * dV[np.newaxis, :, np.newaxis] # Get weighted spectrum. # Neglects the 2 first and last time slices, whose values seems to be - # slightly off (maybe due to lower order derivative at the edges). + # slightly off (maybe due to lower-order derivative at the edges). spectrum, edges = np.histogram( a=np.squeeze(omega)[..., 2:-2], weights=np.squeeze(weights[..., 2:-2]), From c6ea5a596b3f0b8e88cc7d58053776af395970d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81ngel=20Ferran=20Pousa?= Date: Mon, 21 Aug 2023 21:27:10 +0200 Subject: [PATCH 08/30] Test fft --- lasy/utils/laser_utils.py | 89 ++++++++++++++++++++++++++++----------- 1 file changed, 64 insertions(+), 25 deletions(-) diff --git a/lasy/utils/laser_utils.py b/lasy/utils/laser_utils.py index 38eee0d1..bbe1054f 100644 --- a/lasy/utils/laser_utils.py +++ b/lasy/utils/laser_utils.py @@ -206,7 +206,7 @@ def get_full_field(laser, theta=0, slice=0, slice_axis="x", Nt=None): return env, ext -def get_spectrum(grid, dim, bins=20, range=None, omega0=None, phase_unwrap_1d=None): +def get_spectrum(grid, dim, bins=20, range=None, omega0=None, phase_unwrap_1d=None, is_envelope=True, method='fft'): """ Get the the frequency spectrum of an envelope. @@ -249,30 +249,69 @@ def get_spectrum(grid, dim, bins=20, range=None, omega0=None, phase_unwrap_1d=No omega_spectrum : scalar Array with the frequencies of the spectrum. """ - # Get the array of angular frequency. - omega, central_omega = get_frequency( - grid=grid, - dim=dim, - is_envelope=True, - omega0=omega0, - phase_unwrap_1d=phase_unwrap_1d, - ) - # Calculate weights of each frequency (amplitude of the field). - dV = get_grid_cell_volume(grid, dim) - if dim == "xyt": - weights = np.abs(grid.field) * dV - else: # dim == "rt": - weights = np.abs(grid.field) * dV[np.newaxis, :, np.newaxis] - # Get weighted spectrum. - # Neglects the 2 first and last time slices, whose values seems to be - # slightly off (maybe due to lower-order derivative at the edges). - spectrum, edges = np.histogram( - a=np.squeeze(omega)[..., 2:-2], - weights=np.squeeze(weights[..., 2:-2]), - bins=bins, - range=range, - ) - omega_spectrum = edges[1:] - (edges[1] - edges[0]) / 2 + if method == 'fft': + + # spectrum = np.fft.fft(grid.field[0, 0]) * grid.dx[-1] + spectrum = np.fft.fft(grid.field) * grid.dx[-1] + dV = get_grid_cell_volume(grid, dim) + if dim == "xyt": + spectrum = np.sum(spectrum, axis=(0, 1)) + else: + spectrum = np.sum(spectrum * dV[np.newaxis, :, np.newaxis] / dV[0], axis=1)[0] + # spectrum = np.abs(spectrum[:int(len(spectrum) / 2)]) + freq = np.fft.fftfreq(spectrum.shape[-1], d=(grid.axes[-1][1] - grid.axes[-1][0])) + omega_spectrum = 2 * np.pi * freq + + if is_envelope: + omega_spectrum = omega0 - omega_spectrum + + i_sort = np.argsort(omega_spectrum) + omega_spectrum = omega_spectrum[i_sort] + spectrum = np.abs(spectrum[i_sort]) + + i_keep = omega_spectrum >= 0. + omega_spectrum = omega_spectrum[i_keep] + spectrum = spectrum[i_keep] + + omega_interp = np.linspace(*range, bins) + spectrum = np.interp(omega_interp, omega_spectrum, spectrum) + omega_spectrum = omega_interp + + # spectrum = np.fft.fft(grid.field) * grid.dx[-1] + # spectrum = np.abs(spectrum[..., :int(spectrum.shape[-1] / 2)]) + # omega_spectrum = 2 * np.pi / (grid.axes[-1][-1] - grid.axes[-1][0]) * np.arange(spectrum.shape[-1]) + # if is_envelope: + # omega_spectrum = omega_spectrum + # dV = get_grid_cell_volume(grid, dim) + # if dim == "xyt": + # spectrum = np.sum(spectrum * dV, axis=(0, 1)) + # else: + # spectrum = np.sum(spectrum * dV[np.newaxis, :, np.newaxis], axis=1)[0] + else: + # Get the array of angular frequency. + omega, central_omega = get_frequency( + grid=grid, + dim=dim, + is_envelope=True, + omega0=omega0, + phase_unwrap_1d=phase_unwrap_1d, + ) + # Calculate weights of each frequency (amplitude of the field). + dV = get_grid_cell_volume(grid, dim) + if dim == "xyt": + weights = np.abs(grid.field) * dV + else: # dim == "rt": + weights = np.abs(grid.field) * dV[np.newaxis, :, np.newaxis] + # Get weighted spectrum. + # Neglects the 2 first and last time slices, whose values seems to be + # slightly off (maybe due to lower-order derivative at the edges). + spectrum, edges = np.histogram( + a=np.squeeze(omega)[..., 2:-2], + weights=np.squeeze(weights[..., 2:-2]), + bins=bins, + range=range, + ) + omega_spectrum = edges[1:] - (edges[1] - edges[0]) / 2 return spectrum, omega_spectrum From 804f4eba38ffef58a32785783552c67ff24f354b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 21 Aug 2023 19:27:28 +0000 Subject: [PATCH 09/30] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- lasy/utils/laser_utils.py | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/lasy/utils/laser_utils.py b/lasy/utils/laser_utils.py index bbe1054f..4c5fe76b 100644 --- a/lasy/utils/laser_utils.py +++ b/lasy/utils/laser_utils.py @@ -206,7 +206,16 @@ def get_full_field(laser, theta=0, slice=0, slice_axis="x", Nt=None): return env, ext -def get_spectrum(grid, dim, bins=20, range=None, omega0=None, phase_unwrap_1d=None, is_envelope=True, method='fft'): +def get_spectrum( + grid, + dim, + bins=20, + range=None, + omega0=None, + phase_unwrap_1d=None, + is_envelope=True, + method="fft", +): """ Get the the frequency spectrum of an envelope. @@ -249,19 +258,22 @@ def get_spectrum(grid, dim, bins=20, range=None, omega0=None, phase_unwrap_1d=No omega_spectrum : scalar Array with the frequencies of the spectrum. """ - if method == 'fft': - + if method == "fft": # spectrum = np.fft.fft(grid.field[0, 0]) * grid.dx[-1] spectrum = np.fft.fft(grid.field) * grid.dx[-1] dV = get_grid_cell_volume(grid, dim) if dim == "xyt": spectrum = np.sum(spectrum, axis=(0, 1)) else: - spectrum = np.sum(spectrum * dV[np.newaxis, :, np.newaxis] / dV[0], axis=1)[0] + spectrum = np.sum(spectrum * dV[np.newaxis, :, np.newaxis] / dV[0], axis=1)[ + 0 + ] # spectrum = np.abs(spectrum[:int(len(spectrum) / 2)]) - freq = np.fft.fftfreq(spectrum.shape[-1], d=(grid.axes[-1][1] - grid.axes[-1][0])) + freq = np.fft.fftfreq( + spectrum.shape[-1], d=(grid.axes[-1][1] - grid.axes[-1][0]) + ) omega_spectrum = 2 * np.pi * freq - + if is_envelope: omega_spectrum = omega0 - omega_spectrum @@ -269,7 +281,7 @@ def get_spectrum(grid, dim, bins=20, range=None, omega0=None, phase_unwrap_1d=No omega_spectrum = omega_spectrum[i_sort] spectrum = np.abs(spectrum[i_sort]) - i_keep = omega_spectrum >= 0. + i_keep = omega_spectrum >= 0.0 omega_spectrum = omega_spectrum[i_keep] spectrum = spectrum[i_keep] From 9ce1bceaefdfaee7cbc596f439761f7b40bb543b Mon Sep 17 00:00:00 2001 From: Angel Ferran Pousa Date: Tue, 22 Aug 2023 09:41:58 +0200 Subject: [PATCH 10/30] Use range only when it's not None --- lasy/utils/laser_utils.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lasy/utils/laser_utils.py b/lasy/utils/laser_utils.py index bbe1054f..03ec58e8 100644 --- a/lasy/utils/laser_utils.py +++ b/lasy/utils/laser_utils.py @@ -273,9 +273,10 @@ def get_spectrum(grid, dim, bins=20, range=None, omega0=None, phase_unwrap_1d=No omega_spectrum = omega_spectrum[i_keep] spectrum = spectrum[i_keep] - omega_interp = np.linspace(*range, bins) - spectrum = np.interp(omega_interp, omega_spectrum, spectrum) - omega_spectrum = omega_interp + if range is not None: + omega_interp = np.linspace(*range, bins) + spectrum = np.interp(omega_interp, omega_spectrum, spectrum) + omega_spectrum = omega_interp # spectrum = np.fft.fft(grid.field) * grid.dx[-1] # spectrum = np.abs(spectrum[..., :int(spectrum.shape[-1] / 2)]) From 8b30a0b350e011c2985d476abc3685903e036fb9 Mon Sep 17 00:00:00 2001 From: Angel Ferran Pousa Date: Tue, 22 Aug 2023 09:42:36 +0200 Subject: [PATCH 11/30] Divide spectrum by 2 when is envelope --- lasy/utils/laser_utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/lasy/utils/laser_utils.py b/lasy/utils/laser_utils.py index 03ec58e8..217bfe33 100644 --- a/lasy/utils/laser_utils.py +++ b/lasy/utils/laser_utils.py @@ -264,6 +264,7 @@ def get_spectrum(grid, dim, bins=20, range=None, omega0=None, phase_unwrap_1d=No if is_envelope: omega_spectrum = omega0 - omega_spectrum + spectrum /= 2 i_sort = np.argsort(omega_spectrum) omega_spectrum = omega_spectrum[i_sort] From 265fe234e1230b6c3ec6e43d2ca627b83bbcb4d1 Mon Sep 17 00:00:00 2001 From: Angel Ferran Pousa Date: Tue, 22 Aug 2023 17:09:25 +0200 Subject: [PATCH 12/30] Improve FFT implementation --- lasy/utils/laser_utils.py | 146 ++++++++++++++++++-------------------- 1 file changed, 68 insertions(+), 78 deletions(-) diff --git a/lasy/utils/laser_utils.py b/lasy/utils/laser_utils.py index 1b6cdf71..2d391c7b 100644 --- a/lasy/utils/laser_utils.py +++ b/lasy/utils/laser_utils.py @@ -209,12 +209,11 @@ def get_full_field(laser, theta=0, slice=0, slice_axis="x", Nt=None): def get_spectrum( grid, dim, - bins=20, range=None, - omega0=None, - phase_unwrap_1d=None, + bins=20, is_envelope=True, - method="fft", + omega0=None, + on_axis=False, ): """ Get the the frequency spectrum of an envelope. @@ -235,97 +234,88 @@ def get_spectrum( - 'rt' : The laser pulse is represented on a 2D grid: Cylindrical (r) transversely, and temporal (t) longitudinally. - bins : int (optional) - Number of bins of the spectrum. - range : list of float (optional) List of two values indicating the minimum and maximum frequency of the - spectrum. + spectrum. If provided, only the FFT spectrum within this range + will be returned using interpolation. - omega0 : scalar - Angular frequency at which the envelope is defined. + bins : int (optional) + Number of bins of to which to interpolate the spectrum if a `range` + is given. - phase_unwrap_1d : boolean (optional) - Whether the phase unwrapping is done in 1D. - This is not recommended, as the unwrapping will not be accurate, - but it might be the only practical solution when dim is 'xyt'. + is_envelope : bool (optional) + Whether the field provided uses the envelope representation, as used + internally in lasy. If False, field is assumed to represent the + the electric field. + + omega0 : scalar (optional) + Angular frequency at which the envelope is defined. Required if + `is_envelope=True`. + + on_axis : bool (optional) + Whether to get the spectrum on axis or, otherwise, the summed spectrum + along the transverse direction(s). Returns ------- spectrum : ndarray of doubles - Array with the spectrum amplitudes. + Array with the spectrum amplitudes (FFT amplitude * dt). omega_spectrum : scalar Array with the frequencies of the spectrum. """ - if method == "fft": - # spectrum = np.fft.fft(grid.field[0, 0]) * grid.dx[-1] - spectrum = np.fft.fft(grid.field) * grid.dx[-1] - dV = get_grid_cell_volume(grid, dim) + # Get the frequencies of the fft output. + freq = np.fft.fftfreq( + grid.field.shape[-1], d=(grid.axes[-1][1] - grid.axes[-1][0]) + ) + omega_spectrum = 2 * np.pi * freq + + # Get on axis or full field. + if on_axis: if dim == "xyt": - spectrum = np.sum(spectrum, axis=(0, 1)) + nx, ny, nt = grid.field.shape + field = grid.field[nx//2, ny//2] else: - spectrum = np.sum(spectrum * dV[np.newaxis, :, np.newaxis] / dV[0], axis=1)[ - 0 - ] - # spectrum = np.abs(spectrum[:int(len(spectrum) / 2)]) - freq = np.fft.fftfreq( - spectrum.shape[-1], d=(grid.axes[-1][1] - grid.axes[-1][0]) - ) - omega_spectrum = 2 * np.pi * freq - - if is_envelope: - omega_spectrum = omega0 - omega_spectrum - spectrum /= 2 - - i_sort = np.argsort(omega_spectrum) - omega_spectrum = omega_spectrum[i_sort] - spectrum = np.abs(spectrum[i_sort]) - - i_keep = omega_spectrum >= 0.0 - omega_spectrum = omega_spectrum[i_keep] - spectrum = spectrum[i_keep] - - if range is not None: - omega_interp = np.linspace(*range, bins) - spectrum = np.interp(omega_interp, omega_spectrum, spectrum) - omega_spectrum = omega_interp - - # spectrum = np.fft.fft(grid.field) * grid.dx[-1] - # spectrum = np.abs(spectrum[..., :int(spectrum.shape[-1] / 2)]) - # omega_spectrum = 2 * np.pi / (grid.axes[-1][-1] - grid.axes[-1][0]) * np.arange(spectrum.shape[-1]) - # if is_envelope: - # omega_spectrum = omega_spectrum - # dV = get_grid_cell_volume(grid, dim) - # if dim == "xyt": - # spectrum = np.sum(spectrum * dV, axis=(0, 1)) - # else: - # spectrum = np.sum(spectrum * dV[np.newaxis, :, np.newaxis], axis=1)[0] + field = grid.field[0, 0] else: - # Get the array of angular frequency. - omega, central_omega = get_frequency( - grid=grid, - dim=dim, - is_envelope=True, - omega0=omega0, - phase_unwrap_1d=phase_unwrap_1d, - ) - # Calculate weights of each frequency (amplitude of the field). + field = grid.field + + # Get spectrum. + if is_envelope: + # Assume that the FFT of the envelope and the FFT of the complex + # conjugate of the envelope do not overlap. Then we only need + # one of them. + spectrum = 0.5 * np.abs(np.fft.fft(field)) * grid.dx[-1] + omega_spectrum = omega0 - omega_spectrum + else: + spectrum = np.fft.fft(field) * grid.dx[-1] + # Keep only positive frequencies. + i_keep = spectrum.shape[-1] // 2 + spectrum = np.abs(spectrum[:i_keep]) + omega_spectrum = omega_spectrum[:i_keep] + + # Sum spectrum transversely. + if not on_axis: dV = get_grid_cell_volume(grid, dim) if dim == "xyt": - weights = np.abs(grid.field) * dV - else: # dim == "rt": - weights = np.abs(grid.field) * dV[np.newaxis, :, np.newaxis] - # Get weighted spectrum. - # Neglects the 2 first and last time slices, whose values seems to be - # slightly off (maybe due to lower-order derivative at the edges). - spectrum, edges = np.histogram( - a=np.squeeze(omega)[..., 2:-2], - weights=np.squeeze(weights[..., 2:-2]), - bins=bins, - range=range, - ) - omega_spectrum = edges[1:] - (edges[1] - edges[0]) / 2 + spectrum = np.sum(spectrum, axis=(0, 1)) + else: + spectrum = np.sum( + spectrum * dV[np.newaxis, :, np.newaxis] / dV[0], + axis=1 + )[0] + + # Sort freqency array (and the spectrum accordingly). + i_sort = np.argsort(omega_spectrum) + omega_spectrum = omega_spectrum[i_sort] + spectrum = np.abs(spectrum[i_sort]) + + # If the user specified a frequency range, interpolate into it. + if range is not None: + omega_interp = np.linspace(*range, bins) + spectrum = np.interp(omega_interp, omega_spectrum, spectrum) + omega_spectrum = omega_interp + return spectrum, omega_spectrum From b18861bb84dec52492898b8f162043ed869e68c3 Mon Sep 17 00:00:00 2001 From: Angel Ferran Pousa Date: Tue, 22 Aug 2023 17:12:00 +0200 Subject: [PATCH 13/30] Fix description --- lasy/utils/laser_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lasy/utils/laser_utils.py b/lasy/utils/laser_utils.py index 2d391c7b..804cccd7 100644 --- a/lasy/utils/laser_utils.py +++ b/lasy/utils/laser_utils.py @@ -436,7 +436,7 @@ def get_duration(grid, dim): float RMS duration of the envelope in seconds. """ - # Calculate weights of each frequency (amplitude of the field). + # Calculate weights of each grid cell (amplitude of the field). dV = get_grid_cell_volume(grid, dim) if dim == "xyt": weights = np.abs(grid.field) * dV From 2888b52148b4ac292316d55e3e190d3c8e9ecf6a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 22 Aug 2023 15:14:12 +0000 Subject: [PATCH 14/30] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- lasy/utils/laser_utils.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/lasy/utils/laser_utils.py b/lasy/utils/laser_utils.py index 804cccd7..2a1e77ad 100644 --- a/lasy/utils/laser_utils.py +++ b/lasy/utils/laser_utils.py @@ -265,16 +265,14 @@ def get_spectrum( Array with the frequencies of the spectrum. """ # Get the frequencies of the fft output. - freq = np.fft.fftfreq( - grid.field.shape[-1], d=(grid.axes[-1][1] - grid.axes[-1][0]) - ) + freq = np.fft.fftfreq(grid.field.shape[-1], d=(grid.axes[-1][1] - grid.axes[-1][0])) omega_spectrum = 2 * np.pi * freq # Get on axis or full field. if on_axis: if dim == "xyt": nx, ny, nt = grid.field.shape - field = grid.field[nx//2, ny//2] + field = grid.field[nx // 2, ny // 2] else: field = grid.field[0, 0] else: @@ -300,10 +298,9 @@ def get_spectrum( if dim == "xyt": spectrum = np.sum(spectrum, axis=(0, 1)) else: - spectrum = np.sum( - spectrum * dV[np.newaxis, :, np.newaxis] / dV[0], - axis=1 - )[0] + spectrum = np.sum(spectrum * dV[np.newaxis, :, np.newaxis] / dV[0], axis=1)[ + 0 + ] # Sort freqency array (and the spectrum accordingly). i_sort = np.argsort(omega_spectrum) From 3fa9c66b422583a97d5c73809d4ebe748d5e9f7a Mon Sep 17 00:00:00 2001 From: Angel Ferran Pousa Date: Tue, 22 Aug 2023 17:21:58 +0200 Subject: [PATCH 15/30] Fix docstring --- lasy/utils/laser_utils.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/lasy/utils/laser_utils.py b/lasy/utils/laser_utils.py index 804cccd7..5a649714 100644 --- a/lasy/utils/laser_utils.py +++ b/lasy/utils/laser_utils.py @@ -216,18 +216,17 @@ def get_spectrum( on_axis=False, ): """ - Get the the frequency spectrum of an envelope. + Get the the frequency spectrum of an envelope or electric field. Parameters ---------- grid : a Grid object. - It contains a ndarrays with the field data from which the + It contains an ndarray with the field data from which the spectrum is computed, and the associated metadata. The last axis must be the longitudinal dimension. dim : string (optional) - Dimensionality of the array. Only used if is_envelope is False. - Options are: + Dimensionality of the array. Options are: - 'xyt': The laser pulse is represented on a 3D grid: Cartesian (x,y) transversely, and temporal (t) longitudinally. From f96edfe9cb1095ad7e99775049e3aec010070b14 Mon Sep 17 00:00:00 2001 From: Angel Ferran Pousa Date: Tue, 22 Aug 2023 17:27:19 +0200 Subject: [PATCH 16/30] Shorten line --- lasy/utils/laser_utils.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lasy/utils/laser_utils.py b/lasy/utils/laser_utils.py index c10402af..5560ed6c 100644 --- a/lasy/utils/laser_utils.py +++ b/lasy/utils/laser_utils.py @@ -297,9 +297,7 @@ def get_spectrum( if dim == "xyt": spectrum = np.sum(spectrum, axis=(0, 1)) else: - spectrum = np.sum(spectrum * dV[np.newaxis, :, np.newaxis] / dV[0], axis=1)[ - 0 - ] + spectrum = np.sum(spectrum[0] * dV[:, np.newaxis] / dV[0], axis=0) # Sort freqency array (and the spectrum accordingly). i_sort = np.argsort(omega_spectrum) From a56c599fa7c75218e113c40618c489289a84d14d Mon Sep 17 00:00:00 2001 From: Angel Ferran Pousa Date: Tue, 22 Aug 2023 18:05:22 +0200 Subject: [PATCH 17/30] Square spectrum --- lasy/utils/laser_utils.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lasy/utils/laser_utils.py b/lasy/utils/laser_utils.py index 5560ed6c..c6a476c1 100644 --- a/lasy/utils/laser_utils.py +++ b/lasy/utils/laser_utils.py @@ -291,6 +291,9 @@ def get_spectrum( spectrum = np.abs(spectrum[:i_keep]) omega_spectrum = omega_spectrum[:i_keep] + # Square to get energy-like spectrum (check if appropriate). + spectrum = spectrum ** 2 + # Sum spectrum transversely. if not on_axis: dV = get_grid_cell_volume(grid, dim) From 4e26ba85ace65a857ec96ffb4192e47fbc941dba Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 22 Aug 2023 16:05:49 +0000 Subject: [PATCH 18/30] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- lasy/utils/laser_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lasy/utils/laser_utils.py b/lasy/utils/laser_utils.py index c6a476c1..0cdcff68 100644 --- a/lasy/utils/laser_utils.py +++ b/lasy/utils/laser_utils.py @@ -292,7 +292,7 @@ def get_spectrum( omega_spectrum = omega_spectrum[:i_keep] # Square to get energy-like spectrum (check if appropriate). - spectrum = spectrum ** 2 + spectrum = spectrum**2 # Sum spectrum transversely. if not on_axis: From 651f401365dbe722862b4f43d062aecc71cc7618 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81ngel=20Ferran=20Pousa?= Date: Tue, 22 Aug 2023 22:56:42 +0200 Subject: [PATCH 19/30] Implement spectrum "mode" + fixes --- lasy/utils/laser_utils.py | 46 ++++++++++++++++++++++++--------------- 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/lasy/utils/laser_utils.py b/lasy/utils/laser_utils.py index c6a476c1..59c73bf9 100644 --- a/lasy/utils/laser_utils.py +++ b/lasy/utils/laser_utils.py @@ -213,7 +213,7 @@ def get_spectrum( bins=20, is_envelope=True, omega0=None, - on_axis=False, + mode='sum' ): """ Get the the frequency spectrum of an envelope or electric field. @@ -251,14 +251,15 @@ def get_spectrum( Angular frequency at which the envelope is defined. Required if `is_envelope=True`. - on_axis : bool (optional) - Whether to get the spectrum on axis or, otherwise, the summed spectrum - along the transverse direction(s). + mode : {'sum', 'on_axis', 'full'} (optional) + Whether to return the summed spectrum along the transverse direction(s), + the on-axis spectrum, or the full (unsumed) spectrum in the whole + transverse domain. Returns ------- spectrum : ndarray of doubles - Array with the spectrum amplitudes (FFT amplitude * dt). + Array with the spectral density in J/(rad/s). omega_spectrum : scalar Array with the frequencies of the spectrum. @@ -268,7 +269,7 @@ def get_spectrum( omega_spectrum = 2 * np.pi * freq # Get on axis or full field. - if on_axis: + if mode == 'on_axis': if dim == "xyt": nx, ny, nt = grid.field.shape field = grid.field[nx // 2, ny // 2] @@ -295,17 +296,28 @@ def get_spectrum( spectrum = spectrum ** 2 # Sum spectrum transversely. - if not on_axis: - dV = get_grid_cell_volume(grid, dim) + dV = get_grid_cell_volume(grid, dim) + if mode == 'on_axis': if dim == "xyt": - spectrum = np.sum(spectrum, axis=(0, 1)) + spectrum *= dV else: - spectrum = np.sum(spectrum[0] * dV[:, np.newaxis] / dV[0], axis=0) - - # Sort freqency array (and the spectrum accordingly). + spectrum *= dV[0] + else: + if dim == "xyt": + spectrum = spectrum * dV + else: + spectrum = spectrum[0] * dV[:, np.newaxis] + if mode == 'sum': + if dim == "xyt": + spectrum = np.sum(spectrum, axis=(0, 1)) + else: + spectrum = np.sum(spectrum, axis=0) + spectrum *= epsilon_0 / grid.dx[-1] / np.pi + + # Sort frequency array (and the spectrum accordingly). i_sort = np.argsort(omega_spectrum) omega_spectrum = omega_spectrum[i_sort] - spectrum = np.abs(spectrum[i_sort]) + spectrum = spectrum[..., i_sort] # If the user specified a frequency range, interpolate into it. if range is not None: @@ -419,7 +431,7 @@ def get_frequency( def get_duration(grid, dim): - """Get envelope duration, measured as RMS. + """Get duration of the intensity of the envelope, measured as RMS. Parameters ---------- @@ -431,14 +443,14 @@ def get_duration(grid, dim): Returns ------- float - RMS duration of the envelope in seconds. + RMS duration of the envelope intensity in seconds. """ # Calculate weights of each grid cell (amplitude of the field). dV = get_grid_cell_volume(grid, dim) if dim == "xyt": - weights = np.abs(grid.field) * dV + weights = np.abs(grid.field)**2 * dV else: # dim == "rt": - weights = np.abs(grid.field) * dV[np.newaxis, :, np.newaxis] + weights = np.abs(grid.field)**2 * dV[np.newaxis, :, np.newaxis] # project weights to longitudinal axes weights = np.sum(weights, axis=(0, 1)) return weighted_std(grid.axes[-1], weights) From d9fce8129e7f676e5126f80f8ba4b54e1c423a7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81ngel=20Ferran=20Pousa?= Date: Tue, 22 Aug 2023 22:57:18 +0200 Subject: [PATCH 20/30] Add tests --- tests/test_laser_utils.py | 71 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 tests/test_laser_utils.py diff --git a/tests/test_laser_utils.py b/tests/test_laser_utils.py new file mode 100644 index 00000000..faeb9329 --- /dev/null +++ b/tests/test_laser_utils.py @@ -0,0 +1,71 @@ +import numpy as np + +from lasy.laser import Laser +from lasy.profiles.gaussian_profile import GaussianProfile +from lasy.utils.laser_utils import (get_spectrum, compute_laser_energy, get_duration) + + +def get_gaussian_profile(): + # Cases with Gaussian laser + wavelength = 0.8e-6 + pol = (1, 0) + laser_energy = 1.0 # J + t_peak = 0.0e-15 # s + tau = 30.0e-15 # s + w0 = 5.0e-6 # m + profile = GaussianProfile(wavelength, pol, laser_energy, w0, tau, t_peak) + + return profile + + +def get_gaussian_laser(dim): + # - Cylindrical case + if dim == "rt": + lo = (0e-6, -60e-15) + hi = (25e-6, +60e-15) + npoints = (100, 100) + elif dim == "xyt": + lo = (-25e-6, -25e-6, -60e-15) + hi = (+25e-6, +25e-6, +60e-15) + npoints = (100, 100, 100) + return Laser(dim, lo, hi, npoints, get_gaussian_profile()) + + +def test_laser_analysis_utils(): + """Test the different laser analysis utilities in both geometries.""" + for dim in ["xyt", 'rt']: + laser = get_gaussian_laser(dim) + + # Check that energy computed from spectrum agrees with `compute_laser_energy`. + spectrum, omega = get_spectrum(laser.grid, dim, is_envelope=True, + omega0=laser.profile.omega0) + d_omega = omega[1] - omega[0] + spectrum_energy = np.sum(spectrum) * d_omega + energy = compute_laser_energy(dim, laser.grid) + np.testing.assert_approx_equal(spectrum_energy, energy, significant=14) + + # Check that: + # 1. The on-axis spectrum agrees with the on-axis value of the full spectrum + # 2. The summed full spectrum agrees with the summed spectrum. + spectrum_oa, omega = get_spectrum(laser.grid, dim, is_envelope=True, + omega0=laser.profile.omega0, mode='on_axis') + spectrum_full, omega = get_spectrum(laser.grid, dim, is_envelope=True, + omega0=laser.profile.omega0, mode='full') + if dim == "xyt": + nx, ny, nt = laser.grid.field.shape + spectrum_oa_from_full = spectrum_full[nx // 2, ny // 2] + spectrum_sum_from_full = np.sum(spectrum_full, axis=(0, 1)) + else: + spectrum_oa_from_full = spectrum_full[0, 0] + spectrum_sum_from_full = np.sum(spectrum_full, axis=(0)) + np.testing.assert_allclose(spectrum_oa_from_full, spectrum_oa, rtol=1e-13) + np.testing.assert_allclose(spectrum_sum_from_full, spectrum, rtol=1e-13) + + # Check that laser duration agrees with the given one. + tau_rms = get_duration(laser.grid, dim) + np.testing.assert_approx_equal(2*tau_rms, laser.profile.long_profile.tau, significant=3) + + + +if __name__ == "__main__": + test_laser_analysis_utils() From 5fd62e815c91a4feb01e7ea8ea38af7f9f2d0f26 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 22 Aug 2023 20:58:38 +0000 Subject: [PATCH 21/30] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- lasy/utils/laser_utils.py | 18 ++++++------------ tests/test_laser_utils.py | 28 ++++++++++++++++++---------- 2 files changed, 24 insertions(+), 22 deletions(-) diff --git a/lasy/utils/laser_utils.py b/lasy/utils/laser_utils.py index 6b14a27a..799f167c 100644 --- a/lasy/utils/laser_utils.py +++ b/lasy/utils/laser_utils.py @@ -207,13 +207,7 @@ def get_full_field(laser, theta=0, slice=0, slice_axis="x", Nt=None): def get_spectrum( - grid, - dim, - range=None, - bins=20, - is_envelope=True, - omega0=None, - mode='sum' + grid, dim, range=None, bins=20, is_envelope=True, omega0=None, mode="sum" ): """ Get the the frequency spectrum of an envelope or electric field. @@ -269,7 +263,7 @@ def get_spectrum( omega_spectrum = 2 * np.pi * freq # Get on axis or full field. - if mode == 'on_axis': + if mode == "on_axis": if dim == "xyt": nx, ny, nt = grid.field.shape field = grid.field[nx // 2, ny // 2] @@ -297,7 +291,7 @@ def get_spectrum( # Sum spectrum transversely. dV = get_grid_cell_volume(grid, dim) - if mode == 'on_axis': + if mode == "on_axis": if dim == "xyt": spectrum *= dV else: @@ -307,7 +301,7 @@ def get_spectrum( spectrum = spectrum * dV else: spectrum = spectrum[0] * dV[:, np.newaxis] - if mode == 'sum': + if mode == "sum": if dim == "xyt": spectrum = np.sum(spectrum, axis=(0, 1)) else: @@ -448,9 +442,9 @@ def get_duration(grid, dim): # Calculate weights of each grid cell (amplitude of the field). dV = get_grid_cell_volume(grid, dim) if dim == "xyt": - weights = np.abs(grid.field)**2 * dV + weights = np.abs(grid.field) ** 2 * dV else: # dim == "rt": - weights = np.abs(grid.field)**2 * dV[np.newaxis, :, np.newaxis] + weights = np.abs(grid.field) ** 2 * dV[np.newaxis, :, np.newaxis] # project weights to longitudinal axes weights = np.sum(weights, axis=(0, 1)) return weighted_std(grid.axes[-1], weights) diff --git a/tests/test_laser_utils.py b/tests/test_laser_utils.py index faeb9329..ed04c282 100644 --- a/tests/test_laser_utils.py +++ b/tests/test_laser_utils.py @@ -2,7 +2,7 @@ from lasy.laser import Laser from lasy.profiles.gaussian_profile import GaussianProfile -from lasy.utils.laser_utils import (get_spectrum, compute_laser_energy, get_duration) +from lasy.utils.laser_utils import get_spectrum, compute_laser_energy, get_duration def get_gaussian_profile(): @@ -33,12 +33,13 @@ def get_gaussian_laser(dim): def test_laser_analysis_utils(): """Test the different laser analysis utilities in both geometries.""" - for dim in ["xyt", 'rt']: + for dim in ["xyt", "rt"]: laser = get_gaussian_laser(dim) # Check that energy computed from spectrum agrees with `compute_laser_energy`. - spectrum, omega = get_spectrum(laser.grid, dim, is_envelope=True, - omega0=laser.profile.omega0) + spectrum, omega = get_spectrum( + laser.grid, dim, is_envelope=True, omega0=laser.profile.omega0 + ) d_omega = omega[1] - omega[0] spectrum_energy = np.sum(spectrum) * d_omega energy = compute_laser_energy(dim, laser.grid) @@ -47,10 +48,16 @@ def test_laser_analysis_utils(): # Check that: # 1. The on-axis spectrum agrees with the on-axis value of the full spectrum # 2. The summed full spectrum agrees with the summed spectrum. - spectrum_oa, omega = get_spectrum(laser.grid, dim, is_envelope=True, - omega0=laser.profile.omega0, mode='on_axis') - spectrum_full, omega = get_spectrum(laser.grid, dim, is_envelope=True, - omega0=laser.profile.omega0, mode='full') + spectrum_oa, omega = get_spectrum( + laser.grid, + dim, + is_envelope=True, + omega0=laser.profile.omega0, + mode="on_axis", + ) + spectrum_full, omega = get_spectrum( + laser.grid, dim, is_envelope=True, omega0=laser.profile.omega0, mode="full" + ) if dim == "xyt": nx, ny, nt = laser.grid.field.shape spectrum_oa_from_full = spectrum_full[nx // 2, ny // 2] @@ -63,8 +70,9 @@ def test_laser_analysis_utils(): # Check that laser duration agrees with the given one. tau_rms = get_duration(laser.grid, dim) - np.testing.assert_approx_equal(2*tau_rms, laser.profile.long_profile.tau, significant=3) - + np.testing.assert_approx_equal( + 2 * tau_rms, laser.profile.long_profile.tau, significant=3 + ) if __name__ == "__main__": From 629a3a1865c5c1a49458849413742523d66b90b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81ngel=20Ferran=20Pousa?= Date: Tue, 22 Aug 2023 23:13:13 +0200 Subject: [PATCH 22/30] Fix codeQL --- tests/test_laser_utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_laser_utils.py b/tests/test_laser_utils.py index ed04c282..d47961a7 100644 --- a/tests/test_laser_utils.py +++ b/tests/test_laser_utils.py @@ -23,8 +23,8 @@ def get_gaussian_laser(dim): if dim == "rt": lo = (0e-6, -60e-15) hi = (25e-6, +60e-15) - npoints = (100, 100) - elif dim == "xyt": + npoints = (100, 100) + else: # dim == "xyt": lo = (-25e-6, -25e-6, -60e-15) hi = (+25e-6, +25e-6, +60e-15) npoints = (100, 100, 100) From 8054d3132a65f14e6bed1b97fe5238c5e6fb067f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 22 Aug 2023 21:14:03 +0000 Subject: [PATCH 23/30] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/test_laser_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_laser_utils.py b/tests/test_laser_utils.py index d47961a7..9136b1c0 100644 --- a/tests/test_laser_utils.py +++ b/tests/test_laser_utils.py @@ -23,7 +23,7 @@ def get_gaussian_laser(dim): if dim == "rt": lo = (0e-6, -60e-15) hi = (25e-6, +60e-15) - npoints = (100, 100) + npoints = (100, 100) else: # dim == "xyt": lo = (-25e-6, -25e-6, -60e-15) hi = (+25e-6, +25e-6, +60e-15) From e9559d9d37b25110879854205b4a79aac0bef458 Mon Sep 17 00:00:00 2001 From: Angel Ferran Pousa Date: Wed, 23 Aug 2023 15:41:42 +0200 Subject: [PATCH 24/30] Change output depending on calculation method --- lasy/utils/laser_utils.py | 107 +++++++++++++++++++++++--------------- 1 file changed, 65 insertions(+), 42 deletions(-) diff --git a/lasy/utils/laser_utils.py b/lasy/utils/laser_utils.py index 799f167c..d07adf17 100644 --- a/lasy/utils/laser_utils.py +++ b/lasy/utils/laser_utils.py @@ -207,11 +207,41 @@ def get_full_field(laser, theta=0, slice=0, slice_axis="x", Nt=None): def get_spectrum( - grid, dim, range=None, bins=20, is_envelope=True, omega0=None, mode="sum" + grid, dim, range=None, bins=20, is_envelope=True, omega0=None, method="sum" ): - """ + r""" Get the the frequency spectrum of an envelope or electric field. + The spectrum can be calculated in three different ways, depending on the + `method` specified by the user: + + Initially, the spectrum is calculated as the Fourier transform of the + electric field :math:`E(t)`. + + ..math:: + \int E(t) e^{-i \omega t} dt + + neglecting the negative frequencies. If ``method=="raw"``, no further + processing is done and the returned spectrum is a complex array with the + same transverse dimensions as the input grid. The units are + :math:`\mathrm{V / Hz}`. + + For the other methods, the spectral energy density is calculated as + + ..math:: + \frac{\epsilon_0 c}{2\pi} |\int E(t) e^{-i \omega t} dt| ^ 2 + + If ``method=="on_axis"``, a 1D real array with on-axis value of the + equation above is returned. The units are :math:`\mathrm{J / (rad Hz m^2)}`. + + Otherwise, if ``method=="sum"`` (default), the transverse integral of the + spectral energy density is calculated: + + ..math:: + \frac{\epsilon_0 c}{2\pi} \int |\int E(t) e^{-i \omega t} dt| ^ 2 dx dy + + The units of this array are :math:`\mathrm{J / (rad Hz)}` + Parameters ---------- grid : a Grid object. @@ -245,25 +275,24 @@ def get_spectrum( Angular frequency at which the envelope is defined. Required if `is_envelope=True`. - mode : {'sum', 'on_axis', 'full'} (optional) - Whether to return the summed spectrum along the transverse direction(s), - the on-axis spectrum, or the full (unsumed) spectrum in the whole - transverse domain. + method : {'sum', 'on_axis', 'raw'} (optional) + Determines the type of spectrum that is returned as described above. + By default 'sum'. Returns ------- - spectrum : ndarray of doubles - Array with the spectral density in J/(rad/s). + spectrum : ndarray + Array with the spectrum (units and array type depend on ``method``). - omega_spectrum : scalar - Array with the frequencies of the spectrum. + omega : ndarray + Array with the angular frequencies of the spectrum. """ # Get the frequencies of the fft output. freq = np.fft.fftfreq(grid.field.shape[-1], d=(grid.axes[-1][1] - grid.axes[-1][0])) - omega_spectrum = 2 * np.pi * freq + omega = 2 * np.pi * freq # Get on axis or full field. - if mode == "on_axis": + if method == "on_axis": if dim == "xyt": nx, ny, nt = grid.field.shape field = grid.field[nx // 2, ny // 2] @@ -277,49 +306,43 @@ def get_spectrum( # Assume that the FFT of the envelope and the FFT of the complex # conjugate of the envelope do not overlap. Then we only need # one of them. - spectrum = 0.5 * np.abs(np.fft.fft(field)) * grid.dx[-1] - omega_spectrum = omega0 - omega_spectrum + spectrum = 0.5 * np.fft.fft(field) * grid.dx[-1] + omega = omega0 - omega + # Sort frequency array (and the spectrum accordingly). + i_sort = np.argsort(omega) + omega = omega[i_sort] + spectrum = spectrum[..., i_sort] + # Keep only positive frequencies. + i_keep = omega >= 0 + omega = omega[i_keep] + spectrum = spectrum[..., i_keep] else: spectrum = np.fft.fft(field) * grid.dx[-1] # Keep only positive frequencies. i_keep = spectrum.shape[-1] // 2 - spectrum = np.abs(spectrum[:i_keep]) - omega_spectrum = omega_spectrum[:i_keep] + omega = omega[:i_keep] + spectrum = spectrum[:i_keep] - # Square to get energy-like spectrum (check if appropriate). - spectrum = spectrum**2 + # Convert to spectral energy density (J/(m^2 rad Hz)). + if method != "raw": + spectrum = np.abs(spectrum)**2 * epsilon_0 * c / np.pi - # Sum spectrum transversely. - dV = get_grid_cell_volume(grid, dim) - if mode == "on_axis": - if dim == "xyt": - spectrum *= dV - else: - spectrum *= dV[0] - else: + # Integrate transversely. + if method == "sum": + dV = get_grid_cell_volume(grid, dim) + dz = grid.dx[-1] * c if dim == "xyt": - spectrum = spectrum * dV + spectrum = np.sum(spectrum * dV / dz, axis=(0, 1)) else: - spectrum = spectrum[0] * dV[:, np.newaxis] - if mode == "sum": - if dim == "xyt": - spectrum = np.sum(spectrum, axis=(0, 1)) - else: - spectrum = np.sum(spectrum, axis=0) - spectrum *= epsilon_0 / grid.dx[-1] / np.pi - - # Sort frequency array (and the spectrum accordingly). - i_sort = np.argsort(omega_spectrum) - omega_spectrum = omega_spectrum[i_sort] - spectrum = spectrum[..., i_sort] + spectrum = np.sum(spectrum[0] * dV[:, np.newaxis] / dz, axis=0) # If the user specified a frequency range, interpolate into it. if range is not None: omega_interp = np.linspace(*range, bins) - spectrum = np.interp(omega_interp, omega_spectrum, spectrum) - omega_spectrum = omega_interp + spectrum = np.interp(omega_interp, omega, spectrum) + omega = omega_interp - return spectrum, omega_spectrum + return spectrum, omega def get_frequency( From 6e485b5644c5aaac54e24461568577d5a0f72e25 Mon Sep 17 00:00:00 2001 From: Angel Ferran Pousa Date: Wed, 23 Aug 2023 15:41:51 +0200 Subject: [PATCH 25/30] Update test --- tests/test_laser_utils.py | 25 +------------------------ 1 file changed, 1 insertion(+), 24 deletions(-) diff --git a/tests/test_laser_utils.py b/tests/test_laser_utils.py index 9136b1c0..15e13f81 100644 --- a/tests/test_laser_utils.py +++ b/tests/test_laser_utils.py @@ -43,30 +43,7 @@ def test_laser_analysis_utils(): d_omega = omega[1] - omega[0] spectrum_energy = np.sum(spectrum) * d_omega energy = compute_laser_energy(dim, laser.grid) - np.testing.assert_approx_equal(spectrum_energy, energy, significant=14) - - # Check that: - # 1. The on-axis spectrum agrees with the on-axis value of the full spectrum - # 2. The summed full spectrum agrees with the summed spectrum. - spectrum_oa, omega = get_spectrum( - laser.grid, - dim, - is_envelope=True, - omega0=laser.profile.omega0, - mode="on_axis", - ) - spectrum_full, omega = get_spectrum( - laser.grid, dim, is_envelope=True, omega0=laser.profile.omega0, mode="full" - ) - if dim == "xyt": - nx, ny, nt = laser.grid.field.shape - spectrum_oa_from_full = spectrum_full[nx // 2, ny // 2] - spectrum_sum_from_full = np.sum(spectrum_full, axis=(0, 1)) - else: - spectrum_oa_from_full = spectrum_full[0, 0] - spectrum_sum_from_full = np.sum(spectrum_full, axis=(0)) - np.testing.assert_allclose(spectrum_oa_from_full, spectrum_oa, rtol=1e-13) - np.testing.assert_allclose(spectrum_sum_from_full, spectrum, rtol=1e-13) + np.testing.assert_approx_equal(spectrum_energy, energy, significant=10) # Check that laser duration agrees with the given one. tau_rms = get_duration(laser.grid, dim) From 1b0f5ab391bcca0efb0bca75c684f75403d86d84 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 23 Aug 2023 13:42:27 +0000 Subject: [PATCH 26/30] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- lasy/utils/laser_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lasy/utils/laser_utils.py b/lasy/utils/laser_utils.py index d07adf17..fd0dc96e 100644 --- a/lasy/utils/laser_utils.py +++ b/lasy/utils/laser_utils.py @@ -325,7 +325,7 @@ def get_spectrum( # Convert to spectral energy density (J/(m^2 rad Hz)). if method != "raw": - spectrum = np.abs(spectrum)**2 * epsilon_0 * c / np.pi + spectrum = np.abs(spectrum) ** 2 * epsilon_0 * c / np.pi # Integrate transversely. if method == "sum": From 85e4fc1d00a4d8d958cc7e4f6408a1f6c5c09df2 Mon Sep 17 00:00:00 2001 From: Angel Ferran Pousa Date: Wed, 23 Aug 2023 15:55:35 +0200 Subject: [PATCH 27/30] Fix bug --- lasy/utils/laser_utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lasy/utils/laser_utils.py b/lasy/utils/laser_utils.py index fd0dc96e..01924cbb 100644 --- a/lasy/utils/laser_utils.py +++ b/lasy/utils/laser_utils.py @@ -321,7 +321,7 @@ def get_spectrum( # Keep only positive frequencies. i_keep = spectrum.shape[-1] // 2 omega = omega[:i_keep] - spectrum = spectrum[:i_keep] + spectrum = spectrum[..., :i_keep] # Convert to spectral energy density (J/(m^2 rad Hz)). if method != "raw": @@ -337,7 +337,7 @@ def get_spectrum( spectrum = np.sum(spectrum[0] * dV[:, np.newaxis] / dz, axis=0) # If the user specified a frequency range, interpolate into it. - if range is not None: + if method in ["sum", "on_axis"] and range is not None: omega_interp = np.linspace(*range, bins) spectrum = np.interp(omega_interp, omega, spectrum) omega = omega_interp From 75116977614bfccacece0d71a596905a5cdc48b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81ngel=20Ferran=20Pousa?= Date: Wed, 23 Aug 2023 16:27:04 +0200 Subject: [PATCH 28/30] Update lasy/utils/laser_utils.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Maxence Thévenet --- lasy/utils/laser_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lasy/utils/laser_utils.py b/lasy/utils/laser_utils.py index 01924cbb..e48c6a64 100644 --- a/lasy/utils/laser_utils.py +++ b/lasy/utils/laser_utils.py @@ -210,7 +210,7 @@ def get_spectrum( grid, dim, range=None, bins=20, is_envelope=True, omega0=None, method="sum" ): r""" - Get the the frequency spectrum of an envelope or electric field. + Get the frequency spectrum of an envelope or electric field. The spectrum can be calculated in three different ways, depending on the `method` specified by the user: From 07c8f71cec29ea6c27151e61df5ba5371705ff2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81ngel=20Ferran=20Pousa?= Date: Wed, 23 Aug 2023 16:27:21 +0200 Subject: [PATCH 29/30] Update lasy/utils/laser_utils.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Maxence Thévenet --- lasy/utils/laser_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lasy/utils/laser_utils.py b/lasy/utils/laser_utils.py index e48c6a64..4b140e65 100644 --- a/lasy/utils/laser_utils.py +++ b/lasy/utils/laser_utils.py @@ -263,7 +263,7 @@ def get_spectrum( will be returned using interpolation. bins : int (optional) - Number of bins of to which to interpolate the spectrum if a `range` + Number of bins into which to interpolate the spectrum if a `range` is given. is_envelope : bool (optional) From e18c7b484524351a08f771a1ba612ea3158a7c03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81ngel=20Ferran=20Pousa?= Date: Wed, 23 Aug 2023 16:27:32 +0200 Subject: [PATCH 30/30] Update lasy/utils/laser_utils.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Maxence Thévenet --- lasy/utils/laser_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lasy/utils/laser_utils.py b/lasy/utils/laser_utils.py index 4b140e65..b86e9673 100644 --- a/lasy/utils/laser_utils.py +++ b/lasy/utils/laser_utils.py @@ -269,7 +269,7 @@ def get_spectrum( is_envelope : bool (optional) Whether the field provided uses the envelope representation, as used internally in lasy. If False, field is assumed to represent the - the electric field. + the full electric field (with fast oscillations). omega0 : scalar (optional) Angular frequency at which the envelope is defined. Required if