pyTRACTnmr 0.1.1b1__py3-none-any.whl → 0.1.2b1__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.
- pyTRACTnmr/processing.py +197 -32
- pyTRACTnmr/window.py +193 -37
- pytractnmr-0.1.2b1.dist-info/METADATA +117 -0
- pytractnmr-0.1.2b1.dist-info/RECORD +10 -0
- pytractnmr-0.1.2b1.dist-info/licenses/LICENSE +674 -0
- pytractnmr-0.1.1b1.dist-info/METADATA +0 -15
- pytractnmr-0.1.1b1.dist-info/RECORD +0 -9
- {pytractnmr-0.1.1b1.dist-info → pytractnmr-0.1.2b1.dist-info}/WHEEL +0 -0
- {pytractnmr-0.1.1b1.dist-info → pytractnmr-0.1.2b1.dist-info}/entry_points.txt +0 -0
pyTRACTnmr/processing.py
CHANGED
|
@@ -4,6 +4,7 @@ import nmrglue as ng # type: ignore
|
|
|
4
4
|
from scipy.optimize import curve_fit
|
|
5
5
|
from typing import Optional, Tuple, List, Dict
|
|
6
6
|
import logging
|
|
7
|
+
from typing_extensions import deprecated
|
|
7
8
|
|
|
8
9
|
# Configure logging
|
|
9
10
|
logging.basicConfig(level=logging.INFO)
|
|
@@ -25,6 +26,15 @@ class TractBruker:
|
|
|
25
26
|
CSA_BOND_ANGLE = 17 * np.pi / 180
|
|
26
27
|
|
|
27
28
|
def __init__(self, exp_folder: str, delay_list: Optional[str] = None) -> None:
|
|
29
|
+
"""_summary_
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
exp_folder (str): Path to the Bruker experiment folder.
|
|
33
|
+
delay_list (Optional[str], optional): Path to the delay list file. Defaults to None.
|
|
34
|
+
|
|
35
|
+
Raises:
|
|
36
|
+
ValueError: If the experiment cannot be loaded.
|
|
37
|
+
"""
|
|
28
38
|
logger.info(f"Initializing TractBruker with folder: {exp_folder}")
|
|
29
39
|
|
|
30
40
|
try:
|
|
@@ -47,10 +57,7 @@ class TractBruker:
|
|
|
47
57
|
if os.path.exists(vdlist_path):
|
|
48
58
|
self.delays = self._read_delays(vdlist_path)
|
|
49
59
|
else:
|
|
50
|
-
|
|
51
|
-
# Assuming interleaved alpha/beta, so 2 FIDs per delay point
|
|
52
|
-
n_delays = self.fids.shape[1] // 2
|
|
53
|
-
self.delays = np.linspace(0.01, 1.0, n_delays)
|
|
60
|
+
raise ValueError("No delay list found (vdlist) and no external list provided.")
|
|
54
61
|
|
|
55
62
|
self.alpha_spectra: List[np.ndarray] = []
|
|
56
63
|
self.beta_spectra: List[np.ndarray] = []
|
|
@@ -59,24 +66,37 @@ class TractBruker:
|
|
|
59
66
|
self.unit_converter = None
|
|
60
67
|
|
|
61
68
|
def _read_delays(self, file: str) -> np.ndarray:
|
|
69
|
+
"""Uitility function for reading vdlist file and converting it to numpy ndarray
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
file (str): Path to the vdlist file
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
np.ndarray: Numpy array condaining the delays in seconds.
|
|
76
|
+
"""
|
|
62
77
|
with open(file, "r") as list_file:
|
|
63
78
|
delays = list_file.read()
|
|
64
79
|
delays = delays.replace("u", "e-6").replace("m", "e-3")
|
|
65
80
|
return np.array([float(x) for x in delays.splitlines() if x.strip()])
|
|
66
81
|
|
|
67
|
-
def
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
82
|
+
def _get_lb_val(self, lb: float) -> float:
|
|
83
|
+
"""Calculate normalized line broadening value."""
|
|
84
|
+
try:
|
|
85
|
+
sw = self.attributes["acqus"]["SW_h"]
|
|
86
|
+
return lb / sw
|
|
87
|
+
except KeyError, ZeroDivisionError:
|
|
88
|
+
return lb
|
|
89
|
+
|
|
90
|
+
def _process_single_fid(
|
|
91
|
+
self, fid, p0, p1, points, apod_func, lb_val, off, end, pow, nodes
|
|
75
92
|
) -> np.ndarray:
|
|
76
|
-
"""
|
|
77
|
-
fid = self.fids[0, 0]
|
|
93
|
+
"""Internal helper to process a single FID."""
|
|
78
94
|
# Apply apodization
|
|
79
|
-
|
|
95
|
+
if apod_func == "em":
|
|
96
|
+
data = ng.proc_base.em(fid, lb=lb_val)
|
|
97
|
+
else:
|
|
98
|
+
data = ng.proc_base.sp(fid, off=off, end=end, pow=pow)
|
|
99
|
+
|
|
80
100
|
# Zero filling
|
|
81
101
|
data = ng.proc_base.zf_size(data, points)
|
|
82
102
|
# Fourier transform
|
|
@@ -89,6 +109,42 @@ class TractBruker:
|
|
|
89
109
|
data = ng.proc_base.di(data)
|
|
90
110
|
# Reverse spectrum
|
|
91
111
|
data = ng.proc_base.rev(data)
|
|
112
|
+
if nodes is not None and len(nodes) > 1:
|
|
113
|
+
data = ng.proc_bl.base(data, nodes)
|
|
114
|
+
return data
|
|
115
|
+
|
|
116
|
+
def process_first_trace(
|
|
117
|
+
self,
|
|
118
|
+
p0: float,
|
|
119
|
+
p1: float,
|
|
120
|
+
points: int = 2048,
|
|
121
|
+
apod_func: str = "sp",
|
|
122
|
+
lb: float = 0.0,
|
|
123
|
+
off: float = 0.35,
|
|
124
|
+
end: float = 0.98,
|
|
125
|
+
pow: float = 2.0,
|
|
126
|
+
nodes=None,
|
|
127
|
+
) -> np.ndarray:
|
|
128
|
+
"""Process the first plane in the Psuedo-2D experiment. This is useful for phase correction
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
p0 (float): Zeroth order phase correction
|
|
132
|
+
p1 (float): First order phase correction
|
|
133
|
+
points (int, optional): Zero filling points. Defaults to 2048.
|
|
134
|
+
apod_func (str, optional): Apodization function to use. Only "sp" and "em" are supported. Defaults to "sp".
|
|
135
|
+
lb (float, optional): Line broadening in Hz (only for em). Defaults to 0.0.
|
|
136
|
+
off (float, optional): Offset for sp apodization. Defaults to 0.35.
|
|
137
|
+
end (float, optional): End of sp apodization. Defaults to 0.98.
|
|
138
|
+
pow (float, optional): Power for sp apodization. Defaults to 2.0.
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
np.ndarray: Fourier transformed spectrum containng only the real part.
|
|
142
|
+
"""
|
|
143
|
+
fid = self.fids[0, 0]
|
|
144
|
+
lb_val = self._get_lb_val(lb) if apod_func == "em" else 0.0
|
|
145
|
+
data = self._process_single_fid(
|
|
146
|
+
fid, p0, p1, points, apod_func, lb_val, off, end, pow, nodes
|
|
147
|
+
)
|
|
92
148
|
|
|
93
149
|
# Set up unit converter
|
|
94
150
|
udic = ng.bruker.guess_udic(self.attributes, data)
|
|
@@ -100,30 +156,46 @@ class TractBruker:
|
|
|
100
156
|
p0: float,
|
|
101
157
|
p1: float,
|
|
102
158
|
points: int = 2048,
|
|
159
|
+
apod_func: str = "sp",
|
|
160
|
+
lb: float = 0.0,
|
|
103
161
|
off: float = 0.35,
|
|
104
162
|
end: float = 0.98,
|
|
105
163
|
pow: float = 2.0,
|
|
164
|
+
nodes=None,
|
|
106
165
|
) -> None:
|
|
107
|
-
"""
|
|
166
|
+
"""The primary function for processing the Pusedo-2D experiment. This splits the data into alpha and beta state and perform the basic processing of the raw FIDs
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
p0 (float): Zeroth order phase correction.
|
|
170
|
+
p1 (float): First order phase correction.
|
|
171
|
+
points (int, optional): Zero filling points. Defaults to 2048.
|
|
172
|
+
apod_func (str, optional): Apodization function to use. Only "sp" and "em" are supported. Defaults to "sp".
|
|
173
|
+
lb (float, optional): Line broadening in Hz (only for em). Defaults to 0.0.
|
|
174
|
+
off (float, optional): Offset for sp apodization. Defaults to 0.35.
|
|
175
|
+
end (float, optional): End of sp apodization. Defaults to 0.98.
|
|
176
|
+
pow (float, optional): Power for sp apodization. Defaults to 2.0.
|
|
177
|
+
"""
|
|
108
178
|
self.phc0 = p0
|
|
109
179
|
self.phc1 = p1
|
|
110
180
|
self.alpha_spectra = []
|
|
111
181
|
self.beta_spectra = []
|
|
112
182
|
|
|
183
|
+
lb_val = self._get_lb_val(lb) if apod_func == "em" else 0.0
|
|
184
|
+
|
|
113
185
|
for i in range(self.fids.shape[0]):
|
|
114
186
|
for j in range(self.fids[i].shape[0]):
|
|
115
|
-
data = self.
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
187
|
+
data = self._process_single_fid(
|
|
188
|
+
self.fids[i][j],
|
|
189
|
+
p0,
|
|
190
|
+
p1,
|
|
191
|
+
points,
|
|
192
|
+
apod_func,
|
|
193
|
+
lb_val,
|
|
194
|
+
off,
|
|
195
|
+
end,
|
|
196
|
+
pow,
|
|
197
|
+
nodes,
|
|
121
198
|
)
|
|
122
|
-
data = ng.proc_base.ps(data, p0=p0, p1=p1)
|
|
123
|
-
data = ng.proc_base.di(data)
|
|
124
|
-
data = ng.proc_bl.baseline_corrector(data)
|
|
125
|
-
data = ng.proc_base.rev(data)
|
|
126
|
-
|
|
127
199
|
if j % 2 == 0:
|
|
128
200
|
self.beta_spectra.append(data)
|
|
129
201
|
else:
|
|
@@ -134,8 +206,18 @@ class TractBruker:
|
|
|
134
206
|
udic = ng.bruker.guess_udic(self.attributes, self.beta_spectra[0])
|
|
135
207
|
self.unit_converter = ng.fileiobase.uc_from_udic(udic)
|
|
136
208
|
|
|
209
|
+
@deprecated("Use integrate_ppm() instead")
|
|
137
210
|
def integrate_indices(self, start_idx: int, end_idx: int) -> None:
|
|
138
|
-
"""
|
|
211
|
+
"""Intergrate the specified region in the spectra. This accounts for all the alpha and beta spectrum collected.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
start_idx (int): Start index for integration.
|
|
215
|
+
end_idx (int): End index for integration.
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
Raises:
|
|
219
|
+
RuntimeError: If no spectra are available. Run split_process() first.
|
|
220
|
+
"""
|
|
139
221
|
if not self.alpha_spectra or not self.beta_spectra:
|
|
140
222
|
raise RuntimeError("No spectra available. Run split_process() first.")
|
|
141
223
|
|
|
@@ -147,7 +229,16 @@ class TractBruker:
|
|
|
147
229
|
)
|
|
148
230
|
|
|
149
231
|
def integrate_ppm(self, start_ppm: float, end_ppm: float) -> None:
|
|
150
|
-
"""Integrate
|
|
232
|
+
"""Integrate the specified region in all the extracted spectras.
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
start_ppm (float): Start index for integration in ppm.
|
|
236
|
+
end_ppm (float): End index for integration in ppm.
|
|
237
|
+
|
|
238
|
+
Raises:
|
|
239
|
+
RuntimeError: If no spectra are available. Run split_process() first.
|
|
240
|
+
RuntimeError: If no unit converter is available. Run split_process() first.
|
|
241
|
+
"""
|
|
151
242
|
if self.unit_converter is None:
|
|
152
243
|
raise RuntimeError("Unit converter not initialized.")
|
|
153
244
|
|
|
@@ -159,10 +250,26 @@ class TractBruker:
|
|
|
159
250
|
self.integrate_indices(start, end)
|
|
160
251
|
|
|
161
252
|
@staticmethod
|
|
162
|
-
def _relax(x, a, r):
|
|
253
|
+
def _relax(x: np.ndarray[np.float64], a: float, r: float) -> np.ndarray[np.float64]:
|
|
254
|
+
"""Internal function for exponential decay
|
|
255
|
+
|
|
256
|
+
Args:
|
|
257
|
+
x (np.ndarray): X values
|
|
258
|
+
a (float): Amplitude
|
|
259
|
+
r (float): Decay rate
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
float | np.ndarray: Y values
|
|
263
|
+
"""
|
|
163
264
|
return a * np.exp(-r * x)
|
|
164
265
|
|
|
165
266
|
def calc_relaxation(self) -> None:
|
|
267
|
+
"""Calculate the Relaxation rates for alpha and beta states. This function does not return any values but sets
|
|
268
|
+
|
|
269
|
+
Raises:
|
|
270
|
+
RuntimeError: If no integrals are available. Run integrate_ppm() first.
|
|
271
|
+
RuntimeError: If fitting fails.
|
|
272
|
+
"""
|
|
166
273
|
if self.alpha_integrals is None or self.beta_integrals is None:
|
|
167
274
|
raise RuntimeError("Must call integrate() before calc_relaxation()")
|
|
168
275
|
|
|
@@ -192,6 +299,19 @@ class TractBruker:
|
|
|
192
299
|
self.err_Rb: float = np.sqrt(np.diag(self.pcov_beta))[1]
|
|
193
300
|
|
|
194
301
|
def _tc_equation(self, w_N: float, c: float, S2: float = 1.0) -> float:
|
|
302
|
+
"""Function for calculating the Rotational Correlation Time. The equation is are adapted from eq. 15 of:
|
|
303
|
+
'TRACT revisited: an algebraic solution for determining overall rotational correlation times from cross-correlated relaxation rates'
|
|
304
|
+
PMID: 34480265
|
|
305
|
+
doi: 10.1007/s10858-021-00379-5
|
|
306
|
+
|
|
307
|
+
Args:
|
|
308
|
+
w_N (float): Larmor Frequency of Nitrogen atom.
|
|
309
|
+
c (float): Constant derived from the relaxation rate of the alpha state and beta state.
|
|
310
|
+
S2 (float, optional): Square of the order parameter. Defaults to 1.0.
|
|
311
|
+
|
|
312
|
+
Returns:
|
|
313
|
+
float: Rotational Correlation Time in ns.
|
|
314
|
+
"""
|
|
195
315
|
t1 = (5 * c) / (24 * S2)
|
|
196
316
|
A = 336 * (S2**2) * (w_N**2)
|
|
197
317
|
B = 25 * (c**2) * (w_N**4)
|
|
@@ -209,6 +329,13 @@ class TractBruker:
|
|
|
209
329
|
def calc_tc(
|
|
210
330
|
self, B0: Optional[float] = None, S2: float = 1.0, n_bootstrap: int = 1000
|
|
211
331
|
) -> None:
|
|
332
|
+
"""Calculate Rotational Correlation Time by using bootstraping. The Relaxation rates are resampled based on the error estimates derived from the covaraince matrix.
|
|
333
|
+
|
|
334
|
+
Args:
|
|
335
|
+
B0 (Optional[float], optional): Magnetic field in MHz. Defaults to None.
|
|
336
|
+
S2 (float, optional): Square of the Order parameter. Defaults to 1.0.
|
|
337
|
+
n_bootstrap (int, optional): Number of bootstrap samples. Defaults to 1000.
|
|
338
|
+
"""
|
|
212
339
|
if not hasattr(self, "Ra"):
|
|
213
340
|
self.calc_relaxation()
|
|
214
341
|
if B0 is None:
|
|
@@ -233,11 +360,49 @@ class TractBruker:
|
|
|
233
360
|
self.tau_c = np.mean(tau_samples)
|
|
234
361
|
self.err_tau_c = np.std(tau_samples)
|
|
235
362
|
|
|
363
|
+
def calc_confidence_interval(
|
|
364
|
+
self, x: np.ndarray, popt: np.ndarray, pcov: np.ndarray
|
|
365
|
+
) -> np.ndarray:
|
|
366
|
+
"""Calculate 95% confidence interval for the exponential decay."""
|
|
367
|
+
A, R = popt
|
|
368
|
+
# Gradient of f(x) = A * exp(-R * x)
|
|
369
|
+
# df/dA = exp(-R * x)
|
|
370
|
+
# df/dR = -A * x * exp(-R * x)
|
|
371
|
+
df_dA = np.exp(-R * x)
|
|
372
|
+
df_dR = -A * x * np.exp(-R * x)
|
|
373
|
+
|
|
374
|
+
J = np.stack([df_dA, df_dR], axis=1)
|
|
375
|
+
|
|
376
|
+
# sigma^2 = diag(J @ pcov @ J.T)
|
|
377
|
+
sigma2 = np.sum((J @ pcov) * J, axis=1)
|
|
378
|
+
return 1.96 * np.sqrt(sigma2)
|
|
379
|
+
|
|
236
380
|
def get_fit_data(
|
|
237
381
|
self,
|
|
238
|
-
) -> Tuple[
|
|
382
|
+
) -> Tuple[
|
|
383
|
+
np.ndarray,
|
|
384
|
+
np.ndarray,
|
|
385
|
+
np.ndarray,
|
|
386
|
+
np.ndarray,
|
|
387
|
+
np.ndarray,
|
|
388
|
+
np.ndarray,
|
|
389
|
+
np.ndarray,
|
|
390
|
+
]:
|
|
391
|
+
"""Returns the fit data for alpha and beta states
|
|
392
|
+
|
|
393
|
+
Returns:
|
|
394
|
+
Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]: (Delays (s), Ratios of alpha state, Ratios of beta state, optimized parameters for alpha state, optimized parameters for beta state, cov matrix alpha, cov matrix beta)
|
|
395
|
+
"""
|
|
239
396
|
n_pts = min(len(self.alpha_integrals), len(self.delays))
|
|
240
397
|
x = self.delays[:n_pts]
|
|
241
398
|
y_a = self.alpha_integrals[:n_pts] / self.alpha_integrals[0]
|
|
242
399
|
y_b = self.beta_integrals[:n_pts] / self.beta_integrals[0]
|
|
243
|
-
return
|
|
400
|
+
return (
|
|
401
|
+
x,
|
|
402
|
+
y_a,
|
|
403
|
+
y_b,
|
|
404
|
+
self.popt_alpha,
|
|
405
|
+
self.popt_beta,
|
|
406
|
+
self.pcov_alpha,
|
|
407
|
+
self.pcov_beta,
|
|
408
|
+
)
|