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/window.py
CHANGED
|
@@ -21,11 +21,13 @@ from PySide6.QtWidgets import (
|
|
|
21
21
|
QHeaderView,
|
|
22
22
|
QMenu,
|
|
23
23
|
QSlider,
|
|
24
|
+
QComboBox,
|
|
24
25
|
)
|
|
25
26
|
from PySide6.QtGui import QAction
|
|
26
27
|
from PySide6.QtCore import Qt, QPoint
|
|
27
28
|
|
|
28
29
|
from matplotlib.widgets import SpanSelector
|
|
30
|
+
|
|
29
31
|
try:
|
|
30
32
|
from .widgets import MplCanvas, CustomNavigationToolbar
|
|
31
33
|
from . import processing
|
|
@@ -41,13 +43,11 @@ class TractApp(QMainWindow):
|
|
|
41
43
|
self.resize(1200, 800)
|
|
42
44
|
|
|
43
45
|
# Data State
|
|
44
|
-
self.dic = None
|
|
45
|
-
self.data = None
|
|
46
|
-
self.proc_data = None
|
|
47
|
-
self.time_points = None
|
|
48
46
|
self.datasets: List[Dict[str, Any]] = []
|
|
49
47
|
self.current_idx: int = -1
|
|
50
48
|
self.selector: Optional[SpanSelector] = None
|
|
49
|
+
self.baseline_nodes: List[float] = []
|
|
50
|
+
self.picking_baseline: bool = False
|
|
51
51
|
|
|
52
52
|
self.init_ui()
|
|
53
53
|
|
|
@@ -70,7 +70,17 @@ class TractApp(QMainWindow):
|
|
|
70
70
|
self.table_data = QTableWidget()
|
|
71
71
|
self.table_data.setColumnCount(9)
|
|
72
72
|
self.table_data.setHorizontalHeaderLabels(
|
|
73
|
-
[
|
|
73
|
+
[
|
|
74
|
+
"Experiment",
|
|
75
|
+
"Temperature (K)",
|
|
76
|
+
"Delays",
|
|
77
|
+
"Ra (Hz)",
|
|
78
|
+
"Rb (Hz)",
|
|
79
|
+
"Tau_C (ns)",
|
|
80
|
+
"Err Ra",
|
|
81
|
+
"Err Rb",
|
|
82
|
+
"Err Tau_C",
|
|
83
|
+
]
|
|
74
84
|
)
|
|
75
85
|
self.table_data.horizontalHeader().setSectionResizeMode(
|
|
76
86
|
QHeaderView.ResizeMode.Stretch
|
|
@@ -101,6 +111,7 @@ class TractApp(QMainWindow):
|
|
|
101
111
|
# Top: Spectrum
|
|
102
112
|
self.canvas_spec = MplCanvas(self)
|
|
103
113
|
self.toolbar_spec = CustomNavigationToolbar(self.canvas_spec, self)
|
|
114
|
+
self.canvas_spec.mpl_connect("button_press_event", self.on_canvas_click)
|
|
104
115
|
widget_spec = QWidget()
|
|
105
116
|
layout_spec = QVBoxLayout()
|
|
106
117
|
lbl_spec = QLabel("<b>Processed Spectrum (Phase Check)</b>")
|
|
@@ -161,6 +172,14 @@ class TractApp(QMainWindow):
|
|
|
161
172
|
self.input_points = QLineEdit("2048")
|
|
162
173
|
self.input_points.editingFinished.connect(self.process_data)
|
|
163
174
|
|
|
175
|
+
self.combo_apod = QComboBox()
|
|
176
|
+
self.combo_apod.addItems(["Sine Bell (sp)", "Exponential (em)"])
|
|
177
|
+
self.combo_apod.currentIndexChanged.connect(self.update_apod_ui)
|
|
178
|
+
self.combo_apod.currentIndexChanged.connect(self.process_data)
|
|
179
|
+
|
|
180
|
+
self.input_lb = QLineEdit("5.0")
|
|
181
|
+
self.input_lb.editingFinished.connect(self.process_data)
|
|
182
|
+
|
|
164
183
|
self.input_off = QLineEdit("0.35")
|
|
165
184
|
self.input_off.editingFinished.connect(self.process_data)
|
|
166
185
|
|
|
@@ -186,15 +205,31 @@ class TractApp(QMainWindow):
|
|
|
186
205
|
self.create_slider_layout(self.slider_p1_fine, self.input_p1),
|
|
187
206
|
)
|
|
188
207
|
layout_t1.addRow(QLabel("<b>Apodization & ZF</b>"))
|
|
208
|
+
layout_t1.addRow("Function:", self.combo_apod)
|
|
189
209
|
layout_t1.addRow("Points (ZF):", self.input_points)
|
|
210
|
+
layout_t1.addRow("Line Broadening (Hz):", self.input_lb)
|
|
190
211
|
layout_t1.addRow("Sine Offset:", self.input_off)
|
|
191
212
|
layout_t1.addRow("Sine End:", self.input_end)
|
|
192
213
|
layout_t1.addRow("Sine Power:", self.input_pow)
|
|
193
214
|
layout_t1.addRow(QLabel("<b>Integration Range</b>"))
|
|
194
215
|
layout_t1.addRow("Start (ppm):", self.input_int_start)
|
|
195
216
|
layout_t1.addRow("End (ppm):", self.input_int_end)
|
|
217
|
+
|
|
218
|
+
layout_t1.addRow(QLabel("<b>Baseline Correction</b>"))
|
|
219
|
+
self.btn_pick_bl = QPushButton("Pick Nodes")
|
|
220
|
+
self.btn_pick_bl.setCheckable(True)
|
|
221
|
+
self.btn_pick_bl.clicked.connect(self.toggle_picking)
|
|
222
|
+
self.btn_clear_bl = QPushButton("Clear Nodes")
|
|
223
|
+
self.btn_clear_bl.clicked.connect(self.clear_baseline)
|
|
224
|
+
layout_bl = QHBoxLayout()
|
|
225
|
+
layout_bl.addWidget(self.btn_pick_bl)
|
|
226
|
+
layout_bl.addWidget(self.btn_clear_bl)
|
|
227
|
+
layout_t1.addRow(layout_bl)
|
|
228
|
+
|
|
196
229
|
tab1.setLayout(layout_t1)
|
|
197
230
|
|
|
231
|
+
self.update_apod_ui()
|
|
232
|
+
|
|
198
233
|
# Tab 2: Fitting
|
|
199
234
|
tab2 = QWidget()
|
|
200
235
|
layout_t2 = QFormLayout()
|
|
@@ -235,6 +270,30 @@ class TractApp(QMainWindow):
|
|
|
235
270
|
layout.setContentsMargins(0, 0, 0, 0)
|
|
236
271
|
return widget
|
|
237
272
|
|
|
273
|
+
def update_apod_ui(self) -> None:
|
|
274
|
+
is_sp = self.combo_apod.currentText().startswith("Sine")
|
|
275
|
+
layout = self.input_off.parent().layout()
|
|
276
|
+
if isinstance(layout, QFormLayout):
|
|
277
|
+
layout.setRowVisible(self.input_lb, not is_sp)
|
|
278
|
+
layout.setRowVisible(self.input_off, is_sp)
|
|
279
|
+
layout.setRowVisible(self.input_end, is_sp)
|
|
280
|
+
layout.setRowVisible(self.input_pow, is_sp)
|
|
281
|
+
|
|
282
|
+
def toggle_picking(self, checked: bool) -> None:
|
|
283
|
+
self.picking_baseline = checked
|
|
284
|
+
self.process_data()
|
|
285
|
+
|
|
286
|
+
def clear_baseline(self) -> None:
|
|
287
|
+
self.baseline_nodes = []
|
|
288
|
+
self.process_data()
|
|
289
|
+
|
|
290
|
+
def on_canvas_click(self, event) -> None:
|
|
291
|
+
if not self.picking_baseline or event.inaxes != self.canvas_spec.axes:
|
|
292
|
+
return
|
|
293
|
+
if event.button == 1 and event.xdata is not None:
|
|
294
|
+
self.baseline_nodes.append(event.xdata)
|
|
295
|
+
self.process_data()
|
|
296
|
+
|
|
238
297
|
def update_phase_from_text(self) -> None:
|
|
239
298
|
try:
|
|
240
299
|
p0 = float(self.input_p0.text())
|
|
@@ -266,8 +325,13 @@ class TractApp(QMainWindow):
|
|
|
266
325
|
try:
|
|
267
326
|
delay_list = None
|
|
268
327
|
if not os.path.exists(os.path.join(folder, "vdlist")):
|
|
328
|
+
QMessageBox.information(
|
|
329
|
+
self,
|
|
330
|
+
"Delay List Missing",
|
|
331
|
+
"The standard 'vdlist' file was not found. Please select a delay list file manually.",
|
|
332
|
+
)
|
|
269
333
|
delay_list, _ = QFileDialog.getOpenFileName(
|
|
270
|
-
self, "
|
|
334
|
+
self, "Select delay list file", folder
|
|
271
335
|
)
|
|
272
336
|
|
|
273
337
|
tb = processing.TractBruker(folder, delay_list=delay_list)
|
|
@@ -289,6 +353,11 @@ class TractApp(QMainWindow):
|
|
|
289
353
|
if self.current_idx < 0:
|
|
290
354
|
return
|
|
291
355
|
try:
|
|
356
|
+
# Capture current zoom level
|
|
357
|
+
xlim = self.canvas_spec.axes.get_xlim()
|
|
358
|
+
ylim = self.canvas_spec.axes.get_ylim()
|
|
359
|
+
has_zoom = len(self.canvas_spec.axes.lines) > 0
|
|
360
|
+
|
|
292
361
|
p0 = self.slider_p0_coarse.value() + (self.slider_p0_fine.value() / 10.0)
|
|
293
362
|
p1 = self.slider_p1_coarse.value() + (self.slider_p1_fine.value() / 10.0)
|
|
294
363
|
|
|
@@ -296,6 +365,11 @@ class TractApp(QMainWindow):
|
|
|
296
365
|
self.input_p1.setText(f"{p1:.1f}")
|
|
297
366
|
|
|
298
367
|
points = int(self.input_points.text())
|
|
368
|
+
apod_func = (
|
|
369
|
+
"sp" if self.combo_apod.currentText().startswith("Sine") else "em"
|
|
370
|
+
)
|
|
371
|
+
lb = float(self.input_lb.text())
|
|
372
|
+
|
|
299
373
|
off = float(self.input_off.text())
|
|
300
374
|
end = float(self.input_end.text())
|
|
301
375
|
pow_val = float(self.input_pow.text())
|
|
@@ -305,8 +379,24 @@ class TractApp(QMainWindow):
|
|
|
305
379
|
self.datasets[self.current_idx]["p1"] = p1
|
|
306
380
|
|
|
307
381
|
tb = self.datasets[self.current_idx]["handler"]
|
|
382
|
+
|
|
383
|
+
nodes_idx = []
|
|
384
|
+
if self.baseline_nodes and tb.unit_converter:
|
|
385
|
+
for ppm in self.baseline_nodes:
|
|
386
|
+
idx = tb.unit_converter(ppm, "ppm")
|
|
387
|
+
nodes_idx.append(int(idx))
|
|
388
|
+
nodes_idx.sort()
|
|
389
|
+
|
|
308
390
|
trace = tb.process_first_trace(
|
|
309
|
-
p0,
|
|
391
|
+
p0,
|
|
392
|
+
p1,
|
|
393
|
+
points=points,
|
|
394
|
+
apod_func=apod_func,
|
|
395
|
+
lb=lb,
|
|
396
|
+
off=off,
|
|
397
|
+
end=end,
|
|
398
|
+
pow=pow_val,
|
|
399
|
+
nodes=nodes_idx,
|
|
310
400
|
)
|
|
311
401
|
|
|
312
402
|
self.canvas_spec.axes.clear()
|
|
@@ -320,21 +410,35 @@ class TractApp(QMainWindow):
|
|
|
320
410
|
self.canvas_spec.axes.plot(trace, label="First Plane")
|
|
321
411
|
self.canvas_spec.axes.legend()
|
|
322
412
|
|
|
323
|
-
self.
|
|
324
|
-
self.canvas_spec.axes
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
413
|
+
for node in self.baseline_nodes:
|
|
414
|
+
self.canvas_spec.axes.axvline(
|
|
415
|
+
x=node, color="r", linestyle="--", alpha=0.5
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
if has_zoom:
|
|
419
|
+
self.canvas_spec.axes.set_xlim(xlim)
|
|
420
|
+
self.canvas_spec.axes.set_ylim(ylim)
|
|
421
|
+
|
|
422
|
+
if not self.picking_baseline:
|
|
423
|
+
self.selector = SpanSelector(
|
|
424
|
+
self.canvas_spec.axes,
|
|
425
|
+
self.on_span_select,
|
|
426
|
+
"horizontal",
|
|
427
|
+
useblit=True,
|
|
428
|
+
props=dict(alpha=0.2, facecolor="green"),
|
|
429
|
+
interactive=True,
|
|
430
|
+
drag_from_anywhere=True,
|
|
431
|
+
)
|
|
432
|
+
else:
|
|
433
|
+
self.selector = None
|
|
434
|
+
|
|
435
|
+
if self.selector:
|
|
436
|
+
try:
|
|
437
|
+
s = float(self.input_int_start.text())
|
|
438
|
+
e = float(self.input_int_end.text())
|
|
439
|
+
self.selector.extents = (min(s, e), max(s, e))
|
|
440
|
+
except ValueError:
|
|
441
|
+
pass
|
|
338
442
|
|
|
339
443
|
self.canvas_spec.draw()
|
|
340
444
|
except Exception as e:
|
|
@@ -349,6 +453,11 @@ class TractApp(QMainWindow):
|
|
|
349
453
|
p1 = self.slider_p1_coarse.value() + (self.slider_p1_fine.value() / 10.0)
|
|
350
454
|
|
|
351
455
|
points = int(self.input_points.text())
|
|
456
|
+
apod_func = (
|
|
457
|
+
"sp" if self.combo_apod.currentText().startswith("Sine") else "em"
|
|
458
|
+
)
|
|
459
|
+
lb = float(self.input_lb.text())
|
|
460
|
+
|
|
352
461
|
off = float(self.input_off.text())
|
|
353
462
|
end_param = float(self.input_end.text())
|
|
354
463
|
pow_val = float(self.input_pow.text())
|
|
@@ -369,24 +478,52 @@ class TractApp(QMainWindow):
|
|
|
369
478
|
n_boot = 1000
|
|
370
479
|
self.input_bootstraps.setText("1000")
|
|
371
480
|
|
|
372
|
-
|
|
481
|
+
nodes_idx = []
|
|
482
|
+
if self.baseline_nodes and tb.unit_converter:
|
|
483
|
+
for ppm in self.baseline_nodes:
|
|
484
|
+
idx = tb.unit_converter(ppm, "ppm")
|
|
485
|
+
nodes_idx.append(int(idx))
|
|
486
|
+
nodes_idx.sort()
|
|
487
|
+
|
|
488
|
+
tb.split_process(
|
|
489
|
+
p0,
|
|
490
|
+
p1,
|
|
491
|
+
points=points,
|
|
492
|
+
apod_func=apod_func,
|
|
493
|
+
lb=lb,
|
|
494
|
+
off=off,
|
|
495
|
+
end=end_param,
|
|
496
|
+
pow=pow_val,
|
|
497
|
+
nodes=nodes_idx,
|
|
498
|
+
)
|
|
373
499
|
tb.integrate_ppm(start_ppm, end_ppm)
|
|
374
500
|
tb.calc_relaxation()
|
|
375
501
|
|
|
376
502
|
b0 = float(self.input_field.text()) if self.input_field.text() else None
|
|
377
503
|
tb.calc_tc(B0=b0, S2=s2_val, n_bootstrap=n_boot)
|
|
378
504
|
|
|
379
|
-
x, y_a, y_b, popt_a, popt_b = tb.get_fit_data()
|
|
505
|
+
x, y_a, y_b, popt_a, popt_b, pcov_a, pcov_b = tb.get_fit_data()
|
|
380
506
|
|
|
381
507
|
self.canvas_fit.axes.clear()
|
|
382
508
|
self.canvas_fit.axes.plot(x, y_a, "bo", label=r"$\alpha -spin\ state$")
|
|
383
509
|
self.canvas_fit.axes.plot(x, y_b, "ro", label=r"$\beta -spin\ state$")
|
|
384
|
-
|
|
385
|
-
|
|
510
|
+
|
|
511
|
+
# Smooth lines for fit and CI
|
|
512
|
+
x_smooth = np.linspace(0, np.max(x) * 1.1, 100)
|
|
513
|
+
fit_a = processing.TractBruker._relax(x_smooth, *popt_a)
|
|
514
|
+
ci_a = tb.calc_confidence_interval(x_smooth, popt_a, pcov_a)
|
|
515
|
+
fit_b = processing.TractBruker._relax(x_smooth, *popt_b)
|
|
516
|
+
ci_b = tb.calc_confidence_interval(x_smooth, popt_b, pcov_b)
|
|
517
|
+
|
|
518
|
+
self.canvas_fit.axes.plot(x_smooth, fit_a, "b-")
|
|
519
|
+
self.canvas_fit.axes.fill_between(
|
|
520
|
+
x_smooth, fit_a - ci_a, fit_a + ci_a, color="b", alpha=0.2
|
|
386
521
|
)
|
|
387
|
-
self.canvas_fit.axes.plot(
|
|
388
|
-
|
|
522
|
+
self.canvas_fit.axes.plot(x_smooth, fit_b, "r-")
|
|
523
|
+
self.canvas_fit.axes.fill_between(
|
|
524
|
+
x_smooth, fit_b - ci_b, fit_b + ci_b, color="r", alpha=0.2
|
|
389
525
|
)
|
|
526
|
+
|
|
390
527
|
self.canvas_fit.axes.set_xlabel("Delay (s)")
|
|
391
528
|
self.canvas_fit.axes.set_ylabel(r"$I/I_0$")
|
|
392
529
|
|
|
@@ -409,7 +546,7 @@ class TractApp(QMainWindow):
|
|
|
409
546
|
if not path:
|
|
410
547
|
return
|
|
411
548
|
try:
|
|
412
|
-
with open(path,
|
|
549
|
+
with open(path, "w", newline="") as f:
|
|
413
550
|
writer = csv.writer(f)
|
|
414
551
|
headers = []
|
|
415
552
|
for col in range(self.table_data.columnCount()):
|
|
@@ -417,7 +554,12 @@ class TractApp(QMainWindow):
|
|
|
417
554
|
headers.append(item.text() if item else "")
|
|
418
555
|
writer.writerow(headers)
|
|
419
556
|
for row in range(self.table_data.rowCount()):
|
|
420
|
-
row_data = [
|
|
557
|
+
row_data = [
|
|
558
|
+
self.table_data.item(row, col).text()
|
|
559
|
+
if self.table_data.item(row, col)
|
|
560
|
+
else ""
|
|
561
|
+
for col in range(self.table_data.columnCount())
|
|
562
|
+
]
|
|
421
563
|
writer.writerow(row_data)
|
|
422
564
|
except Exception as e:
|
|
423
565
|
QMessageBox.critical(self, "Export Error", str(e))
|
|
@@ -434,7 +576,7 @@ class TractApp(QMainWindow):
|
|
|
434
576
|
# Temperature
|
|
435
577
|
try:
|
|
436
578
|
temp = ds["handler"].attributes["acqus"]["TE"]
|
|
437
|
-
except
|
|
579
|
+
except KeyError, TypeError:
|
|
438
580
|
temp = "N/A"
|
|
439
581
|
item_temp = QTableWidgetItem(str(temp))
|
|
440
582
|
item_temp.setFlags(item_temp.flags() & ~Qt.ItemFlag.ItemIsEditable)
|
|
@@ -503,7 +645,7 @@ class TractApp(QMainWindow):
|
|
|
503
645
|
# Update Field Strength from parameters
|
|
504
646
|
try:
|
|
505
647
|
self.input_field.setText("{:.2f}".format(tb.attributes["acqus"]["SFO1"]))
|
|
506
|
-
except
|
|
648
|
+
except KeyError, AttributeError:
|
|
507
649
|
pass
|
|
508
650
|
|
|
509
651
|
self.slider_p0_coarse.blockSignals(True)
|
|
@@ -526,6 +668,11 @@ class TractApp(QMainWindow):
|
|
|
526
668
|
self.slider_p1_coarse.blockSignals(False)
|
|
527
669
|
self.slider_p1_fine.blockSignals(False)
|
|
528
670
|
|
|
671
|
+
self.baseline_nodes = []
|
|
672
|
+
self.picking_baseline = False
|
|
673
|
+
self.btn_pick_bl.setChecked(False)
|
|
674
|
+
|
|
675
|
+
self.canvas_spec.axes.clear()
|
|
529
676
|
self.process_data()
|
|
530
677
|
|
|
531
678
|
# Update fit display
|
|
@@ -534,14 +681,23 @@ class TractApp(QMainWindow):
|
|
|
534
681
|
|
|
535
682
|
if hasattr(tb, "Ra") and hasattr(tb, "popt_alpha"):
|
|
536
683
|
try:
|
|
537
|
-
x, y_a, y_b, popt_a, popt_b = tb.get_fit_data()
|
|
684
|
+
x, y_a, y_b, popt_a, popt_b, pcov_a, pcov_b = tb.get_fit_data()
|
|
538
685
|
self.canvas_fit.axes.plot(x, y_a, "bo", label="Alpha (Anti-TROSY)")
|
|
539
686
|
self.canvas_fit.axes.plot(x, y_b, "ro", label="Beta (TROSY)")
|
|
540
|
-
|
|
541
|
-
|
|
687
|
+
|
|
688
|
+
x_smooth = np.linspace(0, np.max(x) * 1.1, 100)
|
|
689
|
+
fit_a = processing.TractBruker._relax(x_smooth, *popt_a)
|
|
690
|
+
ci_a = tb.calc_confidence_interval(x_smooth, popt_a, pcov_a)
|
|
691
|
+
fit_b = processing.TractBruker._relax(x_smooth, *popt_b)
|
|
692
|
+
ci_b = tb.calc_confidence_interval(x_smooth, popt_b, pcov_b)
|
|
693
|
+
|
|
694
|
+
self.canvas_fit.axes.plot(x_smooth, fit_a, "b-")
|
|
695
|
+
self.canvas_fit.axes.fill_between(
|
|
696
|
+
x_smooth, fit_a - ci_a, fit_a + ci_a, color="b", alpha=0.2
|
|
542
697
|
)
|
|
543
|
-
self.canvas_fit.axes.plot(
|
|
544
|
-
|
|
698
|
+
self.canvas_fit.axes.plot(x_smooth, fit_b, "r-")
|
|
699
|
+
self.canvas_fit.axes.fill_between(
|
|
700
|
+
x_smooth, fit_b - ci_b, fit_b + ci_b, color="r", alpha=0.2
|
|
545
701
|
)
|
|
546
702
|
|
|
547
703
|
tau_c_val = getattr(tb, "tau_c", 0.0)
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyTRACTnmr
|
|
3
|
+
Version: 0.1.2b1
|
|
4
|
+
Summary: A simple gui based application to process and analyse TRACT data from NMR spectroscopy.
|
|
5
|
+
License-File: LICENSE
|
|
6
|
+
Requires-Python: >=3.14
|
|
7
|
+
Requires-Dist: matplotlib>=3.10.8
|
|
8
|
+
Requires-Dist: nmrglue>=0.11
|
|
9
|
+
Requires-Dist: numpy>=2.4.2
|
|
10
|
+
Requires-Dist: pyside6-stubs>=6.7.3.0
|
|
11
|
+
Requires-Dist: pyside6>=6.10.2
|
|
12
|
+
Requires-Dist: scipy-stubs>=1.17.0.2
|
|
13
|
+
Requires-Dist: scipy>=1.17.0
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# pyTRACTnmr
|
|
17
|
+
|
|
18
|
+
**pyTRACTnmr** is a graphical user interface (GUI) application designed for the processing and analysis of TRACT (TROSY for Rotational Correlation Times) experiments in NMR spectroscopy. It provides a streamlined workflow to go from raw Bruker data to calculated rotational correlation times ($\tau_c$) with robust error estimation. Currently this only supports data collected with Bruker spectrometers using pulseprogram `tractf3gpphwg`.
|
|
19
|
+
|
|
20
|
+
## Features
|
|
21
|
+
|
|
22
|
+
- **User-Friendly Interface**: Built with PySide6 for a responsive and native experience.
|
|
23
|
+
- **Bruker Data Import**: Directly load Bruker experiment directories.
|
|
24
|
+
- **Interactive Spectral Processing**:
|
|
25
|
+
- Real-time 0th and 1st order phase correction.
|
|
26
|
+
- Adjustable Apodization (Sine-bell squared) and Zero Filling.
|
|
27
|
+
- **Interactive Region Selection**: Drag directly on the spectrum to define integration limits.
|
|
28
|
+
- **Advanced Analysis**:
|
|
29
|
+
- Automatic calculation of relaxation rates ($R_\alpha$ and $R_\beta$).
|
|
30
|
+
- Determination of Rotational Correlation Time ($\tau_c$).
|
|
31
|
+
- **Bootstrap Error Analysis**: rigorous uncertainty estimation for $\tau_c$.
|
|
32
|
+
- **Data Management**:
|
|
33
|
+
- Tabular view of multiple loaded experiments.
|
|
34
|
+
- Context menu to export results to CSV.
|
|
35
|
+
|
|
36
|
+
## Installation
|
|
37
|
+
|
|
38
|
+
### Prerequisites
|
|
39
|
+
|
|
40
|
+
* Python 3.14 or higher recommended (Though it is not tested it should run with earlier version of Python 3).
|
|
41
|
+
* [uv](https://github.com/astral-sh/uv) (optional, but recommended for building).
|
|
42
|
+
|
|
43
|
+
### Installation
|
|
44
|
+
#### Quick Start with uv
|
|
45
|
+
The fastest way to try pyTRACTnmr without installation is:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
uvx pytractnmr
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
#### Using pip
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pip install pyTRACTnmr
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
#### From Source
|
|
58
|
+
|
|
59
|
+
1. **Clone the repository:**
|
|
60
|
+
```bash
|
|
61
|
+
git clone https://github.com/debadutta-patra/pyTRACTnmr.git
|
|
62
|
+
cd pyTRACTnmr
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
2. **Install the package:**
|
|
66
|
+
|
|
67
|
+
Using `uv` (fastest):
|
|
68
|
+
```bash
|
|
69
|
+
uv pip install .
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Using standard `pip`:
|
|
73
|
+
```bash
|
|
74
|
+
pip install .
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Usage
|
|
78
|
+
|
|
79
|
+
### Launching the App
|
|
80
|
+
|
|
81
|
+
After installation, you can start the application from the terminal:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
pytractnmr
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Or run it as a python module:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
python -m pyTRACTnmr.main
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Analysis Workflow
|
|
94
|
+
|
|
95
|
+
1. **Load Data**: Click **"Load Bruker Directory"** and select your experiment folder.
|
|
96
|
+
2. **Process**:
|
|
97
|
+
* Use the **Processing** tab to adjust phase correction sliders.
|
|
98
|
+
* Drag the **green selection box** on the top spectrum plot to define the integration region (Start/End ppm).
|
|
99
|
+
3. **Fit**:
|
|
100
|
+
* Switch to the **Fitting** tab.
|
|
101
|
+
* Input experimental parameters (Field Strength, CSA, etc.).
|
|
102
|
+
* Set the number of **Bootstraps** (e.g., 1000).
|
|
103
|
+
* Click **"Calculate Tau_c"**.
|
|
104
|
+
4. **Export**:
|
|
105
|
+
* Right-click the results table to **Export Table to CSV**.
|
|
106
|
+
|
|
107
|
+
## Dependencies
|
|
108
|
+
|
|
109
|
+
* `PySide6`
|
|
110
|
+
* `numpy`
|
|
111
|
+
* `scipy`
|
|
112
|
+
* `matplotlib`
|
|
113
|
+
* `nmrglue`
|
|
114
|
+
|
|
115
|
+
## License
|
|
116
|
+
|
|
117
|
+
This project is licensed under the GNU General Public License v3.0 License - see the LICENSE file for details.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
pyTRACTnmr/__init__.py,sha256=5rxO0E3s6JQ3X_bBwWIR3tOVEZrS4ZIC-_LZPemSnmQ,24
|
|
2
|
+
pyTRACTnmr/main.py,sha256=o1UZv2D9Kay36K4hoKR0iu_xRw3QuFxofzehycXoSSw,296
|
|
3
|
+
pyTRACTnmr/processing.py,sha256=_e4m61tUDtnfMKx0w81Q2stU8LOUD17jB9MrEaNVtkk,15603
|
|
4
|
+
pyTRACTnmr/widgets.py,sha256=DlSSWUZ_soVfeXvkm5s26zPKoxREUW-4x5__vvXtpj4,2818
|
|
5
|
+
pyTRACTnmr/window.py,sha256=boGkK-3MpvOAyo3XzsFPRQb-HCefb0_iDOQHNp3yY8U,30151
|
|
6
|
+
pytractnmr-0.1.2b1.dist-info/METADATA,sha256=ycOTDo6umib8rDNMJwJKv-HdcX78gugotZRffxKF2Ao,3447
|
|
7
|
+
pytractnmr-0.1.2b1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
8
|
+
pytractnmr-0.1.2b1.dist-info/entry_points.txt,sha256=wAW1nWzvGBezl-fd7fcJLk-2iMC4ow11xTi-uSfcUC0,52
|
|
9
|
+
pytractnmr-0.1.2b1.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
10
|
+
pytractnmr-0.1.2b1.dist-info/RECORD,,
|