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 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
- logger.warning("No delay list found. Using dummy delays.")
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 process_first_trace(
68
- self,
69
- p0: float,
70
- p1: float,
71
- points: int = 2048,
72
- off: float = 0.35,
73
- end: float = 0.98,
74
- pow: float = 2.0,
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
- """Process first FID for interactive phase correction."""
77
- fid = self.fids[0, 0]
93
+ """Internal helper to process a single FID."""
78
94
  # Apply apodization
79
- data = ng.proc_base.sp(fid, off=off, end=end, pow=pow)
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
- """Process all FIDs and split into alpha/beta."""
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.fids[i][j]
116
- data = ng.proc_base.sp(data, off=off, end=end, pow=pow)
117
- data = ng.proc_base.zf_size(data, points)
118
- data = ng.proc_base.fft(data)
119
- data = ng.bruker.remove_digital_filter(
120
- self.attributes, data, post_proc=True
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
- """Integrate using point indices."""
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 using ppm range."""
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[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
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 x, y_a, y_b, self.popt_alpha, self.popt_beta
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
+ )