ZaksPhysicsLibrary 1.2.2__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.
@@ -0,0 +1,273 @@
1
+ """
2
+ processing_TDT.py
3
+ -----------------
4
+ Signal processing pipeline for TDT (Tucker-Davis Technologies) fibre photometry data.
5
+
6
+ Handles loading, bleaching correction, denoising, and event marker extraction.
7
+ Depends on the `tdt` Python SDK for reading tank files.
8
+ """
9
+
10
+ import os
11
+
12
+ import numpy as np
13
+ from scipy.optimize import curve_fit
14
+ from scipy.signal import butter, filtfilt
15
+ import tdt
16
+
17
+ from .models import double_exponential_model as double_exponential
18
+
19
+
20
+ def validate_tdt_folder(path):
21
+ """
22
+ Validate whether a directory contains a TDT recording block.
23
+
24
+ Parameters
25
+ ----------
26
+ path : str
27
+ Path to the TDT data folder.
28
+
29
+ Returns
30
+ -------
31
+ tuple
32
+ (bool, str)
33
+ - True + folder name if valid
34
+ - False + error message if invalid
35
+ """
36
+ if not path:
37
+ return False, "No folder selected."
38
+
39
+ has_tbk = any(fname.endswith('.Tbk') for fname in os.listdir(path))
40
+
41
+ if has_tbk:
42
+ return True, os.path.basename(path)
43
+ else:
44
+ return False, "Invalid Folder: No TDT block files (.Tbk) found."
45
+
46
+
47
+ def process_tdt_folder(folder_path):
48
+ """
49
+ Full photometry processing pipeline for a TDT recording.
50
+
51
+ Steps:
52
+ 1. Load TDT block
53
+ 2. Extract 465 nm (signal) and optional 415 nm (reference)
54
+ 3. Perform regression-based motion correction
55
+ 4. Correct photobleaching trend
56
+ 5. Compute ΔF/F
57
+ 6. Denoise signal
58
+ 7. Extract event markers
59
+
60
+ Parameters
61
+ ----------
62
+ folder_path : str
63
+ Path to TDT recording folder.
64
+
65
+ Returns
66
+ -------
67
+ dict
68
+ Processed signals and metadata:
69
+ - x: time vector
70
+ - raw: corrected fluorescence signal
71
+ - corr: final ΔF/F (denoised)
72
+ - dff: same as corr (alias)
73
+ - f0: bleaching baseline
74
+ - fs: sampling frequency
75
+ - store: signal label
76
+ - markers: behavioral event markers
77
+ """
78
+ data_struct = get_tdt_struct(folder_path)
79
+ streams = data_struct.streams.keys()
80
+
81
+ name_465 = next((s for s in streams if '465' in s), None)
82
+ name_415 = next((s for s in streams if '415' in s), None)
83
+
84
+ if not name_465:
85
+ raise ValueError("No 465 signal found")
86
+
87
+ x, y_465, fs = get_plot_data(data_struct, name_465)
88
+
89
+ if name_415:
90
+ _, y_415, _ = get_plot_data(data_struct, name_415)
91
+ p = np.polyfit(y_415, y_465, 1)
92
+ y_fit = np.polyval(p, y_415)
93
+ y_final = y_465 - y_fit
94
+ display_name = f"Corrected {name_465} (via {name_415})"
95
+ else:
96
+ y_final = y_465
97
+ display_name = f"{name_465} (Uncorrected)"
98
+
99
+ _, trend = correct_bleaching(y_final, fs)
100
+
101
+ f0 = np.maximum(trend, 1e-6)
102
+ dff = (y_final - f0) / f0
103
+ dff = denoise_signal(dff, fs, cutoff=5)
104
+
105
+ return {
106
+ "x": x,
107
+ "raw": y_final,
108
+ "corr": dff,
109
+ "dff": dff,
110
+ "f0": f0,
111
+ "fs": fs,
112
+ "store": display_name,
113
+ "markers": get_event_markers(data_struct),
114
+ }
115
+
116
+
117
+ def get_tdt_struct(path):
118
+ """
119
+ Load a Tucker-Davis Technologies (TDT) recording block.
120
+
121
+ Parameters
122
+ ----------
123
+ path : str
124
+ Folder containing TDT data.
125
+
126
+ Returns
127
+ -------
128
+ object
129
+ Parsed TDT data structure.
130
+ """
131
+ data = tdt.read_block(path)
132
+ if data is None:
133
+ raise Exception("TDT returned an empty object.")
134
+ return data
135
+
136
+
137
+ def get_plot_data(data, store_name, channel=0, max_points=None):
138
+ """
139
+ Extract time-series data from a TDT stream.
140
+
141
+ Parameters
142
+ ----------
143
+ data : object
144
+ TDT data structure
145
+ store_name : str
146
+ Stream name (e.g., 'x465A')
147
+ channel : int
148
+ Channel index
149
+ max_points : int or None
150
+ Optional downsampling limit
151
+
152
+ Returns
153
+ -------
154
+ tuple
155
+ (time array, signal array, sampling frequency)
156
+ """
157
+ stream = data.streams[store_name]
158
+ fs = stream.fs
159
+
160
+ data_2d = np.atleast_2d(stream.data)
161
+ if channel >= data_2d.shape[0]:
162
+ channel = 0
163
+
164
+ y_full = data_2d[channel, :]
165
+
166
+ if max_points:
167
+ ds_factor = max(1, len(y_full) // max_points)
168
+ y = y_full[::ds_factor]
169
+ x = np.arange(len(y)) * (ds_factor / fs)
170
+ else:
171
+ y = y_full
172
+ x = np.arange(len(y)) / fs
173
+
174
+ return x, y, fs
175
+
176
+
177
+ def correct_bleaching(y, fs):
178
+ """
179
+ Estimate and correct photobleaching using masked curve fitting.
180
+
181
+ Returns
182
+ -------
183
+ corrected : array
184
+ Bleaching-corrected signal
185
+ trend : array
186
+ Estimated baseline trend
187
+ """
188
+ x = np.arange(len(y)) / fs
189
+
190
+ threshold = np.median(y)
191
+ mask = y > threshold
192
+ x_fit, y_fit = x[mask], y[mask]
193
+
194
+ if len(y_fit) < 100:
195
+ return y, np.zeros_like(y)
196
+
197
+ k_guess = np.percentile(y_fit, 10)
198
+ total_amp = np.max(y_fit) - k_guess
199
+ p0 = [total_amp * 0.6, 0.05, total_amp * 0.4, 0.0001, k_guess]
200
+ lower = [0, 0, 0, 0, k_guess * 0.8]
201
+ upper = [np.inf, 1, np.inf, 0.1, np.max(y_fit)]
202
+
203
+ try:
204
+ popt, _ = curve_fit(double_exponential, x_fit, y_fit, p0=p0,
205
+ bounds=(lower, upper), maxfev=10000)
206
+ trend = double_exponential(x, *popt)
207
+ except Exception:
208
+ # curve_fit failed — fall back to log-linear fit as a rough trend estimate
209
+ import warnings
210
+ warnings.warn(
211
+ "correct_bleaching: double-exponential fit failed; falling back to log-linear trend.",
212
+ RuntimeWarning, stacklevel=2,
213
+ )
214
+ coeffs = np.polyfit(x_fit, np.log(np.maximum(y_fit, 1e-6)), 1)
215
+ trend = np.exp(np.polyval(coeffs, x))
216
+
217
+ corrected = y - trend + trend[0]
218
+ return corrected, trend
219
+
220
+
221
+ def get_event_markers(data):
222
+ """
223
+ Extract behavioral event markers from TDT epoc data.
224
+
225
+ Returns
226
+ -------
227
+ list of dict
228
+ Each dict contains:
229
+ - time
230
+ - label
231
+ - color
232
+ """
233
+ if not hasattr(data.epocs, 'Note'):
234
+ return []
235
+
236
+ notes = data.epocs.Note.notes
237
+ onsets = data.epocs.Note.onset
238
+
239
+ # Experiment-specific label→colour mapping; unknown labels default to black.
240
+ color_map = {'Clap': 'red', 'Sucrose': 'green', 'Stop': 'blue'}
241
+ markers = []
242
+
243
+ for n, t in zip(notes, onsets):
244
+ note_str = n.decode() if isinstance(n, bytes) else str(n)
245
+ note_str = note_str.strip()
246
+ markers.append({
247
+ 'time': t,
248
+ 'label': note_str,
249
+ 'color': color_map.get(note_str, 'black'),
250
+ })
251
+ return markers
252
+
253
+
254
+ def denoise_signal(signal, fs, cutoff=5, order=2):
255
+ """
256
+ Low-pass Butterworth filter for ΔF/F signals.
257
+
258
+ Parameters
259
+ ----------
260
+ cutoff : float
261
+ Cutoff frequency in Hz
262
+ order : int
263
+ Filter order
264
+
265
+ Returns
266
+ -------
267
+ array
268
+ Filtered signal
269
+ """
270
+ nyquist = fs / 2
271
+ normal_cutoff = cutoff / nyquist
272
+ b, a = butter(order, normal_cutoff, btype='low', analog=False)
273
+ return filtfilt(b, a, signal)
@@ -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,16 @@
1
+ PhysicsLibrary/__init__.py,sha256=ZbnUc62IWDhTcDnSN8tEmxxxQowjhi4kVk4vk0jZ25c,1129
2
+ PhysicsLibrary/analysis.py,sha256=8LfCTWtkQWqDExhMNT37F8ESxrUZ2PsfEdQ1zzQw82s,6804
3
+ PhysicsLibrary/dataset.py,sha256=ZSwsSqtBGrXhwEooakp3jhVHpjVDufSrrDzAvmyk9M8,3769
4
+ PhysicsLibrary/file_parser.py,sha256=YuTLvR0HSNOm7mS8bRiksApPwwHM7dPntuFEHxDxg34,1975
5
+ PhysicsLibrary/file_parser_generic.py,sha256=MzPkIqY7-v483sBo-dZrJdMAlV-nR2fMcnAg-VXAJ_c,10217
6
+ PhysicsLibrary/models.py,sha256=BPtwDShKEvP87o2-Ad-ooHmuPOnb4BKz5hKptH8jJbQ,1556
7
+ PhysicsLibrary/processing_TDT.py,sha256=V0SiIHTPc9mEd_BRnQIeEjRvRbA51GVjCUmKnmBT6I4,6901
8
+ PhysicsLibrary/loaders/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ PhysicsLibrary/loaders/oxysoft_loader.py,sha256=4wRnVoWK-LRu33eHWGL836Jl25J4iG_nYq7C4Hez9Rk,8411
10
+ PhysicsLibrary/loaders/pt2_loader.py,sha256=TnDT5hJNNERiqSVOnYgkOxQ6T0TJJFZ_vyEXwkH0FZM,1130
11
+ PhysicsLibrary/loaders/tdt_loader.py,sha256=M-ZAPeBWFrmPpodkMech3SxFERKBKDbj0tdvq6K_Xzk,1431
12
+ zaksphysicslibrary-1.2.2.dist-info/licenses/LICENSE,sha256=1XfUUox2uGriPywq9k64eQ9UVdssOD-1TRLkNxmU-9Q,1063
13
+ zaksphysicslibrary-1.2.2.dist-info/METADATA,sha256=rCeDu0sXb_b0bWWLAujkZvFbnBRX8etQNDFIRruMRlg,5738
14
+ zaksphysicslibrary-1.2.2.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
15
+ zaksphysicslibrary-1.2.2.dist-info/top_level.txt,sha256=fv36T4puLkP8aYfW-W96TYH3SpZ4Gq5jm0J9sYqk3f0,15
16
+ zaksphysicslibrary-1.2.2.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -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 @@
1
+ PhysicsLibrary