torchspin 0.3.0__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.
- torchspin/__init__.py +363 -0
- torchspin/_cardamom_istos.py +327 -0
- torchspin/_cardamom_propagatedm.py +413 -0
- torchspin/_cardamom_utils.py +113 -0
- torchspin/_compile.py +100 -0
- torchspin/_linalg.py +140 -0
- torchspin/angmom.py +742 -0
- torchspin/autograd.py +557 -0
- torchspin/autoguess.py +342 -0
- torchspin/batch.py +225 -0
- torchspin/blochsteady.py +274 -0
- torchspin/cardamom.py +552 -0
- torchspin/chili.py +932 -0
- torchspin/chili_sle.py +1233 -0
- torchspin/constants.py +88 -0
- torchspin/convspec.py +148 -0
- torchspin/ctafft.py +91 -0
- torchspin/curry.py +241 -0
- torchspin/data/FourierSeriesCoefficients.txt +89 -0
- torchspin/data/GaussianCascadeCoefficients.txt +56 -0
- torchspin/data/isotopedata.txt +416 -0
- torchspin/data/spacegroups.txt +637 -0
- torchspin/dataproc.py +361 -0
- torchspin/dipbackground.py +69 -0
- torchspin/dipkernel.py +102 -0
- torchspin/diptensor.py +138 -0
- torchspin/endorfrq.py +198 -0
- torchspin/endorfrq_perturb.py +302 -0
- torchspin/eprload.py +1538 -0
- torchspin/eprsave.py +165 -0
- torchspin/esfit.py +2427 -0
- torchspin/evolve.py +274 -0
- torchspin/ewrls.py +124 -0
- torchspin/excitation.py +107 -0
- torchspin/exciteprofile.py +132 -0
- torchspin/experiment.py +323 -0
- torchspin/exponfit.py +177 -0
- torchspin/fastmotion.py +182 -0
- torchspin/fdaxis.py +57 -0
- torchspin/fitgui.py +350 -0
- torchspin/garlic.py +760 -0
- torchspin/ham.py +145 -0
- torchspin/ham_cf.py +115 -0
- torchspin/ham_ee.py +124 -0
- torchspin/ham_ez.py +119 -0
- torchspin/ham_ezho.py +403 -0
- torchspin/ham_hf.py +122 -0
- torchspin/ham_nn.py +108 -0
- torchspin/ham_nq.py +100 -0
- torchspin/ham_nz.py +136 -0
- torchspin/ham_oz.py +115 -0
- torchspin/ham_so.py +118 -0
- torchspin/ham_zf.py +254 -0
- torchspin/hamsymm.py +723 -0
- torchspin/initstate.py +158 -0
- torchspin/isotopologues.py +296 -0
- torchspin/levels.py +189 -0
- torchspin/levelsplot.py +363 -0
- torchspin/lineshape.py +715 -0
- torchspin/lpsvd.py +219 -0
- torchspin/makespec.py +87 -0
- torchspin/mdhmm.py +573 -0
- torchspin/mdload.py +788 -0
- torchspin/mdtraj2oripot.py +90 -0
- torchspin/ml.py +377 -0
- torchspin/mlpsvd.py +297 -0
- torchspin/nucdata.py +310 -0
- torchspin/nucfrq2d.py +228 -0
- torchspin/orca2torchspin.py +1198 -0
- torchspin/ordering.py +78 -0
- torchspin/oripotentialplot.py +246 -0
- torchspin/orisel.py +176 -0
- torchspin/pepper.py +2019 -0
- torchspin/pepper_autograd.py +613 -0
- torchspin/photoselect.py +174 -0
- torchspin/plegendre.py +131 -0
- torchspin/propint.py +127 -0
- torchspin/pulse.py +652 -0
- torchspin/py.typed +0 -0
- torchspin/rapidscan2spc.py +101 -0
- torchspin/resfields.py +337 -0
- torchspin/resfields_batch.py +446 -0
- torchspin/resfields_eig.py +250 -0
- torchspin/resfields_perturb.py +706 -0
- torchspin/resfreqs_matrix.py +344 -0
- torchspin/resfreqs_perturb.py +299 -0
- torchspin/resonator.py +310 -0
- torchspin/resonatorprofile.py +85 -0
- torchspin/rfmixer.py +147 -0
- torchspin/rotations.py +87 -0
- torchspin/rotutils.py +880 -0
- torchspin/saffron.py +1523 -0
- torchspin/saffron_pathways.py +92 -0
- torchspin/saffron_peaks.py +580 -0
- torchspin/saffron_thyme.py +472 -0
- torchspin/salt.py +680 -0
- torchspin/sigeq.py +71 -0
- torchspin/signalprocessing.py +226 -0
- torchspin/sitetransforms.py +254 -0
- torchspin/sphgrid.py +470 -0
- torchspin/spidyan.py +1508 -0
- torchspin/spinladder.py +161 -0
- torchspin/spinops.py +265 -0
- torchspin/spinsystem.py +1427 -0
- torchspin/stackplot.py +167 -0
- torchspin/stev.py +283 -0
- torchspin/stochtraj_diffusion.py +494 -0
- torchspin/stochtraj_jump.py +195 -0
- torchspin/strainwidth.py +727 -0
- torchspin/transmitter.py +89 -0
- torchspin/utils.py +605 -0
- torchspin-0.3.0.dist-info/METADATA +267 -0
- torchspin-0.3.0.dist-info/RECORD +116 -0
- torchspin-0.3.0.dist-info/WHEEL +5 -0
- torchspin-0.3.0.dist-info/licenses/LICENSE.md +22 -0
- torchspin-0.3.0.dist-info/top_level.txt +1 -0
torchspin/angmom.py
ADDED
|
@@ -0,0 +1,742 @@
|
|
|
1
|
+
"""Angular momentum functions for torchspin.
|
|
2
|
+
|
|
3
|
+
Port of EasySpin's:
|
|
4
|
+
- ``wigner3j.m`` — Wigner 3-j symbol
|
|
5
|
+
- ``wignerd.m`` — Wigner rotation matrix D^J_{m1,m2}
|
|
6
|
+
- ``spherharm.m`` — spherical harmonics Y_L^M(theta, phi)
|
|
7
|
+
- ``clebschgordan.m``— Clebsch-Gordan coefficient
|
|
8
|
+
- ``cgmatrix.m`` — CG transformation matrix (uncoupled→coupled)
|
|
9
|
+
|
|
10
|
+
All functions follow EasySpin's conventions.
|
|
11
|
+
|
|
12
|
+
Notation
|
|
13
|
+
--------
|
|
14
|
+
* Wigner 3-j symbols: ``wigner3j(j1,j2,j3,m1,m2,m3)``
|
|
15
|
+
* Wigner D-matrix: ``wignerd(J, alpha, beta, gamma)``
|
|
16
|
+
- Default phase convention '-': D = exp(-i·m1·α) d(β) exp(-i·m2·γ)
|
|
17
|
+
- Basis ordered m = +J, +J-1, ..., -J (same as EasySpin)
|
|
18
|
+
* Spherical harmonics: ``spherharm(L, M, theta, phi)``
|
|
19
|
+
- theta = polar angle (from +z), phi = azimuthal (from +x)
|
|
20
|
+
- Condon-Shortley phase included for complex harmonics
|
|
21
|
+
* Clebsch-Gordan: ``clebschgordan(j1,j2,j,m1,m2,m)``
|
|
22
|
+
- Related to 3j by: CG = (-1)^(j1-j2+m) * sqrt(2j+1) * W3j(j1,j2,j; m1,m2,-m)
|
|
23
|
+
"""
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import cmath
|
|
27
|
+
import math
|
|
28
|
+
from typing import Optional, Union
|
|
29
|
+
|
|
30
|
+
import numpy as np
|
|
31
|
+
import scipy.special as sps
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
'wigner3j',
|
|
36
|
+
'wigner6j',
|
|
37
|
+
'wignerd',
|
|
38
|
+
'spherharm',
|
|
39
|
+
'clebschgordan',
|
|
40
|
+
'cgmatrix',
|
|
41
|
+
'isto',
|
|
42
|
+
'isto2stev',
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
# ---------------------------------------------------------------------------
|
|
46
|
+
# Internal helpers
|
|
47
|
+
# ---------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
def _logfact(n: int) -> float:
|
|
50
|
+
"""Natural log of n! (n must be a non-negative integer)."""
|
|
51
|
+
return float(sps.gammaln(n + 1))
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _istriangle(a, b, c) -> bool:
|
|
55
|
+
return (a + b >= c) and (b + c >= a) and (c + a >= b)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _ishalfint(x) -> bool:
|
|
59
|
+
return (2 * x) == int(2 * x)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# ---------------------------------------------------------------------------
|
|
63
|
+
# wigner3j
|
|
64
|
+
# ---------------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
def wigner3j(j1, j2, j3, m1, m2, m3) -> float:
|
|
67
|
+
"""Wigner 3-j symbol.
|
|
68
|
+
|
|
69
|
+
Parameters
|
|
70
|
+
----------
|
|
71
|
+
j1, j2, j3 : int or half-int
|
|
72
|
+
Angular momentum quantum numbers (non-negative, integer or half-integer).
|
|
73
|
+
m1, m2, m3 : int or half-int
|
|
74
|
+
Magnetic quantum numbers with ``|mi| <= ji``.
|
|
75
|
+
|
|
76
|
+
Returns
|
|
77
|
+
-------
|
|
78
|
+
float
|
|
79
|
+
Value of the Wigner 3-j symbol.
|
|
80
|
+
|
|
81
|
+
Notes
|
|
82
|
+
-----
|
|
83
|
+
Uses the Racah formula with log-factorials for numerical stability
|
|
84
|
+
(accurate for j up to ~100). Returns 0 for any selection-rule violation.
|
|
85
|
+
|
|
86
|
+
Selection rules (return 0 if violated)
|
|
87
|
+
- m1 + m2 + m3 = 0
|
|
88
|
+
- Triangle inequality for (j1, j2, j3)
|
|
89
|
+
- |mi| <= ji
|
|
90
|
+
- 2*(ji + mi) is integer (half-integer or integer quantum numbers only)
|
|
91
|
+
"""
|
|
92
|
+
# Coerce to float
|
|
93
|
+
j1, j2, j3 = float(j1), float(j2), float(j3)
|
|
94
|
+
m1, m2, m3 = float(m1), float(m2), float(m3)
|
|
95
|
+
|
|
96
|
+
# Selection rules
|
|
97
|
+
if abs(m1 + m2 + m3) > 1e-10:
|
|
98
|
+
return 0.0
|
|
99
|
+
if not _istriangle(j1, j2, j3):
|
|
100
|
+
return 0.0
|
|
101
|
+
if abs(m1) > j1 + 1e-10 or abs(m2) > j2 + 1e-10 or abs(m3) > j3 + 1e-10:
|
|
102
|
+
return 0.0
|
|
103
|
+
for j, m in ((j1, m1), (j2, m2), (j3, m3)):
|
|
104
|
+
if not _ishalfint(j) or not _ishalfint(m):
|
|
105
|
+
return 0.0
|
|
106
|
+
if not _ishalfint(j + m):
|
|
107
|
+
return 0.0
|
|
108
|
+
|
|
109
|
+
# Triangle coefficient (log scale)
|
|
110
|
+
tri_ln = (
|
|
111
|
+
_logfact(int(round(j1 + j2 - j3))) +
|
|
112
|
+
_logfact(int(round(j1 - j2 + j3))) +
|
|
113
|
+
_logfact(int(round(-j1 + j2 + j3))) -
|
|
114
|
+
_logfact(int(round(j1 + j2 + j3 + 1)))
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# Pre-factor (log scale)
|
|
118
|
+
pre_ln = (
|
|
119
|
+
_logfact(int(round(j1 + m1))) + _logfact(int(round(j1 - m1))) +
|
|
120
|
+
_logfact(int(round(j2 + m2))) + _logfact(int(round(j2 - m2))) +
|
|
121
|
+
_logfact(int(round(j3 + m3))) + _logfact(int(round(j3 - m3)))
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
prefactor_ln = (tri_ln + pre_ln) / 2.0
|
|
125
|
+
|
|
126
|
+
# Racah summation
|
|
127
|
+
# t runs over values making all factorials non-negative
|
|
128
|
+
tmin = max(0,
|
|
129
|
+
int(round(j2 - m1 - j3)),
|
|
130
|
+
int(round(j1 + m2 - j3)))
|
|
131
|
+
tmax = min(int(round(j1 + j2 - j3)),
|
|
132
|
+
int(round(j1 - m1)),
|
|
133
|
+
int(round(j2 + m2)))
|
|
134
|
+
|
|
135
|
+
if tmin > tmax:
|
|
136
|
+
return 0.0
|
|
137
|
+
|
|
138
|
+
total = 0.0
|
|
139
|
+
for t in range(tmin, tmax + 1):
|
|
140
|
+
sign = (-1) ** t
|
|
141
|
+
term_ln = -(
|
|
142
|
+
_logfact(t) +
|
|
143
|
+
_logfact(int(round(j1 + j2 - j3 - t))) +
|
|
144
|
+
_logfact(int(round(j1 - m1 - t))) +
|
|
145
|
+
_logfact(int(round(j2 + m2 - t))) +
|
|
146
|
+
_logfact(int(round(j3 - j2 + m1 + t))) +
|
|
147
|
+
_logfact(int(round(j3 - j1 - m2 + t)))
|
|
148
|
+
)
|
|
149
|
+
total += sign * math.exp(prefactor_ln + term_ln)
|
|
150
|
+
|
|
151
|
+
overall_sign = (-1) ** int(round(j1 - j2 - m3))
|
|
152
|
+
return overall_sign * total
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
# ---------------------------------------------------------------------------
|
|
156
|
+
# wignerd
|
|
157
|
+
# ---------------------------------------------------------------------------
|
|
158
|
+
|
|
159
|
+
def wignerd(J, alpha_or_beta, beta=None, gamma=None,
|
|
160
|
+
phase: str = '-') -> Union[complex, np.ndarray]:
|
|
161
|
+
"""Wigner rotation matrix D^J (or reduced d^J).
|
|
162
|
+
|
|
163
|
+
Parameters
|
|
164
|
+
----------
|
|
165
|
+
J : float or sequence [J, m1, m2]
|
|
166
|
+
Angular momentum quantum number. If a 3-element sequence [J, m1, m2],
|
|
167
|
+
only the single element D^J_{m1,m2} is returned.
|
|
168
|
+
alpha_or_beta : float
|
|
169
|
+
If ``beta`` is ``None``: the polar angle β only (α=γ=0, returns real d).
|
|
170
|
+
Otherwise: the first Euler angle α.
|
|
171
|
+
beta : float, optional
|
|
172
|
+
Middle Euler angle β.
|
|
173
|
+
gamma : float, optional
|
|
174
|
+
Last Euler angle γ.
|
|
175
|
+
phase : {'-', '+'}, optional
|
|
176
|
+
Sign convention. Default ``'-'``:
|
|
177
|
+
``D = exp(-i·m1·α) · d(β) · exp(-i·m2·γ)`` (Brink/Satchler, Varshalovich).
|
|
178
|
+
``'+'``: ``D = exp(+i·m1·α) · d(β) · exp(+i·m2·γ)`` (Edmonds).
|
|
179
|
+
|
|
180
|
+
Returns
|
|
181
|
+
-------
|
|
182
|
+
ndarray, shape (2J+1, 2J+1) or complex scalar
|
|
183
|
+
Full matrix (or single element). Basis ordered m = +J, ..., -J.
|
|
184
|
+
When called with β only (α=γ=0), returns a real ndarray.
|
|
185
|
+
|
|
186
|
+
Notes
|
|
187
|
+
-----
|
|
188
|
+
The reduced Wigner d-matrix element is computed via the Wigner sum formula
|
|
189
|
+
using log-factorials (exact for integer and half-integer J).
|
|
190
|
+
"""
|
|
191
|
+
# Parse first argument: scalar J or [J, m1, m2]
|
|
192
|
+
J_arg = np.asarray(J, dtype=float).ravel()
|
|
193
|
+
if J_arg.size == 3:
|
|
194
|
+
Jval, m1_req, m2_req = J_arg
|
|
195
|
+
single_element = True
|
|
196
|
+
elif J_arg.size == 1:
|
|
197
|
+
Jval = float(J_arg[0])
|
|
198
|
+
single_element = False
|
|
199
|
+
m1_req = m2_req = None
|
|
200
|
+
else:
|
|
201
|
+
raise ValueError("First argument must be J (scalar) or [J, m1, m2].")
|
|
202
|
+
|
|
203
|
+
beta_only = beta is None
|
|
204
|
+
if beta_only:
|
|
205
|
+
alpha_val = 0.0
|
|
206
|
+
beta_val = float(alpha_or_beta)
|
|
207
|
+
gamma_val = 0.0
|
|
208
|
+
else:
|
|
209
|
+
alpha_val = float(alpha_or_beta)
|
|
210
|
+
beta_val = float(beta)
|
|
211
|
+
gamma_val = float(gamma)
|
|
212
|
+
|
|
213
|
+
if phase not in ('+', '-'):
|
|
214
|
+
raise ValueError("phase must be '+' or '-'.")
|
|
215
|
+
sign = -1.0 if phase == '-' else +1.0
|
|
216
|
+
|
|
217
|
+
# Basis: m = +J, J-1, ..., -J
|
|
218
|
+
nJ = int(round(2 * Jval)) + 1
|
|
219
|
+
ms = np.arange(Jval, -Jval - 0.5, -1) # +J ... -J
|
|
220
|
+
|
|
221
|
+
def _d_element(m1: float, m2: float) -> float:
|
|
222
|
+
"""Reduced Wigner d^J_{m1,m2}(beta_val)."""
|
|
223
|
+
cb2 = math.cos(beta_val / 2)
|
|
224
|
+
sb2 = math.sin(beta_val / 2)
|
|
225
|
+
|
|
226
|
+
tmin = max(0, int(round(m2 - m1)))
|
|
227
|
+
tmax = min(int(round(Jval + m2)), int(round(Jval - m1)))
|
|
228
|
+
|
|
229
|
+
if tmin > tmax:
|
|
230
|
+
return 0.0
|
|
231
|
+
|
|
232
|
+
# Pre-factor ln: sqrt[(J+m1)!(J-m1)!(J+m2)!(J-m2)!]
|
|
233
|
+
pre_ln = 0.5 * (
|
|
234
|
+
_logfact(int(round(Jval + m1))) +
|
|
235
|
+
_logfact(int(round(Jval - m1))) +
|
|
236
|
+
_logfact(int(round(Jval + m2))) +
|
|
237
|
+
_logfact(int(round(Jval - m2)))
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
total = 0.0
|
|
241
|
+
for t in range(tmin, tmax + 1):
|
|
242
|
+
exp_cos = int(round(2 * Jval - 2 * t + m2 - m1))
|
|
243
|
+
exp_sin = int(round(2 * t + m1 - m2))
|
|
244
|
+
|
|
245
|
+
if exp_cos < 0 or exp_sin < 0:
|
|
246
|
+
continue
|
|
247
|
+
|
|
248
|
+
# Avoid log(0) for sin/cos at 0 or pi
|
|
249
|
+
if cb2 == 0.0 and exp_cos > 0:
|
|
250
|
+
continue
|
|
251
|
+
if sb2 == 0.0 and exp_sin > 0:
|
|
252
|
+
continue
|
|
253
|
+
|
|
254
|
+
# Compute log|term|
|
|
255
|
+
denom_ln = (
|
|
256
|
+
_logfact(int(round(Jval - m1 - t))) +
|
|
257
|
+
_logfact(t) +
|
|
258
|
+
_logfact(int(round(t + m1 - m2))) +
|
|
259
|
+
_logfact(int(round(Jval + m2 - t)))
|
|
260
|
+
)
|
|
261
|
+
ln_abs = pre_ln - denom_ln
|
|
262
|
+
if exp_cos > 0:
|
|
263
|
+
ln_abs += exp_cos * math.log(abs(cb2))
|
|
264
|
+
if exp_sin > 0:
|
|
265
|
+
ln_abs += exp_sin * math.log(abs(sb2))
|
|
266
|
+
|
|
267
|
+
# Signs: (-1)^(m2-m1+t) from cos^a * sin^b convention
|
|
268
|
+
sign_t = (-1) ** int(round(m2 - m1 + t))
|
|
269
|
+
# Account for negative values of cb2, sb2 (sin always ≥ 0 for beta in [0,pi])
|
|
270
|
+
if cb2 < 0 and exp_cos % 2 == 1:
|
|
271
|
+
sign_t *= -1
|
|
272
|
+
if sb2 < 0 and exp_sin % 2 == 1:
|
|
273
|
+
sign_t *= -1
|
|
274
|
+
|
|
275
|
+
total += sign_t * math.exp(ln_abs)
|
|
276
|
+
|
|
277
|
+
return total
|
|
278
|
+
|
|
279
|
+
if single_element:
|
|
280
|
+
m1v = float(m1_req); m2v = float(m2_req)
|
|
281
|
+
d_val = _d_element(m1v, m2v)
|
|
282
|
+
if beta_only:
|
|
283
|
+
return d_val
|
|
284
|
+
# Apply Euler angles with phase sign
|
|
285
|
+
return _cexp(sign * 1j * m1v * alpha_val) * d_val * _cexp(sign * 1j * m2v * gamma_val)
|
|
286
|
+
|
|
287
|
+
# Build full matrix
|
|
288
|
+
D = np.zeros((nJ, nJ), dtype=complex if not beta_only else float)
|
|
289
|
+
for i, m1v in enumerate(ms):
|
|
290
|
+
for j, m2v in enumerate(ms):
|
|
291
|
+
d_val = _d_element(m1v, m2v)
|
|
292
|
+
if beta_only:
|
|
293
|
+
D[i, j] = d_val
|
|
294
|
+
else:
|
|
295
|
+
D[i, j] = (_cexp(sign * 1j * m1v * alpha_val) *
|
|
296
|
+
d_val *
|
|
297
|
+
_cexp(sign * 1j * m2v * gamma_val))
|
|
298
|
+
|
|
299
|
+
return D
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def _cexp(x: complex) -> complex:
|
|
303
|
+
"""Complex exponential exp(x) for pure-imaginary x = i*theta."""
|
|
304
|
+
return cmath.exp(x)
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
# ---------------------------------------------------------------------------
|
|
308
|
+
# spherharm
|
|
309
|
+
# ---------------------------------------------------------------------------
|
|
310
|
+
|
|
311
|
+
def spherharm(L: int, M: int, theta, phi, kind: str = 'c') -> np.ndarray:
|
|
312
|
+
"""Spherical harmonic Y_L^M(theta, phi).
|
|
313
|
+
|
|
314
|
+
Parameters
|
|
315
|
+
----------
|
|
316
|
+
L : int
|
|
317
|
+
Degree (non-negative integer).
|
|
318
|
+
M : int
|
|
319
|
+
Order (``-L <= M <= L``).
|
|
320
|
+
theta : float or array_like
|
|
321
|
+
Polar angle (colatitude from +z), radians.
|
|
322
|
+
phi : float or array_like
|
|
323
|
+
Azimuthal angle (longitude from +x), radians.
|
|
324
|
+
kind : {'c', 'r'}, optional
|
|
325
|
+
``'c'`` (default): complex spherical harmonics with Condon-Shortley phase.
|
|
326
|
+
``'r'``: real spherical harmonics.
|
|
327
|
+
|
|
328
|
+
Returns
|
|
329
|
+
-------
|
|
330
|
+
ndarray or complex
|
|
331
|
+
Spherical harmonic value(s). Shape matches ``theta`` / ``phi``.
|
|
332
|
+
|
|
333
|
+
Notes
|
|
334
|
+
-----
|
|
335
|
+
Uses ``scipy.special.sph_harm`` for complex harmonics.
|
|
336
|
+
Argument order follows EasySpin: ``spherharm(L, M, theta, phi)``
|
|
337
|
+
(theta = polar, phi = azimuthal), which is the reverse of scipy's
|
|
338
|
+
``sph_harm(M, L, phi, theta)``.
|
|
339
|
+
|
|
340
|
+
Real harmonics follow Blanco et al. 1997, Table 1 (non-negative near θ=0, φ=0).
|
|
341
|
+
"""
|
|
342
|
+
L, M = int(L), int(M)
|
|
343
|
+
theta = np.asarray(theta, dtype=float)
|
|
344
|
+
phi = np.asarray(phi, dtype=float)
|
|
345
|
+
if theta.shape != phi.shape:
|
|
346
|
+
raise ValueError("theta and phi must have the same shape.")
|
|
347
|
+
|
|
348
|
+
if kind == 'c':
|
|
349
|
+
# scipy.special.sph_harm_y(n, m, theta, phi) — EasySpin arg order matches
|
|
350
|
+
return sps.sph_harm_y(L, M, theta, phi)
|
|
351
|
+
elif kind == 'r':
|
|
352
|
+
# Real spherical harmonics (Condon-Shortley-free, non-negative near 0)
|
|
353
|
+
absM = abs(M)
|
|
354
|
+
# Normalization
|
|
355
|
+
p = math.factorial(L - absM) / math.factorial(L + absM)
|
|
356
|
+
N = math.sqrt((2 * L + 1) / (4 * math.pi) * p)
|
|
357
|
+
P = sps.lpmv(absM, L, np.cos(theta))
|
|
358
|
+
if M > 0:
|
|
359
|
+
return math.sqrt(2) * N * P * np.cos(M * phi)
|
|
360
|
+
elif M < 0:
|
|
361
|
+
return math.sqrt(2) * N * P * np.sin(absM * phi)
|
|
362
|
+
else:
|
|
363
|
+
return N * P
|
|
364
|
+
else:
|
|
365
|
+
raise ValueError("kind must be 'c' (complex) or 'r' (real).")
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
# ---------------------------------------------------------------------------
|
|
369
|
+
# clebschgordan
|
|
370
|
+
# ---------------------------------------------------------------------------
|
|
371
|
+
|
|
372
|
+
def clebschgordan(j1, j2, j, m1, m2, m) -> float:
|
|
373
|
+
"""Clebsch-Gordan coefficient ``<j1,m1; j2,m2 | j,m>``.
|
|
374
|
+
|
|
375
|
+
Parameters
|
|
376
|
+
----------
|
|
377
|
+
j1, j2 : float
|
|
378
|
+
Angular momenta of the two subsystems.
|
|
379
|
+
j : float
|
|
380
|
+
Total angular momentum.
|
|
381
|
+
m1, m2, m : float
|
|
382
|
+
Magnetic quantum numbers.
|
|
383
|
+
|
|
384
|
+
Returns
|
|
385
|
+
-------
|
|
386
|
+
float
|
|
387
|
+
Clebsch-Gordan coefficient.
|
|
388
|
+
|
|
389
|
+
Notes
|
|
390
|
+
-----
|
|
391
|
+
Related to the Wigner 3-j symbol by:
|
|
392
|
+
``CG = (-1)^(j1-j2+m) * sqrt(2j+1) * W3j(j1,j2,j; m1,m2,-m)``
|
|
393
|
+
(Brink & Satchler, Angular Momentum, 3rd ed., eq. 3.3).
|
|
394
|
+
"""
|
|
395
|
+
j1, j2, j = float(j1), float(j2), float(j)
|
|
396
|
+
m1, m2, m = float(m1), float(m2), float(m)
|
|
397
|
+
return ((-1) ** int(round(j1 - j2 + m)) *
|
|
398
|
+
math.sqrt(2 * j + 1) *
|
|
399
|
+
wigner3j(j1, j2, j, m1, m2, -m))
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
# ---------------------------------------------------------------------------
|
|
403
|
+
# cgmatrix
|
|
404
|
+
# ---------------------------------------------------------------------------
|
|
405
|
+
|
|
406
|
+
def cgmatrix(S1: float, S2: float, Stot=None) -> tuple:
|
|
407
|
+
"""Transformation matrix between uncoupled and coupled spin representations.
|
|
408
|
+
|
|
409
|
+
Parameters
|
|
410
|
+
----------
|
|
411
|
+
S1, S2 : float
|
|
412
|
+
Spin quantum numbers (1/2, 1, 3/2, 2, …).
|
|
413
|
+
Stot : float or None, optional
|
|
414
|
+
If given, return only coupled states with total spin ``Stot``.
|
|
415
|
+
|
|
416
|
+
Returns
|
|
417
|
+
-------
|
|
418
|
+
U2C : ndarray, shape (n, n)
|
|
419
|
+
Transformation matrix with elements ``<Stot,mStot|mS1,mS2>``.
|
|
420
|
+
Apply as: ``psi_c = U2C @ psi_u`` (uncoupled→coupled).
|
|
421
|
+
Smtot : ndarray, shape (n, 2)
|
|
422
|
+
``[Stot, mStot]`` for each coupled basis state.
|
|
423
|
+
m12 : ndarray, shape (n, 2)
|
|
424
|
+
``[mS1, mS2]`` for each uncoupled basis state.
|
|
425
|
+
|
|
426
|
+
Notes
|
|
427
|
+
-----
|
|
428
|
+
Basis ordering matches EasySpin:
|
|
429
|
+
|
|
430
|
+
* Uncoupled: descending mS1 (outer), descending mS2 (inner).
|
|
431
|
+
* Coupled: descending Stot (outer), descending mStot (inner).
|
|
432
|
+
|
|
433
|
+
Uses :func:`clebschgordan` to compute each matrix element.
|
|
434
|
+
"""
|
|
435
|
+
S1, S2 = float(S1), float(S2)
|
|
436
|
+
|
|
437
|
+
if Stot is None:
|
|
438
|
+
Stot_list = list(np.arange(S1 + S2, abs(S1 - S2) - 0.5, -1))
|
|
439
|
+
else:
|
|
440
|
+
Stot = float(Stot)
|
|
441
|
+
if Stot > S1 + S2 or Stot < abs(S1 - S2):
|
|
442
|
+
raise ValueError(f"Stot={Stot} is outside the valid range "
|
|
443
|
+
f"[{abs(S1-S2)}, {S1+S2}].")
|
|
444
|
+
Stot_list = [Stot]
|
|
445
|
+
|
|
446
|
+
nCoupled = int(sum(2 * St + 1 for St in Stot_list))
|
|
447
|
+
nUncoupled = int(round((2 * S1 + 1) * (2 * S2 + 1)))
|
|
448
|
+
U2C = np.zeros((nCoupled, nUncoupled))
|
|
449
|
+
Smtot = np.zeros((nCoupled, 2))
|
|
450
|
+
m12 = np.zeros((nUncoupled, 2))
|
|
451
|
+
|
|
452
|
+
# Build uncoupled index → (mS1, mS2) mapping
|
|
453
|
+
iu = 0
|
|
454
|
+
for mS1 in np.arange(S1, -S1 - 0.5, -1):
|
|
455
|
+
for mS2 in np.arange(S2, -S2 - 0.5, -1):
|
|
456
|
+
m12[iu] = [mS1, mS2]
|
|
457
|
+
ic = 0
|
|
458
|
+
for St in Stot_list:
|
|
459
|
+
for mSt in np.arange(St, -St - 0.5, -1):
|
|
460
|
+
U2C[ic, iu] = clebschgordan(S1, S2, St, mS1, mS2, mSt)
|
|
461
|
+
if iu == 0:
|
|
462
|
+
Smtot[ic] = [St, mSt]
|
|
463
|
+
ic += 1
|
|
464
|
+
iu += 1
|
|
465
|
+
|
|
466
|
+
return U2C, Smtot, m12
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
# ---------------------------------------------------------------------------
|
|
470
|
+
# wigner6j
|
|
471
|
+
# ---------------------------------------------------------------------------
|
|
472
|
+
|
|
473
|
+
def wigner6j(j1, j2, j3, J1, J2, J3) -> float:
|
|
474
|
+
"""Wigner 6-j symbol.
|
|
475
|
+
|
|
476
|
+
Computes::
|
|
477
|
+
|
|
478
|
+
/ j1 j2 j3 \\
|
|
479
|
+
\\ J1 J2 J3 /
|
|
480
|
+
|
|
481
|
+
Parameters
|
|
482
|
+
----------
|
|
483
|
+
j1, j2, j3, J1, J2, J3 : int or half-int
|
|
484
|
+
Angular momentum quantum numbers.
|
|
485
|
+
|
|
486
|
+
Returns
|
|
487
|
+
-------
|
|
488
|
+
float
|
|
489
|
+
Value of the Wigner 6-j symbol (0 if any triangle rule is violated).
|
|
490
|
+
|
|
491
|
+
Notes
|
|
492
|
+
-----
|
|
493
|
+
Uses the Tuzun–Burkhardt–Secrest algorithm (CPC 112, 1998, Formula 80,
|
|
494
|
+
Method 2 — recursive binomial expansion). Accurate for typical EPR
|
|
495
|
+
angular momentum values.
|
|
496
|
+
|
|
497
|
+
Selection rules (return 0 if violated)
|
|
498
|
+
- Triangle inequality for (j1,j2,j3), (j1,J2,J3), (J1,j2,J3), (J1,J2,j3)
|
|
499
|
+
- All six arguments must be non-negative integers or half-integers
|
|
500
|
+
"""
|
|
501
|
+
j1, j2, j3 = float(j1), float(j2), float(j3)
|
|
502
|
+
J1, J2, J3 = float(J1), float(J2), float(J3)
|
|
503
|
+
|
|
504
|
+
# All must be half-integers (2*j is integer)
|
|
505
|
+
for v in (j1, j2, j3, J1, J2, J3):
|
|
506
|
+
if abs(round(2 * v) - 2 * v) > 1e-10 or v < 0:
|
|
507
|
+
return 0.0
|
|
508
|
+
|
|
509
|
+
# Four triangle inequalities
|
|
510
|
+
if not (_istriangle(j1, j2, j3) and _istriangle(j1, J2, J3) and
|
|
511
|
+
_istriangle(J1, j2, J3) and _istriangle(J1, J2, j3)):
|
|
512
|
+
return 0.0
|
|
513
|
+
|
|
514
|
+
def _fac(n: int) -> float:
|
|
515
|
+
"""Factorial as float."""
|
|
516
|
+
return float(math.factorial(int(round(n))))
|
|
517
|
+
|
|
518
|
+
def _bino(a: float, b: float) -> float:
|
|
519
|
+
"""Binomial coefficient C(a, b) = a! / (b! * (a-b)!)."""
|
|
520
|
+
ai, bi = int(round(a)), int(round(b))
|
|
521
|
+
if bi < 0 or ai < bi:
|
|
522
|
+
return 0.0
|
|
523
|
+
v = 1.0
|
|
524
|
+
for i in range(bi):
|
|
525
|
+
v = v * (ai - i) / (i + 1)
|
|
526
|
+
return v
|
|
527
|
+
|
|
528
|
+
# Tuzun 1998 Formula (80) Method 2
|
|
529
|
+
alpha = sorted([j1 + j2 + J1 + J2,
|
|
530
|
+
j1 + j3 + J1 + J3,
|
|
531
|
+
j2 + j3 + J2 + J3])
|
|
532
|
+
A1, A2, A3 = alpha
|
|
533
|
+
|
|
534
|
+
beta = sorted([j1 + j2 + j3,
|
|
535
|
+
j1 + J2 + J3,
|
|
536
|
+
J1 + j2 + J3,
|
|
537
|
+
J1 + J2 + j3])
|
|
538
|
+
B1, B2, B3, B4 = beta
|
|
539
|
+
|
|
540
|
+
# Prefactor (square root form)
|
|
541
|
+
pre = math.sqrt(
|
|
542
|
+
_fac(A1 - B2) * _fac(A1 - B3) * _fac(A1 - B4) / _fac(A1 - B1) / _fac(B1 + 1) *
|
|
543
|
+
_fac(A2 - B1) * _fac(A2 - B3) * _fac(A2 - B4) / _fac(A2 - B2) / _fac(B2 + 1) *
|
|
544
|
+
_fac(A3 - B1) * _fac(A3 - B2) * _fac(A3 - B4) / _fac(A3 - B3) / _fac(B3 + 1) *
|
|
545
|
+
_fac(B4 + 1)
|
|
546
|
+
)
|
|
547
|
+
sumfactor = ((-1) ** int(round(B4)) *
|
|
548
|
+
_bino(A1 - B1, A1 - B4) *
|
|
549
|
+
_bino(A2 - B2, A2 - B4) *
|
|
550
|
+
_bino(A3 - B3, A3 - B4))
|
|
551
|
+
pre = pre * sumfactor
|
|
552
|
+
|
|
553
|
+
if B4 >= A1 - 1e-10:
|
|
554
|
+
return pre
|
|
555
|
+
|
|
556
|
+
# Recursive sum from n = B4+1 to A1
|
|
557
|
+
t = 1.0
|
|
558
|
+
s = t
|
|
559
|
+
for n in range(int(round(B4)) + 1, int(round(A1)) + 1):
|
|
560
|
+
t = -t * (n + 1) / (n - B4) * (A1 - n + 1) / (n - B1) * \
|
|
561
|
+
(A2 - n + 1) / (n - B2) * (A3 - n + 1) / (n - B3)
|
|
562
|
+
s += t
|
|
563
|
+
return pre * s
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
# ---------------------------------------------------------------------------
|
|
567
|
+
# isto (Irreducible Spherical Tensor Operators)
|
|
568
|
+
# ---------------------------------------------------------------------------
|
|
569
|
+
|
|
570
|
+
def isto(J, kq, q_arg=None) -> np.ndarray:
|
|
571
|
+
"""Irreducible spherical tensor operator T^k_q.
|
|
572
|
+
|
|
573
|
+
Port of EasySpin's ``isto.m`` (Ryabov 1999, J. Magn. Reson. 140, 141).
|
|
574
|
+
|
|
575
|
+
Parameters
|
|
576
|
+
----------
|
|
577
|
+
J : float or list of float
|
|
578
|
+
Angular momentum quantum number(s). A single float for one spin,
|
|
579
|
+
a list for a multi-spin product operator.
|
|
580
|
+
kq : array_like or int
|
|
581
|
+
If ``q_arg`` is ``None``:
|
|
582
|
+
- Shape (nSpins, 2): each row ``[k, q]`` for the corresponding spin.
|
|
583
|
+
- Shape (nTerms, 3): each row ``[k, q, spin_index]`` (1-based).
|
|
584
|
+
If ``q_arg`` is given, ``kq`` is the rank ``k`` (single-spin shorthand).
|
|
585
|
+
q_arg : int, optional
|
|
586
|
+
When provided, ``kq`` is ``k`` and ``q_arg`` is ``q`` (single-spin call).
|
|
587
|
+
|
|
588
|
+
Returns
|
|
589
|
+
-------
|
|
590
|
+
ndarray, complex128
|
|
591
|
+
The (dim × dim) irreducible spherical tensor operator matrix, where
|
|
592
|
+
dim = product of (2*Ji + 1) for all spins.
|
|
593
|
+
|
|
594
|
+
Examples
|
|
595
|
+
--------
|
|
596
|
+
>>> T = isto(0.5, 1, 0) # T^1_0 for S=1/2 (= Sz / sqrt(2), unnorm)
|
|
597
|
+
>>> T = isto(1.0, [2, 0]) # T^2_0 for S=1
|
|
598
|
+
"""
|
|
599
|
+
# --- Normalise input ---
|
|
600
|
+
if q_arg is not None:
|
|
601
|
+
# isto(J, k, q) shorthand
|
|
602
|
+
k_single = int(round(float(kq)))
|
|
603
|
+
q_single = int(round(float(q_arg)))
|
|
604
|
+
kq_arr = np.array([[k_single, q_single]], dtype=float)
|
|
605
|
+
J_list = [float(J)]
|
|
606
|
+
else:
|
|
607
|
+
kq_arr = np.atleast_2d(np.asarray(kq, dtype=float))
|
|
608
|
+
if np.isscalar(J) or (hasattr(J, '__len__') and len(J) == 1):
|
|
609
|
+
J_list = [float(J) if np.isscalar(J) else float(J[0])]
|
|
610
|
+
else:
|
|
611
|
+
J_list = [float(j) for j in J]
|
|
612
|
+
|
|
613
|
+
# If kq has 3 columns, it uses [k, q, spin_index] format
|
|
614
|
+
if kq_arr.shape[1] == 3:
|
|
615
|
+
nSpins = len(J_list)
|
|
616
|
+
kq_full = np.zeros((nSpins, 2))
|
|
617
|
+
for row in kq_arr:
|
|
618
|
+
i = int(round(row[2])) - 1 # 1-based → 0-based
|
|
619
|
+
kq_full[i] = row[:2]
|
|
620
|
+
kq_arr = kq_full
|
|
621
|
+
elif kq_arr.shape[0] != len(J_list) and len(J_list) == 1:
|
|
622
|
+
# Single spin, kq given as 1×2 or (2,)
|
|
623
|
+
pass # already fine
|
|
624
|
+
elif kq_arr.shape[0] != len(J_list):
|
|
625
|
+
raise ValueError("kq must have one row per spin in J.")
|
|
626
|
+
|
|
627
|
+
# --- Build product operator via Kronecker products ---
|
|
628
|
+
T = np.array([[1.0 + 0j]]) # 1×1 identity (will be kron-expanded)
|
|
629
|
+
for idx, Ji in enumerate(J_list):
|
|
630
|
+
k_i = int(round(kq_arr[idx, 0]))
|
|
631
|
+
q_i = int(round(kq_arr[idx, 1]))
|
|
632
|
+
Tj = _isto_single(Ji, k_i, q_i)
|
|
633
|
+
T = np.kron(T, Tj)
|
|
634
|
+
return T
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
def _isto_single(J: float, k: int, q: int) -> np.ndarray:
|
|
638
|
+
"""ISTO T^k_q for a single spin J (Ryabov 1999).
|
|
639
|
+
|
|
640
|
+
Parameters
|
|
641
|
+
----------
|
|
642
|
+
J : float spin quantum number (0, 1/2, 1, ...)
|
|
643
|
+
k : int rank (0, 1, 2, ...)
|
|
644
|
+
q : int projection (−k ≤ q ≤ k)
|
|
645
|
+
"""
|
|
646
|
+
if abs(q) > k:
|
|
647
|
+
raise ValueError(f"q={q} must satisfy |q| <= k={k}.")
|
|
648
|
+
if k < 0:
|
|
649
|
+
raise ValueError("k must be a non-negative integer.")
|
|
650
|
+
|
|
651
|
+
dim = int(round(2 * J + 1))
|
|
652
|
+
if J == 0:
|
|
653
|
+
return np.ones((1, 1), dtype=complex)
|
|
654
|
+
|
|
655
|
+
# Build J+ and J- raising/lowering operators
|
|
656
|
+
# basis: m = +J, J-1, ..., -J (descending)
|
|
657
|
+
from torchspin.spinops import sop
|
|
658
|
+
Jp = sop(J, '+').numpy().astype(complex)
|
|
659
|
+
Jm = sop(J, '-').numpy().astype(complex)
|
|
660
|
+
|
|
661
|
+
# T^k_k = N_kk * (J+)^k (Ryabov Eq.[2] + [17])
|
|
662
|
+
Nkk = ((-1.0) ** k) / (2.0 ** (k / 2.0))
|
|
663
|
+
T = Nkk * np.linalg.matrix_power(Jp, k)
|
|
664
|
+
|
|
665
|
+
# Lower projection from k down to |q| via Racah commutation (Ryabov Eq.[1])
|
|
666
|
+
for q_ in range(k, abs(q), -1):
|
|
667
|
+
denom = math.sqrt(k * (k + 1) - q_ * (q_ - 1))
|
|
668
|
+
T = (Jm @ T - T @ Jm) / denom
|
|
669
|
+
|
|
670
|
+
# Negative q via Hermitian conjugate (Ryabov Eq.[3])
|
|
671
|
+
if q < 0:
|
|
672
|
+
T = ((-1.0) ** q) * T.conj().T
|
|
673
|
+
|
|
674
|
+
return T
|
|
675
|
+
|
|
676
|
+
|
|
677
|
+
# ---------------------------------------------------------------------------
|
|
678
|
+
# isto2stev (ISTO ↔ Stevens operator transformation matrix)
|
|
679
|
+
# ---------------------------------------------------------------------------
|
|
680
|
+
|
|
681
|
+
def isto2stev(k: int) -> np.ndarray:
|
|
682
|
+
"""Transformation matrix from ISTOs to Stevens operators.
|
|
683
|
+
|
|
684
|
+
Port of EasySpin's ``isto2stev.m``.
|
|
685
|
+
|
|
686
|
+
Computes the (2k+1) × (2k+1) matrix **C** such that::
|
|
687
|
+
|
|
688
|
+
O_k = C @ T_k
|
|
689
|
+
|
|
690
|
+
where T_k = [T^k_k, T^k_{k-1}, ..., T^k_{-k}] (ISTO column vector)
|
|
691
|
+
and O_k = [O^k_k, O^k_{k-1}, ..., O^k_{-k}] (Stevens operator vector).
|
|
692
|
+
|
|
693
|
+
Parameters
|
|
694
|
+
----------
|
|
695
|
+
k : int
|
|
696
|
+
Rank (non-negative integer).
|
|
697
|
+
|
|
698
|
+
Returns
|
|
699
|
+
-------
|
|
700
|
+
C : ndarray, shape (2k+1, 2k+1), complex128
|
|
701
|
+
Transformation matrix. Inverse is ``numpy.linalg.inv(C)``.
|
|
702
|
+
|
|
703
|
+
Notes
|
|
704
|
+
-----
|
|
705
|
+
Only diagonal and anti-diagonal elements are non-zero.
|
|
706
|
+
Uses ``J = k/2`` (smallest spin for which rank-k operators exist).
|
|
707
|
+
The result is independent of the choice of J.
|
|
708
|
+
|
|
709
|
+
Examples
|
|
710
|
+
--------
|
|
711
|
+
>>> C = isto2stev(2) # 5×5 matrix for rank-2 operators
|
|
712
|
+
>>> C.shape
|
|
713
|
+
(5, 5)
|
|
714
|
+
"""
|
|
715
|
+
if not isinstance(k, int) or k < 0:
|
|
716
|
+
raise ValueError("k must be a non-negative integer.")
|
|
717
|
+
|
|
718
|
+
from torchspin.stev import stev as _stev
|
|
719
|
+
|
|
720
|
+
nq = 2 * k + 1
|
|
721
|
+
# Use smallest usable spin: J = k/2
|
|
722
|
+
# (stev and isto require 2J >= k)
|
|
723
|
+
J = k / 2.0
|
|
724
|
+
|
|
725
|
+
# Build ISTO and Stevens operator vectors (ordering: q = k, k-1, ..., -k)
|
|
726
|
+
T_list = []
|
|
727
|
+
O_list = []
|
|
728
|
+
for q in range(k, -k - 1, -1):
|
|
729
|
+
T_list.append(_isto_single(J, k, q))
|
|
730
|
+
O_arr = _stev(J, k, q).numpy().astype(complex)
|
|
731
|
+
O_list.append(O_arr)
|
|
732
|
+
|
|
733
|
+
C = np.zeros((nq, nq), dtype=complex)
|
|
734
|
+
for t in range(nq):
|
|
735
|
+
traceTT = np.trace(T_list[t].conj().T @ T_list[t])
|
|
736
|
+
if abs(traceTT) < 1e-14:
|
|
737
|
+
continue
|
|
738
|
+
# Only diagonal (o=t) and anti-diagonal (o=nq-1-t) are non-zero
|
|
739
|
+
for o in {t, nq - 1 - t}:
|
|
740
|
+
C[o, t] = np.trace(O_list[o].T @ T_list[t]) / traceTT
|
|
741
|
+
|
|
742
|
+
return C
|