# -*- coding: utf-8 -*-
"""
This module contains all classes and functions to define a System including
multiple :class:`Laser` objects.
Example
-------
Below an empty Lasersystem is created and a single Laser with wavelength 860nm
and Power 20 mW with linear polarization is added::
from MoleCool import Lasersystem
lasers = Lasersystem()
lasers.add(860e-9,20e-3,'lin')
Tip
---
Every object of the classes :class:`~MoleCool.Lasersystem.Lasersystem` or
:class:`~MoleCool.Lasersystem.Laser` class can be printed to display
all attributes via::
print(lasers)
print(lasers[0])
To delete all instances use this command::
del lasers[:]
"""
import numpy as np
import pandas as pd
from scipy.constants import c,h,hbar,pi,g
from numba import jit
import matplotlib.pyplot as plt
import warnings
from MoleCool import tools
#%%
[docs]
class Lasersystem:
def __init__(self,freq_pol_switch=5e6):
"""System consisting of :class:`~MoleCool.Lasersystem.Laser` objects
and methods to add them properly.
These respective objects can be retrieved and also deleted by using the
normal item indexing of a :class:`~MoleCool.Lasersystem.Lasersystem`
object.
Example
-------
::
from MoleCool import Lasersystem
lasers = Lasersystem()
lasers.add(lamb=860e-9,P=20e-3,pol='lin')
lasers.add(lamb=890e-9,I=1000,FWHM=2e-3)
laser1 = lasers[0] # call first Laser object included in lasers
del lasers[-1] # delete last added Laser object
print(lasers[0])
print(lasers)
Parameters
----------
freq_pol_switch : float, optional
Specifies the frequency (without 2pi) with which the polarization is
switched if the polarization switching is enabled. The default is 5e6.
"""
self.entries = []
#: float: Polarization switching frequency. Default is 5e6.
self.freq_pol_switch = freq_pol_switch
self.intensity_func = None
self.intensity_func_sum = None
self.pd_display_options = [
'display.float_format', '{:.3e}'.format,
"display.max_rows", None,
"display.max_columns", None,
]
[docs]
def add(self,lamb=860e-9,P=20e-3,pol='lin',**kwargs):
"""Add a :class:`~MoleCool.Lasersystem.Laser` instance to the laser
system.
Note
----
This is the same as::
from MoleCool import Laser
lasers.entries.append(Laser())
Parameters
----------
**kwargs
Arbitrary keyword arguments. Same as in the ``__init__`` method of
the class :class:`~MoleCool.Lasersystem.Laser`.
"""
self.entries.append( Laser( lamb=lamb, P=P, pol=pol, **kwargs) )
self.intensity_func = None
self.intensity_func_sum = None
[docs]
def getarr(self, attr):
"""Get an array with a specific attribute of all included
:class:`Laser` objects.
Parameters
----------
attr : str
Laser attribute, e.g. ``lamb`` or ``P``.
Returns
-------
numpy.ndarray
Array with the lasers' attributes.
"""
self.check_config()
if not attr in dir(self[0]):
raise ValueError('The attribute {} is not included in the Laser objects'.format(attr))
if attr == 'f_q':
dtype = complex
else:
dtype = float
return np.array([getattr(la,attr) for la in self],dtype=dtype)
def _identify_iter_params(self):
"""Identify which parameters are iterative arrays to loop through.
This function is inevitable to determine whether multiple
evaluations of the OBEs and rate equations are efficiently conducted
using the multiprocessing package from python
(see :meth:`tools.multiproc`).
Returns
-------
iters_dict : dict
Dictionary with all iterative parameters and their number of iterations.
"""
# loop through laser objects to get to know which variables have to get
# iterated and how many iterations
#--> for the dictionaries used here it'S important that the order is ensured
# (this is the case since python 3.6 - now (3.8))
laser_list = []
laser_iters_N = {}
for l1,la in enumerate(self):
laser_dict = {}
for key in ['omega','freq_Rabi','I','phi','beta','k','r_k','f_q']:
value = la.__dict__[key]
if (np.array(value).ndim == 1 and key not in ['k','r_k','f_q']) \
or (np.array(value).ndim == 2 and key in ['k','r_k','f_q']):
laser_dict[key] = value
laser_iters_N[key] = len(value)
laser_list.append(laser_dict)
return laser_iters_N, laser_list
[docs]
def add_sidebands(self,lamb=860e-9,offset_freq=0.0,mod_freq=1e6,
ratios=None,sidebands=[-1,1],**kwargs):
"""Add multiple :class:`~MoleCool.Lasersystem.Laser` instances as
sidebands e.g. to drive multiple hyperfine transitions.
The individual sidebands are detuned from the center
frequency by the modulation frequency ``mod_freq`` times the values
in the list ``sidebands``, i.e. for ``mod_freq=1e6`` and
``sidebands=[-1,0,2]``, the sidebands are detuned by -1 MHz, 0 MHz
and 2 MHz.
The center frequency is given by the wavelength lamb and an additional
general offset frequency ``offset_freq``.
Parameters
----------
lamb : float
wavelength of the main transition.
P : float
Power, i.e. sum of the powers of all sidebands.
Alternativley the sum of the intensities can be provided.
I : float
Sum of all sideband intensities. Can be provided instead of power P.
offset_freq : float
All Laser sidebands are all additionally detuned by the value of
offset_freq (in Hz without 2 pi). Experimentally, this shift is often
realized with an AOM. The default is 0.0.
mod_freq : float
starting from the offset-shifted center frequency, sideband Laserobjects
are added with the detunings `sidebands`*`mod_freq` (without 2 pi).
ratios : array_like, optional
Power/ intensity ratios of the individual sidebands. Must be provided in the
same order as the `mod_freq` parameter.
(Will be normed to specify the individual sideband powers).
The default is equally distributed power.
sidebands : array_like, optional
determines the number of sidebands and their detuning in units of
the `mod_freq` parameter.
**kwargs
optional arguments (see :class:`~MoleCool.Lasersystem.Laser`).
"""
# compatibility for old parameter names:
if 'AOM_shift' in kwargs:
offset_freq = kwargs['AOM_shift']
del kwargs['AOM_shift']
if 'EOM_freq' in kwargs:
mod_freq = kwargs['EOM_freq']
del kwargs['EOM_freq']
# set equally distributed power ratios of not provided
if np.all(ratios) == None:
ratios = len(sidebands)*[1]
if 'I' in kwargs:
PorI = 'I'
elif 'P' in kwargs:
PorI = 'P'
else:
PorI = 'P'
kwargs['P'] = 20e-3
mod_freqs = (np.array(sidebands)*np.expand_dims(mod_freq,axis=-1)).T
PorI_arr = (np.array(ratios)/np.sum(ratios) * np.expand_dims(kwargs[PorI],axis=-1)).T
for i in range(np.array(sidebands).shape[-1]):
kwargs[PorI] = PorI_arr[i]
self.add(lamb=lamb, freq_shift=offset_freq+mod_freqs[i], **kwargs)
#save input parameters offset_freq and mod_freq to be able to look it up later
self.entries[-1].offset_freq = offset_freq
self.entries[-1].mod_freq = mod_freq
[docs]
def make_retrorefl_beams(self, beam_config='oneside',
P_tot=100e-3, FWHM=1.6e-3, w_cylind=0.0,
T_airglass=99.62e-2, R_mirror=98.34e-2,
mirror_sep=463e-3, reflections=34,
int_length=200e-3, x_offset=15e-3, cut_flanks=True,
printing=True, plotting=False,
**laser_kwargs,
):
"""Create all ``Laser`` objects for a realistic retroreflecting beam
configuration for a long cooling interaction region in the experiment.
Parameters
----------
beam_config : str, optional
'oneside' when only one laser beam is retro-reflected from one side.
'twosides' for two laser beams entering the interaction region from both
sides, e.g. for Sisyphus cooling. The default is 'oneside'.
P_tot : float, optional
Total inital power (W) of the incoming laser beam or beams. The default is 100e-3.
FWHM : float, optional
FWHM of the beams. The default is 1.6e-3.
w_cylind : float, optional
width (m) of a cylindrical widened beam. The default is 0.0.
T_airglass : float, optional
Single transmission air - glass. The default is 99.62e-2.
R_mirror : float, optional
Reflectivity of the mirror. The default is 98.34e-2.
mirror_sep : float, optional
Separation (m) between both retro-reflecting mirrors. The default is 463e-3.
reflections : int, optional
number of reflections (counting all reflections of a single incoming beam
on all mirrors). The default is 34.
int_length : float, optional
Total length (m) of the interaction region, i.e. from first to last intensity
peak along the centered axis. The default is 200e-3.
x_offset : float, optional
Additional offset of the reflections, i.e. position of the first reflection
on the centered axis. The default is 15e-3.
cut_flanks : bool, optional
Whether the flanks of the beam profiles should be cutted which is usually
the case on the mirror for two beams from both sides. The default is True.
printing : bool, optional
Whether printing additional information. The default is True.
plotting : bool, optional
Whether plotting the 1D and 2D intensity distributions. The default is False.
**laser_kwargs : kwargs, optional
Further keyword arguments for the created laser objects such as e.g.
wavelength.
"""
# longitudinal difference between two reflecting events
dx = int_length/(reflections-1)
beam_angle = np.arctan(dx/mirror_sep) /(2*np.pi) *360 # angle of beam in degrees
self.retrorefl_beams_kwargs = locals()
if cut_flanks:
if w_cylind == 0:
w_cylind = FWHM*0.8493218002880192
r_cylind_trunc = dx/2#/1.01
else:
r_cylind_trunc = 10.
P = P_tot
for i in np.arange(reflections):
# Calculate current laser power due to losses from transmission and reflection
pm1 = i%2*2 - 1
if i == 0:
# first beam pass only one time transmission through glass
P *= T_airglass**2
else:
P *= T_airglass**2 * R_mirror * T_airglass**2
# print('P= {:.2f}mW'.format(P*1e3))
# Only one laser without sidebands required since only the total
# intensities of all lasers would be simply added:
if beam_config == 'oneside':
P_beam = P
elif beam_config == 'twosides':
P_beam = P/2
self.add(
P = P_beam,
k = [dx/mirror_sep, 0, +pm1],
r_k = [i*dx+x_offset, 0, 0],
FWHM = FWHM,
w_cylind = w_cylind,
r_cylind_trunc= r_cylind_trunc,
dir_cylind = [1, 0, -pm1*dx/mirror_sep],
**laser_kwargs,
)
if beam_config == 'twosides':
self.add(
P = P_beam,
k = [dx/mirror_sep, 0, -pm1],
r_k = [i*dx+x_offset, 0, 0],
FWHM = FWHM,
w_cylind = w_cylind,
r_cylind_trunc= r_cylind_trunc,
dir_cylind = [1, 0, +pm1*dx/mirror_sep],
**laser_kwargs,
)
if printing:
print('k=',[dx/mirror_sep,0,+pm1],', dir_cyl=',[1,0,-pm1*dx/mirror_sep])
print('Power ratio between last and first beam ={:6.2f} %'.format(
P / (P_tot*T_airglass**2) *1e2 ))
print(f"Beam angle ={beam_angle:6.3f}° and {self.pNum:3} laser beam objects in total")
d_travel = np.sqrt(dx**2 + mirror_sep**2)*reflections
print(f"Travelling distance of a single laser beam ={d_travel:6.2f}m")
if plotting:
plt.figure('1D intensity distribution')
z_shift = 0e-3
self.plot_I_1D(ax='x',axshifts=[0e-3,z_shift],limits=[0e-2,23e-2],
label=f"{reflections} refl.", Npoints=10001)
# 2D
plt.figure('2D intensity distribution')
self.plot_I_2D(ax='y',axshift=0,Npoints=501,
limits=([0e-2,23e-2],[-mirror_sep/2,+mirror_sep/2]))
[docs]
def get_intensity_func(self,sum_lasers=True,use_jit=True):
'''Generate a fast function which uses all the current parameters of
all lasers in this Lasersystem for calculating the total intensity.
This function can also be called directly by calling the method
:func:`I_tot` with an input parameter ``r`` as the
position at which the total intensity is calculated.
Parameters
----------
sum_lasers : bool, optional
If True, the returned intensity function evaulates the intensities
of all laser instances for returning the local total intensity sum.
If False, the returned intensity function only returns an array with
the length of defined laser instances. This array contains the factors
which corresponds to the local intensity of each laser divided by
its maximum intensity at the center of the Gaussian distribution.
The default is True.
use_jit : bool, optional
The returned function can be compiled in time to a very fast C code
using the numba package. However, the compilation time can be a few
seconds long the first time the function is called. For all later
calls it is then much faster. The default is True.
Returns
-------
function
it's the same function which is used in the method :func:`I_tot`
'''
if sum_lasers:
if self.intensity_func_sum != None:
return self.intensity_func_sum
else:
if self.intensity_func != None:
return self.intensity_func
pNum = self.pNum
I_arr = self.getarr('I')
w = self.getarr('w')
w_cyl = self.getarr('_w_cylind')
r_cyl_trunc = self.getarr('_r_cylind_trunc')
dir_cyl = self.getarr('_dir_cylind') #unit vectors
k = self.getarr('k') #unit vectors
r_k = self.getarr('r_k')
# very fast function which calculates the total intensity only for the
# parameters which are defined before
# @jit(nopython=False,parallel=False,fastmath=True,forceobj=True)
def I_tot(r):
factors = np.zeros(pNum)
for p in range(pNum):
r_ = r - r_k[p]
if w_cyl[p] != 0.0: # calculation for a beam which is widened by a cylindrical lens
d2_w = np.dot(dir_cyl[p],r_)**2
if d2_w > r_cyl_trunc[p]**2: #test if position is larger than the truncation radius along the dir_cyl direction
continue
else:
d2 = np.dot(np.cross(dir_cyl[p],k[p]),r_)**2
factors[p] = np.exp(-2*(d2_w/w_cyl[p]**2 + d2/w[p]**2))
else:
r_perp = np.cross( r_ , k[p] )
factors[p] = np.exp(-2 * np.dot(r_perp,r_perp) / w[p]**2 )
if sum_lasers:
return np.sum(factors*I_arr)
else:
return factors
if use_jit:
I_tot = jit(nopython=True,parallel=False,fastmath=True)(I_tot)
if sum_lasers:
self.intensity_func_sum = I_tot
else:
self.intensity_func = I_tot
return I_tot
[docs]
def I_tot(self,r,**kwargs):
'''Calculate the total intensity of all lasers in this Lasersystem at
a specific position `r`. For this calculation the function generated by
:func:`get_intensity_func` is used.
Parameters
----------
r : 1D array of size 3
position at which the total intensity is calculated.
**kwargs : keywords
optional keywords of the method :func:`get_intensity_func` can be provided.
Returns
-------
float
total intensity at the position r.
'''
return self.get_intensity_func(**kwargs)(r)
[docs]
def plot_I_2D(self,ax='x',axshift=0,limits=([-0.05,0.05],[-0.05,0.05]),Npoints=201):
"""Plot the 2D intensity distribution of all laser beams along two axes
by using the method :func:`get_intensity_func`.
Parameters
----------
ax : str, optional
axis orthogonal to the plane to be plotted. Can be 'x','y' or 'z'.
The default is 'x'.
axshift : float, optional
shift along the axis `ax` which defines the absolute position of
the plane to be plotted. The default is 0.
limits : tuple(list,list), optional
determines the minimum and maximum limit for both axes which lies
in the plane to be plotted.
The default is ([-0.05,0.05],[-0.05,0.05]).
Npoints : int, optional
Number of plotting points for each axis. The default is 201.
"""
axshift = float(axshift)
xyz = {'x':0,'y':1,'z':2}
ax_ = xyz[ax]
del xyz[ax]
axes_ = np.array([*xyz.values()])
lim1,lim2 = limits
x1,x2 = np.linspace(lim1[0],lim1[1],Npoints),np.linspace(lim2[0],lim2[1],Npoints)
Z = np.zeros((len(x1),len(x2)))
r = np.zeros(3)
for i in range(Npoints):
for j in range(Npoints):
r[ax_] = axshift
r[axes_] = x1[i],x2[j]
Z[i,j] = self.I_tot(r,sum_lasers=True,use_jit=True)
X1,X2 = np.meshgrid(x1,x2)
# plt.figure('Intensity distribution of all laser beams at {}={:.2f}mm'.format(
# ax,axshift*1e3))
plt.contourf(X1*1e3,X2*1e3,Z.T,levels=20)
cbar = plt.colorbar()
cbar.ax.set_ylabel('Intensity $I_{tot}$ in W/m$^2$')
keys = list(xyz.keys())
plt.xlabel('position {} in mm'.format(keys[0]))
plt.ylabel('position {} in mm'.format(keys[1]))
[docs]
def plot_I_1D(self,ax='x',axshifts=[0,0],limits=[-0.05,0.05],
Npoints=1001,label=None):
"""Plot the 1D intensity distribution of all laser beams along an axis
by using the method :func:`get_intensity_func`.
Parameters
----------
ax : str, optional
axis along which the intensity distribution is plotted. The default is 'x'.
axshifts : list, optional
shifts in m of the other two axes besides `ax`. The default is [0,0].
limits : list, optional
determines the minimum and maximum limit for the axis `ax`.
The default is [-0.05,0.05].
Npoints : int, optional
Number of plotting points along the axis `ax`. The default is 1001.
label : str, optional
label for the plotted curve. If None, the label shows the values of
`axshifts`. The default is None.
"""
axshifts = np.array(axshifts,dtype=float) # shifting other two axes with offset
xyz = {'x':0,'y':1,'z':2}
ax_ = xyz[ax] # index of the axis on which we want to plot the intensity
del xyz[ax]
axes = list(xyz.keys())
axes_ = np.array([*xyz.values()])
# plt.figure('Intensity over x')
x_arr = np.linspace(limits[0],limits[1],Npoints)
y_arr = np.zeros(Npoints)
r = np.zeros((Npoints,3))
r[:,axes_] += axshifts
r[:,ax_] = x_arr
for i,r_i in enumerate(r):
y_arr[i] = self.I_tot(r_i,sum_lasers=True,use_jit=True)
if label == None:
label = '{}={:.2f}mm, {}={:.2f}mm'.format(axes[0],axshifts[0]*1e3,axes[1],axshifts[1]*1e3)
plt.plot(x_arr*1e3,y_arr,label=label)
plt.legend()
plt.xlabel('position {} in mm'.format(ax))
plt.ylabel('Intensitiy $I$ in W/m$^2$')
def _get_wavelength_regimes(self, subplot_sep):
# sorted frequencies
fsort = np.sort(np.atleast_2d( self.getarr('f').T )[0])
# indices of laser blocks that are separated by the frequency subplot_sep
inds = [i+1 for i,df in enumerate(np.diff(fsort)) if df > subplot_sep]
inds = [0, *inds]
# mean of each block of frequencies
fmean = np.array([ fsort[i:j].mean() for i,j in zip(inds, inds[1:]+[None])])
return list(c/fmean) # wavelengths
[docs]
def plot_spectrum(self, axs=[], unit='MHz', subplot_sep=1e9, std=0, wavelengths=[],
invert=False, N_points=500, xaxis_ext=5, cmap=None, relative_to_wavelengths=False,
fill_between_kwargs=dict(alpha=0.2, color='grey'),
plot_kwargs=dict(color='grey',ls='-')):
"""Plot the spectrum of :class:`~.Lasersystem.Laser` objects
with their respective intensities.
Either as Gaussians for each single laser object or as vertical lines.
Note
----
This method also supports plotting multiple detunings with different colors
from ``matplotlib.colormap`` into a single subplot.
Parameters
----------
axs : list of ``matplotlib.pyplot.axis`` objects, optional
axis/axes to put the plot(s) on. The default is [].
unit : str, optional
Unit of the x-axis to be plotted.
Can be one of ``['GHz','MHz','kHz','Hz']``. Default is 'MHz'.
subplot_sep : float, optional
Defines the range of the plotted x-axis and the separation for the
automatic inclusion of all wavlengths (see parameter ``wavelengths``)
in Hz. The default is 1e9.
std : float, optional
Standard deviation of the individual Gaussians to be drawn in MHz.
The default is 0 meaning that instead of Gaussians, vertical lines
are drawn.
wavelengths : list, optional
wavelengths that should be plotted within the range ``subplot_sep``.
By default all available laser wavelengths are used.
invert : bool, optional
Whether the plot y-axis should be inverted. The default is False.
N_points : int, optional
number of points plotted. The default is 500.
xaxis_ext : TYPE, optional
The range of the xaxis, given by the lowest and highest transition
frequency, is extended by this factor multiplied by the sum of the
Gaussian broadening (``std``). The default is 5.
cmap : std, optional
``matplotlib.colormap`` for plotting multiple spectra for multiple
detunings. The default is None.
relative_to_wavelengths : bool, optional
Whether the x-axis should be plotted in absolute frequency units or
relative to ``wavelengths``. The default is False.
fill_between_kwargs : dict, optional
kwargs for the method ``matplotlib.pyplot.fill_between``.
For disabling this plotting, just provide an empty dictionary or False.
The default is dict(alpha=0.2, color='grey').
plot_kwargs : dict, optional
kwargs for the method normal line plotting method ``matplotlib.pyplot.plot``.
For disabling this plotting, just provide an empty dictionary or False.
The default is dict(color='grey',ls='-').
Returns
-------
axs : list of ``matplotlib.pyplot.axis``
Axes of the subplot(s).
"""
# get wavelengths of all laser wavelength blocks that span subplot_sep*Gamma
if not wavelengths:
wavelengths = self._get_wavelength_regimes(subplot_sep=subplot_sep)
# check iterating laser parameters
iters = self._identify_iter_params()[0]
if 'omega' in iters:
iters.pop('omega')
if iters:
raise Exception(
(f"The iterating parameters {list(iters.keys())} of laser "
"objects is not supported for plotting (except for 'omega')."))
# generate subplots
axs = tools.auto_subplots(len(wavelengths), axs=axs,
ylabel=f'Intensity (W/m$^2$)')
f_arr = np.atleast_2d( self.getarr('f').T )
# scaling x
fac_x = {'GHz':1e9, 'MHz':1e6, 'kHz':1e3, 'Hz':1}[unit]
for ax,wavelength in zip(axs,wavelengths):
xconv = lambda x: (x - c/wavelength*int(bool(relative_to_wavelengths)))/fac_x
yconv = lambda y: y
inds = np.argwhere((f_arr[0]>c/wavelength-subplot_sep) \
& (f_arr[0]<c/wavelength+subplot_sep))[:,0]
for j,f_arr_i in enumerate(f_arr):
if f_arr.shape[0] != 1:
color = plt.get_cmap(cmap)(j/(len(f_arr)-1))
fill_between_kwargs.update(color=color)
f_cut = f_arr_i[inds]
spectrum = []
for i in inds:
la = self[i]
if std:
x_ext = std*xaxis_ext # extension on the x axis
xaxis = np.linspace(min(f_cut)-x_ext, max(f_cut)+x_ext, N_points)
y = tools.gaussian(xaxis, a=la.I, x0=f_arr_i[i], std=std, y_off=0)
spectrum.append(y)
else:
ax.plot(2*[xconv(f_arr_i[i])], [0, yconv(la.I)],
lw=2, **plot_kwargs)
# ax.axvline((la.f-c/wavelength)/fac_x, ymin=0, ymax=la.I/Inorm,
# color='grey', lw=2, alpha=1)
if std:
spectrum = np.sum(spectrum, axis=0)
if fill_between_kwargs:
ax.fill_between(xconv(xaxis), 0, yconv(spectrum), **fill_between_kwargs)
if plot_kwargs:
ax.plot(xconv(xaxis), yconv(spectrum),
**plot_kwargs)
xlabel = f'Frequency ({unit})'
if relative_to_wavelengths:
xlabel += f" at {wavelength*1e9:f} nm"
ax.set_xlabel(xlabel)
if invert:
ax.yaxis.set_inverted(True) # inverted axis with autoscaling
return axs
def __delitem__(self,index):
"""delete lasers using del system.lasers[<normal indexing>], or delete all del system.lasers[:]"""
#delete lasers with del system.lasers[<normal indexing>], or delete all del system.lasers[:]
del self.entries[index]
self.intensity_func = None
self.intensity_func_sum = None
def __getitem__(self,index):
#if indeces are integers or slices (e.g. obj[3] or obj[2:4])
if isinstance(index, (int, slice,np.integer)):
return self.entries[index]
#if indices are tuples instead (e.g. obj[1,3,2])
return [self.entries[i] for i in index]
def __str__(self):
#__str__ method is called when an object of a class is printed with print(obj)
with pd.option_context(*self.pd_display_options):
return f"{self.description}\n{self.DF()}"
def __repr__(self):
with pd.option_context(*self.pd_display_options):
return repr(self.DF())
def _repr_html_(self):
with pd.option_context(*self.pd_display_options):
return self.DF()._repr_html_()
[docs]
def DF(self, **kwargs):
"""
Create a pretty :class:`pandas.DataFrame` object with all lasers and
their attributes. This method basically concatenates the individual
laser dataframes from :class:`Laser.DF()`.
This method is e.g. used when calling ``print(lasers)``.
Parameters
----------
**kwargs : kwargs
Keyword arguments of the respective method :meth:`Laser.DF()`.
Returns
-------
pandas.DataFrame
Dataframe with the lasers attributes.
"""
if self.pNum == 0:
return None
else:
return pd.concat([la.DF().T for la in self],
ignore_index=True)
[docs]
def check_config(self,raise_Error=False):
"""Check the configuration for simulating internal dynamics using
the OBEs or rate equations.
"""
if self.pNum == 0:
Err_str = 'There are no lasers defined!'
if raise_Error: raise Exception(Err_str)
else: warnings.warn(Err_str)
#maybe also check if some dipole matrices are completely zero or
# if the wavelengths are in wrong order of magnitude??
@property
def description(self):
"""
Display a short description with the number of included laser objects.
Returns
-------
str
description of the lasersystem.
"""
return "{:d} - Lasersystem".format(self.pNum)
@property
def pNum(self):
"""int: Calculate the number of included Laser objects."""
return len(self.entries)
@property
def I_sum(self):
"""
Calculate the sum of the peak intensities of all laser beams
Returns
-------
numpy.ndarray or float
Sum of peak intensities.
"""
return np.array([la.I for la in self]).sum(axis=0)
@property
def P_sum(self):
"""
Calculate the sum of the powers of all laser beams
Returns
-------
numpy.ndarray or float
Sum of all lasers' powers.
"""
return np.array([la.P for la in self]).sum()
#%%
[docs]
class Laser:
#: units of the laser beam properties
UNITS = dict(
lamb = 'm',
I = 'W/m^2',
P = 'W',
FWHM = 'm',
k = '1',
r_k = 'm',
phi = 'rad',
beta = 'Hz/s',
f_q = '1',
w = 'm',
_w_clind= 'm',
_dir_clind='m',
)
def __init__(self,lamb=860e-9,freq_shift=0,pol='lin',pol_direction=None,
P=20e-3,I=None,FWHM=5e-3,w=None,
w_cylind=.0,r_cylind_trunc=5e-2,dir_cylind=[1,0,0],
freq_Rabi=None,k=[0,0,1],r_k=[0,0,0],beta=0.,phi=0.0,
pol2=None,pol2_direction=None):
"""This class contains all physical properties of a laser which can
be assembled in the class :class:`Lasersystem`.
Note
----
``freq_shift`` is given as non-angualar frequency, i.e. without the
:math:`2 \pi` factor.
Parameters
----------
lamb : float, optional
wavelength lambda. The default is 860e-9.
freq_shift : float, optional
Shift of the laser's frequency (without 2 pi) additional to the
frequency determined by Parameter lamb. The default is 0.0.
pol : str, tuple(str,str), optional
polarization of the laserbeam. Can be either 'lin', 'sigmap' or
'sigmam' for linear or circular polarized light of the laser.
For polarization switching a tuple of two polarizations is needed.
The default is 'lin'.
pol_direction : str, optional
optional addition to the ``pol`` parameter to be considered in the
OBEs calculation. Can be either 'x','y','z' for linear polarization
or 'xy','xz','yz' for circular polarization. Given the default value
None the linear polarization is aong the quantization axis 'z'
and the circular ones in 'xy'.
P : float, optional
Laser power in W. The default is 20e-3.
I : float, optional
Intensity of the laser beam. When specified a given power P is
ignored. The default is None.
FWHM : float, optional
FWHM (full width at half maximum) of the Gaussian intensity
distribution of the laserbeam. When this value is adjusted after
the initialization of the object the w value is automatically
corrected but to further adjust the intensity the power has to be
set again. The default is 5e-3.
w : float, optional
:math:`1/e^2` beam radius of the Gaussian intensity distribution.
When this value is adjusted after the initialization of the object
the FWHM value is automatically corrected but to further adjust the
intensity the power has to be set again. The default is None.
w_cylind : float, optional
:math:`1/e^2` beam radius of the Gaussian intensity distribution
along x direction for the specific configuration where the
laser beam is aligned in y axis direction and has a widened intensity
distribution along x axis with radius `w_cylind`. The distribution
along the z axis is given by the radius `w`.
The default is 0.0.
r_cylind_trunc : float, optional
specifies the radial distance along the direction `dir_cylind`
(widened by a cylindrical lens) at which the intensity is truncated.
The default is 5e-2.
dir_cylind : 1D array of size 3, optional
Direction in which the beam is widened by a cylindrical lens.
This direction has to be orthogonal to the laser wave vector `k`.
This variable has only an effect when the input parameter
`w_cylind` is non-zero. The default is [1,0,0].
freq_Rabi : float, optional
Rabi frequency in terms of angular frequency 2 pi. The appropriate
intensity is first set to an arbitrary value since it is adjusted
later during the calculation where the levels are involved.
The default is None.
k : list or array type of dimension 3, optional
direction of the wave vector :math:`\hat{k}` of the laserbeam.
The inserted array is automatically normalized to unit vector.
The default is [0,0,1].
r_k : list or array type of dimension 3, optional
a certain point which is located anywhere within the laserbeam.
The default is [0,0,0].
beta : float, optional
When the frequency of the laser should be varied linearly in time,
then `beta` defines the chirping rate in Hz/s (without factor of 2 pi).
The default is 0.0.
phi : float, optional
phase offset of the laser's electric field in rad (important e.g.
for standing waves). The default is 0.0.
Raises
------
Exception
When the given type of the ``pol`` Parameter is not accepted.
Example
-------
A fast way to calculate the power of a laser with certain beam radii
to reach a certain intensity (or the other way around for an intensity)::
from MoleCool import Laser
print( Laser(I = 1000., w = 1e-3, w_cylind = 5e-2).P )
print( Laser(P = 0.02, FWHM = 5e-3).I )
"""
#: float: angular frequency :math:`\omega`
self.omega = 2*pi*(c/lamb + freq_shift)
# different quantities when a cylindrical lens is used widening the laser beam along one transversal axis
self._w_cylind, self._r_cylind_trunc = w_cylind, r_cylind_trunc
self._dir_cylind = np.array(dir_cylind)/np.expand_dims(np.linalg.norm(dir_cylind,axis=-1),axis=-1) #unit vector
#___definition of the beam width:
# if a 1/e^2 radius is given. It is used for further calculations. Otherwise the FWHM value is used.
if np.any(w != None):
self.w = w # old **default** value: (2*(pi*1.5e-3**2))**0.5 --> arbitrary value to compare to old MATLAB rate equations
elif np.any(FWHM != None):
self.FWHM = FWHM
#___intensity definition or calculation via P and beam widths w & w_cylind:
#: Rabi frequency in terms of angular frequency 2 pi
self.freq_Rabi = freq_Rabi
if np.any(freq_Rabi != None):
self.I = 1.0 #arbitrarily setting initial value for intensity since it is adjusted later during the calculation where the levels are involved.
self._P = None
# intensity I is important quantity for calculations instead of the power P.
elif np.any(I != None):
self.I = I
self._P = None
else:
self.P = P #calculation of the intensity using the power and beam widths.
#: unit wavevector :math:`\hat{k}`
self.k = np.array(k)/np.expand_dims(np.linalg.norm(k,axis=-1),axis=-1) #unit vector
if (w_cylind != 0.0) and (np.dot(self._dir_cylind,self.k) != 0.0):
raise Exception('input variable dir_cylind has to be orthogonal to the wave vector k')
#: any point which is passed by the laser wave vector (i.e. the point lying in the propagation line of the laser)
self.r_k = np.array(r_k) #point which is lying in the laserbeam
#: laser chirping rate for linear varying the laser frequency in time
self.beta = beta
#: phase offset of the laser's electric field (important e.g. for standing waves)
self.phi = phi
#___define the laser polarizations (and polarization direction)
self.f_q = self._get_polarization_comps(pol,pol_direction)
if pol2 != None:
self.pol_switching = True
self.f_q2 = self._get_polarization_comps(pol2,pol2_direction)
else:
self.pol_switching = False
self.f_q2 = self.f_q.copy()
def _get_polarization_comps(self,pol,pol_direction):
# check if pol has the right datatype and then if it has the right value
if type(pol) != str:
raise Exception("Wrong datatype or length of <pol>: only str allowed")
pol_list = ['lin','sigmap','sigmam']
if not (pol in pol_list):
raise Exception("'{}' is not valid for <pol>, it can only be '{}','{}', or '{}'".format(pol,*pol_list))
if pol_direction == None:
if pol == 'lin': f_q = np.array([0.,1.,0.]) #q= 0; mF -> mF'= mF
elif pol == 'sigmam': f_q = np.array([0.,0.,1.]) #q=+1; mF -> mF'= mF-1
elif pol == 'sigmap': f_q = np.array([1.,0.,0.]) #q=-1; mF -> mF'= mF+1
else:
p = pol_direction
x = np.array([+1., 0,-1.])/np.sqrt(2)
y = np.array([+1., 0,+1.])*1j/np.sqrt(2)
z = np.array([ 0, +1, 0 ])
if isinstance(p,(list,np.ndarray)):
f_q = p[0]*x + p[1]*y + p[2]*z # not yet programmed in the best way!?
elif isinstance(p,str):
if len(p) == 1:
if p == 'x': f_q = x
elif p == 'y': f_q = y
elif p == 'z': f_q = z
if len(p) == 2:
if pol == 'sigmam':
a1,a2 = -1., -1j
elif pol == 'sigmap':
a1,a2 = +1., -1j
if p == 'xy': f_q = a1*x + a2*y
elif p == 'xz': f_q = a1*z + a2*x
elif p == 'yz': f_q = a1*y + a2*z
else: #maybe also check if the string values of pol_direction is correct?!
raise Exception("Wrong datatype of <pol_direction>")
return np.array([ -f_q[2], +f_q[1], -f_q[0] ]) / np.linalg.norm(f_q)
def __str__(self):
#__str__ method is called when an object of a class is printed with print(obj)
with pd.option_context('display.float_format','{:.3e}'.format):
return self.DF().to_string()
def __repr__(self):
with pd.option_context('display.float_format','{:.3e}'.format):
return repr(self.DF())
def _repr_html_(self):
with pd.option_context('display.float_format','{:.3e}'.format):
return self.DF()._repr_html_()
[docs]
def DF(self,
attrs = ['lamb','I','P','FWHM','k','r_k','phi','beta'],
units = {},
):
"""
Create a DataFrame with attributes of the Laser instance. This method
is e.g. used when calling ``print(lasers[0])``.
Parameters
----------
attrs : list, optional
list of attributes as properties of the Laser.
The default is ['lamb', 'I', 'P', 'FWHM', 'k',
'r_k', 'phi', 'beta'].
units : dict, optional
units of the physical attributes. The default is {}.
Returns
-------
df : pandas.DataFrame
All specified attributes with units as a Dataframe.
"""
units = dict(self.UNITS, **units)
for attr in attrs:
if not hasattr(self, attr):
raise ValueError(f"attr {attr} is not an attribute of Laser")
if attr not in units:
units[attr] = '?'
df = pd.DataFrame(
[getattr(self, attr) for attr in attrs],
index = [f"{attr} ({units[attr]})" for attr in attrs]
)
return df
@property
def w(self):
"""Calculate the 1/e^2 beam radius"""
return self._w
@w.setter
def w(self,w):
self._w = w
self._FWHM = 2*w / ( np.sqrt(2)/np.sqrt(np.log(2)) )
self.intensity_func = None
self.intensity_func_sum = None
@property
def FWHM(self):
"""Calculate the FWHM (full width at half maximum) of the Gaussian
intensity distribution of the laserbeam
"""
return self._FWHM
@FWHM.setter
def FWHM(self,FWHM):
self._FWHM = FWHM
self._w = np.sqrt(2)/np.sqrt(np.log(2))*FWHM/2 # ~= 1.699*FWHM/2
self.intensity_func = None
self.intensity_func_sum = None
@property
def P(self):
"""Calculate the Power of the single beam"""
if np.any(self._P != None): return self._P
else:
if np.any(np.array(self._w_cylind) != 0.0):
return self.I*(pi*self.w*self._w_cylind)/2
else: return self.I*(pi*self.w**2)/2
@P.setter
def P(self,P):
"""When the power P is set to a value the intensity is automatically
calculated using the beam widths."""
self._P = P
if np.any(np.array(self._w_cylind) != 0.0):
self.I = 2*self.P/(pi*self.w*self._w_cylind)
else:
#: float: :math:`I =P/A` with the Area :math:`A=\pi w_1 w_2/2` of a 2dim Gaussian beam
self.I = 2*self.P/(pi*self.w**2)
self.intensity_func = None
self.intensity_func_sum = None
@property
def kabs(self):
"""Calculate the absolute value of the wave vector
(:math:`= 2 \pi/\lambda = \omega/c`)
in :math:`\\text{rad}/\\text{m}`.
Note:
``self.k`` is a unit vector and defines the direction of the wave vector"""
return self.omega/c
@property
def lamb(self):
"""Calculate the wavelength of the single laser"""
return 2*pi*c/self.omega
@property
def f(self):
"""Calculate the frequency (non-angular)"""
return self.omega/(2*pi)
@property
def E(self):
"""Energy of the laser's photons."""
return self.omega * hbar