Laser from longitudinal and transverse profiles#

In this tutorial, you will learn to create a Laser from an experimental measurement of the laser spectrum and a 2D image of the transverse intensity profile. We’ll start by loading all packages required.

[1]:
import matplotlib.pyplot as plt
import numpy as np
from mpl_toolkits.axes_grid1 import make_axes_locatable
from PIL import Image

from lasy.laser import Laser
from lasy.profiles.combined_profile import CombinedLongitudinalTransverseProfile
from lasy.profiles.longitudinal import LongitudinalProfileFromData
from lasy.profiles.transverse import TransverseProfileFromData
from lasy.profiles.transverse.hermite_gaussian_profile import (
    HermiteGaussianTransverseProfile,
)
from lasy.utils.mode_decomposition import hermite_gauss_decomposition

Next, let’s define the physical parameters defining our laser pulse.

[2]:
polarization = (1, 0)  # Linearly polarized in the x direction
energy_J = 1  # Pulse energy in Joules
cal = 0.2e-6  # Camera pixel size in meters. Used for calibration

In what follows, we do a few steps to create a LASY profile. Profile is the object used in LASY to describe the properties of the pulse. This profile is then used to create a Laser object, i.e., a regular mesh with the values of the vector potential of the pulse. This mesh can be 3D for a 3D (xyt) geometry, or can consist of a few 2D grids for cylindrical geometry with mode decomposition.

Reconstruct longitudinal profile#

We start from an example dataset obtained with a Frequency-resolved optical grating (FROG) measurement. This provides us with the spectrum and spectral phase.

[3]:
# Load from online dataset and store in appropriate variables
file_longitudinal = (
    "https://github.com/user-attachments/files/17414077/df_intensity_spectral_v3.csv"
)

exp_frequency = np.loadtxt(file_longitudinal, usecols=0, dtype="float")  # Hz
exp_spectrum = np.loadtxt(file_longitudinal, usecols=1, dtype="float")  # Arbitary units
exp_phase = np.loadtxt(file_longitudinal, usecols=2, dtype="float")  # rad

Now, we initialize a LASY LongitudinalProfile from experimentally measured spectrum. The central wavelength is calculated in this step, and user later on.

[4]:
longitudinal_data = {
    "datatype": "spectral",
    "axis_is_wavelength": False,
    "axis": exp_frequency,
    "intensity": exp_spectrum,
    "phase": exp_phase,
    "dt": 1e-15,
}

# Create the longitudinal profile. The temporal range is from -200 to +200 femtoseconds
longitudinal_profile = LongitudinalProfileFromData(
    longitudinal_data, lo=-200e-15, hi=200e-15
)

Plot the longitudinal profile data.

[5]:
# Plot both the temporal and spectral data
fig, ax = plt.subplots(1, 2, figsize=(12, 4), tight_layout=True)

# Spectral data
exp_spectrum /= np.max(exp_spectrum)  # Normalize the spectrum
color = "tab:red"
ax[0].set_xlabel("Frequency (Hz)", fontsize=12)
ax[0].set_ylabel("Spectral intensity (AU)", color=color, fontsize=12)
ax[0].plot(exp_frequency, exp_spectrum, color=color)
ax[0].tick_params(axis="y", labelcolor=color)
ax[0].set_title("Measured data (Spectral space)", fontsize=15)
ax0 = ax[0].twinx()
color = "tab:blue"
ax0.set_ylabel("Spectral phase (radian)", color=color, fontsize=12)
ax0.plot(exp_frequency, exp_phase, color=color)
ax0.tick_params(axis="y", labelcolor=color)


# Temporal data
color = "tab:red"
ax[1].set_xlabel("Time (s)", fontsize=12)
ax[1].set_ylabel("Amplitude (AU)", color=color, fontsize=12)
ax[1].plot(
    longitudinal_profile.time,
    np.sqrt(longitudinal_profile.temporal_intensity),
    color=color,
)
ax[1].tick_params(axis="y", labelcolor=color)
ax[1].set_title("Reconstructed data (Temporal space)", fontsize=15)
ax1 = ax[1].twinx()
color = "tab:blue"
ax1.set_ylabel("Temporal phase (radian)", color=color, fontsize=12)
ax1.plot(longitudinal_profile.time, longitudinal_profile.temporal_phase, color=color)
ax1.tick_params(axis="y", labelcolor=color)

fig.tight_layout()
plt.show()
../_images/tutorials_denoised_laser_12_0.png

Reconstruct transverse profile#

For the following reconstruction, the data is provided in the .png image format, which can be read using pillow package.

[6]:
# Define the transverse profile of the laser pulse, and perform minor cleaning
!curl https://user-images.githubusercontent.com/27694869/228038930-d6ab03b1-a726-4b41-a378-5f4a83dc3778.png -o transverse_profile.png
file_transverse = "transverse_profile.png"
img = Image.open(file_transverse)
intensity_data = np.array(img)
intensity_scale = np.max(intensity_data)  # Maximum value of the intensity

intensity_data[intensity_data < intensity_scale / 100] = 0
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  235k  100  235k    0     0  4666k      0 --:--:-- --:--:-- --:--:-- 4710k

Create a LASY TransverseProfile from our experimental data.

[7]:
nx, ny = intensity_data.shape
lo = (0, 0)  # Lower bounds in x and y
hi = (ny * cal, nx * cal)  # Upper bounds in x and y

# Create the transverse profile. This also centers the data by default
transverse_profile = TransverseProfileFromData(
    intensity_data, [lo[0], lo[1]], [hi[0], hi[1]]
)

Plot the original transverse profile from the file. The laser occupies a small fraction of the image.

[8]:
fig, ax = plt.subplots()
cax = ax.imshow(
    intensity_data,
    aspect="auto",
    extent=np.array([lo[0], hi[0], lo[1], hi[1]]) * 1e6,
)
color_bar = fig.colorbar(cax)
color_bar.set_label(r"Fluence  (J/cm$^2$)")
ax.set_xlabel("x ($ \\mu m $)")
ax.set_ylabel("y ($ \\mu m $)")
plt.show()
../_images/tutorials_denoised_laser_19_0.png

Combine longitudinal and transverse profiles#

[9]:
laser_profile_raw = CombinedLongitudinalTransverseProfile(
    wavelength=longitudinal_profile.lambda0,
    pol=polarization,
    laser_energy=energy_J,
    long_profile=longitudinal_profile,
    trans_profile=transverse_profile,
)

Clean/denoise the profile#

LASY functions can be used for denoising/cleaning. Here, the measured profile is decomposed into Hermite-Gauss modes. The cleaning is done by keeping only the first few modes. Take a look at the following example.

[10]:
# Maximum Hermite-Gauss mode index in x and y
m_max = 10
n_max = 10

# Calculate the decomposition and waist of the laser pulse
modeCoeffs, waistX, waistY = hermite_gauss_decomposition(
    transverse_profile,
    longitudinal_profile.lambda0,
    m_max=m_max,
    n_max=n_max,
    res=cal,
    lo=[-2e-4, -2e-4],
    hi=[2e-4, 2e-4],
)

# Create a num profile, summing over the first few modes
energy_frac = 0
for i, mode_key in enumerate(list(modeCoeffs)):
    tmp_transverse_profile = HermiteGaussianTransverseProfile(
        waistX, waistY, mode_key[0], mode_key[1], longitudinal_profile.lambda0
    )
    energy_frac += modeCoeffs[mode_key] ** 2  # Energy fraction of the mode
    if i == 0:  # First mode (0,0)
        laser_profile_cleaned = modeCoeffs[
            mode_key
        ] * CombinedLongitudinalTransverseProfile(
            longitudinal_profile.lambda0,
            polarization,
            longitudinal_profile,
            tmp_transverse_profile,
            laser_energy=energy_J,
        )
    else:  # All other modes
        laser_profile_cleaned += modeCoeffs[
            mode_key
        ] * CombinedLongitudinalTransverseProfile(
            longitudinal_profile.lambda0,
            polarization,
            longitudinal_profile,
            tmp_transverse_profile,
            laser_energy=energy_J,
        )

# Energy loss due to decomposition
energy_loss = 1 - energy_frac
print(f"Energy loss: {energy_loss * 100:.2f}%")
Estimated w0(x-axis) = 12.03 microns (1/e^2 width)
Estimated w0(y-axis) = 10.73 microns (1/e^2 width)
Energy loss: 1.73%
[11]:
# Plot the original and denoised profiles
# Create a grid for plotting
x = np.linspace(-5 * waistX, 5 * waistX, 500)
X, Y = np.meshgrid(x, x)

# Determine the figure parameters
fig, ax = plt.subplots(1, 3, figsize=(12, 4), tight_layout=True)
fig.suptitle(
    "Hermite-Gauss Reconstruction using m_max = %i, n_max = %i" % (m_max, n_max)
)
pltextent = np.array([np.min(x), np.max(x), np.min(x), np.max(x)]) * 1e6  # in microns
cbar_labels = ["Intensity (norm.)", "Intensity (norm.)", "|Intensity Error| (%)"]
title = ["Original Profile", "Reconstructed Profile", "Error"]
scale = ["linear", "log"]
lw = [1.0, 2.5]


# Original profile
prof1 = np.abs(laser_profile_raw.evaluate(X, Y, 0)) ** 2
maxInten = np.max(prof1)
prof1 /= maxInten

# Reconstructed profile
prof2 = np.abs(laser_profile_cleaned.evaluate(X, Y, 0)) ** 2
prof2 /= maxInten

# Normalized error
prof3 = (prof1 - prof2) / np.max(prof1)
prof = [prof1, prof2, prof3]

fig2, ax2 = plt.subplots(2, 1, figsize=(8, 6), tight_layout=True)
fig2.suptitle(
    "Hermite-Gauss Reconstruction using m_max = %i, n_max = %i" % (m_max, n_max)
)

for i in range(3):
    divider = make_axes_locatable(ax[i])
    ax_cb = divider.append_axes("right", size="5%", pad=0.05)
    pl = ax[i].imshow(100 * np.abs(prof[i]), cmap="magma", extent=pltextent)
    cbar = fig.colorbar(pl, cax=ax_cb)
    cbar.set_label(cbar_labels[i])
    ax[i].set_xlim(pltextent[0], pltextent[1])
    ax[i].set_ylim(pltextent[2], pltextent[3])
    ax[i].set_xlabel("x ($ \\mu m $)")
    ax[i].set_ylabel("y ($ \\mu m $)")
    ax[i].set_title(title[i])
    if i < 2:
        ax2[i].plot(
            x * 1e6,
            prof2[int(len(x) / 2), :],
            label=title[1],
            color=(1, 0.5, 0.5),
            lw=2.5,
        )
        ax2[i].plot(
            x * 1e6,
            prof1[int(len(x) / 2), :],
            label=title[0],
            color=(0.3, 0.3, 0.3),
            lw=1.0,
        )
        ax2[i].legend()
        ax2[1].set_yscale(scale[i])
        ax2[i].set_xlim(pltextent[0], pltextent[1])
        ax2[i].set_xlabel("x ($ \\mu m $)")
        ax2[i].set_ylabel("Intensity (norm.)")

plt.show()
../_images/tutorials_denoised_laser_24_0.png
../_images/tutorials_denoised_laser_24_1.png

Create a laser#

Now the hard part is done! From the cleaned profile, we create a LASY Laser object (3D cartesian or cylindrical geometry) and write to a file compliant with the openPMD standard.

[12]:
# First, 3D geometry
dimensions = "xyt"  # Use 3D geometry
lo = (-40e-6, -20e-6, -150e-15)  # Lower bounds of the simulation box
hi = (40e-6, 20e-6, 150e-15)  # Upper bounds of the simulation box
num_points = (
    50,
    50,
    20,
)  # Number of points in each dimension. Use (300, 300, 200) for production.

# Constructing the object using 3D geometry might take a while to run depending on the hardware used.
laser_xyt = Laser(dimensions, lo, hi, num_points, laser_profile_cleaned)  # Laser
laser_xyt.normalize(energy_J * energy_frac)  # Normalize the laser energy
laser_xyt.show()

# Save the laser object to a file
laser_xyt.write_to_file("Laser_xyt_denoised", "h5", save_as_vector_potential=True)
../_images/tutorials_denoised_laser_27_0.png
[13]:
# Then, cylindrical geometry. Here, we also propagate the pulse backwards by 0.5 mm.
dimensions = "rt"  # Use cylindrical geometry
lo = (0, -150e-15)
hi = (40e-6, 150e-15)
num_points = (500, 400)

laser = Laser(dimensions, lo, hi, num_points, laser_profile_cleaned)
laser.normalize(energy_J * energy_frac)
laser.propagate(-0.5e-3)  # Propagate the laser pulse backwards
laser.show()

# Save the laser object to a file
laser.write_to_file("Laser_rt_propagated", "h5", save_as_vector_potential=True)
Available backends are: NP
NP is chosen
../_images/tutorials_denoised_laser_28_1.png