Skip to Content
DocsTutorialsBeam focusing

Beam focusing

In this tutorial we simulate the focusing of a Gaussian beam by a thin lens and compare the result with the diffraction limit.

Theory

Gaussian beam

A Gaussian beam has the field distribution

E(r)=E0exp⁡(−r2w02)E(r) = E_0 \exp\left(-\frac{r^2}{w_0^2}\right)

where w0w_0 is the waist radius — the distance from the axis at which the intensity drops by a factor of e2e^2.

Diffraction limit

When a beam of waist w0w_0 is focused by a lens of focal length ff, the amplitude waist in the focal plane is

wfocus=λfπw0,w_{\text{focus}} = \frac{\lambda f}{\pi w_0},

which corresponds to an intensity full width at half maximum of

FWHM=wfocus2ln⁡2.\text{FWHM} = w_{\text{focus}}\sqrt{2\ln 2}.

This is the diffraction limit for a Gaussian beam.

Step 1: Imports and parameters

import math import torch import matplotlib.pyplot as plt from svetlanna import SimulationParameters, Wavefront from svetlanna.elements import ThinLens, FreeSpace, RoundAperture from svetlanna.units import ureg params = SimulationParameters.from_ranges( x_range=(-1.5*ureg.mm, 1.5*ureg.mm), x_points=2048, y_range=(-1.5*ureg.mm, 1.5*ureg.mm), y_points=2048, wavelength=632.8*ureg.nm, # HeNe laser ) w0 = 0.5*ureg.mm # beam waist radius f = 100*ureg.mm # focal length wavelength = 632.8*ureg.nm

The grid step is ≈1.5 μm\approx 1.5\ \mu\text{m}, so the focal spot is sampled by roughly 30 points — enough to measure its width reliably.

Step 2: Creating the Gaussian beam

wf = Wavefront.gaussian_beam(params, waist_radius=w0) print(f"Shape: {wf.shape}") print(f"Dtype: {wf.dtype}") print(f"Max intensity: {wf.max_intensity:.3f}")

Output:

Shape: torch.Size([2048, 2048]) Dtype: torch.complex64 Max intensity: 1.000

Step 3: The optical setup

Aperture → lens → propagation to the focal plane.

# aperture, truncating the beam aperture = RoundAperture(params, radius=1.2*ureg.mm) # thin lens lens = ThinLens(params, focal_length=f) # propagation over one focal length propagate = FreeSpace(params, distance=f, method="zpASM")

Step 4: Propagating through the setup

wf_after_aperture = aperture(wf) wf_after_lens = lens(wf_after_aperture) wf_focus = propagate(wf_after_lens) print(f"Intensity in the focus: {wf_focus.max_intensity:.2e}")

Output:

Intensity in the focus: 1.53e+02

Step 5: Analysing the result

fwhm_x, fwhm_y = wf_focus.fwhm(params) print(f"Measured FWHM: {fwhm_x*1e6:.2f} × {fwhm_y*1e6:.2f} um") w_focus = float(wavelength) * float(f) / (math.pi * float(w0)) fwhm_theory = w_focus * math.sqrt(2 * math.log(2)) print(f"Theoretical FWHM: {fwhm_theory*1e6:.2f} um")

Output:

Measured FWHM: 45.43 × 45.43 um Theoretical FWHM: 47.43 um

The 4 % difference comes from two sources: fwhm() counts only the grid points that lie strictly above half maximum (so it under-reports by up to one grid step), and the aperture truncates the tails of the Gaussian. Refining the grid brings the measurement closer to the analytical value.

Step 6: Visualisation

extent = [-1.5, 1.5, -1.5, 1.5] fig, axes = plt.subplots(2, 3, figsize=(15, 10)) axes[0, 0].imshow(wf.intensity.cpu(), cmap='hot', extent=extent) axes[0, 0].set_title('Input Gaussian beam') axes[0, 0].set_xlabel('x, mm'); axes[0, 0].set_ylabel('y, mm') axes[0, 1].imshow(wf_after_aperture.intensity.cpu(), cmap='hot', extent=extent) axes[0, 1].set_title('After the aperture') axes[0, 1].set_xlabel('x, mm') axes[0, 2].imshow(wf_after_lens.phase.cpu(), cmap='twilight', extent=extent) axes[0, 2].set_title('Phase after the lens') axes[0, 2].set_xlabel('x, mm') axes[1, 0].imshow(wf_focus.intensity.cpu(), cmap='hot', extent=extent) axes[1, 0].set_title('Focal plane') axes[1, 0].set_xlabel('x, mm'); axes[1, 0].set_ylabel('y, mm') center, window = 1024, 60 axes[1, 1].imshow( wf_focus.intensity[center-window:center+window, center-window:center+window].cpu(), cmap='hot', ) axes[1, 1].set_title('Focal spot (zoom)') profile = wf_focus.intensity[center, :].cpu() x_um = params.x.cpu() * 1e6 axes[1, 2].plot(x_um, profile / profile.max()) axes[1, 2].set_xlim(-150, 150) axes[1, 2].set_xlabel('x, um') axes[1, 2].set_ylabel('Normalised intensity') axes[1, 2].set_title('Profile in the focus') axes[1, 2].grid(True) plt.tight_layout() plt.show()

Step 7: Effect of the aperture

How does the aperture size change the focus?

aperture_radii = [0.4, 0.6, 0.8, 1.2] # mm print("Aperture radius | FWHM | Max I") print("-" * 40) for r in aperture_radii: aperture = RoundAperture(params, radius=r*ureg.mm) wf_out = propagate(lens(aperture(wf))) fwhm_x, _ = wf_out.fwhm(params) print(f"{r:.1f} mm | {fwhm_x*1e6:5.1f} um | {wf_out.max_intensity:.3e}")

Output:

Aperture radius | FWHM | Max I ---------------------------------------- 0.4 mm | 83.5 um | 3.436e+01 0.6 mm | 60.1 um | 8.957e+01 0.8 mm | 51.3 um | 1.309e+02 1.2 mm | 45.4 um | 1.527e+02

A hard aperture much smaller than the beam acts as the dominant diffracting element: the spot broadens and the peak intensity drops. Once the aperture stops clipping the beam (radius ≳2w0\gtrsim 2 w_0), the focus converges to the diffraction limit.

Full code

import math import torch from svetlanna import SimulationParameters, Wavefront from svetlanna.elements import ThinLens, FreeSpace, RoundAperture from svetlanna.units import ureg params = SimulationParameters.from_ranges( x_range=(-1.5*ureg.mm, 1.5*ureg.mm), x_points=2048, y_range=(-1.5*ureg.mm, 1.5*ureg.mm), y_points=2048, wavelength=632.8*ureg.nm, ) w0, f = 0.5*ureg.mm, 100*ureg.mm wf = Wavefront.gaussian_beam(params, waist_radius=w0) aperture = RoundAperture(params, radius=1.2*ureg.mm) lens = ThinLens(params, focal_length=f) propagate = FreeSpace(params, distance=f, method="zpASM") wf_focus = propagate(lens(aperture(wf))) fwhm_x, fwhm_y = wf_focus.fwhm(params) print(f"FWHM: {fwhm_x*1e6:.2f} × {fwhm_y*1e6:.2f} um") print(f"Max I: {wf_focus.max_intensity:.2e}")

Conclusions

  1. SVETlANNa reproduces diffraction-limited focusing accurately.
  2. The measured spot size matches the analytical prediction once the grid is fine enough.
  3. The aperture controls both the spot size and the peak intensity.

What next?