diffpes 2026.3.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- diffpes/__init__.py +65 -0
- diffpes/inout/__init__.py +88 -0
- diffpes/inout/chgcar.py +459 -0
- diffpes/inout/doscar.py +248 -0
- diffpes/inout/eigenval.py +258 -0
- diffpes/inout/hdf5.py +818 -0
- diffpes/inout/helpers.py +295 -0
- diffpes/inout/kpoints.py +433 -0
- diffpes/inout/plotting.py +757 -0
- diffpes/inout/poscar.py +174 -0
- diffpes/inout/procar.py +331 -0
- diffpes/inout/py.typed +0 -0
- diffpes/maths/__init__.py +68 -0
- diffpes/maths/dipole.py +368 -0
- diffpes/maths/gaunt.py +496 -0
- diffpes/maths/spherical_harmonics.py +336 -0
- diffpes/py.typed +0 -0
- diffpes/radial/__init__.py +47 -0
- diffpes/radial/bessel.py +198 -0
- diffpes/radial/integrate.py +128 -0
- diffpes/radial/wavefunctions.py +335 -0
- diffpes/simul/__init__.py +146 -0
- diffpes/simul/broadening.py +258 -0
- diffpes/simul/crosssections.py +196 -0
- diffpes/simul/expanded.py +1021 -0
- diffpes/simul/forward.py +586 -0
- diffpes/simul/oam.py +112 -0
- diffpes/simul/polarization.py +443 -0
- diffpes/simul/py.typed +0 -0
- diffpes/simul/resolution.py +122 -0
- diffpes/simul/self_energy.py +142 -0
- diffpes/simul/spectrum.py +1116 -0
- diffpes/simul/workflow.py +424 -0
- diffpes/tightb/__init__.py +68 -0
- diffpes/tightb/diagonalize.py +340 -0
- diffpes/tightb/hamiltonian.py +286 -0
- diffpes/tightb/projections.py +134 -0
- diffpes/types/__init__.py +162 -0
- diffpes/types/aliases.py +52 -0
- diffpes/types/bands.py +1064 -0
- diffpes/types/dos.py +510 -0
- diffpes/types/geometry.py +306 -0
- diffpes/types/kpath.py +365 -0
- diffpes/types/params.py +496 -0
- diffpes/types/py.typed +0 -0
- diffpes/types/radial_params.py +482 -0
- diffpes/types/self_energy.py +271 -0
- diffpes/types/tb_model.py +531 -0
- diffpes/types/volumetric.py +608 -0
- diffpes/utils/__init__.py +30 -0
- diffpes/utils/math.py +255 -0
- diffpes/utils/py.typed +0 -0
- diffpes-2026.3.1.dist-info/METADATA +176 -0
- diffpes-2026.3.1.dist-info/RECORD +55 -0
- diffpes-2026.3.1.dist-info/WHEEL +4 -0
diffpes/maths/gaunt.py
ADDED
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
r"""Gaunt coefficient table for dipole transitions.
|
|
2
|
+
|
|
3
|
+
Extended Summary
|
|
4
|
+
----------------
|
|
5
|
+
Precomputes real-valued Gaunt integrals for electric dipole
|
|
6
|
+
transitions in real spherical harmonics:
|
|
7
|
+
|
|
8
|
+
.. math::
|
|
9
|
+
|
|
10
|
+
G(l, m, l', m') = \int Y_l^m(\hat{r})\, r_q\, Y_{l'}^{m'}(\hat{r})\,d\Omega
|
|
11
|
+
|
|
12
|
+
where :math:`r_q` is one of the three components of the position
|
|
13
|
+
operator expressed in the real spherical harmonic basis (q = -1, 0, +1).
|
|
14
|
+
|
|
15
|
+
Selection rules: :math:`l' = l \pm 1` and :math:`|m' - m| \leq 1`.
|
|
16
|
+
|
|
17
|
+
The table is computed once at module load time using pure Python
|
|
18
|
+
(not JAX-traced) and stored as a JAX array for O(1) lookup.
|
|
19
|
+
|
|
20
|
+
Routine Listings
|
|
21
|
+
----------------
|
|
22
|
+
:data:`GAUNT_TABLE`
|
|
23
|
+
Module-level precomputed Gaunt coefficient table for l_max=4.
|
|
24
|
+
:data:`L_MAX`
|
|
25
|
+
Maximum angular momentum supported by the precomputed table.
|
|
26
|
+
:func:`build_gaunt_table`
|
|
27
|
+
Build the dipole Gaunt coefficient lookup table.
|
|
28
|
+
:func:`gaunt_lookup`
|
|
29
|
+
Look up a single Gaunt coefficient from the precomputed table.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
import math
|
|
33
|
+
from functools import cache
|
|
34
|
+
|
|
35
|
+
import numpy as np
|
|
36
|
+
import jax.numpy as jnp
|
|
37
|
+
from jaxtyping import Array, Float
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _wigner3j(j1: int, j2: int, j3: int, m1: int, m2: int, m3: int) -> float:
|
|
41
|
+
r"""Evaluate Wigner 3-j symbol using the Racah formula.
|
|
42
|
+
|
|
43
|
+
Extended Summary
|
|
44
|
+
----------------
|
|
45
|
+
Computes the Wigner 3-j symbol
|
|
46
|
+
|
|
47
|
+
.. math::
|
|
48
|
+
|
|
49
|
+
\begin{pmatrix} j_1 & j_2 & j_3 \\ m_1 & m_2 & m_3 \end{pmatrix}
|
|
50
|
+
|
|
51
|
+
using the explicit Racah formula, which expresses the 3-j symbol as a
|
|
52
|
+
finite sum over factorials. The formula is:
|
|
53
|
+
|
|
54
|
+
.. math::
|
|
55
|
+
|
|
56
|
+
\begin{pmatrix} j_1 & j_2 & j_3 \\ m_1 & m_2 & m_3 \end{pmatrix}
|
|
57
|
+
= (-1)^{j_1 - j_2 - m_3} \Delta(j_1, j_2, j_3) \cdot P \cdot
|
|
58
|
+
\sum_t \frac{(-1)^t}{t! \cdot D(t)}
|
|
59
|
+
|
|
60
|
+
where :math:`\Delta(j_1, j_2, j_3)` is the triangle coefficient
|
|
61
|
+
|
|
62
|
+
.. math::
|
|
63
|
+
|
|
64
|
+
\Delta = \sqrt{
|
|
65
|
+
\frac{(j_1+j_2-j_3)!(j_1-j_2+j_3)!(-j_1+j_2+j_3)!}
|
|
66
|
+
{(j_1+j_2+j_3+1)!}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
and :math:`P` is the product of square roots of magnetic-number
|
|
70
|
+
factorials:
|
|
71
|
+
|
|
72
|
+
.. math::
|
|
73
|
+
|
|
74
|
+
P = \sqrt{(j_1+m_1)!(j_1-m_1)!(j_2+m_2)!(j_2-m_2)!(j_3+m_3)!(j_3-m_3)!}
|
|
75
|
+
|
|
76
|
+
The summation index *t* runs over all integers for which all
|
|
77
|
+
factorial arguments are non-negative, determined by ``t_min`` and
|
|
78
|
+
``t_max``.
|
|
79
|
+
|
|
80
|
+
**Selection rules** enforced before computation:
|
|
81
|
+
|
|
82
|
+
- :math:`m_1 + m_2 + m_3 = 0` (magnetic quantum number conservation)
|
|
83
|
+
- :math:`|m_i| \le j_i` for each i (projection bounds)
|
|
84
|
+
- :math:`|j_1 - j_2| \le j_3 \le j_1 + j_2` (triangle inequality)
|
|
85
|
+
|
|
86
|
+
If any selection rule is violated, the function returns 0.0 immediately
|
|
87
|
+
without computing the sum.
|
|
88
|
+
|
|
89
|
+
Only needed for small angular momenta (l_max <= 5), so the
|
|
90
|
+
factorial-based formula is efficient and exact (no floating-point
|
|
91
|
+
cancellation issues at this scale).
|
|
92
|
+
|
|
93
|
+
Parameters
|
|
94
|
+
----------
|
|
95
|
+
j1, j2, j3 : int
|
|
96
|
+
Angular momentum quantum numbers (non-negative integers).
|
|
97
|
+
m1, m2, m3 : int
|
|
98
|
+
Magnetic quantum numbers satisfying :math:`|m_i| \le j_i`.
|
|
99
|
+
|
|
100
|
+
Returns
|
|
101
|
+
-------
|
|
102
|
+
value : float
|
|
103
|
+
The Wigner 3-j symbol value.
|
|
104
|
+
"""
|
|
105
|
+
if m1 + m2 + m3 != 0:
|
|
106
|
+
return 0.0
|
|
107
|
+
if abs(m1) > j1 or abs(m2) > j2 or abs(m3) > j3:
|
|
108
|
+
return 0.0
|
|
109
|
+
if j3 < abs(j1 - j2) or j3 > j1 + j2:
|
|
110
|
+
return 0.0
|
|
111
|
+
|
|
112
|
+
t_min: int = max(0, j2 - j3 - m1, j1 - j3 + m2)
|
|
113
|
+
t_max: int = min(j1 + j2 - j3, j1 - m1, j2 + m2)
|
|
114
|
+
|
|
115
|
+
prefactor: float = math.sqrt(
|
|
116
|
+
math.factorial(j1 + m1)
|
|
117
|
+
* math.factorial(j1 - m1)
|
|
118
|
+
* math.factorial(j2 + m2)
|
|
119
|
+
* math.factorial(j2 - m2)
|
|
120
|
+
* math.factorial(j3 + m3)
|
|
121
|
+
* math.factorial(j3 - m3)
|
|
122
|
+
)
|
|
123
|
+
triangle: float = math.sqrt(
|
|
124
|
+
math.factorial(j1 + j2 - j3)
|
|
125
|
+
* math.factorial(j1 - j2 + j3)
|
|
126
|
+
* math.factorial(-j1 + j2 + j3)
|
|
127
|
+
/ math.factorial(j1 + j2 + j3 + 1)
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
total: float = 0.0
|
|
131
|
+
for t in range(t_min, t_max + 1):
|
|
132
|
+
sign: int = (-1) ** t
|
|
133
|
+
denom: int = (
|
|
134
|
+
math.factorial(t)
|
|
135
|
+
* math.factorial(j1 + j2 - j3 - t)
|
|
136
|
+
* math.factorial(j1 - m1 - t)
|
|
137
|
+
* math.factorial(j2 + m2 - t)
|
|
138
|
+
* math.factorial(j3 - j2 + m1 + t)
|
|
139
|
+
* math.factorial(j3 - j1 - m2 + t)
|
|
140
|
+
)
|
|
141
|
+
total += sign / denom
|
|
142
|
+
|
|
143
|
+
value: float = float((-1) ** (j1 - j2 - m3) * prefactor * triangle * total)
|
|
144
|
+
return value
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@cache
|
|
148
|
+
def _complex_gaunt(
|
|
149
|
+
l1: int, m1: int, l2: int, m2: int, l3: int, m3: int
|
|
150
|
+
) -> float:
|
|
151
|
+
r"""Complex Gaunt integral for three complex spherical harmonics.
|
|
152
|
+
|
|
153
|
+
Extended Summary
|
|
154
|
+
----------------
|
|
155
|
+
Computes the Gaunt integral (three-Y integral) defined as:
|
|
156
|
+
|
|
157
|
+
.. math::
|
|
158
|
+
|
|
159
|
+
G(l_1,m_1; l_2,m_2; l_3,m_3)
|
|
160
|
+
= \int Y_{l_1}^{m_1} Y_{l_2}^{m_2} Y_{l_3}^{m_3} \, d\Omega
|
|
161
|
+
|
|
162
|
+
Using the standard decomposition into Wigner 3-j symbols:
|
|
163
|
+
|
|
164
|
+
.. math::
|
|
165
|
+
|
|
166
|
+
G = (-1)^{m_3}
|
|
167
|
+
\sqrt{\frac{(2l_1+1)(2l_2+1)(2l_3+1)}{4\pi}}
|
|
168
|
+
\begin{pmatrix} l_1 & l_2 & l_3 \\ 0 & 0 & 0 \end{pmatrix}
|
|
169
|
+
\begin{pmatrix} l_1 & l_2 & l_3 \\ m_1 & m_2 & -m_3 \end{pmatrix}
|
|
170
|
+
|
|
171
|
+
The first 3-j symbol enforces the parity selection rule
|
|
172
|
+
:math:`l_1 + l_2 + l_3 = \text{even}`, and is evaluated first as
|
|
173
|
+
an early-exit optimization. The result is cached via
|
|
174
|
+
``functools.cache`` since the same (l, m) combinations appear
|
|
175
|
+
repeatedly during Gaunt table construction.
|
|
176
|
+
|
|
177
|
+
Parameters
|
|
178
|
+
----------
|
|
179
|
+
l1, l2, l3 : int
|
|
180
|
+
Angular momentum quantum numbers.
|
|
181
|
+
m1, m2, m3 : int
|
|
182
|
+
Magnetic quantum numbers.
|
|
183
|
+
|
|
184
|
+
Returns
|
|
185
|
+
-------
|
|
186
|
+
value : float
|
|
187
|
+
The complex Gaunt integral.
|
|
188
|
+
"""
|
|
189
|
+
w3j_000: float = _wigner3j(l1, l2, l3, 0, 0, 0)
|
|
190
|
+
if w3j_000 == 0.0:
|
|
191
|
+
return 0.0
|
|
192
|
+
w3j_mmm: float = _wigner3j(l1, l2, l3, m1, m2, -m3)
|
|
193
|
+
if w3j_mmm == 0.0:
|
|
194
|
+
return 0.0
|
|
195
|
+
|
|
196
|
+
prefactor: float = (-1) ** m3 * math.sqrt(
|
|
197
|
+
(2 * l1 + 1) * (2 * l2 + 1) * (2 * l3 + 1) / (4.0 * math.pi)
|
|
198
|
+
)
|
|
199
|
+
value: float = prefactor * w3j_000 * w3j_mmm
|
|
200
|
+
return value
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _real_gaunt_dipole(l: int, m: int, lp: int, mp: int, q: int) -> float:
|
|
204
|
+
r"""Gaunt coefficient for real spherical harmonics with dipole operator.
|
|
205
|
+
|
|
206
|
+
Extended Summary
|
|
207
|
+
----------------
|
|
208
|
+
Computes the integral of :math:`Y_l^m(\text{real}) \cdot r_q \cdot
|
|
209
|
+
Y_{l'}^{m'}(\text{real})` over the unit sphere, where
|
|
210
|
+
:math:`r_q` (q = -1, 0, +1) is a dipole operator component.
|
|
211
|
+
|
|
212
|
+
Because the Gaunt integral is natively defined for complex spherical
|
|
213
|
+
harmonics, this function performs a basis transformation: each real
|
|
214
|
+
harmonic is expanded in the complex basis using unitary coefficients,
|
|
215
|
+
and the resulting triple sum of complex Gaunt integrals is accumulated.
|
|
216
|
+
|
|
217
|
+
**Real-to-complex transformation:**
|
|
218
|
+
|
|
219
|
+
.. math::
|
|
220
|
+
|
|
221
|
+
Y_l^m(\text{real}) = \frac{1}{\sqrt{2}}
|
|
222
|
+
\bigl(Y_l^m + (-1)^m Y_l^{-m}\bigr)
|
|
223
|
+
\quad (m > 0)
|
|
224
|
+
|
|
225
|
+
Y_l^0(\text{real}) = Y_l^0 \quad (m = 0)
|
|
226
|
+
|
|
227
|
+
Y_l^m(\text{real}) = \frac{-i}{\sqrt{2}}
|
|
228
|
+
\bigl((-1)^{|m|} Y_l^{|m|} - Y_l^{-|m|}\bigr)
|
|
229
|
+
\quad (m < 0)
|
|
230
|
+
|
|
231
|
+
Each real harmonic thus becomes a sum of at most two complex harmonics
|
|
232
|
+
with known coefficients (computed by the nested helper
|
|
233
|
+
``_real_to_complex_coeffs``).
|
|
234
|
+
|
|
235
|
+
**Dipole operator in complex basis:**
|
|
236
|
+
|
|
237
|
+
The position operator component :math:`r_q` corresponds to the l=1
|
|
238
|
+
real spherical harmonic :math:`Y_1^q(\text{real})`, which is itself
|
|
239
|
+
expanded in the complex basis using the same transformation.
|
|
240
|
+
|
|
241
|
+
**Assembly:**
|
|
242
|
+
|
|
243
|
+
The full real Gaunt coefficient is:
|
|
244
|
+
|
|
245
|
+
.. math::
|
|
246
|
+
|
|
247
|
+
G_{\text{real}} = \sum_{\mu, \nu, \rho}
|
|
248
|
+
\overline{U_{\text{final}}(m', \rho)} \cdot
|
|
249
|
+
U_{\text{dip}}(q, \nu) \cdot
|
|
250
|
+
U_{\text{init}}(m, \mu) \cdot
|
|
251
|
+
(-1)^{\rho} \cdot
|
|
252
|
+
G_{\text{complex}}(l', -\rho; 1, \nu; l, \mu)
|
|
253
|
+
|
|
254
|
+
where :math:`U` matrices are the real-to-complex transformation
|
|
255
|
+
coefficients. The conjugation on the final-state coefficients arises
|
|
256
|
+
because the complex Gaunt integral involves :math:`Y_{l'}^{\rho *}`,
|
|
257
|
+
and :math:`Y_l^{m *} = (-1)^m Y_l^{-m}`.
|
|
258
|
+
|
|
259
|
+
The result is guaranteed to be real; an imaginary part exceeding
|
|
260
|
+
:math:`10^{-12}` triggers a ``ValueError`` as a consistency check.
|
|
261
|
+
|
|
262
|
+
Parameters
|
|
263
|
+
----------
|
|
264
|
+
l : int
|
|
265
|
+
Initial state angular momentum.
|
|
266
|
+
m : int
|
|
267
|
+
Initial state magnetic quantum number.
|
|
268
|
+
lp : int
|
|
269
|
+
Final state angular momentum.
|
|
270
|
+
mp : int
|
|
271
|
+
Final state magnetic quantum number.
|
|
272
|
+
q : int
|
|
273
|
+
Dipole component index (-1, 0, or +1).
|
|
274
|
+
|
|
275
|
+
Returns
|
|
276
|
+
-------
|
|
277
|
+
value : float
|
|
278
|
+
The real Gaunt coefficient for the specified quantum numbers.
|
|
279
|
+
"""
|
|
280
|
+
sqrt2: float = math.sqrt(2.0)
|
|
281
|
+
|
|
282
|
+
# Build transformation coefficients for Y_l^m(real)
|
|
283
|
+
# in terms of complex Y_l^mu: Y_l^m(real) = sum_mu U_{m,mu} Y_l^mu
|
|
284
|
+
def _real_to_complex_coeffs(ll: int, mm: int) -> list[tuple[complex, int]]:
|
|
285
|
+
r"""Return real-to-complex expansion coefficients.
|
|
286
|
+
|
|
287
|
+
Computes the unitary transformation coefficients :math:`U_{m,\mu}`
|
|
288
|
+
such that :math:`Y_l^m(\text{real}) = \sum_\mu U_{m,\mu} Y_l^\mu`.
|
|
289
|
+
For m > 0, two terms with :math:`\mu = \pm m` contribute with
|
|
290
|
+
coefficients :math:`1/\sqrt{2}` and :math:`(-1)^m/\sqrt{2}`.
|
|
291
|
+
For m = 0, a single term with coefficient 1. For m < 0, two
|
|
292
|
+
terms with imaginary coefficients that isolate the sine component.
|
|
293
|
+
|
|
294
|
+
Parameters
|
|
295
|
+
----------
|
|
296
|
+
ll : int
|
|
297
|
+
Angular momentum (unused in the formula but kept for clarity).
|
|
298
|
+
mm : int
|
|
299
|
+
Magnetic quantum number of the real harmonic.
|
|
300
|
+
|
|
301
|
+
Returns
|
|
302
|
+
-------
|
|
303
|
+
coeffs : list of (complex, int)
|
|
304
|
+
List of ``(coefficient, mu)`` pairs.
|
|
305
|
+
"""
|
|
306
|
+
if mm > 0:
|
|
307
|
+
return [
|
|
308
|
+
(1.0 / sqrt2, mm),
|
|
309
|
+
((-1) ** mm / sqrt2, -mm),
|
|
310
|
+
]
|
|
311
|
+
if mm == 0:
|
|
312
|
+
return [(1.0, 0)]
|
|
313
|
+
am: int = abs(mm)
|
|
314
|
+
return [
|
|
315
|
+
(-1j * (-1) ** am / sqrt2, am),
|
|
316
|
+
(1j / sqrt2, -am),
|
|
317
|
+
]
|
|
318
|
+
|
|
319
|
+
# The dipole operator r_q in terms of complex Y_1^mu:
|
|
320
|
+
# r_q is proportional to Y_1^q(complex)
|
|
321
|
+
# We need the complex m-value for the dipole component
|
|
322
|
+
# Convention: q=-1 -> m_dip=+1 (y), q=0 -> m_dip=0 (z), q=+1 -> m_dip=-1 (x)
|
|
323
|
+
# Actually, using the standard convention:
|
|
324
|
+
# x = sqrt(4pi/3) * Y_1^{-1}(real)
|
|
325
|
+
# y = sqrt(4pi/3) * Y_1^{+1}(real) ... but let's use complex:
|
|
326
|
+
# r_{-1} = sqrt(2pi/3) (x - iy) / sqrt(2) ~ Y_1^{-1}(complex)
|
|
327
|
+
# r_0 = sqrt(4pi/3) z ~ Y_1^0(complex)
|
|
328
|
+
# r_{+1} = -sqrt(2pi/3) (x + iy) / sqrt(2) ~ Y_1^{+1}(complex)
|
|
329
|
+
#
|
|
330
|
+
# For the real dipole operator r_q (q index for the 3 Cartesian components
|
|
331
|
+
# mapped to real spherical harmonics of l=1):
|
|
332
|
+
# r_q(real) corresponds to Y_1^q(real)
|
|
333
|
+
# Transform Y_1^q(real) to complex basis:
|
|
334
|
+
dip_coeffs: list[tuple[complex, int]] = _real_to_complex_coeffs(1, q)
|
|
335
|
+
|
|
336
|
+
# Coefficients for initial state Y_l^m(real)
|
|
337
|
+
init_coeffs: list[tuple[complex, int]] = _real_to_complex_coeffs(l, m)
|
|
338
|
+
# Coefficients for final state Y_{l'}^{m'}(real)
|
|
339
|
+
final_coeffs: list[tuple[complex, int]] = _real_to_complex_coeffs(lp, mp)
|
|
340
|
+
|
|
341
|
+
# The real Gaunt integral is:
|
|
342
|
+
# G_real = sum_{mu, nu, rho} conj(U_final(mp,rho)) * U_dip(q,nu) * U_init(m,mu)
|
|
343
|
+
# * integral Y_{lp}^{rho*} Y_1^nu Y_l^mu d Omega
|
|
344
|
+
# where the integral is the complex Gaunt coefficient.
|
|
345
|
+
total: complex = 0.0 + 0.0j
|
|
346
|
+
for c_init, mu in init_coeffs:
|
|
347
|
+
for c_dip, nu in dip_coeffs:
|
|
348
|
+
for c_final, rho in final_coeffs:
|
|
349
|
+
# integral of Y_{lp}^{rho*} * Y_1^nu * Y_l^mu
|
|
350
|
+
# = (-1)^rho * integral Y_{lp}^{-rho} Y_1^nu Y_l^mu
|
|
351
|
+
# Using our _complex_gaunt which computes
|
|
352
|
+
# integral Y_{l1}^{m1} Y_{l2}^{m2} Y_{l3}^{m3}:
|
|
353
|
+
cg: float = _complex_gaunt(lp, -rho, 1, nu, l, mu)
|
|
354
|
+
coeff: complex = (
|
|
355
|
+
complex(c_final).conjugate()
|
|
356
|
+
* complex(c_dip)
|
|
357
|
+
* complex(c_init)
|
|
358
|
+
)
|
|
359
|
+
total += coeff * (-1) ** rho * cg
|
|
360
|
+
|
|
361
|
+
result: float = total.real
|
|
362
|
+
if abs(total.imag) > 1e-12:
|
|
363
|
+
msg: str = f"Imaginary part {total.imag} in real Gaunt coefficient"
|
|
364
|
+
raise ValueError(msg)
|
|
365
|
+
return result
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def build_gaunt_table(
|
|
369
|
+
l_max: int = 4,
|
|
370
|
+
) -> Float[Array, "L_src M_src 3 L_dst M_dst"]:
|
|
371
|
+
r"""Build the dipole Gaunt coefficient lookup table.
|
|
372
|
+
|
|
373
|
+
Extended Summary
|
|
374
|
+
----------------
|
|
375
|
+
Precomputes every non-zero real Gaunt coefficient for electric
|
|
376
|
+
dipole transitions up to angular momentum ``l_max``. The table
|
|
377
|
+
is stored as a dense 5-D NumPy array and then converted to a
|
|
378
|
+
JAX array for O(1) lookup during forward-model evaluation.
|
|
379
|
+
|
|
380
|
+
The five axes correspond to:
|
|
381
|
+
|
|
382
|
+
- **axis 0** (``l``): initial angular momentum, size ``l_max + 1``.
|
|
383
|
+
- **axis 1** (``m + l_max``): offset initial magnetic quantum number,
|
|
384
|
+
size ``2 * l_max + 1``, so that m = -l_max maps to index 0.
|
|
385
|
+
- **axis 2** (``q + 1``): dipole component index, size 3, for
|
|
386
|
+
q in {-1, 0, +1}.
|
|
387
|
+
- **axis 3** (``lp``): final angular momentum, size ``l_max + 2``
|
|
388
|
+
(because the dipole operator can promote l to l + 1).
|
|
389
|
+
- **axis 4** (``mp + l_max``): offset final magnetic quantum number,
|
|
390
|
+
size ``2 * (l_max + 1) + 1``.
|
|
391
|
+
|
|
392
|
+
The construction loops over all valid quantum number combinations
|
|
393
|
+
respecting the dipole selection rules :math:`l' = l \pm 1` and
|
|
394
|
+
calls `_real_gaunt_dipole` for each. Zero entries (forbidden
|
|
395
|
+
transitions) are left at their initialized value.
|
|
396
|
+
|
|
397
|
+
The table is indexed as ``GAUNT_TABLE[l, m + l_max, q + 1, lp, mp + l_max]``
|
|
398
|
+
where q in {-1, 0, +1} indexes the three dipole components.
|
|
399
|
+
|
|
400
|
+
Parameters
|
|
401
|
+
----------
|
|
402
|
+
l_max : int
|
|
403
|
+
Maximum angular momentum. Default 4 (s through g).
|
|
404
|
+
|
|
405
|
+
Returns
|
|
406
|
+
-------
|
|
407
|
+
table : Float[Array, "..."]
|
|
408
|
+
Dense array of Gaunt coefficients.
|
|
409
|
+
Shape: ``(l_max+1, 2*l_max+1, 3, l_max+2, 2*(l_max+1)+1)``.
|
|
410
|
+
|
|
411
|
+
Notes
|
|
412
|
+
-----
|
|
413
|
+
This function uses pure Python / NumPy (not JAX-traced) because it
|
|
414
|
+
runs once at module import time. The result is frozen as a JAX
|
|
415
|
+
array constant, so it does not appear as a trainable parameter in
|
|
416
|
+
any gradient computation.
|
|
417
|
+
"""
|
|
418
|
+
l_src_dim: int = l_max + 1
|
|
419
|
+
m_src_dim: int = 2 * l_max + 1
|
|
420
|
+
q_dim: int = 3
|
|
421
|
+
l_dst_dim: int = l_max + 2
|
|
422
|
+
m_dst_dim: int = 2 * (l_max + 1) + 1
|
|
423
|
+
|
|
424
|
+
table: np.ndarray = np.zeros(
|
|
425
|
+
(l_src_dim, m_src_dim, q_dim, l_dst_dim, m_dst_dim),
|
|
426
|
+
dtype=np.float64,
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
for l in range(l_src_dim):
|
|
430
|
+
for m in range(-l, l + 1):
|
|
431
|
+
for q in (-1, 0, 1):
|
|
432
|
+
for lp in (l - 1, l + 1):
|
|
433
|
+
if lp < 0 or lp >= l_dst_dim:
|
|
434
|
+
continue
|
|
435
|
+
for mp in range(-lp, lp + 1):
|
|
436
|
+
val: float = _real_gaunt_dipole(l, m, lp, mp, q)
|
|
437
|
+
table[l, m + l_max, q + 1, lp, mp + l_max] = val
|
|
438
|
+
|
|
439
|
+
return jnp.asarray(table, dtype=jnp.float64)
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
GAUNT_TABLE: Float[Array, "..."] = build_gaunt_table(l_max=4)
|
|
443
|
+
"""Module-level precomputed Gaunt coefficient table for l_max=4."""
|
|
444
|
+
|
|
445
|
+
L_MAX: int = 4
|
|
446
|
+
"""Maximum angular momentum supported by the precomputed table."""
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def gaunt_lookup(l: int, m: int, q: int, lp: int, mp: int) -> float:
|
|
450
|
+
r"""Look up a single Gaunt coefficient from the precomputed table.
|
|
451
|
+
|
|
452
|
+
Extended Summary
|
|
453
|
+
----------------
|
|
454
|
+
Provides a convenience accessor for the module-level ``GAUNT_TABLE``
|
|
455
|
+
array, converting the physical quantum numbers (l, m, q, l', m')
|
|
456
|
+
into the offset indices used by the dense storage layout. The
|
|
457
|
+
index mapping is:
|
|
458
|
+
|
|
459
|
+
- ``l`` indexes axis 0 directly.
|
|
460
|
+
- ``m + L_MAX`` offsets the magnetic quantum number to a
|
|
461
|
+
non-negative index on axis 1.
|
|
462
|
+
- ``q + 1`` maps q in {-1, 0, +1} to indices {0, 1, 2} on axis 2.
|
|
463
|
+
- ``lp`` indexes axis 3 directly.
|
|
464
|
+
- ``mp + L_MAX`` offsets m' on axis 4.
|
|
465
|
+
|
|
466
|
+
The returned value is cast to a Python float for use in
|
|
467
|
+
non-JAX contexts. For JAX-traced code, direct indexing into
|
|
468
|
+
``GAUNT_TABLE`` is preferred to avoid Python-level overhead.
|
|
469
|
+
|
|
470
|
+
Parameters
|
|
471
|
+
----------
|
|
472
|
+
l : int
|
|
473
|
+
Initial state angular momentum.
|
|
474
|
+
m : int
|
|
475
|
+
Initial state magnetic quantum number.
|
|
476
|
+
q : int
|
|
477
|
+
Dipole component (-1, 0, or +1).
|
|
478
|
+
lp : int
|
|
479
|
+
Final state angular momentum.
|
|
480
|
+
mp : int
|
|
481
|
+
Final state magnetic quantum number.
|
|
482
|
+
|
|
483
|
+
Returns
|
|
484
|
+
-------
|
|
485
|
+
coeff : float
|
|
486
|
+
The Gaunt coefficient.
|
|
487
|
+
"""
|
|
488
|
+
return float(GAUNT_TABLE[l, m + L_MAX, q + 1, lp, mp + L_MAX])
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
__all__: list[str] = [
|
|
492
|
+
"GAUNT_TABLE",
|
|
493
|
+
"L_MAX",
|
|
494
|
+
"build_gaunt_table",
|
|
495
|
+
"gaunt_lookup",
|
|
496
|
+
]
|