Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add thin optical elements in lasy #199

Merged
merged 28 commits into from
Jul 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
a641a0c
Add `show` method to visualize the laser
RemiLehe Oct 26, 2023
8c61135
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Oct 26, 2023
8c4ba85
Fix doc
RemiLehe Oct 26, 2023
773381f
Apply suggestions from code review
RemiLehe Oct 26, 2023
2f0752c
Base class for optical elements
RemiLehe Oct 20, 2023
b9b22f3
Add documentation
RemiLehe Oct 20, 2023
bbb95b2
Add parabolic mirror
RemiLehe Oct 20, 2023
63ad71e
Add automated test for parabolic mirror
RemiLehe Oct 24, 2023
751c6a6
Add 3D automated test
RemiLehe Oct 24, 2023
4458be0
Update comments
RemiLehe Oct 24, 2023
d6bd90a
Add axiparabola
RemiLehe Oct 25, 2023
4b5bb3e
Fix minor bugs
RemiLehe Oct 25, 2023
85d1fde
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Oct 25, 2023
3cf7182
Implement simplified axiparabola
RemiLehe Oct 26, 2023
3d57f18
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Oct 26, 2023
452dc5f
Fix documentations
RemiLehe Oct 26, 2023
c1a1996
Add tutorial with axiparabola
RemiLehe Oct 27, 2023
ca23bac
Merge branch 'development' into optical_elements
RemiLehe Oct 27, 2023
50ba746
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Oct 27, 2023
4ba6ec5
Merge branch 'development' into optical_elements
RemiLehe Jul 15, 2024
343e05e
Remove axiparabola
RemiLehe Jul 15, 2024
c63a122
Add new API
RemiLehe Jul 15, 2024
12d6ee8
Add missing documentation
RemiLehe Jul 15, 2024
7c082c8
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 15, 2024
0c09ed8
Update lasy/optical_elements/optical_element.py
RemiLehe Jul 16, 2024
411747b
Apply suggestions from code review
RemiLehe Jul 16, 2024
0d76c51
Avoid confusion between omega and w
RemiLehe Jul 16, 2024
66178cf
Add comment explaining why we can multiply each azimuthal component
RemiLehe Jul 16, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/source/api/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ The sections below describe the main objects that are accessible in the ``lasy``

laser
profiles/index
optical_elements/index
utils/index

If you are looking for a specific class or function, see the :ref:`genindex` or use the search bar of this website.
7 changes: 7 additions & 0 deletions docs/source/api/optical_elements/index.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Optical elements
================

.. toctree::
:maxdepth: 1

parabolic_mirror
5 changes: 5 additions & 0 deletions docs/source/api/optical_elements/parabolic_mirror.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Parabolic mirror
================

.. autoclass:: lasy.optical_elements.ParabolicMirror
:members:
47 changes: 47 additions & 0 deletions lasy/laser.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,51 @@ def normalize(self, value, kind="energy"):
else:
raise ValueError(f'kind "{kind}" not recognized')

def apply_optics(self, optical_element):
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New API

"""
Propagate the laser pulse through a thin optical element.

Parameter
---------
optical_element: an :class:`.OpticalElement` object (optional)
Represents a thin optical element, through which the laser
propagates.
"""
# Transform the field from temporal to frequency domain
time_axis_indx = -1
field_fft = np.fft.ifft(self.grid.field, axis=time_axis_indx, norm="backward")
Copy link
Member Author

@RemiLehe RemiLehe Jul 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The implementation is not efficient currently, as it performs FFTs forward and backward.
This will be fixed in #256, by keeping the Fourier-space fields in memory and having a flag that keeps track of this.


# Create the frequency axis
dt = self.grid.dx[time_axis_indx]
omega0 = self.profile.omega0
Nt = self.grid.field.shape[time_axis_indx]
omega_1d = 2 * np.pi * np.fft.fftfreq(Nt, dt) + omega0

# Apply optical element
if self.dim == "rt":
r, omega = np.meshgrid(self.grid.axes[0], omega_1d, indexing="ij")
# The line below assumes that amplitude_multiplier
# is cylindrically symmetric, hence we pass
# `r` as `x` and 0 as `y`
multiplier = optical_element.amplitude_multiplier(r, 0, omega)
# The azimuthal modes are the components of the Fourier transform
# along theta (FT_theta). Because the multiplier is assumed to be
# cylindrically symmetric (i.e. theta-independent):
# FT_theta[ multiplier * field ] = multiplier * FT_theta[ field ]
# Thus, we can simply multiply each azimuthal mode by the multiplier.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added more explanations here.

for i_m in range(self.grid.azimuthal_modes.size):
field_fft[i_m, :, :] *= multiplier
else:
x, y, omega = np.meshgrid(
self.grid.axes[0], self.grid.axes[1], omega_1d, indexing="ij"
)
field_fft *= optical_element.amplitude_multiplier(x, y, omega)

# Transform field from frequency to temporal domain
self.grid.field[:, :, :] = np.fft.fft(
field_fft, axis=time_axis_indx, norm="backward"
)

def propagate(self, distance, nr_boundary=None, backend="NP", show_progress=True):
"""
Propagate the laser pulse by the distance specified.
Expand All @@ -173,6 +218,7 @@ def propagate(self, distance, nr_boundary=None, backend="NP", show_progress=True
Number of cells at the end of radial axis, where the field
will be attenuated (to assert proper Hankel transform).
Only used for ``'rt'``.

backend : string (optional)
Backend used by axiprop (see axiprop documentation).
show_progress : bool (optional)
Expand Down Expand Up @@ -212,6 +258,7 @@ def propagate(self, distance, nr_boundary=None, backend="NP", show_progress=True
omega0 = self.profile.omega0
Nt = self.grid.field.shape[time_axis_indx]
omega = 2 * np.pi * np.fft.fftfreq(Nt, dt) + omega0

# make 3D shape for the frequency axis
omega_shape = (1, 1, self.grid.field.shape[time_axis_indx])

Expand Down
3 changes: 3 additions & 0 deletions lasy/optical_elements/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .parabolic_mirror import ParabolicMirror

__all__ = ["ParabolicMirror"]
41 changes: 41 additions & 0 deletions lasy/optical_elements/optical_element.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import numpy as np


class OpticalElement(object):
"""
Base class to model thin optical elements.

Any optical element should inherit from this class, and define its own
`amplitude_multiplier` method, using the same signature as the method below.
"""

def __init__(self):
pass

def amplitude_multiplier(self, x, y, omega):
r"""
Return the amplitude multiplier :math:`T`.

This number multiplies the complex amplitude of the laser
just before this thin element, in order to obtain the complex
amplitude output laser just after this thin element:

.. math::

\tilde{\mathcal{E}}_{out}(x, y, \omega) = T(x, y, \omega)\tilde{\mathcal{E}}_{in}(x, y, \omega)

Parameters
----------
x, y, omega: ndarrays of floats
Define points on which to evaluate the multiplier.
These arrays need to all have the same shape.
MaxThevenet marked this conversation as resolved.
Show resolved Hide resolved

Returns
-------
multiplier: ndarray of complex numbers
Contains the value of the multiplier at the specified points
This array has the same shape as the arrays x, y, omega
"""
# The base class only defines dummy multiplier
# (This should be replaced by any class that inherits from this one.)
return np.ones_like(x, dtype="complex128")
46 changes: 46 additions & 0 deletions lasy/optical_elements/parabolic_mirror.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
from .optical_element import OpticalElement
import numpy as np
from scipy.constants import c


class ParabolicMirror(OpticalElement):
r"""
Class for a parabolic mirror.

More precisely, the amplitude multiplier corresponds to:

.. math::

T(\boldsymbol{x}_\perp,\omega) = \exp(-i\omega \sqrt{x^2+y^2}/2cf)

where
:math:`\boldsymbol{x}_\perp` is the transverse coordinate (orthogonal
to the propagation direction). The other parameters in this formula
are defined below.

Parameters
----------
f : float (in meter)
The focal length of the parabolic mirror.
"""

def __init__(self, f):
self.f = f

def amplitude_multiplier(self, x, y, omega):
"""
Return the amplitude multiplier.

Parameters
----------
x, y, omega: ndarrays of floats
Define points on which to evaluate the multiplier.
These arrays need to all have the same shape.

Returns
-------
multiplier: ndarray of complex numbers
Contains the value of the multiplier at the specified points
This array has the same shape as the arrays x, y, omega
"""
return np.exp(-1j * omega * (x**2 + y**2) / (2 * c * self.f))
80 changes: 80 additions & 0 deletions tests/test_parabolic_mirror.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# -*- coding: utf-8 -*-
"""
This test checks the implementation of the parabolic mirror
by initializing a Gaussian pulse in the near field, and
propagating it through a parabolic mirror, and then to
the focal position ; we then check that the waist as the
expected value in the far field (i.e. in the focal plane)
"""

import numpy as np
from lasy.laser import Laser
from lasy.profiles.gaussian_profile import GaussianProfile
from lasy.optical_elements import ParabolicMirror

wavelength = 0.8e-6
w0 = 5.0e-3 # m, initialized in near field

# The laser is initialized in the near field
pol = (1, 0)
laser_energy = 1.0 # J
t_peak = 0.0e-15 # s
tau = 30.0e-15 # s
gaussian_profile = GaussianProfile(wavelength, pol, laser_energy, w0, tau, t_peak)


def get_w0(laser):
# Calculate the laser waist
if laser.dim == "xyt":
Nx, Ny, Nt = laser.grid.field.shape
A2 = (np.abs(laser.grid.field[Nx // 2 - 1, :, :]) ** 2).sum(-1)
ax = laser.grid.axes[1]
else:
A2 = (np.abs(laser.grid.field[0, :, :]) ** 2).sum(-1)
ax = laser.grid.axes[0]
if ax[0] > 0:
A2 = np.r_[A2[::-1], A2]
ax = np.r_[-ax[::-1], ax]
else:
A2 = np.r_[A2[::-1][:-1], A2]
ax = np.r_[-ax[::-1][:-1], ax]

sigma = 2 * np.sqrt(np.average(ax**2, weights=A2))

return sigma


def check_parabolic_mirror(laser):
# Propagate laser after parabolic mirror + vacuum
f0 = 8.0 # focal distance in m
laser.apply_optics(ParabolicMirror(f=f0))
laser.propagate(f0)
# Check that the value is the expected one in the near field
w0_num = get_w0(laser)
w0_theor = wavelength * f0 / (np.pi * w0)
err = 2 * np.abs(w0_theor - w0_num) / (w0_theor + w0_num)
assert err < 1e-3


def test_3D_case():
# - 3D case
# The laser is initialized in the near field
dim = "xyt"
lo = (-12e-3, -12e-3, -60e-15)
hi = (+12e-3, +12e-3, +60e-15)
npoints = (500, 500, 100)

laser = Laser(dim, lo, hi, npoints, gaussian_profile)
check_parabolic_mirror(laser)


def test_RT_case():
# - Cylindrical case
# The laser is initialized in the near field
dim = "rt"
lo = (0e-6, -60e-15)
hi = (15e-3, +60e-15)
npoints = (750, 100)

laser = Laser(dim, lo, hi, npoints, gaussian_profile)
check_parabolic_mirror(laser)
Loading