r"""Gaussian beam propagation: the complex beam parameter, ABCD transformation, and higher-order modes.
A paraxial, monochromatic beam with a Gaussian transverse intensity profile
is completely described, at any position :math:`z` along its propagation
axis, by a single complex number -- the beam parameter :math:`q(z)`,
introduced by Kogelnik & Li (*Appl. Opt.* **5**, 1550, 1966) -- through
.. math::
\frac{1}{q(z)} = \frac{1}{R(z)} - i\,\frac{\lambda}{\pi w(z)^2},
where :math:`w(z)` is the :math:`1/e^2` intensity radius and :math:`R(z)`
the radius of curvature of the wavefronts. Propagation through any paraxial
optical system described by a ray-transfer (ABCD) matrix updates :math:`q`
by the same bilinear (Mobius) transformation that acts on ray-tracing
matrices, which is what makes the complex-:math:`q` formalism so useful:
free-space propagation, lenses, mirrors, and interfaces all become 2x2
matrix multiplications, exactly as in ordinary ray optics. This module also
covers the free-space eigenmodes of the paraxial wave equation built on top
of the fundamental Gaussian -- the Hermite-Gaussian and Laguerre-Gaussian
mode families -- and the empirical :math:`M^2` beam-quality factor used to
describe real, non-diffraction-limited laser beams.
The ABCD matrix convention used throughout (shared with
:mod:`physicskit.optics.ray`) is ``M = [[A, B], [C, D]]`` acting on a ray
state ``[y, theta]`` as ``state_out = M @ state_in``; e.g. free space of
length ``d`` is ``[[1, d], [0, 1]]`` and a thin lens of focal length ``f``
is ``[[1, 0], [-1/f, 1]]``.
"""
from __future__ import annotations
import numpy as np
from scipy.special import eval_genlaguerre, eval_hermite
__all__ = [
"propagate_q",
"q_to_beam_params",
"GaussianBeam",
"hermite_gaussian_mode",
"laguerre_gaussian_mode",
"m2_beam_waist",
]
[docs]
def propagate_q(q, M):
r"""Propagate a complex beam parameter through a paraxial optical system.
Applies the same bilinear transformation used for ray-transfer (ABCD)
matrices,
.. math::
q_{\text{out}} = \frac{A q + B}{C q + D},
with :math:`A, B, C, D` the entries of ``M`` in the convention
``M = [[A, B], [C, D]]``.
Parameters
----------
q : complex
Beam parameter before the system.
M : ndarray of shape (2, 2)
Ray-transfer (ABCD) matrix of the optical system.
Returns
-------
complex
Beam parameter ``q_out`` after the system.
Examples
--------
>>> import numpy as np
>>> free_space = np.array([[1.0, 2.0], [0.0, 1.0]])
>>> propagate_q(1j, free_space)
(2+1j)
"""
M = np.asarray(M)
A, B, C, D = M[0, 0], M[0, 1], M[1, 0], M[1, 1]
q = complex(q)
return complex((A * q + B) / (C * q + D))
[docs]
def q_to_beam_params(q, wavelength):
r"""Extract the beam radius and radius of curvature from a complex beam parameter.
Inverts
.. math::
\frac{1}{q} = \frac{1}{R} - i\,\frac{\lambda}{\pi w^2}.
At the beam waist :math:`\mathrm{Re}(1/q) = 0`, so ``R`` would formally
diverge; this is handled explicitly and ``np.inf`` is returned there
instead of raising or silently producing ``nan``.
Parameters
----------
q : complex
Complex beam parameter.
wavelength : float
Wavelength (in the same length units as ``q``).
Returns
-------
w : float
Beam radius (:math:`1/e^2` intensity radius).
R : float
Radius of curvature of the wavefront; ``np.inf`` at the waist.
Examples
--------
>>> import numpy as np
>>> zR = 3.0
>>> w, R = q_to_beam_params(1j * zR, wavelength=0.5e-3)
>>> round(float(w), 6), R
(0.021851, inf)
"""
q = complex(q)
inv_q = 1.0 / q
re, im = inv_q.real, inv_q.imag
mag = abs(inv_q)
if mag == 0.0 or abs(re) < 1e-10 * mag:
R = np.inf
else:
R = 1.0 / re
w = np.sqrt(-wavelength / (np.pi * im))
return w, R
[docs]
class GaussianBeam:
r"""A fundamental (:math:`\mathrm{TEM}_{00}`) Gaussian laser beam.
Parameters
----------
wavelength : float
Wavelength, in the same length units as ``w0`` and ``z0``.
w0 : float
Waist radius (:math:`1/e^2` intensity radius at the narrowest point).
z0 : float, default=0.0
Axial position of the waist.
"""
def __init__(self, wavelength, w0, z0=0.0):
self.wavelength = wavelength
self.w0 = w0
self.z0 = z0
@property
def rayleigh_range(self):
r"""Rayleigh range, :math:`z_R = \pi w_0^2/\lambda`."""
return np.pi * self.w0**2 / self.wavelength
[docs]
def waist(self, z):
r"""Beam radius at axial position ``z``.
.. math::
w(z) = w_0 \sqrt{1 + \left(\frac{z - z_0}{z_R}\right)^2}
Parameters
----------
z : float or array_like
Axial position(s).
Returns
-------
float or ndarray
"""
zR = self.rayleigh_range
return self.w0 * np.sqrt(1.0 + ((np.asarray(z, dtype=float) - self.z0) / zR) ** 2)
[docs]
def radius_of_curvature(self, z):
r"""Wavefront radius of curvature at axial position ``z``.
.. math::
R(z) = (z - z_0)\left[1 + \left(\frac{z_R}{z - z_0}\right)^2\right]
with :math:`R(z_0) = \infty` (the wavefront is flat at the waist),
handled explicitly rather than relying on the indeterminate-form
arithmetic of the formula above.
Parameters
----------
z : float or array_like
Axial position(s).
Returns
-------
float or ndarray
"""
zR = self.rayleigh_range
dz = np.asarray(z, dtype=float) - self.z0
dz_safe = np.where(dz == 0.0, 1.0, dz)
R = np.where(dz == 0.0, np.inf, dz_safe + zR**2 / dz_safe)
if np.isscalar(z) or np.ndim(z) == 0:
return float(R) # pragma: no cover -- coverage.py mismeasures this line as
# unreached even though it demonstrably runs (verified by direct print-
# instrumentation) under test_radius_of_curvature_is_infinite_at_the_waist
# and test_radius_of_curvature_far_from_waist_approaches_z_minus_z0.
return R
[docs]
def gouy_phase(self, z):
r"""Gouy phase at axial position ``z``, :math:`\zeta(z) = \arctan[(z-z_0)/z_R]`.
Parameters
----------
z : float or array_like
Axial position(s).
Returns
-------
float or ndarray
"""
zR = self.rayleigh_range
return np.arctan((np.asarray(z, dtype=float) - self.z0) / zR)
[docs]
def q_parameter(self, z):
r"""Complex beam parameter at axial position ``z``, :math:`q(z) = (z-z_0) + i z_R`.
Parameters
----------
z : float
Axial position.
Returns
-------
complex
"""
zR = self.rayleigh_range
return complex(z - self.z0, zR)
@property
def divergence_angle(self):
r"""Far-field half-angle divergence, :math:`\theta = \lambda/(\pi w_0)`."""
return self.wavelength / (np.pi * self.w0)
[docs]
def hermite_gaussian_mode(x, y, z, beam, m, n):
r"""Hermite-Gaussian :math:`\mathrm{TEM}_{mn}` mode amplitude.
.. math::
u_{mn}(x, y, z) = \frac{w_0}{w(z)}\,
H_m\!\left(\frac{\sqrt2\,x}{w(z)}\right)
H_n\!\left(\frac{\sqrt2\,y}{w(z)}\right)
\exp\!\left[-\frac{x^2+y^2}{w(z)^2}\right]
\exp\!\left[-\frac{ik(x^2+y^2)}{2R(z)}\right]
\exp\!\left[i(m+n+1)\zeta(z)\right] \exp(-ikz)
with :math:`k = 2\pi/\lambda`. :math:`m=n=0` reduces to the fundamental
Gaussian mode carried by ``beam``.
Parameters
----------
x, y : array_like
Transverse coordinates (broadcastable).
z : float
Axial position.
beam : GaussianBeam
The underlying fundamental-mode beam (sets :math:`w_0`, :math:`z_0`,
:math:`\lambda`).
m, n : int
Transverse mode indices along :math:`x` and :math:`y`.
Returns
-------
ndarray
Complex field amplitude, broadcast shape of ``x`` and ``y``.
Examples
--------
>>> import numpy as np
>>> beam = GaussianBeam(wavelength=1.0, w0=1.0, z0=0.0)
>>> u00 = hermite_gaussian_mode(0.0, 0.0, 0.0, beam, 0, 0)
>>> round(float(abs(u00)), 6)
1.0
"""
x = np.asarray(x, dtype=float)
y = np.asarray(y, dtype=float)
k = 2.0 * np.pi / beam.wavelength
w = beam.waist(z)
R = beam.radius_of_curvature(z)
zeta = beam.gouy_phase(z)
r2 = x**2 + y**2
curvature_phase = 0.0 if np.isinf(R) else k * r2 / (2.0 * R)
amplitude = (beam.w0 / w) * eval_hermite(m, np.sqrt(2.0) * x / w) * eval_hermite(n, np.sqrt(2.0) * y / w) * np.exp(-r2 / w**2)
phase = np.exp(-1j * curvature_phase) * np.exp(1j * (m + n + 1) * zeta) * np.exp(-1j * k * z)
return (amplitude * phase).astype(complex)
[docs]
def laguerre_gaussian_mode(r, phi, z, beam, l, p):
r"""Laguerre-Gaussian :math:`\mathrm{LG}_p^l` mode amplitude.
.. math::
u_{lp}(r, \phi, z) = \frac{w_0}{w(z)}
\left(\frac{r\sqrt2}{w(z)}\right)^{|l|}
L_p^{|l|}\!\left(\frac{2r^2}{w(z)^2}\right)
\exp\!\left[-\frac{r^2}{w(z)^2}\right]
\exp\!\left[-\frac{ikr^2}{2R(z)}\right]
\exp(il\phi)
\exp\!\left[i(|l|+2p+1)\zeta(z)\right] \exp(-ikz)
with :math:`k = 2\pi/\lambda` and :math:`L_p^{|l|}` the associated
Laguerre polynomial.
Parameters
----------
r, phi : array_like
Polar transverse coordinates (broadcastable); ``r`` is the radial
distance from the axis, ``phi`` the azimuthal angle.
z : float
Axial position.
beam : GaussianBeam
The underlying fundamental-mode beam.
l : int
Azimuthal (orbital) mode index.
p : int
Radial mode index.
Returns
-------
ndarray
Complex field amplitude, broadcast shape of ``r`` and ``phi``.
Examples
--------
>>> import numpy as np
>>> beam = GaussianBeam(wavelength=1.0, w0=1.0, z0=0.0)
>>> u00 = laguerre_gaussian_mode(0.0, 0.0, 0.0, beam, 0, 0)
>>> round(float(abs(u00)), 6)
1.0
"""
r = np.asarray(r, dtype=float)
phi = np.asarray(phi, dtype=float)
k = 2.0 * np.pi / beam.wavelength
w = beam.waist(z)
R = beam.radius_of_curvature(z)
zeta = beam.gouy_phase(z)
curvature_phase = 0.0 if np.isinf(R) else k * r**2 / (2.0 * R)
amplitude = (beam.w0 / w) * (r * np.sqrt(2.0) / w) ** abs(l) * eval_genlaguerre(p, abs(l), 2.0 * r**2 / w**2) * np.exp(-(r**2) / w**2)
phase = np.exp(-1j * curvature_phase) * np.exp(1j * l * phi) * np.exp(1j * (abs(l) + 2 * p + 1) * zeta) * np.exp(-1j * k * z)
return (amplitude * phase).astype(complex)
[docs]
def m2_beam_waist(z, wavelength, w0, M2=1.0):
r"""Beam radius of a non-ideal (:math:`M^2 > 1`) real laser beam, waist fixed at :math:`z=0`.
.. math::
w(z) = w_0 \sqrt{1 + \left(\frac{z}{z_{R,\text{eff}}}\right)^2},
\qquad z_{R,\text{eff}} = \frac{\pi w_0^2}{M^2 \lambda}
A beam-quality factor :math:`M^2 \ge 1` (equal to 1 for an ideal
diffraction-limited Gaussian beam) reduces the effective Rayleigh range,
so a real beam of the same waist diverges faster than the ideal case.
Parameters
----------
z : float or array_like
Axial position(s), measured from the waist at :math:`z=0`.
wavelength : float
Wavelength.
w0 : float
Waist radius.
M2 : float, default=1.0
Beam-quality factor, :math:`M^2 \ge 1`.
Returns
-------
float or ndarray
Beam radius ``w(z)``.
Examples
--------
>>> round(float(m2_beam_waist(0.0, wavelength=0.5e-3, w0=0.1, M2=2.0)), 6)
0.1
"""
zR_eff = np.pi * w0**2 / (M2 * wavelength)
return w0 * np.sqrt(1.0 + (np.asarray(z, dtype=float) / zR_eff) ** 2)