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/blochsteady.py
ADDED
|
@@ -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
|