Reference
atom
Atom
An atom object, containing states and transitions
Attributes:
Name | Type | Description |
---|---|---|
name |
str |
The name of the atom |
states: StateRegistry
property
readonly
StateRegistry: the atomic states.
transitions: TransitionRegistry
property
readonly
TransitionRegistry: the atomic transitions.
units
property
readonly
pint.UnitRegistry(): readonly access to the pint UnitRegistry used by the atom.
calc
special
polarizability
scalar(state, omega=0)
Calculate the scalar polarizability of a state |Ψ⟩
Approximate the dynamical scalar polarizability α0(ω) of the state |Ψ⟩ by summing over matrix elements
Parameters:
Name | Type | Description | Default |
---|---|---|---|
state |
State |
state |Ψ⟩ to calculate the polarizability |
required |
omega |
Quantity |
Angular frequency of the field to calculate
the dynamical polarizability. Defaults to zero,
in which case it calculates the static polarizability.
Must have units of |
0 |
Returns:
Type | Description |
---|---|
Quantity |
Scalar polarizability. Has units of |
Source code in atomphys/calc/polarizability.py
def scalar(state, omega: pint.Quantity = 0) -> pint.Quantity:
"""Calculate the scalar polarizability of a state |Ψ⟩
Approximate the dynamical scalar polarizability α0(ω)
of the state |Ψ⟩ by summing over matrix elements
$$
\\alpha^0(\\omega) =
\\frac{1}{3(2J+1)}
\\sum_i
\\left(
\\frac{1}{\\hbar (E_i - E) - \\hbar \\omega} +
\\frac{1}{\\hbar (E_i - E) + \\hbar \\omega}
\\right)
\\left|\\left<\\Psi\\left|D\\right| \\Psi_i\\right>\\right|^2
$$
Arguments:
state (State): state |Ψ⟩ to calculate the polarizability
omega: Angular frequency of the field to calculate
the dynamical polarizability. Defaults to zero,
in which case it calculates the static polarizability.
Must have units of `Hz`.
Returns:
Scalar polarizability. Has units of `C m / (V/m)`, or dipole moment / electric field.
"""
ω = omega
ħ = state._ureg.ħ
J = state.J
X = 1 / (3 * (2 * J + 1)) / ħ
α = 0
for transition in state.up:
ω0 = transition.ω
d = transition.d
α += (1 / (ω0 - ω) + 1 / (ω0 + ω)) * d ** 2
for transition in state.down:
ω0 = -transition.ω
d = transition.d
α += (1 / (ω0 - ω) + 1 / (ω0 + ω)) * d ** 2
return (α * X).to_base_units()
tensor(state, omega=0)
Calculate the tensor polarizability of a state |Ψ⟩
Approximate the dynamical tensor polarizability α0(ω) of the state |Ψ⟩ by summing over matrix elements
Parameters:
Name | Type | Description | Default |
---|---|---|---|
state |
State |
state |Ψ⟩ to calculate the polarizability |
required |
omega |
Quantity |
Angular frequency of the field to calculate
the dynamical polarizability. Defaults to zero,
in which case it calculates the static polarizability.
Must have units of |
0 |
Returns:
Type | Description |
---|---|
Tensor polarizability. Has units of |
Source code in atomphys/calc/polarizability.py
def tensor(state, omega: pint.Quantity = 0):
"""Calculate the tensor polarizability of a state |Ψ⟩
Approximate the dynamical tensor polarizability α0(ω)
of the state |Ψ⟩ by summing over matrix elements
\\[
\\alpha^2(\\omega) = -\\sqrt{\\frac{20J(2J-1)}{6(J+1)(2J+1)(2J+3)}}
\\sum_i
\\left\\{
\\begin{matrix}
1 & 1 & 2 \\\\ J & J & J_i
\\end{matrix}
\\right\\}
(-1)^{1+J+J_i}
\\left(
\\frac{1}{\\hbar (E_i - E) - \\hbar \\omega} +
\\frac{1}{\\hbar (E_i - E) + \\hbar \\omega}\\right)
\\left|
\\left<\\Psi\\left|D\\right| \\Psi_i\\right>\\right|^2
\\]
Arguments:
state (State): state |Ψ⟩ to calculate the polarizability
omega: Angular frequency of the field to calculate
the dynamical polarizability. Defaults to zero,
in which case it calculates the static polarizability.
Must have units of `Hz`.
Returns:
Tensor polarizability. Has units of `C m / (V/m)`, or dipole moment / electric field.
"""
ω = omega
ħ = state._ureg.ħ
J = state.J
X = (
-(
((20 * J * (2 * J - 1)) / (6 * (J + 1) * (2 * J + 1) * (2 * J + 3)))
** (1 / 2)
)
/ ħ
)
α = 0
for transition in state.up:
ω0 = transition.ω
d = transition.d
Jp = transition.f.J
sixJ = wigner_6j(1, 1, 2, J, J, Jp)
α += (-1) ** (J + Jp + 1) * sixJ * (1 / (ω0 - ω) + 1 / (ω0 + ω)) * d ** 2
for transition in state.down:
ω0 = -transition.ω
d = transition.d
Jp = transition.i.J
sixJ = wigner_6j(1, 1, 2, J, J, Jp)
α += (-1) ** (J + Jp + 1) * sixJ * (1 / (ω0 - ω) + 1 / (ω0 + ω)) * d ** 2
return (α * X).to_base_units()
total(state, mJ=None, omega=0, A=0, theta_k=0, theta_p=1.5707963267948966)
Calculate the polarizability of a state for a given field polarization
Calculates the dynamical polarizability α(ω) by taking the sum of the scalar, vector, and tensor poarts according to
Parameters:
Name | Type | Description | Default |
---|---|---|---|
state |
State |
state |Ψ⟩ to calculate the polarizability |
required |
mJ |
float |
Zeeman sublevel to calculate the vector and tensor
polarizability. If |
None |
omega |
Quantity |
angular frequency of the field to calculate
the dynamical polarizability. Defaults to zero,
in which case it calculates the static polarizability.
Must have units of |
0 |
A |
float |
degree of circular polarization, with ±1 being circular polarization and 0 being linear polarization |
0 |
theta_k |
float |
angle between wave vector and quantization axis |
0 |
theta_p |
float |
angle between polarization vector and quantization axis |
1.5707963267948966 |
Returns:
Type | Description |
---|---|
Quantity |
Polarizability. Has units of |
Source code in atomphys/calc/polarizability.py
def total(
state,
mJ: float = None,
omega: pint.Quantity = 0,
A: float = 0,
theta_k: float = 0,
theta_p: float = π / 2,
) -> pint.Quantity:
"""Calculate the polarizability of a state for a given field polarization
Calculates the dynamical polarizability α(ω) by taking the sum of the
scalar, vector, and tensor poarts according to
\\[
\\alpha(\\omega) = \\alpha^0(\\omega) +
A \\cos(\\theta_k) \\frac{m_J}{J} \\alpha^1(\\omega) +
\\frac{1}{2}\\left(3 \\cos^2(\\theta_p) - 1\\right)\\frac{3m_J^2 - J(J+1)}{J(2J-1)}\\alpha^2(\\omega)
\\]
Arguments:
state (State): state |Ψ⟩ to calculate the polarizability
mJ: Zeeman sublevel to calculate the vector and tensor
polarizability. If `None` only calculates the scalar
component. Must be an integer if J is an integer, or
a half-integer if J is a half integer.
omega: angular frequency of the field to calculate
the dynamical polarizability. Defaults to zero,
in which case it calculates the static polarizability.
Must have units of `Hz`.
A: degree of circular polarization, with ±1 being circular
polarization and 0 being linear polarization
theta_k: angle between wave vector and quantization axis
theta_p: angle between polarization vector and quantization axis
Returns:
Polarizability. Has units of `C m / (V/m)`, or dipole moment / electric field.
Raises:
ZeroDivisionError
"""
θk = theta_k
θp = theta_p
J = state.J
α0 = scalar(state, omega)
α1 = vector(state, omega)
α2 = tensor(state, omega)
if mJ is None:
return α0
if not (isint(mJ) or (ishalfint(mJ) and ishalfint(J) and not isint(J))):
raise ValueError(
"mJ must be an integer, or a half-integer if J is a half-integer"
)
C1 = A * cos(θk) * mJ / J
try:
C2 = (
(3 * cos(θp) ** 2 - 1) / 2 * (3 * mJ ** 2 - J * (J + 1)) / (J * (2 * J - 1))
)
except ZeroDivisionError:
C2 = 0
return (α0 + C1 * α1 + C2 * α2).to_base_units()
vector(state, omega=0)
Calculate the vector polarizability of a state |Ψ⟩
Approximate the dynamical vector polarizability α1(ω) of the state |Ψ⟩ by summing over matrix elements
Parameters:
Name | Type | Description | Default |
---|---|---|---|
state |
State |
state |Ψ⟩ to calculate the polarizability |
required |
omega |
Quantity |
Angular frequency of the field to calculate
the dynamical polarizability. Defaults to zero,
in which case it calculates the static polarizability.
Must have units of |
0 |
Returns:
Type | Description |
---|---|
Vector polarizability. Has units of |
Source code in atomphys/calc/polarizability.py
def vector(state, omega: pint.Quantity = 0):
"""Calculate the vector polarizability of a state |Ψ⟩
Approximate the dynamical vector polarizability α1(ω)
of the state |Ψ⟩ by summing over matrix elements
\\[
\\alpha^1(\\omega) =
\\sqrt{\\frac{6J}{(J+1)(2J+1)}}
\\sum_i
\\left\\{
\\begin{matrix}
1 & 1 & 1 \\\\ J & J & J_i
\\end{matrix}
\\right\\}
(-1)^{1+J+J_i}
\\left(
\\frac{1}{\\hbar (E_i - E) - \\hbar \\omega} +
\\frac{1}{\\hbar (E_i - E) + \\hbar \\omega}
\\right)
\\left|\\left<\\Psi\\left|D\\right| \\Psi_i\\right>\\right|^2
\\]
Arguments:
state (State): state |Ψ⟩ to calculate the polarizability
omega: Angular frequency of the field to calculate
the dynamical polarizability. Defaults to zero,
in which case it calculates the static polarizability.
Must have units of `Hz`.
Returns:
Vector polarizability. Has units of `C m / (V/m)`, or dipole moment / electric field.
"""
ω = omega
ħ = state._ureg.ħ
J = state.J
X = ((6 * J) / (4 * (2 * J + 1) * (J + 1))) ** (1 / 2) / ħ
α = 0
for transition in state.up:
ω0 = transition.ω
d = transition.d
Jp = transition.f.J
sixJ = wigner_6j(1, 1, 1, J, J, Jp)
α += (-1) ** (J + Jp + 1) * sixJ * (1 / (ω0 - ω) + 1 / (ω0 + ω)) * d ** 2
for transition in state.down:
ω0 = -transition.ω
d = transition.d
Jp = transition.i.J
sixJ = wigner_6j(1, 1, 1, J, J, Jp)
α += -((-1) ** (J + Jp + 1)) * sixJ * (1 / (ω0 - ω) + 1 / (ω0 + ω)) * d ** 2
return (α * X).to_base_units()
wigner
ishalfint(x)
check if value is half integer
Parameters:
Name | Type | Description | Default |
---|---|---|---|
x |
float |
is this a half integer? |
required |
Returns:
Type | Description |
---|---|
bool |
True if x is a half integer and False if it is not. |
Source code in atomphys/calc/wigner.py
def ishalfint(x: float) -> bool:
"""check if value is half integer
Arguments:
x: is this a half integer?
Returns:
True if x is a half integer and False if it is not.
"""
return 2 * x == floor(2 * x)
isint(x)
checks if value is an integer
Parameters:
Name | Type | Description | Default |
---|---|---|---|
x |
float |
is this an integer? |
required |
Returns:
Type | Description |
---|---|
bool |
True if x is an integer and False if it is not. |
Source code in atomphys/calc/wigner.py
def isint(x: float) -> bool:
"""checks if value is an integer
Arguments:
x: is this an integer?
Returns:
True if x is an integer and False if it is not.
"""
return x == floor(x)
istriangle(a, b, c)
checks if triad (a, b, c) obeys the triangle inequality
Parameters:
Name | Type | Description | Default |
---|---|---|---|
a |
float |
required | |
b |
float |
required | |
c |
float |
required |
Returns:
Type | Description |
---|---|
bool |
True if the triangle inequality is satisfied and False if it is not. |
Source code in atomphys/calc/wigner.py
def istriangle(a: float, b: float, c: float) -> bool:
"""checks if triad (a, b, c) obeys the triangle inequality
Arguments:
a:
b:
c:
Returns:
True if the triangle inequality is satisfied and False if it is not.
"""
return abs(a - b) <= c and c <= a + b
wigner_3j(j1, j2, j3, m1, m2, m3)
Calculate the Wigner 3-j symbol numerically
The Wigner 3-j symbol is calculated numerically using a recursion relation.
This approximate calculation is often faster than the exact analytic
calculation that is done by sympy.physics.wigner
.
Due to the finite precision of floating point numbers, this is only good
for
The results are cached in the private variable _wigner_3j_cache
to
speed subsequent calculations.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
j1 |
float |
required | |
j2 |
float |
required | |
j3 |
float |
required | |
m1 |
float |
required | |
m2 |
float |
required | |
m3 |
float |
required |
Returns:
Type | Description |
---|---|
float |
Source code in atomphys/calc/wigner.py
@lru_cache(maxsize=None)
def wigner_3j(
j1: float, j2: float, j3: float, m1: float, m2: float, m3: float
) -> float:
"""Calculate the Wigner 3-j symbol numerically
The Wigner 3-j symbol is calculated numerically using a recursion relation.
This approximate calculation is often faster than the exact analytic
calculation that is done by `sympy.physics.wigner`.
Due to the finite precision of floating point numbers, this is only good
for $j$ and $m$ up to about 40, after which it becomes too inaccurate.
The results are cached in the private variable `_wigner_3j_cache` to
speed subsequent calculations.
$\\left(
\\begin{matrix}
j_1 & j_2 & j_3 \\\\ m_1 & m_2 & m_3
\\end{matrix}
\\right)$
Arguments:
j1:
j2:
j3:
m1:
m2:
m3:
Returns:
$\\left(
\\begin{matrix}
j_1 & j_2 & j_3 \\\\ m_1 & m_2 & m_3
\\end{matrix}
\\right)$
"""
if (
not ishalfint(j1)
or not ishalfint(j2)
or not ishalfint(j3)
or not ishalfint(m1)
or not ishalfint(m2)
or not ishalfint(m3)
):
raise ValueError("All arguments must be integers or half-integers")
if j1 > 40 or j2 > 40 or j3 > 40 or m1 > 40 or m2 > 40 or m3 > 40:
raise OverflowError(
"can't handle numbers this larger, use a sympy.physics.wigner"
)
# sum of second row must equal zero
if m1 + m2 + m3 != 0:
return 0
# triangle inequality
if not istriangle(j1, j2, j3):
return 0
if (
j1 - m1 != floor(j1 - m1)
or j2 - m2 != floor(j2 - m2)
or j3 - m3 != floor(j3 - m3)
):
return 0
if abs(m1) > j1 or abs(m2) > j2 or abs(m3) > j3:
return 0
wigner = 0
for t in range(
int(max(0, j2 - m1 - j3, j1 + m2 - j3)),
int(min(j1 + j2 - j3, j1 - m1, j2 + m2)) + 1,
):
wigner += (-1) ** t / (
factorial(t)
* factorial(t - (j2 - m1 - j3))
* factorial(t - (j1 + m2 - j3))
* factorial(j1 + j2 - j3 - t)
* factorial(j1 - m1 - t)
* factorial(j2 + m2 - t)
)
wigner *= (-1) ** (j1 - j2 - m3) * sqrt(
Δ(j1, j2, j3)
* factorial(j1 + m1)
* factorial(j1 - m1)
* factorial(j2 + m2)
* factorial(j2 - m2)
* factorial(j3 + m3)
* factorial(j3 - m3)
)
return wigner
wigner_6j(j1, j2, j3, J1, J2, J3)
Calculate the Wigner 6-j symbol numerically
The Wigner 6-j symbol is calculated numerically using a recursion relation.
This approximate calculation is often faster than the exact analytic
calculation that is done by sympy.physics.wigner
.
The results are cached in the private variable _wigner_6j_cache
to
speed subsequent calculations.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
j1 |
float |
required | |
j2 |
float |
required | |
j3 |
float |
required | |
J1 |
float |
required | |
J2 |
float |
required | |
J3 |
float |
required |
Returns:
Type | Description |
---|---|
float |
Source code in atomphys/calc/wigner.py
@lru_cache(maxsize=None)
def wigner_6j(
j1: float, j2: float, j3: float, J1: float, J2: float, J3: float
) -> float:
"""Calculate the Wigner 6-j symbol numerically
The Wigner 6-j symbol is calculated numerically using a recursion relation.
This approximate calculation is often faster than the exact analytic
calculation that is done by `sympy.physics.wigner`.
The results are cached in the private variable `_wigner_6j_cache` to
speed subsequent calculations.
$\\left\\{
\\begin{matrix}
j_1 & j_2 & j_3 \\\\ J_1 & J_2 & J_3
\\end{matrix}
\\right\\}$
Arguments:
j1:
j2:
j3:
J1:
J2:
J3:
Returns:
$\\left\\{
\\begin{matrix}
j_1 & j_2 & j_3 \\\\ J_1 & J_2 & J_3
\\end{matrix}
\\right\\}$
"""
if (
not ishalfint(j1)
or not ishalfint(j2)
or not ishalfint(j3)
or not ishalfint(J1)
or not ishalfint(J2)
or not ishalfint(J3)
):
raise ValueError("All arguments must be integers or half-integers")
# triangle inequality for each triad
if (
not istriangle(j1, j2, j3)
or not istriangle(j1, J2, J3)
or not istriangle(J1, j2, J3)
or not istriangle(J1, J2, j3)
):
return 0
# each triad must sum to an integer
if (
not isint(j1 + j2 + j3)
or not isint(j1 + J2 + J3)
or not isint(J1 + j2 + J3)
or not isint(J1 + J2 + j3)
):
return 0
wigner = 0
for t in range(
int(max(0, j1 + j2 + j3, j1 + J2 + J3, J1 + j2 + J3, J1 + J2 + j3)),
int(min(j1 + j2 + J1 + J2, j2 + j3 + J2 + J3, j3 + j1 + J3 + J1)) + 1,
):
wigner += (
(-1) ** t
* factorial(t + 1)
/ (
factorial(t - (j1 + j2 + j3))
* factorial(t - (j1 + J2 + J3))
* factorial(t - (J1 + j2 + J3))
* factorial(t - (J1 + J2 + j3))
* factorial((j1 + j2 + J1 + J2) - t)
* factorial((j2 + j3 + J2 + J3) - t)
* factorial((j3 + j1 + J3 + J1) - t)
)
)
wigner *= sqrt(Δ(j1, j2, j3) * Δ(j1, J2, J3) * Δ(J1, j2, J3) * Δ(J1, J2, j3))
return wigner
Δ(a, b, c)
Helper function for wigner symbols
Calculates the intermediate expression
Parameters:
Name | Type | Description | Default |
---|---|---|---|
a |
float |
required | |
b |
float |
required | |
c |
float |
required |
Returns:
Type | Description |
---|---|
float |
Source code in atomphys/calc/wigner.py
def Δ(a: float, b: float, c: float) -> float:
"""Helper function for wigner symbols
Calculates the intermediate expression
$\\Delta = \\frac{(a+b-c)!(a-b+c)!(-a+b+c)!}{(a+b+c+1)!}$
Arguments:
a:
b:
c:
Returns:
$\\Delta$
"""
return (
factorial(a + b - c)
* factorial(a - b + c)
* factorial(-a + b + c)
/ factorial(a + b + c + 1)
)
data
special
nist
remove_annotations(s)
remove annotations from energy strings in NIST ASD
Source code in atomphys/data/nist.py
def remove_annotations(s: str) -> str:
"""remove annotations from energy strings in NIST ASD"""
# re_energy = re.compile("-?\\d+\\.\\d*|$")
# return re_energy.findall(s)[0]
# this is about 3.5× faster than re.findall, but it's less flexible
# overall this can make a several hundred ms difference when loading
return s.strip("()[]aluxyz +?").replace("†", "")
state
Coupling (Enum)
An enumeration.
StateRegistry (UserList)
append(self, state)
S.append(value) -- append value to the end of the sequence
Source code in atomphys/state.py
def append(self, state: State):
self._assert_State(state)
super().append(state)
extend(self, states)
S.extend(iterable) -- extend sequence by appending elements from the iterable
Source code in atomphys/state.py
def extend(self, states):
[self._assert_State(state) for state in states]
super().extend(states)
insert(self, index, state)
S.insert(index, value) -- insert value before index
Source code in atomphys/state.py
def insert(self, index: int, state: State):
self._assert_State(state)
super().insert(index, state)
term
Coupling (Enum)
An enumeration.
parse_term(term)
parse term symbol string in NIST ASD
Source code in atomphys/term.py
def parse_term(term: str) -> dict:
"""
parse term symbol string in NIST ASD
"""
if "Limit" in term:
return {"ionization_limit": True}
parity = -1 if "*" in term else 1
match = LS_term.search(term)
if match is None:
match = J1J2_term.search(term)
if match is None:
match = LK_term.search(term)
if match is None:
return {"parity": parity}
def convert(key, value):
if key in ["S", "S2"]:
return float(Fraction((int(value) - 1) / 2))
if key in ["J1", "J2", "J", "K"]:
return float(Fraction(value))
if key == "L":
return L[value]
term = {
key: convert(key, value) for key, value in match.groupdict().items() if value
}
return {**term, "parity": parity}
transition
TransitionRegistry (UserList)
append(self, transition)
S.append(value) -- append value to the end of the sequence
Source code in atomphys/transition.py
def append(self, transition: Transition):
self._assert_Transition(transition)
super().append(transition)
extend(self, transitions)
S.extend(iterable) -- extend sequence by appending elements from the iterable
Source code in atomphys/transition.py
def extend(self, transitions):
[self._assert_Transition(transition) for transition in transitions]
super().extend(transitions)
insert(self, index, transition)
S.insert(index, value) -- insert value before index
Source code in atomphys/transition.py
def insert(self, index: int, transition: Transition):
self._assert_Transition(transition)
super().insert(index, transition)
util
fsolve(func, x0, x1=None, tol=1.49012e-08, maxfev=100)
Find the roots of a function using the secant method.
Return the roots of the equation func(x) = 0
given a
starting estimate x0
. A second starting estimate x1
for the next iteration of the secant method can be supplied.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
func |
Callable |
a function |
required |
x0 |
The starting estimate for the roots of `func(x) = 0`` |
required | |
x1 |
A second starting estimate for the next iteration of |
None |
|
tol |
float |
The calculation will terminate if the relative
error between two consecutive iterates is at most |
1.49012e-08 |
maxfev |
int |
The maximum number of calls to the function. |
100 |
Returns:
Type | Description |
---|---|
The root of the function |
Source code in atomphys/util.py
def fsolve(func: Callable, x0, x1=None, tol: float = 1.49012e-08, maxfev: int = 100):
"""
Find the roots of a function using the secant method.
Return the roots of the equation `func(x) = 0` given a
starting estimate `x0`. A second starting estimate `x1`
for the next iteration of the secant method can be supplied.
Arguments:
func: a function `f(x)` that takes a single argument `x`
x0: The starting estimate for the roots of `func(x) = 0``
x1: A second starting estimate for the next iteration of
the secant method. Defaults to `1.0001 * x0`.
tol: The calculation will terminate if the relative
error between two consecutive iterates is at most `tol`
maxfev: The maximum number of calls to the function.
Returns:
The root of the function `f(x)`.
"""
if x1 is None:
x1 = x0 * 1.001 if x0 else 0.001
fx0, fx1 = func(x0), func(x1)
i = 2
while (abs(fx0) > 0) and (abs((fx1 - fx0) / fx0) > tol) and (i < maxfev + 1):
x0, x1 = x1, x1 - fx1 * (x1 - x0) / (fx1 - fx0)
fx0, fx1 = fx1, func(x1)
i += 1
return x1