-
Notifications
You must be signed in to change notification settings - Fork 19
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Rabi frequencies #73
Rabi frequencies #73
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,147 @@ | ||
import cmath | ||
from dataclasses import dataclass, field | ||
import numpy as np | ||
import scipy.constants as consts | ||
|
||
"""Vectors to convert a Cartesian vector to the spherical basis, with q=-1, 0, 1 | ||
corresponding to the components of the beam that can cause absorbtion between | ||
states with M_u - M_l = q.""" | ||
C1_P1 = 1 / np.sqrt(2) * np.array([-1.0, -1.0j, 0]) | ||
C1_0 = np.array([0, 0, 1.0]) | ||
C1_M1 = 1 / np.sqrt(2) * np.array([1.0, -1.0j, 0]) | ||
|
||
"""Matrices to convert a Cartesian tensor to the spherical basis, with q=-2, -1, 0, 1, 2 | ||
corresponding to the components of the beam that can cause absorbtion between | ||
states with M_u - M_l = q.""" | ||
C2_P2 = 1 / np.sqrt(6) * np.array([[1, 1.0j, 0], [1.0j, -1, 0], [0, 0, 0]]) | ||
C2_P1 = 1 / np.sqrt(6) * np.array([[0, 0, -1], [0, 0, -1.0j], [-1, -1.0j, 0]]) | ||
C2_0 = 1 / 3 * np.array([[-1, 0, 0], [0, -1, 0], [0, 0, 2]]) | ||
C2_M1 = 1 / np.sqrt(6) * np.array([[0, 0, 1], [0, 0, -1.0j], [1, -1.0j, 0]]) | ||
C2_M2 = 1 / np.sqrt(6) * np.array([[1, -1.0j, 0], [-1.0j, -1, 0], [0, 0, 0]]) | ||
|
||
|
||
@dataclass | ||
class Beam: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This object feels really awkward given the rest of the package - it just sort of ignores a lot of design decisions and starts again. For example, in the original design, we made the decision to have a The design here also feels quite confused - it's kind of a grab bag for a bunch of disparate things, which don't obviously belong together. Feels like it should really just be a set of helper functions rather than a class. For example, so far we've adopted a convention where all intensities are in saturation intensity units and we provide a simpler helper function to convert power + spot size to saturation intensity units. This beam class goes in a completely different direction and works in a waist radius and power. |
||
"""Contains all parameters and properties of a circular, Gaussian laser beam. | ||
|
||
The beam takes a total power and a (minimum) waist radius, defined as the 1/e^2 | ||
intensity radius. | ||
|
||
The beam polarisation is defined using the three angles phi, gamma and theta. | ||
Phi is the angle between the beam and the B-field, such that if they are | ||
parallel, phi=0. The beam direction and the B-field then define a plane: we | ||
decompose the polarisation vector into two unit vectors, one in this plane | ||
(k1) and one perpendicular to it (k2). The beam polarisation is then defined | ||
as | ||
e = cos(gamma) k1 + exp(i theta) sin(gamma) k2 | ||
Gamma = 0 gives linear polarisation that has maximal projection along the B-field. | ||
Gamma = +- pi/4, theta = 0 gives linear polarisation along +-45 deg to the | ||
plane. | ||
Gamma = pi/4, theta = +-pi/2 give circular polarisations. | ||
|
||
:param wavelength: The wavelength of the light in m. | ||
:param waist_radius: The (minimum) waist radius of the beam in m. | ||
:param power: The power in the beam in W. | ||
:param phi: The angle in rad between the beam and the B-field. | ||
:param gamma: The angle in rad between the (complex) polarisation vector | ||
and the plane described by the B-field and beam vector. | ||
:param theta: The phase angle in rad between the polarisation component in the | ||
plane and out of the plane.""" | ||
|
||
wavelength: float | ||
waist_radius: float | ||
power: float | ||
phi: float | ||
gamma: float | ||
theta: float | ||
I_peak: float = field(init=False) | ||
E_field: float = field( | ||
init=False | ||
) # The magnitude of the E-field at the beam centre | ||
rayleigh_range: float = field(init=False) | ||
omega: float = field(init=False) | ||
|
||
def __post_init__(self): | ||
self.I_peak = 2 * self.power / (np.pi * self.waist_radius**2) | ||
self.E_field = np.sqrt(2 * self.I_peak / (consts.c * consts.epsilon_0)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should not be a class attribute - trivially calculable from other properties. At most it should be a helper function. |
||
self.rayleigh_range = np.pi * self.waist_radius**2 / self.wavelength | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This attribute does not contain any new information - it is just a different view on the waist radius and wavelength. If anything we should just add a helper function to calculate this. |
||
self.omega = consts.c / self.wavelength * 2 * np.pi | ||
|
||
def beam_direction_cartesian(self): | ||
"""Return the unit vector along the beam direction in Cartesian | ||
coordinates [x, y, z]""" | ||
return [np.sin(self.phi), 0, np.cos(self.phi)] | ||
|
||
def polarisation_cartesian(self): | ||
"""Return a list of the Cartesian components of the (complex) polarisation | ||
vector [x, y, z]""" | ||
return [ | ||
np.cos(self.phi) * np.cos(self.gamma), | ||
cmath.exp(1j * self.theta) * np.sin(self.gamma), | ||
-np.sin(self.phi) * np.cos(self.gamma), | ||
] | ||
|
||
def polarisation_rank1_polar(self): | ||
""" | ||
Returns the rank-1 tensor components of the polarisation vector that drive | ||
dipole transitions, i.e. the sigma_m, sigma_p and pi components of the | ||
polarisation vector. Output is a list of the form [sigma_m, pi, sigma_p] | ||
in polar form. | ||
""" | ||
return [cmath.polar(q) for q in self.polarisation_rank1()] | ||
|
||
def polarisation_rank1(self): | ||
""" | ||
Returns the rank-1 tensor components of the polarisation vector that drive | ||
dipole transitions, i.e. the sigma_m, sigma_p and pi components of the | ||
polarisation vector. Output is a list of the form [sigma_m, pi, sigma_p] | ||
in rectangular form (i.e. re + j im). | ||
""" | ||
vect = np.array(self.polarisation_cartesian()) | ||
sigma_m = np.sum(vect * C1_M1) | ||
pi = np.sum(vect * C1_0) | ||
sigma_p = np.sum(vect * C1_P1) | ||
return [sigma_m, pi, sigma_p] | ||
|
||
def polarisation_rank2_polar(self): | ||
""" | ||
Returns the rank-2 tensor components of the polarisation tensor, which | ||
drive quadrupole transitions. Output is a list containing the five components | ||
in increasing order from q_m2 to q_p2 in polar form. | ||
""" | ||
return [cmath.polar(q) for q in self.polarisation_rank2()] | ||
|
||
def polarisation_rank2(self): | ||
""" | ||
Returns the rank-2 tensor components of the polarisation tensor, which | ||
drive quadrupole transitions. Output is a list containing the five components | ||
in increasing order from q_m2 to q_p2 in rectangular form (i.e. re + j im). | ||
""" | ||
beam_vect = np.array(self.beam_direction_cartesian()) | ||
pol_vect = np.array(self.polarisation_cartesian()) | ||
|
||
q_m2 = pol_vect.T @ C2_M2 @ beam_vect | ||
q_m1 = pol_vect.T @ C2_M1 @ beam_vect | ||
q_0 = pol_vect.T @ C2_0 @ beam_vect | ||
q_p1 = pol_vect.T @ C2_P1 @ beam_vect | ||
q_p2 = pol_vect.T @ C2_P2 @ beam_vect | ||
|
||
return [q_m2, q_m1, q_0, q_p1, q_p2] | ||
|
||
def print_rank1(self, title=None): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please remove these print functions. If anything, this is the kind of thing we'd have in an example script, but it doesn't belong in the library code. |
||
"""Pretty-print the rank-1 tensor components of the beam.""" | ||
if title is None: | ||
title = "" | ||
sigma_m, pi, sigma_p = self.polarisation_rank1_rect() | ||
print(title) | ||
print(f"sigma_m: {sigma_m:.3f}") | ||
print(f"pi: {pi:.3f}") | ||
print(f"sigma_p: {sigma_p:.3f}") | ||
|
||
def print_rank2(self, title=None): | ||
"""Pretty-print the rank-2 tensor components of the beam.""" | ||
if title is None: | ||
title = "" | ||
print(title) | ||
for idx, p in enumerate(self.polarisation_rank1_rect()): | ||
print(f"q_{idx-2}: {p:.3f}") |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -32,6 +32,12 @@ | |
atom.delta) | ||
""" | ||
|
||
State = namedtuple("State", ["level", "F", "M"]) | ||
State.__doc__ = """Convenient holder for storing atomic state data. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not having a first-class representation of a state was a deliberate decision because this package is intended to work at arbitrary magnetic field, so one cannot generally assume that Having a first-class object for this would be a much more major change. |
||
:param level: the Level object that the state is in. | ||
:param F: total angular momentum number. | ||
:param M: z-projection of F.""" | ||
|
||
|
||
class LevelData: | ||
"""Stored atomic structure information about a single level.""" | ||
|
@@ -131,6 +137,11 @@ def __init__( | |
|
||
self.levels = levels | ||
self.transitions = transitions | ||
# Store the transitions indexed by the upper and lower levels it connects, | ||
# useful fro reverse searching | ||
self.reverse_transitions = { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This shouldn't be stored as class attribute - bad practice to store the same information multiple times in different formats. |
||
(t.lower, t.upper): t for t in self.transitions.values() | ||
} | ||
|
||
# ordered in terms of increasing state energies | ||
self.num_states = None # Total number of electronic states | ||
|
@@ -422,9 +433,7 @@ def calc_Epole(self): | |
since they are somewhat more convenient to work with. The two are | ||
related by constants and power of the transition frequencies. | ||
|
||
To do: define what we mean by a matrix element, and how the amplitudes | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Having a helper util for converting to Rabi frequencies is great, but it doesn't remove the need to add some documentation to define what we actually mean by a matrix element and to remind people how this is converted to physically measurable Rabi frequencies (and what we mean by a Rabi frequency since there is more than one convention). |
||
stored in ePole are related to the matrix elements and to the Rabi | ||
frequencies. | ||
For converting to Rabi frequencies, use the functions in atomic_physics.rabi. | ||
|
||
To do: double check the signs of the amplitudes. | ||
""" | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why docstring formatting when this is basically a comment?