ZaksPhysicsLibrary 1.2.2__tar.gz

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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 zakgm2
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,142 @@
1
+ Metadata-Version: 2.4
2
+ Name: ZaksPhysicsLibrary
3
+ Version: 1.2.2
4
+ Summary: Data processing and analysis library for TDT, Oxysoft NIRS, and Terranova EFNMR lab data
5
+ Author: zakgm2
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/zakgm2/PhysicsLibrary
8
+ Project-URL: Repository, https://github.com/zakgm2/PhysicsLibrary
9
+ Project-URL: Changelog, https://github.com/zakgm2/PhysicsLibrary/blob/main/CHANGELOG.md
10
+ Keywords: physics,neuroscience,fibre-photometry,NIRS,TDT,EFNMR,signal-processing
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Intended Audience :: Science/Research
17
+ Classifier: Topic :: Scientific/Engineering
18
+ Requires-Python: >=3.10
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Requires-Dist: numpy
22
+ Requires-Dist: scipy
23
+ Requires-Dist: tdt
24
+ Dynamic: license-file
25
+
26
+ # PhysicsLibrary
27
+
28
+ Data processing and analysis library backing [PhysicsAnalysis](https://github.com/zakgm2/PhysicsAnalysis) — file parsing, signal processing, and curve-fitting logic, with no GUI code of its own. Any interface (tkinter, PyQt6, a script, a notebook) can sit on top of it.
29
+
30
+ ---
31
+
32
+ ## What it does
33
+
34
+ - **Loads lab data** from three instrument formats plus generic tabular files:
35
+ - **TDT** (Tucker-Davis Technologies) fibre photometry tanks
36
+ - **Oxysoft / Artinis** (Oxymon, OctaMon, PortaMon …) NIRS `.txt` exports
37
+ - **Terranova Prospa** `.pt2` EFNMR/MRI 2D images
38
+ - Generic **Excel / CSV / TSV / plain text**, with automatic sub-table detection for side-by-side data layouts on one sheet
39
+ - **Processes signals** — bleach correction, denoising, Z-score PETH slicing, FFT with peak annotation, slope/segment analysis
40
+ - **Fits curves** — linear, single/double exponential, exponential rise, Gaussian, sinusoidal, and a photon-entanglement visibility model, all via `scipy.optimize.curve_fit`
41
+
42
+ ---
43
+
44
+ ## Structure
45
+
46
+ ```
47
+ PhysicsLibrary/
48
+ __init__.py Public API — see below
49
+ dataset.py Dataset struct, DataFormat enum, format detection, folder picker
50
+ file_parser.py Top-level dispatcher: load_dataset(), load_dataset_file()
51
+ file_parser_generic.py Generic Excel/CSV/TSV/text parser with sub-table detection
52
+ processing_TDT.py TDT tank reading, bleach correction, denoising, event markers
53
+ analysis.py PETH/Z-score, FFT, slope segments, curve-fit runner
54
+ models.py Parametric model functions for curve fitting
55
+ loaders/
56
+ tdt_loader.py Wraps processing_TDT into a Dataset
57
+ oxysoft_loader.py Oxysoft .txt parsing (folder + single-file) into a Dataset
58
+ pt2_loader.py .pt2 EFNMR/MRI image parser
59
+ ```
60
+
61
+ Each loader/parser is single-purpose and has no knowledge of the others — `file_parser.py` is the only place that ties format detection to the right loader.
62
+
63
+ ---
64
+
65
+ ## Installation
66
+
67
+ ```bash
68
+ pip install git+https://github.com/zakgm2/PhysicsLibrary.git
69
+ ```
70
+
71
+ Or as a dependency in another project's `requirements.txt`:
72
+
73
+ ```
74
+ git+https://github.com/zakgm2/PhysicsLibrary.git
75
+ ```
76
+
77
+ ### Requirements
78
+
79
+ - Python 3.10+
80
+ - `numpy`, `scipy`, `tdt` (installed automatically)
81
+ - `openpyxl` — only needed for `.xlsx`/`.xls` files; imported lazily with a clear error if missing when you actually try to load Excel
82
+
83
+ ---
84
+
85
+ ## Usage
86
+
87
+ ```python
88
+ import PhysicsLibrary as pl
89
+
90
+ # Detect + load a TDT tank or Oxysoft export folder
91
+ fmt = pl.detect_format(folder_path)
92
+ dataset = pl.load_dataset(folder_path, fmt)
93
+
94
+ # Or load a single Oxysoft .txt file directly
95
+ dataset = pl.load_dataset_file(file_path)
96
+
97
+ # Every loader returns the same universal Dataset struct
98
+ dataset.source_format # "TDT" | "Oxysoft"
99
+ dataset.sample_rate # Hz
100
+ dataset.signals # (num_channels, num_samples)
101
+ dataset.channel_names # list[str]
102
+ dataset.events # [{'label': str, 'sample': int}, ...]
103
+ ```
104
+
105
+ ```python
106
+ # Generic tabular data (Excel/CSV/TSV/text) — returns one GenericTable per
107
+ # detected sub-table, since a single sheet can contain several side-by-side
108
+ tables = pl.load_any_file(path)
109
+ table = tables[0]
110
+ table.headers # list[str]
111
+ table.data # (n_rows, n_cols) float64, NaN for missing
112
+
113
+ # Terranova .pt2 EFNMR/MRI image — returns a raw 2D array, not a Dataset
114
+ img = pl.load_pt2(path) # (n, n) float32
115
+ ```
116
+
117
+ ```python
118
+ # Analysis
119
+ x_seg, z = pl.get_zscore_slice(time_array, signal, center_t, window=30)
120
+ freqs, power, seg_x, seg_y = pl.compute_fft_slice(time_array, signal, center_t, fs)
121
+ pl.annotate_fft_peaks(ax, freqs, power, color='blue') # matplotlib peak labels
122
+
123
+ # Curve fitting
124
+ result = pl.fit_model_to_segment(x_seg, y_seg, pl.single_exponential_model, p0_fn)
125
+ result["popt"], result["r2"], result["y_fit"]
126
+ ```
127
+
128
+ See [CHANGELOG.md](CHANGELOG.md) for the version history.
129
+
130
+ ---
131
+
132
+ ## Public API
133
+
134
+ Everything importable from `PhysicsLibrary` directly:
135
+
136
+ | Category | Names |
137
+ |----------|-------|
138
+ | Format detection | `choose_file`, `detect_format`, `detect_format_file`, `DataFormat`, `Dataset` |
139
+ | Loading | `load_dataset`, `load_dataset_file`, `load_any_file`, `load_pt2` |
140
+ | TDT processing | `process_tdt_folder`, `validate_tdt_folder`, `get_tdt_struct`, `get_plot_data`, `correct_bleaching`, `denoise_signal`, `get_event_markers` |
141
+ | Analysis | `get_zscore_slice`, `smooth_signal`, `bin_for_heatmap`, `compute_fft_slice`, `annotate_fft_peaks`, `compute_slope_segment`, `fit_model_to_segment` |
142
+ | Curve fit models | `linear_model`, `single_exponential_model`, `exponential_rise_model`, `double_exponential_model`, `gaussian_model`, `sinusoidal_model`, `visibility_model` |
@@ -0,0 +1,59 @@
1
+ """
2
+ PhysicsLibrary
3
+ --------------
4
+ Data processing and analysis library for Physics Analysis GUI.
5
+ """
6
+
7
+ from importlib.metadata import version as _version, PackageNotFoundError
8
+
9
+ try:
10
+ __version__ = _version("PhysicsLibrary")
11
+ except PackageNotFoundError:
12
+ __version__ = "unknown"
13
+
14
+ from .file_parser_generic import load_any_file
15
+
16
+ from .dataset import (
17
+ choose_file,
18
+ detect_format,
19
+ detect_format_file,
20
+ DataFormat,
21
+ Dataset,
22
+ )
23
+
24
+ from .file_parser import (
25
+ load_dataset,
26
+ load_dataset_file,
27
+ )
28
+
29
+ from .loaders.pt2_loader import load_pt2
30
+
31
+ from .processing_TDT import (
32
+ process_tdt_folder,
33
+ validate_tdt_folder,
34
+ get_tdt_struct,
35
+ get_plot_data,
36
+ correct_bleaching,
37
+ denoise_signal,
38
+ get_event_markers,
39
+ )
40
+
41
+ from .analysis import (
42
+ get_zscore_slice,
43
+ smooth_signal,
44
+ bin_for_heatmap,
45
+ compute_fft_slice,
46
+ annotate_fft_peaks,
47
+ compute_slope_segment,
48
+ fit_model_to_segment,
49
+ )
50
+
51
+ from .models import (
52
+ double_exponential_model,
53
+ visibility_model,
54
+ linear_model,
55
+ single_exponential_model,
56
+ exponential_rise_model,
57
+ gaussian_model,
58
+ sinusoidal_model,
59
+ )
@@ -0,0 +1,251 @@
1
+ """
2
+ analysis.py
3
+ -----------
4
+ Format-agnostic analysis routines for Physics Analysis GUI.
5
+
6
+ Currently:
7
+ - Z-Score PETH (get_zscore_slice, smooth_signal, bin_for_heatmap)
8
+ - FFT (compute_fft_slice, annotate_fft_peaks)
9
+ - Curve fitting (compute_slope_segment, fit_model_to_segment)
10
+ """
11
+
12
+ import numpy as np
13
+ from scipy.signal import detrend, find_peaks
14
+ from scipy.optimize import curve_fit
15
+
16
+
17
+ def get_zscore_slice(time_array, signal, center_t, window=30):
18
+ """
19
+ Extract and z-score a time window around an event.
20
+
21
+ Parameters
22
+ ----------
23
+ time_array : array
24
+ signal : array
25
+ center_t : float
26
+ Event time in seconds
27
+ window : float
28
+ Total window size in seconds
29
+
30
+ Returns
31
+ -------
32
+ (time segment, z-scored signal)
33
+ """
34
+ half_win = window / 2
35
+ start_idx = np.searchsorted(time_array, center_t - half_win)
36
+ end_idx = np.searchsorted(time_array, center_t + half_win)
37
+
38
+ seg_y = signal[start_idx:end_idx]
39
+ seg_x = time_array[start_idx:end_idx]
40
+
41
+ # Clip extreme artefacts before z-scoring so outliers don't dominate the baseline std.
42
+ seg_y = np.clip(seg_y, -5, 5)
43
+
44
+ baseline_end = len(seg_y) // 2
45
+ baseline_period = seg_y[:baseline_end]
46
+ mu = np.mean(baseline_period)
47
+ std = np.std(baseline_period)
48
+
49
+ if std < 1e-6:
50
+ return seg_x, np.zeros_like(seg_y)
51
+
52
+ return seg_x, (seg_y - mu) / std
53
+
54
+
55
+ def smooth_signal(data, fs, window_sec=0.5):
56
+ """
57
+ Moving average smoothing filter.
58
+
59
+ Parameters
60
+ ----------
61
+ data : array
62
+ fs : float
63
+ Sampling frequency in Hz
64
+ window_sec : float
65
+ Smoothing window in seconds
66
+
67
+ Returns
68
+ -------
69
+ array
70
+ Smoothed signal
71
+ """
72
+ window_size = int(fs * window_sec)
73
+ if window_size % 2 == 0:
74
+ window_size += 1
75
+ return np.convolve(data, np.ones(window_size) / window_size, mode='same')
76
+
77
+
78
+ def bin_for_heatmap(z_seg, num_bins=300):
79
+ """
80
+ Bin a signal into equal segments for heatmap plotting.
81
+
82
+ Parameters
83
+ ----------
84
+ z_seg : array
85
+ num_bins : int
86
+
87
+ Returns
88
+ -------
89
+ array
90
+ Binned signal
91
+ """
92
+ if z_seg is None or len(z_seg) == 0:
93
+ return np.zeros(num_bins)
94
+ bin_edges = np.linspace(0, len(z_seg), num_bins + 1).astype(int)
95
+ return np.array([np.mean(z_seg[bin_edges[i]:bin_edges[i+1]]) for i in range(num_bins)])
96
+
97
+
98
+ def compute_fft_slice(time_array, signal, center_t, fs, window=30):
99
+ """
100
+ Extract a time window around center_t and compute its FFT.
101
+
102
+ Applies mean removal and linear detrending before FFT to eliminate
103
+ the DC spike and slow drift, making physiological frequencies
104
+ (breathing ~0.3 Hz, heart rate ~1 Hz) visible.
105
+
106
+ Parameters
107
+ ----------
108
+ time_array : array
109
+ signal : array
110
+ center_t : float
111
+ Center time in seconds
112
+ fs : float
113
+ Sampling frequency in Hz
114
+ window : float
115
+ Total window size in seconds
116
+
117
+ Returns
118
+ -------
119
+ freqs : array
120
+ power : array
121
+ seg_x : array
122
+ seg_y : array
123
+ """
124
+ half_win = window / 2
125
+ start_idx = np.searchsorted(time_array, center_t - half_win)
126
+ end_idx = np.searchsorted(time_array, center_t + half_win)
127
+
128
+ seg_y = signal[start_idx:end_idx]
129
+ seg_x = time_array[start_idx:end_idx]
130
+
131
+ if len(seg_y) < 4:
132
+ return np.array([]), np.array([]), seg_x, seg_y
133
+
134
+ seg_y = detrend(seg_y, type='linear') # removes mean and linear trend
135
+ windowed = seg_y * np.hanning(len(seg_y))
136
+
137
+ n = len(windowed)
138
+ fft_y = np.fft.rfft(windowed)
139
+ freqs = np.fft.rfftfreq(n, d=1.0 / fs)
140
+ power = (np.abs(fft_y) ** 2) / n
141
+
142
+ return freqs, power, seg_x, seg_y
143
+
144
+
145
+ def annotate_fft_peaks(ax_f, freqs, power, color, n_peaks=3):
146
+ """
147
+ Find top N peaks in a power spectrum and annotate them with
148
+ frequency and BPM labels directly on the axes.
149
+
150
+ Parameters
151
+ ----------
152
+ ax_f : matplotlib Axes
153
+ freqs : array
154
+ power : array
155
+ color : str
156
+ n_peaks: int
157
+ """
158
+ mask = freqs >= 0.05
159
+ f_m = freqs[mask]
160
+ p_m = power[mask]
161
+ if len(p_m) < 3:
162
+ return
163
+ min_prom = 0.05 * p_m.max()
164
+ peaks, _ = find_peaks(p_m, prominence=min_prom)
165
+ if len(peaks) == 0:
166
+ return
167
+ top = sorted(peaks, key=lambda i: p_m[i], reverse=True)[:n_peaks]
168
+ for idx in top:
169
+ freq = f_m[idx]
170
+ pwr = p_m[idx]
171
+ bpm = freq * 60
172
+ ax_f.annotate(
173
+ f"{freq:.2f} Hz\n({bpm:.0f} bpm)",
174
+ xy=(freq, pwr),
175
+ xytext=(freq + 0.05, pwr * 0.92),
176
+ fontsize=7, color=color, fontweight='bold',
177
+ arrowprops=dict(arrowstyle='->', color=color, lw=0.8),
178
+ )
179
+ ax_f.axvline(freq, color=color, lw=0.7, linestyle=':', alpha=0.5)
180
+
181
+
182
+ def compute_slope_segment(x_data, y_data, p1_idx, p2_idx, padding_pct=0.05):
183
+ """
184
+ Least-squares linear regression slope between two index boundaries.
185
+
186
+ Parameters
187
+ ----------
188
+ x_data : array
189
+ y_data : array
190
+ p1_idx : int
191
+ p2_idx : int
192
+ padding_pct : float visual context padding
193
+
194
+ Returns
195
+ -------
196
+ dict with slope, intercept, crop_x, crop_y, x1, y1, x2, y2
197
+ """
198
+ idx1, idx2 = sorted([p1_idx, p2_idx])
199
+
200
+ fit_x = x_data[idx1:idx2 + 1]
201
+ fit_y = y_data[idx1:idx2 + 1]
202
+
203
+ if len(fit_x) < 2:
204
+ slope, intercept = 0.0, 0.0
205
+ else:
206
+ slope, intercept = np.polyfit(fit_x, fit_y, 1)
207
+
208
+ x1, y1 = fit_x[0], fit_y[0]
209
+ x2, y2 = fit_x[-1], fit_y[-1]
210
+
211
+ pad = max(5, int(len(x_data) * padding_pct))
212
+ start_idx = max(0, idx1 - pad)
213
+ end_idx = min(len(x_data) - 1, idx2 + pad)
214
+
215
+ return {
216
+ 'slope': slope,
217
+ 'intercept': intercept,
218
+ 'crop_x': x_data[start_idx:end_idx + 1],
219
+ 'crop_y': y_data[start_idx:end_idx + 1],
220
+ 'x1': x1, 'y1': y1,
221
+ 'x2': x2, 'y2': y2,
222
+ }
223
+
224
+
225
+ def fit_model_to_segment(x_seg, y_seg, model_fn, p0_fn):
226
+ """
227
+ Fit a model function to a data segment using scipy curve_fit.
228
+
229
+ Parameters
230
+ ----------
231
+ x_seg : array
232
+ y_seg : array
233
+ model_fn : callable f(x, *params) -> y
234
+ p0_fn : callable f(x_seg, y_seg) -> list of initial guesses
235
+
236
+ Returns
237
+ -------
238
+ dict with popt, y_fit, r2, success, error
239
+ """
240
+ try:
241
+ p0 = p0_fn(x_seg, y_seg)
242
+ popt, _ = curve_fit(model_fn, x_seg, y_seg, p0=p0, maxfev=10000)
243
+ y_fit = model_fn(x_seg, *popt)
244
+ ss_res = np.sum((y_seg - y_fit) ** 2)
245
+ ss_tot = np.sum((y_seg - y_seg.mean()) ** 2)
246
+ r2 = 1 - ss_res / ss_tot if ss_tot > 0 else 0.0
247
+ return {"popt": popt, "y_fit": y_fit, "r2": r2,
248
+ "success": True, "error": None}
249
+ except Exception as e:
250
+ return {"popt": None, "y_fit": np.zeros_like(y_seg),
251
+ "r2": 0.0, "success": False, "error": str(e)}
@@ -0,0 +1,125 @@
1
+ """
2
+ dataset.py
3
+ ----------
4
+ The universal Dataset container returned by every loader, plus format
5
+ detection and folder selection. No parsing logic lives here — that's in
6
+ loaders/.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import os
12
+ from dataclasses import dataclass, field
13
+ from enum import Enum, auto
14
+ from tkinter import filedialog
15
+ from typing import Optional
16
+
17
+ import numpy as np
18
+
19
+
20
+ # ---------------------------------------------------------------------------
21
+ # Folder selection
22
+ # ---------------------------------------------------------------------------
23
+
24
+ def choose_file(parent_window=None) -> tuple[Optional[str], Optional[str]]:
25
+ """
26
+ Opens a native folder selection dialog.
27
+
28
+ Returns
29
+ -------
30
+ (folder_path, folder_name) or (None, None) if cancelled.
31
+ """
32
+ folder_path = filedialog.askdirectory(
33
+ parent=parent_window,
34
+ title="Open Lab Data Folder",
35
+ )
36
+ if not folder_path:
37
+ return None, None
38
+ return folder_path, os.path.basename(folder_path)
39
+
40
+
41
+ # ---------------------------------------------------------------------------
42
+ # Universal output struct
43
+ # ---------------------------------------------------------------------------
44
+
45
+ @dataclass
46
+ class Dataset:
47
+ """Universal container returned by every loader."""
48
+
49
+ source_format: str # 'TDT' | 'Oxysoft'
50
+ folder_path: str
51
+ folder_name: str
52
+
53
+ # timing
54
+ sample_rate: float = 0.0 # Hz
55
+ num_samples: int = 0
56
+ duration_s: float = 0.0 # seconds
57
+
58
+ # signals – shape (num_channels, num_samples)
59
+ signals: Optional[np.ndarray] = None
60
+
61
+ # channel metadata
62
+ channel_names: list[str] = field(default_factory=list)
63
+ num_channels: int = 0
64
+
65
+ # events – list of dicts with at least {'label': str, 'sample': int}
66
+ events: list[dict] = field(default_factory=list)
67
+
68
+ # raw header / metadata blob (format-specific)
69
+ metadata: dict = field(default_factory=dict)
70
+
71
+
72
+ # ---------------------------------------------------------------------------
73
+ # Format detection
74
+ # ---------------------------------------------------------------------------
75
+
76
+ class DataFormat(Enum):
77
+ TDT = auto()
78
+ OXYSOFT = auto()
79
+ UNKNOWN = auto()
80
+
81
+
82
+ _TDT_EXTENSIONS = {'.tbk', '.tdx', '.tev', '.tsq', '.sev'}
83
+ _OXYSOFT_HEADER_MARKER = 'Datafile sample rate'
84
+
85
+
86
+ def detect_format(folder_path: str) -> DataFormat:
87
+ """
88
+ Inspect the contents of *folder_path* and return the matching DataFormat.
89
+ Priority: TDT first (proprietary extensions), then Oxysoft (.txt marker).
90
+ """
91
+ entries = os.listdir(folder_path)
92
+ extensions = {os.path.splitext(e)[1].lower() for e in entries}
93
+
94
+ if extensions & _TDT_EXTENSIONS:
95
+ return DataFormat.TDT
96
+
97
+ txt_files = [e for e in entries if e.lower().endswith('.txt')]
98
+ for fname in txt_files:
99
+ fpath = os.path.join(folder_path, fname)
100
+ try:
101
+ with open(fpath, 'r', encoding='utf-8', errors='replace') as fh:
102
+ for _ in range(30):
103
+ if _OXYSOFT_HEADER_MARKER in fh.readline():
104
+ return DataFormat.OXYSOFT
105
+ except OSError:
106
+ continue
107
+
108
+ return DataFormat.UNKNOWN
109
+
110
+
111
+ def detect_format_file(file_path: str) -> DataFormat:
112
+ """
113
+ Detect the format of a single file (as opposed to a folder).
114
+ Currently supports Oxysoft .txt exports.
115
+ """
116
+ if not file_path.lower().endswith('.txt'):
117
+ return DataFormat.UNKNOWN
118
+ try:
119
+ with open(file_path, 'r', encoding='utf-8', errors='replace') as fh:
120
+ for _ in range(30):
121
+ if _OXYSOFT_HEADER_MARKER in fh.readline():
122
+ return DataFormat.OXYSOFT
123
+ except OSError:
124
+ pass
125
+ return DataFormat.UNKNOWN
@@ -0,0 +1,59 @@
1
+ """
2
+ file_parser.py
3
+ --------------
4
+ Top-level dispatcher: detects the recording format inside a selected
5
+ folder/file and routes to the matching loader, returning a universal
6
+ Dataset struct.
7
+
8
+ - TDT (Tucker-Davis Technologies) – loaders/tdt_loader.py
9
+ - Oxysoft / Artinis (Oxymon, OctaMon, PortaMon …) – loaders/oxysoft_loader.py
10
+
11
+ Format detection, the Dataset struct, and folder selection live in
12
+ dataset.py. The .pt2 EFNMR/MRI image format is a standalone parser in
13
+ loaders/pt2_loader.py (it returns a raw image array, not a Dataset).
14
+
15
+ Usage
16
+ -----
17
+ from file_parser import detect_format, load_dataset, DataFormat
18
+
19
+ fmt = detect_format(folder_path)
20
+ dataset = load_dataset(folder_path, fmt)
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import os
26
+ from typing import Optional
27
+
28
+ from .dataset import Dataset, DataFormat, detect_format, detect_format_file
29
+ from .loaders.tdt_loader import load_tdt
30
+ from .loaders.oxysoft_loader import load_oxysoft, load_oxysoft_file
31
+
32
+
33
+ def load_dataset_file(file_path: str) -> Dataset:
34
+ """Load a single file and return a Dataset."""
35
+ fmt = detect_format_file(file_path)
36
+ if fmt == DataFormat.OXYSOFT:
37
+ return load_oxysoft_file(file_path)
38
+ raise ValueError(f"Unrecognised file format: {file_path}")
39
+
40
+
41
+ def load_dataset(folder_path: str, fmt: Optional[DataFormat] = None) -> Dataset:
42
+ """
43
+ Load a recording folder and return a Dataset.
44
+ If *fmt* is None, detect_format() is called automatically.
45
+ """
46
+ folder_name = os.path.basename(folder_path.rstrip('/\\'))
47
+
48
+ if fmt is None:
49
+ fmt = detect_format(folder_path)
50
+
51
+ if fmt is DataFormat.TDT:
52
+ return load_tdt(folder_path, folder_name)
53
+ elif fmt is DataFormat.OXYSOFT:
54
+ return load_oxysoft(folder_path, folder_name)
55
+ else:
56
+ raise ValueError(
57
+ f"Could not identify a supported data format in: {folder_path}\n"
58
+ "Expected TDT proprietary files (.Tbk/.tev/…) or an Oxysoft .txt export."
59
+ )