Skip to content

Commit a8f53e7

Browse files
add squared angular operator
1 parent 91192af commit a8f53e7

5 files changed

Lines changed: 89 additions & 4 deletions

File tree

src/rydstate/angular/angular_ket.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
calc_reduced_raw_quantum_number_matrix_element,
1111
calc_reduced_spherical_matrix_element,
1212
calc_reduced_spin_matrix_element,
13+
calc_reduced_spin_squared_matrix_element,
1314
)
1415
from rydstate.angular.core_ket import CoreKet
1516
from rydstate.angular.utils import (
@@ -646,7 +647,7 @@ def calc_reduced_matrix_element(
646647
cache[cache_key] = self._calc_reduced_matrix_element(other, operator, kappa)
647648
return cache[cache_key]
648649

649-
def _calc_reduced_matrix_element( # noqa: C901
650+
def _calc_reduced_matrix_element( # noqa: C901, PLR0912
650651
self: Self, other: AngularKetBase[Any], operator: AngularOperatorType, kappa: int
651652
) -> float:
652653
if not is_angular_operator_type(operator):
@@ -663,8 +664,8 @@ def _calc_reduced_matrix_element( # noqa: C901
663664

664665
if is_angular_momentum_quantum_number(operator) and kappa != 1:
665666
raise ValueError("Only kappa=1 is supported for spin operators.")
666-
if operator.startswith(("identity_", "raw_value_")) and kappa != 0:
667-
raise ValueError("Only kappa=0 is supported for identity/raw_value operators.")
667+
if operator.startswith(("identity_", "raw_value_", "squared_")) and kappa != 0:
668+
raise ValueError("Only kappa=0 is supported for identity/raw_value/squared operators.")
668669

669670
qn_self, qn_other = self.get_qn(qn_name), other.get_qn(qn_name)
670671
if is_unknown(qn_self) or is_unknown(qn_other):
@@ -676,6 +677,8 @@ def _calc_reduced_matrix_element( # noqa: C901
676677
complete_reduced_matrix_element = calc_reduced_spin_matrix_element(qn_self, qn_other)
677678
elif operator.startswith("identity_"):
678679
complete_reduced_matrix_element = calc_reduced_raw_quantum_number_matrix_element(qn_self, qn_other, 0)
680+
elif operator.startswith("squared_"):
681+
complete_reduced_matrix_element = calc_reduced_spin_squared_matrix_element(qn_self, qn_other)
679682
elif operator.startswith("raw_value_"):
680683
# raw_value_x is the diagonal scalar operator x^exponent; reusing the same reduced matrix element as
681684
# identity (exponent=0) ensures the coupled-scheme prefactor and Wigner-Eckart normalization are applied,

src/rydstate/angular/angular_matrix_element.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,31 @@ def calc_reduced_spin_matrix_element(s_final: float, s_initial: float) -> float:
6767
return math.sqrt((2 * s_final + 1) * (s_final + 1) * s_final)
6868

6969

70+
def calc_reduced_spin_squared_matrix_element(s_final: float, s_initial: float) -> float:
71+
r"""Calculate the reduced squared spin matrix element (s_final || \hat{s}^2 || s_initial).
72+
73+
The squared spin operator \hat{s}^2 = \hat{s} \cdot \hat{s} is a scalar (rank kappa=0) operator.
74+
Within a subspace of fixed spin quantum number s it is proportional to the identity,
75+
\hat{s}^2 = s(s+1) \id, so with the convention from Edmonds 1985 "Angular Momentum in Quantum Mechanics"
76+
(using equation (5.4.1) and (3.7.9)) its reduced matrix element is given by:
77+
78+
.. math::
79+
(s_final || \hat{s}^2 || s_initial)
80+
= s_initial (s_initial + 1) \sqrt{2 * s_initial + 1} * \delta_{s_final, s_initial}
81+
82+
Args:
83+
s_final: The spin quantum number of the final state.
84+
s_initial: The spin quantum number of the initial state.
85+
86+
Returns:
87+
The reduced matrix element :math:`(s_final || \hat{s}^2 || s_initial)`.
88+
89+
"""
90+
if s_final != s_initial:
91+
return 0
92+
return s_initial * (s_initial + 1) * math.sqrt(2 * s_initial + 1)
93+
94+
7095
@lru_cache(maxsize=1_000)
7196
def calc_reduced_raw_quantum_number_matrix_element(s_final: float, s_initial: float, exponent: int) -> float:
7297
r"""Calculate the reduced matrix element (s_final || s_final^exponent || s_initial).

src/rydstate/angular/utils.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,20 @@ def lru_cache(maxsize: int) -> Callable[[Callable[P, R]], Callable[P, R]]:
4545
"identity_f_c",
4646
"identity_f_tot",
4747
]
48+
SquaredSpinOperators = Literal[
49+
"squared_i_c",
50+
"squared_s_c",
51+
"squared_l_c",
52+
"squared_s_r",
53+
"squared_l_r",
54+
"squared_s_tot",
55+
"squared_l_tot",
56+
"squared_j_c",
57+
"squared_j_r",
58+
"squared_j_tot",
59+
"squared_f_c",
60+
"squared_f_tot",
61+
]
4862
RawValueOperators = Literal[
4963
"raw_value_i_c",
5064
"raw_value_s_c",
@@ -79,6 +93,7 @@ def lru_cache(maxsize: int) -> Callable[[Callable[P, R]], Callable[P, R]]:
7993
"spherical_core",
8094
AngularMomentumQuantumNumbers,
8195
IdentityOperators,
96+
SquaredSpinOperators,
8297
RawValueOperators,
8398
RawValueOperators2,
8499
]
@@ -238,6 +253,8 @@ def get_qn_name_from_operator(operator: AngularOperatorType) -> AngularMomentumQ
238253
qn = "l_c"
239254
elif operator.startswith("identity_"):
240255
qn = operator.removeprefix("identity_")
256+
elif operator.startswith("squared_"):
257+
qn = operator.removeprefix("squared_")
241258
elif operator.startswith("raw_value_"):
242259
qn = operator.removeprefix("raw_value_")
243260
qn = qn.removesuffix("_2")

src/rydstate/rydberg_state/rydberg_ket.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,7 @@ def _get_ks(self, operator: MatrixElementOperator) -> tuple[int, int]:
254254
return MatrixElementOperatorRanks[operator]
255255
if is_angular_operator_type(operator):
256256
k_radial = 0
257-
if operator.startswith(("identity_", "raw_value_")):
257+
if operator.startswith(("identity_", "raw_value_", "squared_")):
258258
return k_radial, 0
259259
if is_angular_momentum_quantum_number(operator):
260260
return k_radial, 1

tests/test_angular_matrix_elements.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,46 @@ def test_reduced_raw_value(ket: AngularKetBase[AllKnown]) -> None:
145145
assert np.isclose(raw**2, ket_m.calc_matrix_element(ket_m, "raw_value_" + op + "_2", kappa=0, q=0)) # type: ignore [arg-type]
146146

147147

148+
@pytest.mark.parametrize("ket", TEST_KETS)
149+
def test_reduced_spin_squared(ket: AngularKetBase[AllKnown]) -> None:
150+
op: AngularMomentumQuantumNumbers
151+
coupling_schemes: list[CouplingScheme] = ["LS", "JJ", "FJ"]
152+
for scheme in coupling_schemes:
153+
state = ket.to_state(scheme)
154+
for op in state.kets[0].quantum_number_names:
155+
# the squared spin operator is diagonal in the scheme's own basis with eigenvalue qn * (qn + 1),
156+
# so for a superposition the reduced matrix element is the weighted average over the components
157+
exp_squared = sum(coeff**2 * s_ket.get_qn(op) * (s_ket.get_qn(op) + 1) for coeff, s_ket in state)
158+
reduced_squared = exp_squared * np.sqrt(2 * ket.f_tot + 1)
159+
assert np.isclose(reduced_squared, state.calc_reduced_matrix_element(state, "squared_" + op, kappa=0)) # type: ignore [arg-type]
160+
161+
162+
def test_spin_squared_expectation_value() -> None:
163+
"""The expectation value of s^2 must be s(s+1) for good quantum numbers and match the sum rule otherwise."""
164+
ket = AngularKetFJ(l_r=0, j_r=0.5, f_c=1, m=0.5, f_tot=0.5, species="Yb171")
165+
166+
# s_r and s_c are good quantum numbers in every ket, so <s^2> = s(s+1) = 3/4
167+
assert np.isclose(ket.calc_matrix_element(ket, "squared_s_r", 0, q=0), 0.75)
168+
assert np.isclose(ket.calc_matrix_element(ket, "squared_s_c", 0, q=0), 0.75)
169+
170+
# s_tot is not a good quantum number of an FJ ket, so <s_tot^2> is a weighted average
171+
# over the LS decomposition: sum_i |c_i|^2 * s_tot_i * (s_tot_i + 1)
172+
expected = sum(coeff**2 * ls_ket.s_tot * (ls_ket.s_tot + 1) for coeff, ls_ket in ket.to_state("LS"))
173+
assert np.isclose(ket.calc_matrix_element(ket, "squared_s_tot", 0, q=0), expected)
174+
175+
# <s^2> must also equal the sum over all final states and components q of the squared
176+
# matrix elements of the (rank-1) spin operator: <s^2> = sum_{f,q} |<f|s_q|ket>|^2
177+
finals = [
178+
AngularKetFJ(l_r=0, j_r=0.5, f_c=f_c, m=m / 2, f_tot=f_tot, species="Yb171")
179+
for f_c in (0, 1)
180+
for f_tot in {abs(f_c - 0.5), f_c + 0.5}
181+
for m in range(-int(2 * f_tot), int(2 * f_tot) + 1, 2)
182+
]
183+
for op in ("s_r", "s_c"):
184+
sum_rule = sum(f.calc_matrix_element(ket, op, 1, q=q) ** 2 for f in finals for q in (-1, 0, 1))
185+
assert np.isclose(sum_rule, ket.calc_matrix_element(ket, "squared_" + op, 0, q=0)) # type: ignore [arg-type]
186+
187+
148188
@pytest.mark.parametrize(("ket1", "ket2"), TEST_KET_PAIRS)
149189
def test_matrix_elements_in_different_coupling_schemes(
150190
ket1: AngularKetBase[AllKnown], ket2: AngularKetBase[AllKnown], coupling_scheme: CouplingScheme

0 commit comments

Comments
 (0)