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
@@ -0,0 +1,274 @@
1
+ """Steady-state Bloch equation solutions under field modulation.
2
+
3
+ Port of EasySpin's ``blochsteady.m``.
4
+
5
+ Computes the periodic steady-state solution of the Bloch equations for a
6
+ single S=1/2 spin in the presence of CW microwave irradiation and a sinusoidal
7
+ field modulation. The solution is obtained in the frequency domain via a
8
+ sparse pentadiagonal linear system, then converted to the time domain by
9
+ inverse Fourier transform.
10
+
11
+ References
12
+ ----------
13
+ Tseitlin, Eaton, Eaton, Appl. Magn. Reson. 44, 1373–1379 (2013).
14
+ https://doi.org/10.1007/s00723-013-0494-2
15
+ """
16
+ from __future__ import annotations
17
+
18
+ import math
19
+ from dataclasses import dataclass, field
20
+ from typing import Optional, Tuple
21
+
22
+ import numpy as np
23
+ import scipy.sparse as sp
24
+ from scipy.sparse.linalg import spsolve
25
+
26
+ from torchspin.constants import BMAGN, PLANCK
27
+
28
+ # ħ = h / (2π)
29
+ _HBAR = PLANCK / (2.0 * math.pi)
30
+
31
+
32
+ @dataclass
33
+ class BlochOptions:
34
+ """Computational options for :func:`blochsteady`.
35
+
36
+ Parameters
37
+ ----------
38
+ kmax:
39
+ Maximum Fourier order. ``None`` = auto-determined from T2 and modulation
40
+ depth (recommended).
41
+ nPoints:
42
+ Number of output time points. ``None`` = auto (2·kmax − 1).
43
+ method:
44
+ Time-domain reconstruction method.
45
+ ``'fft'`` (default) — inverse FFT (faster).
46
+ ``'td'`` — explicit complex-exponential sum (slower, for verification).
47
+ """
48
+ kmax: Optional[int] = None
49
+ nPoints: Optional[int] = None
50
+ method: str = 'fft'
51
+
52
+
53
+ def blochsteady(
54
+ g: float,
55
+ T1: float,
56
+ T2: float,
57
+ DeltaB0: float,
58
+ B1: float,
59
+ modAmp: float,
60
+ modFreq: float,
61
+ opt: Optional[BlochOptions] = None,
62
+ ) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
63
+ """Compute the steady-state Bloch magnetization under sinusoidal field modulation.
64
+
65
+ Parameters
66
+ ----------
67
+ g:
68
+ Electron g-value (scalar, S=1/2).
69
+ T1:
70
+ Longitudinal relaxation time, µs.
71
+ T2:
72
+ Transverse relaxation time, µs. Must satisfy T2 ≤ T1.
73
+ DeltaB0:
74
+ Static field offset from resonance, mT. Positive = above resonance.
75
+ B1:
76
+ Microwave field amplitude (rotating frame), mT.
77
+ modAmp:
78
+ Peak-to-peak modulation field amplitude, mT.
79
+ modFreq:
80
+ Modulation frequency, kHz.
81
+ opt:
82
+ :class:`BlochOptions`. ``None`` uses defaults.
83
+
84
+ Returns
85
+ -------
86
+ t : ndarray, shape (nPoints,)
87
+ Time axis over one modulation period, µs.
88
+ Mx : ndarray, shape (nPoints,)
89
+ Dispersion (in-phase) component of transverse magnetization.
90
+ My : ndarray, shape (nPoints,)
91
+ Absorption (quadrature) component of transverse magnetization.
92
+ Mz : ndarray, shape (nPoints,)
93
+ Longitudinal magnetization.
94
+
95
+ Notes
96
+ -----
97
+ The field modulation is Bmod(t) = (modAmp/2) · cos(ωm·t), so modAmp is the
98
+ peak-to-peak amplitude. The steady-state Mx, My, Mz are periodic with
99
+ period T = 1/modFreq.
100
+
101
+ M0 (equilibrium magnetization) is normalized to 1.
102
+
103
+ Examples
104
+ --------
105
+ >>> from torchspin.blochsteady import blochsteady
106
+ >>> t, Mx, My, Mz = blochsteady(g=2.0, T1=10.0, T2=1.0,
107
+ ... DeltaB0=0.0, B1=0.01,
108
+ ... modAmp=0.5, modFreq=100.0)
109
+ """
110
+ if opt is None:
111
+ opt = BlochOptions()
112
+
113
+ # ── Input checks ─────────────────────────────────────────────────────────
114
+ if not np.isscalar(g) or g <= 0:
115
+ raise ValueError("blochsteady: g must be a single positive number.")
116
+ if T1 <= 0:
117
+ raise ValueError("blochsteady: T1 must be positive.")
118
+ if T2 <= 0:
119
+ raise ValueError("blochsteady: T2 must be positive.")
120
+ if T2 > T1:
121
+ raise ValueError("blochsteady: T2 cannot exceed T1.")
122
+ if B1 <= 0:
123
+ raise ValueError("blochsteady: B1 must be positive.")
124
+ if modAmp <= 0:
125
+ raise ValueError("blochsteady: modAmp must be positive.")
126
+ if modFreq <= 0:
127
+ raise ValueError("blochsteady: modFreq must be positive.")
128
+
129
+ # ── Unit conversions ──────────────────────────────────────────────────────
130
+ T1_s = T1 * 1e-6 # µs → s
131
+ T2_s = T2 * 1e-6 # µs → s
132
+ dB0_T = DeltaB0 * 1e-3 # mT → T
133
+ B1_T = B1 * 1e-3 # mT → T
134
+ mAmp_T = modAmp * 1e-3 # mT → T
135
+ mFreq_Hz = modFreq * 1e3 # kHz → Hz
136
+ omegam = 2.0 * math.pi * mFreq_Hz # rad/s
137
+
138
+ M0 = 1.0
139
+ gamma = BMAGN / _HBAR * g # rad / (s·T)
140
+
141
+ # Derived coupling coefficients (frequency domain)
142
+ b_coef = gamma * dB0_T # resonance offset, rad/s
143
+ c_coef = gamma * B1_T # microwave coupling, rad/s
144
+ d_coef = gamma * mAmp_T / 4.0 # modulation coupling (per 3j), rad/s
145
+
146
+ # ── Determine kmax ────────────────────────────────────────────────────────
147
+ if opt.kmax is not None:
148
+ kmax = int(opt.kmax)
149
+ else:
150
+ # Estimate 1: based on maximum field offset within modulation cycle
151
+ max_field_off = max(mAmp_T / 2.0 - abs(dB0_T), abs(dB0_T) + mAmp_T / 2.0)
152
+ max_field_off = min(max_field_off, mAmp_T)
153
+ max_freq_off = (gamma / (2.0 * math.pi)) * max_field_off
154
+ kmax_field = math.ceil(max_freq_off / mFreq_Hz * 1.4)
155
+
156
+ # Estimate 2: based on T2 envelope threshold 1e-6
157
+ kmax_T2 = math.ceil(-math.log(1e-6) / 2.0 * gamma * mAmp_T * T2_s)
158
+
159
+ kmax = max(max(kmax_field, kmax_T2), 20)
160
+
161
+ # ── Build frequency-domain auxiliary arrays ───────────────────────────────
162
+ # k = -(kmax+1), ..., kmax+1 (length 2*kmax+3)
163
+ k_full = np.arange(-(kmax + 1), kmax + 2) # shape (2*kmax+3,)
164
+ a_full = 1j * k_full * omegam # i·k·ωm
165
+
166
+ tau1_full = 1.0 / (a_full + 1.0 / T1_s) # shape (2*kmax+3,)
167
+ tau2_full = 1.0 / (a_full + 1.0 / T2_s)
168
+
169
+ # First q selection: MATLAB q = (1:2*kmax+1)+1 = 2:2*kmax+2 (1-indexed)
170
+ # → Python indices 1 to 2*kmax+1 (inclusive) = slice(1, 2*kmax+2)
171
+ # These build the nRows = 2*kmax+1 diagonal vectors.
172
+ q = slice(1, 2 * kmax + 2) # main
173
+ qm1 = slice(0, 2 * kmax + 1) # q-1
174
+ qp1 = slice(2, 2 * kmax + 3) # q+1
175
+
176
+ tau2_q = tau2_full[q] # shape (2*kmax+1,)
177
+ tau2_qm1 = tau2_full[qm1]
178
+ tau2_qp1 = tau2_full[qp1]
179
+ tau1_q = tau1_full[q]
180
+ a_q = a_full[q]
181
+
182
+ c2m_v = d_coef**2 * tau2_qm1
183
+ c2p_v = d_coef**2 * tau2_qp1
184
+ c1m_v = b_coef * d_coef * (tau2_q + tau2_qm1)
185
+ c1p_v = b_coef * d_coef * (tau2_q + tau2_qp1)
186
+ c0_v = (a_q + 1.0 / T2_s
187
+ + b_coef**2 * tau2_q
188
+ + c_coef**2 * tau1_q
189
+ + d_coef**2 * (tau2_qm1 + tau2_qp1))
190
+
191
+ # RHS scalar: tau1 at k=0; MATLAB tau1(kmax+2) [1-indexed] = Python tau1_full[kmax+1]
192
+ cL = c_coef * M0 * tau1_full[kmax + 1] / T1_s
193
+
194
+ # ── Assemble pentadiagonal sparse matrix ──────────────────────────────────
195
+ # After MATLAB's column-selection trick P(:, 3:end-2), the diagonals become:
196
+ # c2m_v[2:] on diagonal -2
197
+ # c1m_v[1:] on diagonal -1
198
+ # c0_v on diagonal 0
199
+ # c1p_v[:-1] on diagonal +1
200
+ # c2p_v[:-2] on diagonal +2
201
+ nRows = 2 * kmax + 1
202
+ P = sp.diags(
203
+ [c2m_v[2:], c1m_v[1:], c0_v, c1p_v[:-1], c2p_v[:-2]],
204
+ [-2, -1, 0, 1, 2],
205
+ shape=(nRows, nRows),
206
+ dtype=complex,
207
+ format='csc',
208
+ )
209
+
210
+ C0 = np.zeros(nRows, dtype=complex)
211
+ C0[kmax] = cL # k=0 element sits at index kmax in Y
212
+
213
+ # ── Solve sparse linear system for Y (Fourier coefficients of My) ─────────
214
+ Y = spsolve(P, C0) # shape (nRows,) = (2*kmax+1,)
215
+
216
+ # ── Compute Xk, Yk, Zk (second q selection: MATLAB q = 2:2*kmax) ─────────
217
+ # Python indices into Y: 1 to 2*kmax-1; length 2*kmax-1
218
+ Yk = Y[1: 2 * kmax] # absorption Fourier coefficients
219
+ Y_prv = Y[0: 2 * kmax - 1] # Y(q-1)
220
+ Y_nxt = Y[2: 2 * kmax + 1] # Y(q+1)
221
+
222
+ # tau at q+1 (MATLAB) = Python indices 2..2*kmax of tau2_full
223
+ tau2_q2p1 = tau2_full[2: 2 * kmax + 1] # length 2*kmax-1
224
+ tau1_q2p1 = tau1_full[2: 2 * kmax + 1]
225
+
226
+ Xk = tau2_q2p1 * (b_coef * Yk + d_coef * (Y_prv + Y_nxt))
227
+
228
+ delta_k0 = np.zeros(2 * kmax - 1, dtype=complex)
229
+ delta_k0[kmax - 1] = 1.0 # k=0 position in the 2nd selection
230
+
231
+ Zk = tau1_q2p1 * (-c_coef * Yk + M0 * delta_k0 / T1_s)
232
+
233
+ # ── Time-domain reconstruction ─────────────────────────────────────────────
234
+ npts_fft = len(Yk) # = 2*kmax-1
235
+ npts_out = opt.nPoints if opt.nPoints is not None else npts_fft
236
+
237
+ if opt.method == 'fft':
238
+ if npts_out <= npts_fft:
239
+ My_td = npts_fft * np.real(np.fft.ifft(np.fft.ifftshift(Yk)))
240
+ Mx_td = npts_fft * np.real(np.fft.ifft(np.fft.ifftshift(Xk)))
241
+ Mz_td = npts_fft * np.real(np.fft.ifft(np.fft.ifftshift(Zk)))
242
+ if npts_out < npts_fft:
243
+ t_full = np.linspace(0.0, 1.0 / mFreq_Hz, npts_fft, endpoint=False)
244
+ t_req = np.linspace(0.0, 1.0 / mFreq_Hz, npts_out, endpoint=False)
245
+ My_td = np.interp(t_req, t_full, My_td)
246
+ Mx_td = np.interp(t_req, t_full, Mx_td)
247
+ Mz_td = np.interp(t_req, t_full, Mz_td)
248
+ else:
249
+ # Zero-pad in the middle of the shifted spectrum (MATLAB fill trick)
250
+ def _ft_zeropad(m):
251
+ m_sh = np.fft.ifftshift(m) # k=0 moved to index 0
252
+ n_zeros = npts_out - len(m)
253
+ padded = np.concatenate([m_sh[:kmax],
254
+ np.zeros(n_zeros, dtype=complex),
255
+ m_sh[kmax:]])
256
+ return npts_out * np.real(np.fft.ifft(padded))
257
+
258
+ My_td = _ft_zeropad(Yk)
259
+ Mx_td = _ft_zeropad(Xk)
260
+ Mz_td = _ft_zeropad(Zk)
261
+
262
+ elif opt.method == 'td':
263
+ t_s = np.linspace(0.0, 1.0 / mFreq_Hz, npts_out, endpoint=False)
264
+ k_range = np.arange(-(kmax - 1), kmax) # k = -(kmax-1)..kmax-1
265
+ phases = np.exp(1j * np.outer(k_range * omegam, t_s)) # (2*kmax-1, npts)
266
+ My_td = np.real(Yk @ phases)
267
+ Mx_td = np.real(Xk @ phases)
268
+ Mz_td = np.real(Zk @ phases)
269
+ else:
270
+ raise ValueError(f"blochsteady: unknown method '{opt.method}'; use 'fft' or 'td'.")
271
+
272
+ t_out = np.linspace(0.0, 1.0 / mFreq_Hz, npts_out, endpoint=False) * 1e6 # µs
273
+
274
+ return t_out, Mx_td, My_td, Mz_td