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.
Files changed (116) hide show
  1. torchspin/__init__.py +363 -0
  2. torchspin/_cardamom_istos.py +327 -0
  3. torchspin/_cardamom_propagatedm.py +413 -0
  4. torchspin/_cardamom_utils.py +113 -0
  5. torchspin/_compile.py +100 -0
  6. torchspin/_linalg.py +140 -0
  7. torchspin/angmom.py +742 -0
  8. torchspin/autograd.py +557 -0
  9. torchspin/autoguess.py +342 -0
  10. torchspin/batch.py +225 -0
  11. torchspin/blochsteady.py +274 -0
  12. torchspin/cardamom.py +552 -0
  13. torchspin/chili.py +932 -0
  14. torchspin/chili_sle.py +1233 -0
  15. torchspin/constants.py +88 -0
  16. torchspin/convspec.py +148 -0
  17. torchspin/ctafft.py +91 -0
  18. torchspin/curry.py +241 -0
  19. torchspin/data/FourierSeriesCoefficients.txt +89 -0
  20. torchspin/data/GaussianCascadeCoefficients.txt +56 -0
  21. torchspin/data/isotopedata.txt +416 -0
  22. torchspin/data/spacegroups.txt +637 -0
  23. torchspin/dataproc.py +361 -0
  24. torchspin/dipbackground.py +69 -0
  25. torchspin/dipkernel.py +102 -0
  26. torchspin/diptensor.py +138 -0
  27. torchspin/endorfrq.py +198 -0
  28. torchspin/endorfrq_perturb.py +302 -0
  29. torchspin/eprload.py +1538 -0
  30. torchspin/eprsave.py +165 -0
  31. torchspin/esfit.py +2427 -0
  32. torchspin/evolve.py +274 -0
  33. torchspin/ewrls.py +124 -0
  34. torchspin/excitation.py +107 -0
  35. torchspin/exciteprofile.py +132 -0
  36. torchspin/experiment.py +323 -0
  37. torchspin/exponfit.py +177 -0
  38. torchspin/fastmotion.py +182 -0
  39. torchspin/fdaxis.py +57 -0
  40. torchspin/fitgui.py +350 -0
  41. torchspin/garlic.py +760 -0
  42. torchspin/ham.py +145 -0
  43. torchspin/ham_cf.py +115 -0
  44. torchspin/ham_ee.py +124 -0
  45. torchspin/ham_ez.py +119 -0
  46. torchspin/ham_ezho.py +403 -0
  47. torchspin/ham_hf.py +122 -0
  48. torchspin/ham_nn.py +108 -0
  49. torchspin/ham_nq.py +100 -0
  50. torchspin/ham_nz.py +136 -0
  51. torchspin/ham_oz.py +115 -0
  52. torchspin/ham_so.py +118 -0
  53. torchspin/ham_zf.py +254 -0
  54. torchspin/hamsymm.py +723 -0
  55. torchspin/initstate.py +158 -0
  56. torchspin/isotopologues.py +296 -0
  57. torchspin/levels.py +189 -0
  58. torchspin/levelsplot.py +363 -0
  59. torchspin/lineshape.py +715 -0
  60. torchspin/lpsvd.py +219 -0
  61. torchspin/makespec.py +87 -0
  62. torchspin/mdhmm.py +573 -0
  63. torchspin/mdload.py +788 -0
  64. torchspin/mdtraj2oripot.py +90 -0
  65. torchspin/ml.py +377 -0
  66. torchspin/mlpsvd.py +297 -0
  67. torchspin/nucdata.py +310 -0
  68. torchspin/nucfrq2d.py +228 -0
  69. torchspin/orca2torchspin.py +1198 -0
  70. torchspin/ordering.py +78 -0
  71. torchspin/oripotentialplot.py +246 -0
  72. torchspin/orisel.py +176 -0
  73. torchspin/pepper.py +2019 -0
  74. torchspin/pepper_autograd.py +613 -0
  75. torchspin/photoselect.py +174 -0
  76. torchspin/plegendre.py +131 -0
  77. torchspin/propint.py +127 -0
  78. torchspin/pulse.py +652 -0
  79. torchspin/py.typed +0 -0
  80. torchspin/rapidscan2spc.py +101 -0
  81. torchspin/resfields.py +337 -0
  82. torchspin/resfields_batch.py +446 -0
  83. torchspin/resfields_eig.py +250 -0
  84. torchspin/resfields_perturb.py +706 -0
  85. torchspin/resfreqs_matrix.py +344 -0
  86. torchspin/resfreqs_perturb.py +299 -0
  87. torchspin/resonator.py +310 -0
  88. torchspin/resonatorprofile.py +85 -0
  89. torchspin/rfmixer.py +147 -0
  90. torchspin/rotations.py +87 -0
  91. torchspin/rotutils.py +880 -0
  92. torchspin/saffron.py +1523 -0
  93. torchspin/saffron_pathways.py +92 -0
  94. torchspin/saffron_peaks.py +580 -0
  95. torchspin/saffron_thyme.py +472 -0
  96. torchspin/salt.py +680 -0
  97. torchspin/sigeq.py +71 -0
  98. torchspin/signalprocessing.py +226 -0
  99. torchspin/sitetransforms.py +254 -0
  100. torchspin/sphgrid.py +470 -0
  101. torchspin/spidyan.py +1508 -0
  102. torchspin/spinladder.py +161 -0
  103. torchspin/spinops.py +265 -0
  104. torchspin/spinsystem.py +1427 -0
  105. torchspin/stackplot.py +167 -0
  106. torchspin/stev.py +283 -0
  107. torchspin/stochtraj_diffusion.py +494 -0
  108. torchspin/stochtraj_jump.py +195 -0
  109. torchspin/strainwidth.py +727 -0
  110. torchspin/transmitter.py +89 -0
  111. torchspin/utils.py +605 -0
  112. torchspin-0.3.0.dist-info/METADATA +267 -0
  113. torchspin-0.3.0.dist-info/RECORD +116 -0
  114. torchspin-0.3.0.dist-info/WHEEL +5 -0
  115. torchspin-0.3.0.dist-info/licenses/LICENSE.md +22 -0
  116. torchspin-0.3.0.dist-info/top_level.txt +1 -0
torchspin/__init__.py ADDED
@@ -0,0 +1,363 @@
1
+ """torchspin — PyTorch EPR/ESR spin Hamiltonian engine.
2
+
3
+ A Python/PyTorch port of EasySpin's core physics layer.
4
+ All energies are in **MHz**; magnetic fields in **mT**; Euler angles in **radians**.
5
+
6
+ Quick start::
7
+
8
+ import torch
9
+ from torchspin import SpinSystem, ham
10
+
11
+ # S=1/2 nitroxide-like spin system
12
+ sys = SpinSystem(
13
+ S=[0.5],
14
+ g=[[2.009, 2.006, 2.002]],
15
+ Nucs=['14N'],
16
+ A=[[10.0, 10.0, 95.0]], # MHz
17
+ )
18
+
19
+ B0 = [0.0, 0.0, 340.0] # mT, along z
20
+ H = ham(sys, B0)
21
+ energies = torch.linalg.eigvalsh(H) # MHz
22
+ """
23
+ from importlib.metadata import version as _pkg_version, PackageNotFoundError as _PkgNotFound
24
+ try:
25
+ __version__ = _pkg_version("torchspin")
26
+ except _PkgNotFound:
27
+ # Editable install during development, or version metadata unavailable.
28
+ __version__ = "0.3.0+dev"
29
+
30
+ from torchspin._compile import maybe_compile, set_compile_enabled, is_compile_available
31
+ from torchspin.autograd import differentiable_spectrum
32
+ from torchspin.batch import batch_pepper, batch_simulate
33
+ from torchspin.cardamom import cardamom, CardamomPar, CardamomOptions, MDInput
34
+ from torchspin.ctafft import ctafft
35
+ from torchspin.ewrls import ewrls
36
+ from torchspin.lpsvd import lpsvd, LPSVDResult
37
+ from torchspin.rapidscan2spc import rapidscan2spc
38
+ from torchspin.stochtraj_diffusion import stochtraj_diffusion, DiffusionPar
39
+ from torchspin.stochtraj_jump import stochtraj_jump, JumpPar
40
+ from torchspin.autoguess import (estimate_parameters, field_from_g, g_from_field,
41
+ print_parameter_report)
42
+ from torchspin.lineshape import (gaussian, lorentzian, voigtian, apowin, addnoise, deriv, lshape)
43
+ from torchspin.dataproc import basecorr, fieldmod, rescaledata
44
+ from torchspin.utils import (datasmooth, equivcouple, equivsplit,
45
+ hilberttrans, larmorfrq, rcfilt, unitconvert,
46
+ mhz2mt, mt2mhz, hsdim, eprconvert)
47
+ from torchspin.nucdata import nucabund, nucspin, nucgval, nucqmom
48
+ from torchspin.blochsteady import blochsteady, BlochOptions
49
+ from torchspin.chili import chili, ChiliOptions
50
+ from torchspin.endorfrq import endorfrq
51
+ from torchspin.salt import salt
52
+ from torchspin.constants import (BMAGN, BOLTZMANN, CLIGHT, EVOLT, GFREE, NMAGN, PLANCK,
53
+ HBAR, AMU, ECHARGE, EMASS, PMASS, NMASS, EPS0, MU0,
54
+ FARADAY, AVOGADRO, BOHRRAD, HARTREE, RYDBERG, GAMMAE, GAMMAN,
55
+ ANGSTROM, DEGREE, BARN, MOLGAS)
56
+ from torchspin.convspec import convspec
57
+ from torchspin.eprload import eprload, eprload_info
58
+ from torchspin.eprsave import eprsave, eprsave_info
59
+ from torchspin.esfit import esfit, FitOptions, FitResult
60
+ from torchspin.experiment import Experiment, Options
61
+ from torchspin.curry import curry
62
+ from torchspin.fastmotion import fastmotion
63
+ from torchspin.garlic import garlic
64
+ from torchspin.ham import ham
65
+ from torchspin.levels import levels
66
+ from torchspin.resfreqs_matrix import resfreqs_matrix
67
+ from torchspin.ham_cf import ham_cf
68
+ from torchspin.ham_ee import ham_ee
69
+ from torchspin.ham_ez import ham_ez
70
+ from torchspin.ham_ezho import ham_ezho
71
+ from torchspin.ham_hf import ham_hf
72
+ from torchspin.ham_nn import ham_nn
73
+ from torchspin.ham_nq import ham_nq
74
+ from torchspin.ham_nz import ham_nz
75
+ from torchspin.ham_oz import ham_oz
76
+ from torchspin.ham_so import ham_so
77
+ from torchspin.ham_zf import ham_zf, zfsframes
78
+ from torchspin.hamsymm import hamsymm
79
+ from torchspin.makespec import makespec
80
+ from torchspin.pepper import pepper
81
+ from torchspin.pepper_autograd import pepper_autograd
82
+ from torchspin.rotations import erot
83
+ from torchspin.rotutils import (eulang, euler2quat, quat2euler, quat2rotmat,
84
+ rotmat2quat, rotaxi2mat, rotmat2axi,
85
+ quatinv, quatmult, quatvecmult,
86
+ vec2ang, ang2vec,
87
+ tensor_cart2sph, tensor_sph2cart, rotateframe)
88
+ from torchspin.diptensor import diptensor
89
+ from torchspin.spinladder import spinladder, SpinManifold
90
+ from torchspin.orisel import orisel, OriselOptions
91
+ from torchspin.spinops import sop, commute
92
+ from torchspin.sphgrid import sphgrid, sphrand
93
+ from torchspin.angmom import (wigner3j, wigner6j, wignerd, spherharm, clebschgordan, cgmatrix,
94
+ isto, isto2stev)
95
+ from torchspin.endorfrq_perturb import endorfrq_perturb
96
+ from torchspin.resfreqs_perturb import resfreqs_perturb
97
+ from torchspin.resfields_eig import resfields_eig, EigOptions
98
+ from torchspin.spidyan import spidyan, SpidyanOptions, SpidyanInfo
99
+ from torchspin.mdload import mdload, MDData, MDInfo, MDOptions
100
+ from torchspin.mdhmm import mdhmm, HMMResult, HMMOptions
101
+ from torchspin.mdtraj2oripot import mdtraj2oripot
102
+ from torchspin.orca2torchspin import orca2torchspin, OrcaData
103
+ from torchspin.mlpsvd import mlpsvd, MLPSVDResult
104
+ from torchspin.oripotentialplot import oripotentialplot
105
+ from torchspin.saffron import saffron, PulseExperiment, SaffronOptions
106
+ from torchspin.signalprocessing import signalprocessing
107
+ from torchspin.saffron_pathways import find_refocusing_pathways
108
+ from torchspin.spinsystem import SpinSystem, nucspinrmv, nucspinkeep, nucspinadd, isotopologues, spinvec
109
+ from torchspin.sigeq import sigeq
110
+ from torchspin.fdaxis import fdaxis
111
+ from torchspin.plegendre import plegendre
112
+ from torchspin.propint import propint
113
+ from torchspin.resonatorprofile import resonatorprofile
114
+ from torchspin.rfmixer import rfmixer
115
+ from torchspin.transmitter import transmitter
116
+ from torchspin.pulse import pulse
117
+ from torchspin.exciteprofile import exciteprofile
118
+ from torchspin.resonator import resonator
119
+ from torchspin.photoselect import photoselect
120
+ from torchspin.levelsplot import levelsplot, LevelsPlotOptions
121
+ from torchspin.stackplot import stackplot
122
+ from torchspin.dipkernel import dipkernel
123
+ from torchspin.dipbackground import dipbackground
124
+ from torchspin.exponfit import exponfit
125
+ from torchspin.nucfrq2d import nucfrq2d
126
+ from torchspin.evolve import evolve
127
+
128
+ __all__ = [
129
+ # Version
130
+ '__version__',
131
+ # Differentiable simulator
132
+ 'differentiable_spectrum',
133
+ 'pepper_autograd',
134
+ # Core
135
+ 'SpinSystem',
136
+ 'nucspinrmv',
137
+ 'nucspinkeep',
138
+ 'nucspinadd',
139
+ 'isotopologues',
140
+ 'spinvec',
141
+ 'sop',
142
+ 'erot',
143
+ # Hamiltonians
144
+ 'ham',
145
+ 'ham_cf',
146
+ 'ham_ez',
147
+ 'ham_ezho',
148
+ 'ham_hf',
149
+ 'ham_ee',
150
+ 'ham_nz',
151
+ 'ham_nq',
152
+ 'ham_nn',
153
+ 'ham_oz',
154
+ 'ham_so',
155
+ 'ham_zf',
156
+ # Spectrum simulation
157
+ 'blochsteady',
158
+ 'BlochOptions',
159
+ 'pepper',
160
+ 'garlic',
161
+ 'fastmotion',
162
+ 'chili',
163
+ 'ChiliOptions',
164
+ 'endorfrq',
165
+ 'salt',
166
+ 'levels',
167
+ 'resfreqs_matrix',
168
+ 'hamsymm',
169
+ 'Experiment',
170
+ 'Options',
171
+ 'sphgrid',
172
+ 'sphrand',
173
+ 'makespec',
174
+ 'convspec',
175
+ # Magnetometry
176
+ 'curry',
177
+ # Data I/O (save)
178
+ 'eprsave',
179
+ 'eprsave_info',
180
+ # Signal processing utilities
181
+ 'larmorfrq',
182
+ 'equivsplit',
183
+ 'equivcouple',
184
+ 'datasmooth',
185
+ 'rcfilt',
186
+ 'hilberttrans',
187
+ 'unitconvert',
188
+ 'mhz2mt',
189
+ 'mt2mhz',
190
+ 'hsdim',
191
+ # Nuclear data
192
+ 'nucabund',
193
+ 'nucspin',
194
+ 'nucgval',
195
+ 'nucqmom',
196
+ # Lineshape functions
197
+ 'gaussian',
198
+ 'lorentzian',
199
+ 'voigtian',
200
+ 'apowin',
201
+ 'addnoise',
202
+ 'deriv',
203
+ 'lshape',
204
+ # Data processing
205
+ 'basecorr',
206
+ 'fieldmod',
207
+ 'rescaledata',
208
+ # Angular momentum functions
209
+ 'wigner3j',
210
+ 'wigner6j',
211
+ 'wignerd',
212
+ 'spherharm',
213
+ 'clebschgordan',
214
+ 'cgmatrix',
215
+ 'isto',
216
+ 'isto2stev',
217
+ # Perturbation-theory resonance/ENDOR
218
+ 'endorfrq_perturb',
219
+ 'resfreqs_perturb',
220
+ 'resfields_eig',
221
+ 'EigOptions',
222
+ # Spin dynamics (spidyan)
223
+ 'spidyan',
224
+ 'SpidyanOptions',
225
+ 'SpidyanInfo',
226
+ # MD trajectory analysis (cardamom Phase 5)
227
+ 'mdload',
228
+ 'MDData',
229
+ 'MDInfo',
230
+ 'MDOptions',
231
+ 'mdhmm',
232
+ 'HMMResult',
233
+ 'HMMOptions',
234
+ 'mdtraj2oripot',
235
+ # Rotation utilities
236
+ 'eulang',
237
+ 'euler2quat',
238
+ 'quat2euler',
239
+ 'quat2rotmat',
240
+ 'rotmat2quat',
241
+ 'rotaxi2mat',
242
+ 'rotmat2axi',
243
+ 'quatinv',
244
+ 'quatmult',
245
+ 'quatvecmult',
246
+ 'vec2ang',
247
+ 'ang2vec',
248
+ 'tensor_cart2sph',
249
+ 'tensor_sph2cart',
250
+ 'rotateframe',
251
+ # Spin operators
252
+ 'commute',
253
+ # Dipolar coupling
254
+ 'diptensor',
255
+ # ZFS analysis
256
+ 'zfsframes',
257
+ # Spin ladder
258
+ 'spinladder',
259
+ 'SpinManifold',
260
+ # Orientation selectivity
261
+ 'orisel',
262
+ 'OriselOptions',
263
+ # Data I/O
264
+ 'eprload',
265
+ 'eprload_info',
266
+ 'orca2torchspin',
267
+ 'OrcaData',
268
+ # Pulse EPR
269
+ 'saffron',
270
+ 'PulseExperiment',
271
+ 'SaffronOptions',
272
+ 'find_refocusing_pathways',
273
+ # Parameter estimation
274
+ 'estimate_parameters',
275
+ 'field_from_g',
276
+ 'g_from_field',
277
+ 'print_parameter_report',
278
+ # Fitting
279
+ 'esfit',
280
+ 'FitOptions',
281
+ 'FitResult',
282
+ # Batch simulation
283
+ 'batch_pepper',
284
+ 'batch_simulate',
285
+ # Trajectory-based simulation (cardamom)
286
+ 'cardamom',
287
+ 'CardamomPar',
288
+ 'CardamomOptions',
289
+ 'MDInput',
290
+ 'stochtraj_diffusion',
291
+ 'DiffusionPar',
292
+ 'stochtraj_jump',
293
+ 'JumpPar',
294
+ # Compilation
295
+ 'maybe_compile',
296
+ 'set_compile_enabled',
297
+ 'is_compile_available',
298
+ # Constants
299
+ 'BMAGN',
300
+ 'PLANCK',
301
+ 'GFREE',
302
+ 'NMAGN',
303
+ 'BOLTZMANN',
304
+ 'CLIGHT',
305
+ 'EVOLT',
306
+ 'HBAR',
307
+ 'AMU',
308
+ 'ECHARGE',
309
+ 'EMASS',
310
+ 'PMASS',
311
+ 'NMASS',
312
+ 'EPS0',
313
+ 'MU0',
314
+ 'FARADAY',
315
+ 'AVOGADRO',
316
+ 'BOHRRAD',
317
+ 'HARTREE',
318
+ 'RYDBERG',
319
+ 'GAMMAE',
320
+ 'GAMMAN',
321
+ 'ANGSTROM',
322
+ 'DEGREE',
323
+ 'BARN',
324
+ 'MOLGAS',
325
+ # EPR conversion
326
+ 'eprconvert',
327
+ # Tier 1 utilities
328
+ 'sigeq',
329
+ 'fdaxis',
330
+ 'plegendre',
331
+ 'photoselect',
332
+ 'levelsplot',
333
+ 'LevelsPlotOptions',
334
+ 'stackplot',
335
+ # Tier 2 utilities
336
+ 'dipkernel',
337
+ 'dipbackground',
338
+ 'exponfit',
339
+ 'nucfrq2d',
340
+ 'evolve',
341
+ # Tier 3b — Signal Processing
342
+ 'ctafft',
343
+ 'ewrls',
344
+ 'lpsvd',
345
+ 'LPSVDResult',
346
+ 'mlpsvd',
347
+ 'MLPSVDResult',
348
+ 'rapidscan2spc',
349
+ 'signalprocessing',
350
+ # Visualization
351
+ 'oripotentialplot',
352
+ # Tier 3c — Higher-order Zeeman
353
+ 'ham_ezho',
354
+ # Tier 3d-i — Pulse EPR primitives
355
+ 'resonatorprofile',
356
+ 'transmitter',
357
+ 'rfmixer',
358
+ 'propint',
359
+ # Tier 3d-ii — Pulse waveforms
360
+ 'pulse',
361
+ 'exciteprofile',
362
+ 'resonator',
363
+ ]
@@ -0,0 +1,327 @@
1
+ """IST decomposition and Wigner D-matrices for cardamom ISTOs method.
2
+
3
+ Implements:
4
+ - ``istotensor``: irreducible spherical tensor operators from two vector operators
5
+ - ``magint``: IST decomposition of spin Hamiltonian interactions
6
+ - ``wigD``: rank-2 Wigner D-matrices from quaternion trajectories
7
+
8
+ References
9
+ ----------
10
+ [1] Oganesyan, Phys. Chem. Chem. Phys. 13, 4724 (2011)
11
+ [2] Mehring, Principles of High Resolution NMR in Solids, Appendix A (1983)
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import numpy as np
16
+ from torchspin.constants import BMAGN, NMAGN, PLANCK
17
+ from torchspin.spinops import sop
18
+
19
+
20
+ # ---------------------------------------------------------------------------
21
+ # istotensor — rank-0, rank-1, rank-2 IST operators from two vector operators
22
+ # ---------------------------------------------------------------------------
23
+
24
+ def istotensor(
25
+ a: list[np.ndarray] | np.ndarray,
26
+ b: list[np.ndarray] | np.ndarray,
27
+ ) -> tuple[np.ndarray, list[np.ndarray], list[np.ndarray]]:
28
+ """Build irreducible spherical tensor operators from two vector operators.
29
+
30
+ Port of EasySpin's ``private/istotensor.m``.
31
+
32
+ Parameters
33
+ ----------
34
+ a, b:
35
+ Each is either a list/tuple of 3 matrices [ax, ay, az] (spin operators)
36
+ or a length-3 array (e.g. magnetic field components).
37
+
38
+ Returns
39
+ -------
40
+ T0:
41
+ Rank-0 tensor (scalar or matrix).
42
+ T1:
43
+ Rank-1 tensor, list of 3 elements [T1(+1), T1(0), T1(-1)].
44
+ T2:
45
+ Rank-2 tensor, list of 5 elements [T2(+2), T2(+1), T2(0), T2(-1), T2(-2)].
46
+ """
47
+ if isinstance(a, (list, tuple)):
48
+ ax, ay, az = a[0], a[1], a[2]
49
+ else:
50
+ a = np.asarray(a)
51
+ ax, ay, az = a[0], a[1], a[2]
52
+
53
+ if isinstance(b, (list, tuple)):
54
+ bx, by, bz = b[0], b[1], b[2]
55
+ else:
56
+ b = np.asarray(b)
57
+ bx, by, bz = b[0], b[1], b[2]
58
+
59
+ # Rank 0: T0 = -(1/sqrt(3)) * (ax*bx + ay*by + az*bz)
60
+ T0 = -(1 / np.sqrt(3)) * (ax @ bx + ay @ by + az @ bz) \
61
+ if isinstance(ax, np.ndarray) and ax.ndim == 2 \
62
+ else -(1 / np.sqrt(3)) * (ax * bx + ay * by + az * bz)
63
+
64
+ # Helper for products (handles both matrix and scalar cases)
65
+ def _prod(x, y):
66
+ if isinstance(x, np.ndarray) and x.ndim == 2:
67
+ return x @ y
68
+ return x * y
69
+
70
+ # Rank 1
71
+ T1 = [
72
+ -0.5 * (_prod(ax, bz) - _prod(az, bx) + 1j * (_prod(ay, bz) - _prod(az, by))), # (+1)
73
+ -(1j / np.sqrt(2)) * (_prod(ay, bx) - _prod(ax, by)), # (0)
74
+ -0.5 * (_prod(ax, bz) - _prod(az, bx) - 1j * (_prod(ay, bz) - _prod(az, by))), # (-1)
75
+ ]
76
+
77
+ # Rank 2
78
+ T2 = [
79
+ 0.5 * (_prod(ax, bx) - _prod(ay, by) + 1j * (_prod(ax, by) + _prod(ay, bx))), # (+2)
80
+ -0.5 * (_prod(ax, bz) + _prod(az, bx) + 1j * (_prod(ay, bz) + _prod(az, by))), # (+1)
81
+ np.sqrt(2 / 3) * (_prod(az, bz) - 0.5 * (_prod(ax, bx) + _prod(ay, by))), # (0)
82
+ 0.5 * (_prod(ax, bz) + _prod(az, bx) - 1j * (_prod(ay, bz) + _prod(az, by))), # (-1)
83
+ 0.5 * (_prod(ax, bx) - _prod(ay, by) - 1j * (_prod(ax, by) + _prod(ay, bx))), # (-2)
84
+ ]
85
+
86
+ return T0, T1, T2
87
+
88
+
89
+ # ---------------------------------------------------------------------------
90
+ # magint — IST decomposition of spin Hamiltonian
91
+ # ---------------------------------------------------------------------------
92
+
93
+ def magint(
94
+ spins: list[float],
95
+ g: np.ndarray,
96
+ CenterField: float,
97
+ A: np.ndarray | None = None,
98
+ D: np.ndarray | None = None,
99
+ gn: list[float] | None = None,
100
+ nuc_spins: list[float] | None = None,
101
+ include_nuc_zeeman: bool = False,
102
+ ) -> tuple[dict, dict]:
103
+ """Decompose spin Hamiltonian interactions into IST components.
104
+
105
+ Port of EasySpin's ``private/magint.m``.
106
+
107
+ Parameters
108
+ ----------
109
+ spins:
110
+ List of spin quantum numbers [S1, ..., I1, ...].
111
+ g:
112
+ g-tensor, shape (nElectrons, 3) or (3,) for single electron.
113
+ CenterField:
114
+ Center magnetic field in mT.
115
+ A:
116
+ Hyperfine tensor principal values in MHz, shape (nNuclei, 3) or (3,).
117
+ D:
118
+ ZFS tensor principal values in MHz, shape (nElectrons, 3).
119
+ gn:
120
+ Nuclear g-values.
121
+ nuc_spins:
122
+ Nuclear spin quantum numbers.
123
+ include_nuc_zeeman:
124
+ Whether to include nuclear Zeeman interaction.
125
+
126
+ Returns
127
+ -------
128
+ T:
129
+ dict with 'T0' (list of rank-0 ops), 'T2' (list of 5-element lists of rank-2 ops)
130
+ F:
131
+ dict with 'F0' (array of rank-0 spatial), 'F2' (array of rank-2 spatial, shape (nInt, 5))
132
+ """
133
+ g = np.atleast_2d(np.asarray(g, dtype=float))
134
+ nElSpins = g.shape[0]
135
+
136
+ # Count interactions
137
+ nNuclei = 0
138
+ if nuc_spins is not None:
139
+ nNuclei = len(nuc_spins)
140
+
141
+ # Build spin operators for all spins
142
+ SpinOps = []
143
+ for iSpin in range(len(spins)):
144
+ Sx = sop(spins, [iSpin + 1, 1]).numpy()
145
+ Sy = sop(spins, [iSpin + 1, 2]).numpy()
146
+ Sz = sop(spins, [iSpin + 1, 3]).numpy()
147
+ SpinOps.append([Sx, Sy, Sz])
148
+
149
+ # B0 field vector (z-direction)
150
+ B0 = [0.0, 0.0, CenterField / 1e3] # mT → T
151
+
152
+ T0_list = []
153
+ T2_list = []
154
+ F0_list = []
155
+ F2_list = []
156
+
157
+ # --- Electron Zeeman: muB * B * g * S / h ---
158
+ for iEl in range(nElSpins):
159
+ g_mat = np.diag(g[iEl, :])
160
+ # IST operators: istotensor(B0, S)
161
+ T0, T1, T2 = istotensor(B0, SpinOps[iEl])
162
+ T0_list.append(T0)
163
+ T2_list.append(T2)
164
+ # Spatial components: tensor_cart2sph(g * bmagn / planck) in Hz
165
+ g_scaled = g_mat * (BMAGN / PLANCK) # Hz/T
166
+ F0_scalar = _cart2sph_rank0(g_scaled)
167
+ F2_vec = _tensor_cart2sph_rank2(g_scaled) # 5-element
168
+ F0_list.append(F0_scalar)
169
+ F2_list.append(F2_vec)
170
+
171
+ # --- Hyperfine: S * A * I ---
172
+ if A is not None and nNuclei > 0:
173
+ A = np.atleast_2d(np.asarray(A, dtype=float))
174
+ for iEl in range(nElSpins):
175
+ for iNuc in range(nNuclei):
176
+ A_mat = np.diag(A[iNuc, :]) * 1e6 # MHz → Hz
177
+ S_ops = SpinOps[iEl]
178
+ I_ops = SpinOps[nElSpins + iNuc]
179
+ T0, T1, T2 = istotensor(S_ops, I_ops)
180
+ T0_list.append(T0)
181
+ T2_list.append(T2)
182
+ F0_scalar = _cart2sph_rank0(A_mat)
183
+ F2_vec = _tensor_cart2sph_rank2(A_mat)
184
+ F0_list.append(F0_scalar)
185
+ F2_list.append(F2_vec)
186
+
187
+ # --- ZFS: S * D * S ---
188
+ if D is not None:
189
+ D = np.atleast_2d(np.asarray(D, dtype=float))
190
+ for iEl in range(nElSpins):
191
+ if spins[iEl] < 1:
192
+ continue
193
+ D_mat = np.diag(D[iEl, :]) * 1e6 # MHz → Hz
194
+ T0, T1, T2 = istotensor(SpinOps[iEl], SpinOps[iEl])
195
+ T0_list.append(T0)
196
+ T2_list.append(T2)
197
+ F0_scalar = _cart2sph_rank0(D_mat)
198
+ F2_vec = _tensor_cart2sph_rank2(D_mat)
199
+ F0_list.append(F0_scalar)
200
+ F2_list.append(F2_vec)
201
+
202
+ # --- Nuclear Zeeman ---
203
+ if include_nuc_zeeman and gn is not None:
204
+ for iNuc in range(nNuclei):
205
+ I_ops = SpinOps[nElSpins + iNuc]
206
+ T0, T1, T2 = istotensor(B0, I_ops)
207
+ T0_list.append(T0)
208
+ T2_list.append(T2)
209
+ gn_scaled = -gn[iNuc] * NMAGN / PLANCK # Hz/T (scalar → isotropic tensor)
210
+ F0_list.append(_cart2sph_rank0(gn_scaled * np.eye(3)))
211
+ F2_list.append(_tensor_cart2sph_rank2(gn_scaled * np.eye(3)))
212
+
213
+ F0 = np.array(F0_list, dtype=complex)
214
+ F2 = np.array(F2_list, dtype=complex) if len(F2_list) > 0 else np.zeros((0, 5), dtype=complex)
215
+
216
+ return (
217
+ {'T0': T0_list, 'T2': T2_list},
218
+ {'F0': F0, 'F2': F2},
219
+ )
220
+
221
+
222
+ def _tensor_cart2sph_rank2(T: np.ndarray) -> np.ndarray:
223
+ """Convert 3x3 Cartesian tensor to rank-2 spherical tensor components.
224
+
225
+ Uses the Mehring convention (same as EasySpin's tensor_cart2sph.m):
226
+ T2(0) = sqrt(2/3) * (Tzz - 0.5*(Txx + Tyy))
227
+
228
+ Returns [T2(+2), T2(+1), T2(0), T2(-1), T2(-2)].
229
+ """
230
+ T = np.asarray(T, dtype=complex)
231
+ Txx, Txy, Txz = T[0, 0], T[0, 1], T[0, 2]
232
+ Tyx, Tyy, Tyz = T[1, 0], T[1, 1], T[1, 2]
233
+ Tzx, Tzy, Tzz = T[2, 0], T[2, 1], T[2, 2]
234
+
235
+ T2p2 = 0.5 * ((Txx - Tyy) + 1j * (Txy + Tyx))
236
+ T2p1 = -0.5 * ((Txz + Tzx) + 1j * (Tyz + Tzy))
237
+ T20 = np.sqrt(2 / 3) * (Tzz - 0.5 * (Txx + Tyy))
238
+ T2m1 = 0.5 * ((Txz + Tzx) - 1j * (Tyz + Tzy))
239
+ T2m2 = 0.5 * ((Txx - Tyy) - 1j * (Txy + Tyx))
240
+
241
+ return np.array([T2p2, T2p1, T20, T2m1, T2m2])
242
+
243
+
244
+ def _cart2sph_rank0(T: np.ndarray) -> complex:
245
+ """Extract rank-0 (isotropic) component from a 3x3 tensor.
246
+
247
+ F0 = -(1/sqrt(3)) * Tr(T)
248
+ """
249
+ T = np.asarray(T, dtype=complex)
250
+ return -(1 / np.sqrt(3)) * np.trace(T)
251
+
252
+
253
+ # ---------------------------------------------------------------------------
254
+ # wigD — rank-2 Wigner D-matrices from quaternion trajectories
255
+ # ---------------------------------------------------------------------------
256
+
257
+ def wigD(q: np.ndarray) -> np.ndarray:
258
+ """Compute rank-2 Wigner D-matrices from quaternion trajectories.
259
+
260
+ Port of the ``wigD`` subfunction in EasySpin's ``cardamom_propagatedm.m``.
261
+
262
+ Parameters
263
+ ----------
264
+ q:
265
+ Quaternion trajectory, shape ``(4, nSteps, nTraj)`` or ``(4, nSteps)``.
266
+ Convention: q = [q0, q1, q2, q3] where q0 is scalar part.
267
+
268
+ Returns
269
+ -------
270
+ D2:
271
+ Rank-2 Wigner D-matrix trajectory, shape ``(5, 5, nSteps, nTraj)``.
272
+ Indices correspond to m' = +2, +1, 0, -1, -2 (rows)
273
+ and m = +2, +1, 0, -1, -2 (columns).
274
+ """
275
+ if q.ndim == 2:
276
+ q = q[:, :, np.newaxis]
277
+
278
+ # Quaternion components → Cayley-Klein parameters
279
+ A = q[0] - 1j * q[3]
280
+ B = -q[2] - 1j * q[1]
281
+ Ast = q[0] + 1j * q[3]
282
+ Bst = -q[2] + 1j * q[1]
283
+
284
+ Z = A * Ast - B * Bst
285
+
286
+ ASq = A * A
287
+ BSq = B * B
288
+ AstSq = Ast * Ast
289
+ BstSq = Bst * Bst
290
+
291
+ nSteps = q.shape[1]
292
+ nTraj = q.shape[2]
293
+ D2 = np.zeros((5, 5, nSteps, nTraj), dtype=complex)
294
+
295
+ sqrt6 = np.sqrt(6)
296
+
297
+ D2[0, 0] = ASq * ASq
298
+ D2[0, 1] = 2 * A**3 * B
299
+ D2[0, 2] = sqrt6 * ASq * BSq
300
+ D2[0, 3] = 2 * A * B**3
301
+ D2[0, 4] = BSq * BSq
302
+
303
+ D2[1, 0] = -2 * A**3 * Bst
304
+ D2[1, 1] = ASq * (2 * Z - 1)
305
+ D2[1, 2] = sqrt6 * A * B * Z
306
+ D2[1, 3] = BSq * (2 * Z + 1)
307
+ D2[1, 4] = 2 * Ast * B**3
308
+
309
+ D2[2, 0] = sqrt6 * ASq * BstSq
310
+ D2[2, 1] = -sqrt6 * A * Bst * Z
311
+ D2[2, 2] = 0.5 * (3 * Z**2 - 1)
312
+ D2[2, 3] = sqrt6 * Ast * B * Z
313
+ D2[2, 4] = sqrt6 * AstSq * BSq
314
+
315
+ D2[3, 0] = -2 * A * Bst**3
316
+ D2[3, 1] = BstSq * (2 * Z + 1)
317
+ D2[3, 2] = -sqrt6 * Ast * Bst * Z
318
+ D2[3, 3] = AstSq * (2 * Z - 1)
319
+ D2[3, 4] = 2 * Ast**3 * B
320
+
321
+ D2[4, 0] = BstSq * BstSq
322
+ D2[4, 1] = -2 * Ast * Bst**3
323
+ D2[4, 2] = sqrt6 * AstSq * BstSq
324
+ D2[4, 3] = -2 * Ast**3 * Bst
325
+ D2[4, 4] = AstSq * AstSq
326
+
327
+ return D2