Source code for atomsmltr.environment.lasers.beams

"""Laser Beams
================

Here we implement the generic ``LaserBeam`` class, as well as some actual
laser beam classes

Examples
---------

Setup a Gaussian beam

.. code-block:: python

    from atomsmltr.environment.lasers import GaussianLaserBeam
    from atomsmltr.environment.lasers.polarization import CircularLeft


    beam = GaussianLaserBeam(
        wavelength=399e-9,
        waist=50e-6,
        power=30e-3,
        waist_position=(0, 0, 0),
        direction=(0, 0, 1),
        polarization=CircularLeft(),
    )

See also
--------
atomsmltr.environment.lasers.polarization

"""

# % IMPORTS
import numpy as np
import matplotlib.pyplot as plt

from abc import abstractmethod

# % LOCAL IMPORTS
from .polarization import Vertical, Polarization
from ..envbase import EnvObject
from ...utils.infostring import InfoString


# % GLOBAL DEFINITIONS

DIRECTION_TYPES = ["vector", "thetaphi"]  # allowed values for `direction_type``


# % TOOL FUNCTIONS


def _intensity_gauss(
    r: float, z: float, w0: float, P: float, wavelength: float
) -> float:
    """Computes intensity for a Gaussian beam of waist w0 and power P0 at position
    (r, z), in cynlindrical coordinates. The beam is propagating along z, and the waist
    is located at r = z = 0. Lengths should be given in meters, and powers in watts.
    Intensity is returned in W/m^2

    Args:
        r (float): radial coordinate (distance to beam axis) in _meters_
        z (float): axial coordinate (distance to beam waist) in _meters_
        w0 (float): Gaussian beam waist radius (1/e^2) in _meters_
        P (float): laser power in _Watts_
        wavelength (float): laser wavelength in _meters_

    Returns:
        intensity (float): laser intensity in _W/m^2_
    """

    zR = np.pi * w0**2 / wavelength
    wz = w0 * np.sqrt(1 + z**2 / zR**2)
    I0 = 2 * P / np.pi / w0**2
    intensity = I0 * (w0 / wz) ** 2 * np.exp(-2 * (r**2) / wz**2)
    return intensity


# % ABSTRACT CLASSES


[docs] class LaserBeam(EnvObject): """Representing laser beams Parameters ---------- wavelength : float, optional vacuum wavelength (m), by default 399e-9 waist : float, optional 1/e^2 waist radius (m), by default 1e-3 power : float, optional laser power (W), by default 1e-3 waist_position : array, shape (,3), optional cartesian coordinates of the waist / focus position, in meters and in the lab frame, by default (0, 0, 0) direction : array, shape (,3) or (,2), optional depending on 'direction type', a vector or a (theta, phi) couple giving the propagation direction of the beam direction_type : str, optional type of direction : "vector" or "thetaphi", by default "vector" polarization : Polarization, optional laser polarization, by default Vertical() tag : str, optional laser tag, by default None """ def __init__( self, wavelength: float = 399e-9, waist: float = 1e-3, power: float = 1e-3, waist_position: np.ndarray = (0, 0, 0), direction: np.ndarray = (0, 0, 1), direction_type: str = "vector", polarization: Polarization = Vertical(), tag: str = None, ): self.wavelength = wavelength self.waist = waist self.power = power self.waist_position = waist_position # /!\ direction_type has to be defined BEFORE direction !! self.direction_type = direction_type self.direction = direction self.polarization = polarization super(LaserBeam, self).__init__(tag=tag) # -- REQUESTED PROPERTY FOR ENVOBJECTS @property def vector(self): return False # -- COMMON METHODS DEFINED HERE def _convert_coordinates_to_laser_frame( self, position: np.ndarray, nocheck=False ) -> np.ndarray: """Converts lab frame cartesian coordinates to laser frame coordinates. Parameters ---------- position : array of shape (3,) or (n1, n2, ..., 3) cartesian coordinates in the lab frame nocheck : bool, optional if set to True, function will not check that the shape of position matches requirements, by default False Returns ------- position_laser : array of shape (3,) or (n1, n2, ..., 3) (same as position) cartesian coordinates in the laser frame Notes ------- 'position' should be an array of shape (3,) or (n1,n2,..,3) last axis contains coordinates x, y, z The laser frame is centered at the laser waist, and has the z axis aligned with the laser propagation. The unit vector defining laser propagation is defined with two angles, theta and phi : theta is the angle between the unit vector and the z axis of the lab frame, and phi is the angle of the unit vector project on the (x, y) plane of the lab frame, w.r.t the x axis. To define the new coordinates (x_laser, y_laser, z_laser) in the laser frame, we proceed as follow: 1) we shift the frame to center it on the waist position: (x, y, z) > (xc, yc, zc) 2) we perform a rotation with an angle phi around the lab frame z axis: (xc, yc, zc) > (x', y', z') 3) we perform a rotation with an angle theta around the y' axis of the new frame: (x', y', z') > (x_laser, y_laser, z_laser) For convenience reasons, we also return polar coordinates in the laser frame Note ----- Note: in some cases (elliptical beams for instance) it might be interesting to include a final rotation around the laser propagation axis in the laser frame. We decided that this rotation will be handled in the `intensity()` method of the corresponding class. """ # convert to array if needed position = self._check_position_array(position, nocheck) # get coordinates x, y, z = position.T # shift center x0, y0, z0 = self._waist_position xc = x - x0 yc = y - y0 zc = z - z0 # rotate : phi around z axis, then theta along new y axis # see function docstring and documentation for rotation & frames definitions theta = self._unit_vector_theta phi = self._unit_vector_phi x_laser = ( xc * np.cos(theta) * np.cos(phi) + yc * np.cos(theta) * np.sin(phi) - zc * np.sin(theta) ) y_laser = -xc * np.sin(phi) + yc * np.cos(phi) z_laser = ( xc * np.sin(theta) * np.cos(phi) + yc * np.sin(theta) * np.sin(phi) + zc * np.cos(theta) ) # also yield cylindrical coordinates - NOT ANYMORE # rho_laser = np.sqrt(x_laser**2 + y_laser**2) # th_laser = np.arctan2(y_laser, x_laser) position_laser = np.array([x_laser, y_laser, z_laser]).T return position_laser def _convert_vector_to_laser_frame( self, vec: np.ndarray, nocheck: bool = False ) -> np.ndarray: """Rotates a vector from lab frame to laser frame. Parameters ---------- vec : array of shape (3,) or (n1, n2, ..., 3) cartesian coordinates of the vectors in the lab frame nocheck : bool, optional if set to True, function will not check that the shape of position matches requirements, by default False Returns ------- vec_laser : array of shape (3,) or (n1, n2, ..., 3) cartesian coordinates of the vectors in the laser frame Notes ------- 'vec' should be an array of shape (3,) or (n1,n2,..,3) last axis contains vector coordinates x, y, z The unit vector defining laser propagation is defined with two angles, theta and phi : theta is the angle between the unit vector and the z axis of the lab frame, and phi is the angle of the unit vector project on the (x, y) plane of the lab frame, w.r.t the x axis. To perform a rotation from lab frame (x, y, z) to laser frame (x_laser, y_laser, z_laser): 1) we perform a rotation with an angle phi around the lab frame z axis: (x, y, z) > (x', y', z') 2) we perform a rotation with an angle theta around the y' axis of the new frame: (x', y', z') > (x_laser, y_laser, z_laser) """ # convert vec vec = self._check_position_array(vec, nocheck) x, y, z = vec.T # rotate : phi around z axis, then theta along new y axis # see function docstring and documentation for rotation & frames definitions # shorthands costheta = self.__costheta sintheta = self.__sintheta cosphi = self.__cosphi sinphi = self.__sinphi # compute x_laser = x * costheta * cosphi + y * costheta * sinphi - z * sintheta y_laser = -x * sinphi + y * cosphi z_laser = x * sintheta * cosphi + y * sintheta * sinphi + z * costheta vec_laser = np.array([x_laser, y_laser, z_laser]).T return vec_laser def _convert_vector_to_lab_frame( self, vec: np.ndarray, nocheck=False ) -> np.ndarray: """Rotates a vector from laser frame to lab frame. Parameters ---------- vec : array of shape (3,) or (n1, n2, ..., 3) cartesian coordinates of the vectors in the laser frame nocheck : bool, optional if set to True, function will not check that the shape of position matches requirements, by default False Returns ------- vec_lab : array of shape (3,) or (n1, n2, ..., 3) cartesian coordinates of the vectors in the lab frame Notes ------ Realizes the reverse operation of `_convert_vector_to_laser_frame`. See `_convert_vector_to_laser_frame` docstring for more information """ # convert vec vec = self._check_position_array(vec, nocheck) x, y, z = vec.T # rotate : phi around z axis, then theta along new y axis # see function docstring and documentation for rotation & frames definitions # shorthands costheta = self.__costheta sintheta = self.__sintheta cosphi = self.__cosphi sinphi = self.__sinphi # compute x_lab = x * costheta * cosphi - y * sinphi + z * sintheta * cosphi y_lab = x * costheta * sinphi + y * cosphi + z * sintheta * sinphi z_lab = -x * sintheta + z * costheta vec_lab = np.array([x_lab, y_lab, z_lab]).T return vec_lab
[docs] def get_polarization_vector_in_laser_frame(self) -> np.ndarray: """Returns the polarization vector describing the current polarization state, in the **LASER** frame Returns ------- p_vec : array of shape (,3) cartesian coordinates of the polarization vector (laser frame) Notes ------ See documentation for the exact definition of the vector. In short : | ``> p_vec = (1, 0, 0)`` : linear polarization along x (vertical) | ``> p_vec = (0, 1, 0)`` : linear polarization along y (horizontal) | ``> p_vec = (0, 0, 1)`` : circular right polarization | ``> p_vec = (0, 0, -1)`` : circular left polarization """ return self.polarization.vector
[docs] def get_polarization_vector_in_lab_frame(self) -> np.ndarray: """Returns the polarization vector describing the current polarization state, in the **LAB** frame Returns ------- p_vec : array of shape (,3) cartesian coordinates of the polarization vector (lab frame) Notes ------ See documentation for the exact definition of the vector. In short : | ``> p_vec = (1, 0, 0)`` : linear polarization along x (vertical) | ``> p_vec = (0, 1, 0)`` : linear polarization along y (horizontal) | ``> p_vec = (0, 0, 1)`` : circular right polarization | ``> p_vec = (0, 0, -1)`` : circular left polarization """ p_vec_laser_frame = self.polarization.vector p_vec_lab_frame = self._convert_vector_to_lab_frame(p_vec_laser_frame) return p_vec_lab_frame
[docs] def get_polarization_quant_amplitude( self, quantization_axis: np.ndarray, nocheck: bool = False ) -> np.ndarray: """Returns the projection of the polarization state |Ψ⟩ on |σ+⟩, |σ-⟩ and |π⟩, using the vector ``quantization_axis`` as a quantification axis. See documentation for a derivation of this projection. Parameters ---------- quantization_axis : array of shape (3,) or (n1, n2, ..., 3) cartesian coordinates of the quantization axis vector in the lab frame nocheck : bool, optional if set to True, function will not check that the shape of position matches requirements, by default False Returns ------- polar_amp : array of shape (3,) or (n1, n2, ..., 3) contains the polarization amplitude for π, σ+ and σ- components Notes ----- The input ``quantization_axis`` should be an array of shape (3,) or (n1, n2, .., 3), where the cartesian coordinates of the quantization axis are stored in the last dimension (of size 3) the result ``polar_amp`` is an array whose size matches the one of ``quantization_axis``, where the last dimension of size 3 contains the projections of the polarization state on π, σ+ and σ- That is : >>> pi_amp, sigmaplus_amp, sigma_minus_amp = polar_amp.T With: | ``pi_amp`` = 〈Ψ|π⟩ | ``sigmaplus_amp`` = 〈Ψ|σ+⟩ | ``sigma_minus_amp`` = 〈Ψ|σ-⟩ See Also -------- get_polarization_quant() get_polarization_quant_amplitude_dict() get_polarization_quant_dict() """ # -- process input # - check quantization_axis = self._check_position_array(quantization_axis, nocheck) # -- compute angles of B field w.r.t k vector, in the laser frame # 1) coordinates of uB in laser frame uB_laser = self._convert_vector_to_laser_frame(quantization_axis, nocheck) # 2) compute angles xl, yl, zl = uB_laser.T alpha = np.arctan2(np.sqrt(xl * xl + yl * yl), zl) # polar angle beta = np.arctan2(yl, xl) # azimuthal angle # -- get angles of polarization vector in the laser frame u, v = self.polarization.get_polarization_vector_angles() # -- projections of polarization state |Ψ⟩ on |x⟩ and |y⟩ # >>> see documentation for explanation x_proj = (1 / np.sqrt(2)) * ( np.exp(-1j * v) * np.cos(u / 2) + np.exp(1j * v) * np.sin(u / 2) ) y_proj = (1j / np.sqrt(2)) * ( np.exp(-1j * v) * np.cos(u / 2) - np.exp(1j * v) * np.sin(u / 2) ) # -- projections of polarization state |Ψ⟩ on |σ+⟩, |σ-⟩ and |π⟩ # >>> see documentation for explanation # shorthands sinB = np.sin(beta) cosB = np.cos(beta) sinA = np.sin(alpha) cosA = np.cos(alpha) sq2 = np.sqrt(2) # |σ+⟩ sigma_plus_proj = (cosB * cosA + 1j * sinB) / sq2 * x_proj sigma_plus_proj += (sinB * cosA - 1j * cosB) / sq2 * y_proj # |σ-⟩ sigma_minus_proj = (cosB * cosA - 1j * sinB) / sq2 * x_proj sigma_minus_proj += (sinB * cosA + 1j * cosB) / sq2 * y_proj # |π⟩ pi_proj = cosB * sinA * x_proj + sinB * sinA * y_proj # -- result polar_amp = np.array([pi_proj, sigma_plus_proj, sigma_minus_proj]).T return polar_amp
[docs] def get_polarization_quant( self, quantization_axis: np.ndarray, nocheck: bool = False ) -> np.ndarray: """Returns **squared norm** the projection of the polarization state |Ψ⟩ on |σ+⟩, |σ-⟩ and |π⟩, using the vector ``quantization_axis`` as a quantification axis. See documentation for a derivation of this projection. Parameters ---------- quantization_axis : array of shape (3,) or (n1, n2, ..., 3) cartesian coordinates of the quantization axis vector in the lab frame nocheck : bool, optional if set to True, function will not check that the shape of position matches requirements, by default False Returns ------- polar_norm : array of shape (3,) or (n1, n2, ..., 3) contains the polarization norm for π, σ+ and σ- components Notes ----- The input ``quantization_axis`` should be an array of shape (3,) or (n1, n2, .., 3), where the cartesian coordinates of the quantization axis are stored in the last dimension (of size 3) the result ``polar_norm`` is an array whose size matches the one of ``quantization_axis``, where the last dimension of size 3 contains the projections of the polarization state on π, σ+ and σ- That is : >>> pi_amp, sigmaplus_amp, sigma_minus_amp = polar_norm.T With: | ``pi_amp`` = | 〈Ψ|π⟩ | ** 2 | ``sigmaplus_amp`` = | 〈Ψ|σ+⟩ | ** 2 | ``sigma_minus_amp`` = | 〈Ψ|σ-⟩ | ** 2 See Also -------- get_polarization_quant_amplitude() get_polarization_quant_amplitude_dict() get_polarization_quant_dict() """ polar_amp = self.get_polarization_quant_amplitude(quantization_axis, nocheck) polar_norm = np.abs(polar_amp) ** 2 return polar_norm
[docs] def get_polarization_quant_amplitude_dict( self, quantization_axis: np.ndarray ) -> dict: """Returns the projection of the polarization state |Ψ⟩ on |σ+⟩, |σ-⟩ and |π⟩, using the vector ``quantization_axis`` as a quantification axis. See documentation for a derivation of this projection. Parameters ---------- quantization_axis : array of shape (3,) or (n1, n2, ..., 3) cartesian coordinates of the quantization axis vector in the lab frame Returns ------- res : dict dict containing the polarization amplitude for π, σ+ and σ- components Notes ------ The result is returned as a dictionnary `res`, such as : | ``res["sigma+"]`` = 〈Ψ|σ+⟩ | ``res["sigma-"]`` = 〈Ψ|σ-⟩ | ``res["pi"]`` = 〈Ψ|π⟩ See Also -------- get_polarization_quant_amplitude() get_polarization_quant() get_polarization_quant_dict() """ # -- get result in array form polar_amp = self.get_polarization_quant_amplitude(quantization_axis) pi_amp, sigma_plus_amp, sigma_minus_amp = polar_amp.T # -- result res = {"sigma+": sigma_plus_amp, "sigma-": sigma_minus_amp, "pi": pi_amp} return res
[docs] def get_polarization_quant_dict(self, quantization_axis): """Returns the **squared norm** of projection of the polarization state |Ψ⟩ on |σ+⟩, |σ-⟩ and |π⟩, using the magnetic field vector ``quantization_axis`` as a quantification axis. See documentation for a derivation of this projection. Parameters ---------- quantization_axis : array of shape (3,) or (n1, n2, ..., 3) cartesian coordinates of the quantization axis vector in the lab frame Returns ------- res : dict dict containing the polarization norm for π, σ+ and σ- components Notes ------ The result is returned as a dictionnary `res`, such as : | ``res["sigma+"]`` = | 〈Ψ|σ+⟩ | ** 2 | ``res["sigma-"]`` = | 〈Ψ|σ-⟩ | ** 2 | ``res["pi"]`` = | 〈Ψ|π⟩ | ** 2 See Also -------- get_polarization_quant_amplitude() get_polarization_quant() get_polarization_quant_amplitude_dict() """ projection_amplitude = self.get_polarization_quant_amplitude_dict( quantization_axis ) res = {} for k, v in projection_amplitude.items(): res[k] = np.linalg.norm(v) ** 2 return res
# -- REQUIRED ABSTRACT METHODS
[docs] def get_value(self, position: np.ndarray, nocheck=False) -> np.ndarray: """Returns laser intensity at a given position in the lab frame Parameters ---------- position : array of shape (3,) or (n1, n2, ..., 3) cartesian coordinates in the lab frame nocheck : bool, optional if set to True, function will not check that the shape of position matches requirements, by default False Returns ------- intensity : float or array of shape (n1, n2, ..., 1) laser intensity at position Notes ------- position is an array_like object, with shape (3,) or (n1, n2, .., 3). In all cases, the last dimension contains cordinates (x, y, z), in meter and in the lab frame """ # Check position position = self._check_position_array(position, nocheck) # call hidden function that actually does the computation return self._intensity_func(self, position)
@abstractmethod def _intensity_func(self, position): """Actual method for field computation ; defined for each subclass"""
[docs] @abstractmethod def set_power_from_I(self, target_I: float): """Sets the power to reach a target intensity Parameters ---------- target_I : float target intensity (W/m^2) """
[docs] @abstractmethod def set_waist_from_I(self, target_I: float): """Sets the waist radius to reach a target intensity Parameters ---------- target_I : float target intensity (W/m^2) """
# -- CLASS PROPERTIES GETTERS & SETTERS # - wavelength @property def wavelength(self) -> float: """float: laser vacuum wavelength (m)""" return self._wavelength @wavelength.setter def wavelength(self, value: float) -> None: self._positive_float_check("wavelength", value) if value > 3e-6 or value < 100e-9: raise Warning( "Value given for wavelength is outside the 100nm-3µm range, which is rather strange. Check that you have given the wavelength value in _meters_" ) self._wavelength = float(value) # - waist @property def waist(self) -> float: """float: laser 1/e^2 radius (m)""" return self._waist @waist.setter def waist(self, value: float) -> None: self._positive_float_check("waist", value) self._waist = float(value) # - power @property def power(self) -> float: """float: laser power (W)""" return self._power @power.setter def power(self, value: float) -> None: self._positive_float_check("power", value) self._power = float(value) # - waist position @property def waist_position(self) -> np.ndarray: """array of shape (,3): cartesian coordinates of the laser focus / waist position in the lab frame (m)""" return self._waist_position @waist_position.setter def waist_position(self, value: np.ndarray) -> None: value = np.asanyarray(value) if value.size != 3: raise ValueError("'waist_position' should be an array-like of size 3") self._waist_position = value # - direction_type @property def direction_type(self) -> str: """str: type of direction setting. Can be "vector" or "thetaphi" """ return self._direction_type @direction_type.setter def direction_type(self, value: str) -> None: if value not in DIRECTION_TYPES: raise ValueError(f"'direction_type' should be in {DIRECTION_TYPES}") self._direction_type = value # - direction @property def direction(self) -> np.ndarray: """array: either a vector or a (theta, phi) tuple describing the laser direction""" return self._direction @direction.setter def direction(self, value: np.ndarray) -> None: # convert to array value = np.asanyarray(value) # check that the size is OK errormsg = "When 'direction_type' is set to '{direction_type}', 'direction' should be an array of size {size}" if self.direction_type == "vector" and value.size != 3: raise ValueError(errormsg.format(direction_type="vector", size=3)) elif self.direction_type == "thetaphi" and value.size != 2: raise ValueError(errormsg.format(direction_type="thetaphi", size=2)) # compute unit vector if self.direction_type == "vector": # first case : a unit vector is provided # 1 - normalize norm = np.linalg.norm(value) if norm == 0: raise ValueError("Wrong value for the unit vector: norm is zero") unit_vector = value / norm # 2 - compute theta and phi ux, uy, uz = unit_vector theta = np.arctan2(np.sqrt(ux**2 + uy**2), uz) phi = np.arctan2(uy, ux) elif self.direction_type == "thetaphi": # second case : theta and phi are provided theta, phi = value unit_vector = np.array( [ np.sin(theta) * np.cos(phi), # x np.sin(theta) * np.sin(phi), # y np.cos(theta), # z ] ) pass # store self._unit_vector = unit_vector self._unit_vector_phi = phi self._unit_vector_theta = theta self._direction = value # pre compute some values, for later self.__costheta = np.cos(theta) self.__sintheta = np.sin(theta) self.__cosphi = np.cos(phi) self.__sinphi = np.sin(phi) # - polarization @property def polarization(self) -> Polarization: """Polarization: laser polarization object""" return self._polarization @polarization.setter def polarization(self, value: Polarization) -> None: if not isinstance(value, Polarization): msg = "`polarization` should be a Polarization object, from atomsmltr.environment.lasers.polarization" raise TypeError(msg) self._polarization = value # - others @property def unit_vector(self) -> np.ndarray: """array of shape (,3): unit vector describing laser propagation""" return self._unit_vector @property def k(self) -> float: """float: laser wavenumber k = 2π / λ (m^-1)""" return 2 * np.pi / self.wavelength @property def kvec(self) -> np.ndarray: """array: vector version of the laser wavenumber k = 2π / λ (m^-1)""" return self.k * self.unit_vector # -- hidden methods def _positive_float_check(self, param_name: str, value: float) -> None: """internal function to check that a parameter is a positive float, raises a `ValueError` if not. Args: param_name (str): name of the checked parameter, to give context in the exception value (float): value of the paramater to check """ if isinstance(value, int): value = float(value) if not isinstance(value, float): raise ValueError(f"'{param_name}' has to be a float") if value < 0: raise ValueError(f"'{param_name}' has to be a positive") # -- PLOT FUNCTIONS def plot1D(self): pass
[docs] def plot2D( self, limits: np.ndarray, Npoints: np.ndarray, cut: float = 0, ax=None, plane: str = "XY", cmap=None, show: bool = False, space_scale: float = 1.0, ): """Plots a 2D cut of the laser intensity, using Matplotlib pcolormesh() Parameters ---------- limits : array, shape (4,) an array of size 4, providing (xmin, xmax, ymin, ymax). Npoints : int or array of shape (2,) number of points for each dimension, either a int or an array of two ints (Nx, Ny). cut : float, optional coordinate of the third axis for the cut. Defaults to 0. ax : Matplotlib Axes, optional the matplotlib axis on which to plot. If None is given a new figure is created. Defaults to None. plane : str, optional the plane for the cut. Accepted values are "XY", "YZ" and "ZX". Defaults to "XY". cmap : Matplotlib cmap, optional passed to matplotlib pcolormesh() function show : bool, optional whether to show the figure after calling the method. Defaults to False. space_scale : float, optional space coordinates will be multiplied by this when plotting. Defaults to 1. Returns ------- ax : Matplotlib Axes the axis on which the plot was performed. Notes ------ The limits are given via an array of size 4 'limits', providing providing (xmin, xmax, ymin, ymax) Number of points are given with 'Npoints', either as an integer (same value for x and y) or an array of size 2 the coordinate of the cut axis is given by 'cut' Examples --------- >>> beam.plot2D(limits=(-5, 5, -4, 4), Npoints=200) >>> beam.plot2D(limits=(-5, 5, -4, 4), Npoints=200, cut=-5) >>> beam.plot2D(limits=(-5, 5, -4, 4), Npoints=(200, 100)) """ # - process arguments using the Plottable builtin method ax, position, X, Y = self._process_2D_plot_args( ax=ax, plane=plane, limits=limits, Npoints=Npoints, cut=cut, ) # - compute intensity intensity = self.get_value(position) # - plot ax.pcolormesh(X * space_scale, Y * space_scale, intensity, cmap=cmap) ax.set_xlabel(plane.upper()[0]) ax.set_ylabel(plane.upper()[1]) # - show ? if show: plt.show() return ax
[docs] def plot3D( self, ax=None, color: str = None, name: str = None, vscale: float = None, show: bool = False, ): """plots a 3D reprensentation of the laser beam, including: - a line : laser axis - an arrow along the propagation direction - a point : laser focus position - a dotted arrow : laser polarization vector Parameters ---------- ax : custom Axes3D, optional the axis in which to plot. If None is given (default value) a new ax is generated color : str, optional a matplotlib compatible color. Defaults to None. name : str, optional the name of the laser, passed as a label when plotting. If none is given, use the laser tag vscale : float, optional A scaling factor. Use it to tweak the arrow size if needed. Defaults to None. show : bool, optional Whether the show the figure after calling the method. Defaults to False. Returns ------- ax : custom Axes3D the figure axis in which the laser is plotted. Note ---- When providing an axis via the ``ax`` parameter, make sure to use our custom implementation of matplotlib ``Axes3D``, as this function uses custom arrow drawing methods. The class can be imported via ``from atomsmltr.utils.plotter import Axes3D`` """ # - init ax (if needed) ax = self._init_ax(ax, ax3D=True) # - get laser information unit_vector = np.asanyarray(self._unit_vector) polar_vector_laserframe = np.asanyarray(self.polarization.vector) polar_vector = self._convert_vector_to_lab_frame(polar_vector_laserframe) waist_position = np.asanyarray(self.waist_position) # - scale xmin, xmax = ax.get_xlim() ymin, ymax = ax.get_ylim() zmin, zmax = ax.get_zlim() dr = np.array([xmax - xmin, ymax - ymin, zmax - zmin]) if vscale is None: vscale = np.max(dr) / 5 # - PLOT # waist position label = self.tag if name is None else name ax.scatter(*waist_position, marker="o", color=color, label=label) # plot laser r1 = waist_position + dr * unit_vector * 5 r2 = waist_position - dr * unit_vector * 5 x = np.linspace(-100, 100, 1000) r = waist_position[:, np.newaxis] + (unit_vector * dr)[:, np.newaxis] * x ax.plot(r[0, :], r[1, :], r[2, :], color=color) # plot propagation vector epsilon = 0.2 ax.arrow3D( *(waist_position - unit_vector * vscale * (1 + epsilon)), *(vscale * unit_vector), mutation_scale=15, arrowstyle="simple", ec="k", fc=color, ) # plot polarisation vector ax.arrow3D( *(waist_position - unit_vector * vscale * (1 + epsilon)), *(vscale * polar_vector * 0.7), mutation_scale=20, arrowstyle="-|>", linestyle="dashed", color=color, ) if show: plt.show() return ax
# -- INFO STRING @property @abstractmethod def disp_type(self) -> str: return ""
[docs] def gen_infostring_obj(self, show_polar=True): """Generates an info string object""" title = self.type title = title[:1].upper() + title[1:] # capitalize first letter info = InfoString(title=title) info.add_section("Parameters") info.add_element(f"type", f"{self.disp_type}") info.add_element(f"tag", f"{self.tag}") info.add_element(f"waist (m)", f"{self.waist:.3g}") info.add_element(f"power (W)", f"{self.power:.3g}") info.add_element(f"waist position (m)", f"{self.waist_position}") info.add_element(f"direction type", f"{self.direction_type}") info.add_element(f"direction", f"{self.direction}") info.add_element(f"unit vector", f"{self._unit_vector}") info.add_element(f"unit vector phi", f"π × {self._unit_vector_phi / np.pi}") info.add_element(f"unit vector theta", f"π × {self._unit_vector_theta / np.pi}") if show_polar: info_polar = self.polarization.gen_infostring_obj() info.merge(info_polar, prefix="") return info
[docs] def print_info(self, show_polar=True): info_str = self.gen_infostring_obj(show_polar) print(info_str.generate())
def print_polar_proj(self, mag_field_vector): mag_field_vector = self._check_position_array(mag_field_vector) res = self.get_polarization_quant_dict(mag_field_vector) print("> Local polarization projection") print(f" + B = {mag_field_vector*1e4} (G)") print(f" + π = {res["pi"]:.2f}") print(f" + σ+ = {res["sigma+"]:.2f}") print(f" + σ- = {res["sigma-"]:.2f}")
# % IMPLEMENTED CLASSES
[docs] class GaussianLaserBeam(LaserBeam): """A Gaussian laser beam Parameters ---------- wavelength : float, optional vacuum wavelength (m), by default 399e-9 waist : float, optional 1/e^2 waist radius (m), by default 1e-3 power : float, optional laser power (W), by default 1e-3 waist_position : array, shape (,3), optional cartesian coordinates of the waist / focus position, in meters and in the lab frame, by default (0, 0, 0) direction : array, shape (,3) or (,2), optional depending on 'direction type', a vector or a (theta, phi) couple giving the propagation direction of the beam direction_type : str, optional type of direction : "vector" or "thetaphi", by default "vector" polarization : Polarization, optional laser polarization, by default Vertical() tag : str, optional laser tag, by default None """ @property def type(self): return "Gaussian Laser Beam" @property def disp_type(self) -> str: return "Gaussian beam" # -- REQUIRED METHOD FOR LASER BEAM CLASSES # pylint : disable=method_hidden @staticmethod def _intensity_func(self, position): """Returns laser intensity at point position position should be an array of shape (3,) or (n1,n2,..,3) last axis contains coordinates x, y, z NB: position is already checked and converted to an array in the `LaserBeam` class """ # - get coordinates in laser frame # NB : x, y and phi are not needed here position_laser = self._convert_coordinates_to_laser_frame(position) x_laser, y_laser, z_laser = position_laser.T rho_laser = np.sqrt(x_laser**2 + y_laser**2) # - compute gaussian beam intensity intensity = _intensity_gauss( rho_laser, z_laser, self.waist, self.power, self.wavelength ) intensity = intensity.T return intensity
[docs] def set_power_from_I(self, target_I: float) -> None: # NB, for a Gaussian beam : I0 = 2 * P / np.pi / w0**2 power = target_I * self.waist**2 * np.pi / 2 self.power = power
[docs] def set_waist_from_I(self, target_I: float) -> None: # NB, for a Gaussian beam : I0 = 2 * P / np.pi / w0**2 waist = np.sqrt(2 * self.power / np.pi * target_I) self.waist = waist
@property def rayleigh_length(self) -> float: """float: the laser Rayleigh length""" return np.pi * self.waist**2 / self.wavelength
[docs] def gen_infostring_obj(self, show_polar=True): info = super().gen_infostring_obj(show_polar) info.add_element( "Rayleigh length", f"{self.rayleigh_length:.2g} m", section="Parameters" ) return info
[docs] class PlaneWaveLaserBeam(LaserBeam): """Implements a plane wave. For convenience, we still define the waist and power, and the intensity is constant and corresponds to the peak intensity of a Gaussian beam with same power and waist.""" @property def type(self): return "Plane Wave Laser Beam" @property def disp_type(self) -> str: return "Plane wave beam" # -- REQUIRED METHOD FOR LASER BEAM CLASSES # pylint : disable=method_hidden @staticmethod def _intensity_func(self, position: np.ndarray) -> np.ndarray: """Returns laser intensity at point position position should be an array of shape (3,) or (n1,n2,..,3) last axis contains coordinates x, y, z NB: position is already checked and converted to an array in the `LaserBeam` class """ # - get coordinates in laser frame # NB : x, y and phi are not needed here x, _, _ = position.T x = x.T # - compute gaussian beam intensity intensity = _intensity_gauss( 0 * x, 0 * x, self.waist, self.power, self.wavelength ) return intensity
[docs] def set_power_from_I(self, target_I: float) -> None: # NB, for a Gaussian beam : I0 = 2 * P / np.pi / w0**2 power = target_I * self.waist**2 * np.pi / 2 self.power = power
[docs] def set_waist_from_I(self, target_I: float) -> None: # NB, for a Gaussian beam : I0 = 2 * P / np.pi / w0**2 waist = np.sqrt(2 * self.power / np.pi * target_I) self.waist = waist
[docs] def gen_infostring_obj(self, show_polar=True): info = super().gen_infostring_obj(show_polar) info.rm_element("waist position (m)", section="Parameters") return info