ZaksPhysicsLibrary 1.2.2__tar.gz → 1.3.0__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.
- {zaksphysicslibrary-1.2.2 → zaksphysicslibrary-1.3.0}/PKG-INFO +1 -1
- {zaksphysicslibrary-1.2.2 → zaksphysicslibrary-1.3.0}/PhysicsLibrary/__init__.py +1 -2
- {zaksphysicslibrary-1.2.2 → zaksphysicslibrary-1.3.0}/PhysicsLibrary/analysis.py +44 -14
- {zaksphysicslibrary-1.2.2 → zaksphysicslibrary-1.3.0}/PhysicsLibrary/dataset.py +2 -24
- {zaksphysicslibrary-1.2.2 → zaksphysicslibrary-1.3.0}/PhysicsLibrary/processing_TDT.py +112 -22
- {zaksphysicslibrary-1.2.2 → zaksphysicslibrary-1.3.0}/ZaksPhysicsLibrary.egg-info/PKG-INFO +1 -1
- {zaksphysicslibrary-1.2.2 → zaksphysicslibrary-1.3.0}/pyproject.toml +1 -1
- {zaksphysicslibrary-1.2.2 → zaksphysicslibrary-1.3.0}/LICENSE +0 -0
- {zaksphysicslibrary-1.2.2 → zaksphysicslibrary-1.3.0}/PhysicsLibrary/file_parser.py +0 -0
- {zaksphysicslibrary-1.2.2 → zaksphysicslibrary-1.3.0}/PhysicsLibrary/file_parser_generic.py +0 -0
- {zaksphysicslibrary-1.2.2 → zaksphysicslibrary-1.3.0}/PhysicsLibrary/loaders/__init__.py +0 -0
- {zaksphysicslibrary-1.2.2 → zaksphysicslibrary-1.3.0}/PhysicsLibrary/loaders/oxysoft_loader.py +0 -0
- {zaksphysicslibrary-1.2.2 → zaksphysicslibrary-1.3.0}/PhysicsLibrary/loaders/pt2_loader.py +0 -0
- {zaksphysicslibrary-1.2.2 → zaksphysicslibrary-1.3.0}/PhysicsLibrary/loaders/tdt_loader.py +0 -0
- {zaksphysicslibrary-1.2.2 → zaksphysicslibrary-1.3.0}/PhysicsLibrary/models.py +0 -0
- {zaksphysicslibrary-1.2.2 → zaksphysicslibrary-1.3.0}/README.md +0 -0
- {zaksphysicslibrary-1.2.2 → zaksphysicslibrary-1.3.0}/ZaksPhysicsLibrary.egg-info/SOURCES.txt +0 -0
- {zaksphysicslibrary-1.2.2 → zaksphysicslibrary-1.3.0}/ZaksPhysicsLibrary.egg-info/dependency_links.txt +0 -0
- {zaksphysicslibrary-1.2.2 → zaksphysicslibrary-1.3.0}/ZaksPhysicsLibrary.egg-info/requires.txt +0 -0
- {zaksphysicslibrary-1.2.2 → zaksphysicslibrary-1.3.0}/ZaksPhysicsLibrary.egg-info/top_level.txt +0 -0
- {zaksphysicslibrary-1.2.2 → zaksphysicslibrary-1.3.0}/setup.cfg +0 -0
|
@@ -7,14 +7,13 @@ Data processing and analysis library for Physics Analysis GUI.
|
|
|
7
7
|
from importlib.metadata import version as _version, PackageNotFoundError
|
|
8
8
|
|
|
9
9
|
try:
|
|
10
|
-
__version__ = _version("
|
|
10
|
+
__version__ = _version("ZaksPhysicsLibrary")
|
|
11
11
|
except PackageNotFoundError:
|
|
12
12
|
__version__ = "unknown"
|
|
13
13
|
|
|
14
14
|
from .file_parser_generic import load_any_file
|
|
15
15
|
|
|
16
16
|
from .dataset import (
|
|
17
|
-
choose_file,
|
|
18
17
|
detect_format,
|
|
19
18
|
detect_format_file,
|
|
20
19
|
DataFormat,
|
|
@@ -14,26 +14,40 @@ from scipy.signal import detrend, find_peaks
|
|
|
14
14
|
from scipy.optimize import curve_fit
|
|
15
15
|
|
|
16
16
|
|
|
17
|
-
def get_zscore_slice(time_array, signal, center_t, window=
|
|
17
|
+
def get_zscore_slice(time_array, signal, center_t, window=None, pre=None, post=None):
|
|
18
18
|
"""
|
|
19
19
|
Extract and z-score a time window around an event.
|
|
20
20
|
|
|
21
|
+
Supports either a symmetric `window` (split evenly before/after
|
|
22
|
+
center_t, legacy behaviour) or an asymmetric `pre`/`post` pair — e.g.
|
|
23
|
+
10s before the event and 20s after. If pre/post are given they take
|
|
24
|
+
precedence over window.
|
|
25
|
+
|
|
21
26
|
Parameters
|
|
22
27
|
----------
|
|
23
28
|
time_array : array
|
|
24
29
|
signal : array
|
|
25
30
|
center_t : float
|
|
26
31
|
Event time in seconds
|
|
27
|
-
window : float
|
|
28
|
-
Total window size in seconds
|
|
32
|
+
window : float or None
|
|
33
|
+
Total window size in seconds, split evenly before/after center_t.
|
|
34
|
+
Ignored if pre/post are given.
|
|
35
|
+
pre : float or None
|
|
36
|
+
Seconds before center_t to include. Defaults to window/2.
|
|
37
|
+
post : float or None
|
|
38
|
+
Seconds after center_t to include. Defaults to window/2.
|
|
29
39
|
|
|
30
40
|
Returns
|
|
31
41
|
-------
|
|
32
42
|
(time segment, z-scored signal)
|
|
33
43
|
"""
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
44
|
+
if pre is None or post is None:
|
|
45
|
+
half_win = (window if window is not None else 30) / 2
|
|
46
|
+
pre = pre if pre is not None else half_win
|
|
47
|
+
post = post if post is not None else half_win
|
|
48
|
+
|
|
49
|
+
start_idx = np.searchsorted(time_array, center_t - pre)
|
|
50
|
+
end_idx = np.searchsorted(time_array, center_t + post)
|
|
37
51
|
|
|
38
52
|
seg_y = signal[start_idx:end_idx]
|
|
39
53
|
seg_x = time_array[start_idx:end_idx]
|
|
@@ -41,8 +55,11 @@ def get_zscore_slice(time_array, signal, center_t, window=30):
|
|
|
41
55
|
# Clip extreme artefacts before z-scoring so outliers don't dominate the baseline std.
|
|
42
56
|
seg_y = np.clip(seg_y, -5, 5)
|
|
43
57
|
|
|
44
|
-
|
|
45
|
-
|
|
58
|
+
# Baseline is the pre-event portion — the part of the window that
|
|
59
|
+
# actually precedes the event, not just "the first half of the segment"
|
|
60
|
+
# (those differ once pre != post).
|
|
61
|
+
baseline_mask = seg_x < center_t
|
|
62
|
+
baseline_period = seg_y[baseline_mask] if baseline_mask.any() else seg_y
|
|
46
63
|
mu = np.mean(baseline_period)
|
|
47
64
|
std = np.std(baseline_period)
|
|
48
65
|
|
|
@@ -95,7 +112,7 @@ def bin_for_heatmap(z_seg, num_bins=300):
|
|
|
95
112
|
return np.array([np.mean(z_seg[bin_edges[i]:bin_edges[i+1]]) for i in range(num_bins)])
|
|
96
113
|
|
|
97
114
|
|
|
98
|
-
def compute_fft_slice(time_array, signal, center_t, fs, window=
|
|
115
|
+
def compute_fft_slice(time_array, signal, center_t, fs, window=None, pre=None, post=None):
|
|
99
116
|
"""
|
|
100
117
|
Extract a time window around center_t and compute its FFT.
|
|
101
118
|
|
|
@@ -103,6 +120,10 @@ def compute_fft_slice(time_array, signal, center_t, fs, window=30):
|
|
|
103
120
|
the DC spike and slow drift, making physiological frequencies
|
|
104
121
|
(breathing ~0.3 Hz, heart rate ~1 Hz) visible.
|
|
105
122
|
|
|
123
|
+
Supports either a symmetric `window` (split evenly before/after
|
|
124
|
+
center_t, legacy behaviour) or an asymmetric `pre`/`post` pair. If
|
|
125
|
+
pre/post are given they take precedence over window.
|
|
126
|
+
|
|
106
127
|
Parameters
|
|
107
128
|
----------
|
|
108
129
|
time_array : array
|
|
@@ -111,8 +132,13 @@ def compute_fft_slice(time_array, signal, center_t, fs, window=30):
|
|
|
111
132
|
Center time in seconds
|
|
112
133
|
fs : float
|
|
113
134
|
Sampling frequency in Hz
|
|
114
|
-
window : float
|
|
115
|
-
Total window size in seconds
|
|
135
|
+
window : float or None
|
|
136
|
+
Total window size in seconds, split evenly before/after center_t.
|
|
137
|
+
Ignored if pre/post are given.
|
|
138
|
+
pre : float or None
|
|
139
|
+
Seconds before center_t to include. Defaults to window/2.
|
|
140
|
+
post : float or None
|
|
141
|
+
Seconds after center_t to include. Defaults to window/2.
|
|
116
142
|
|
|
117
143
|
Returns
|
|
118
144
|
-------
|
|
@@ -121,9 +147,13 @@ def compute_fft_slice(time_array, signal, center_t, fs, window=30):
|
|
|
121
147
|
seg_x : array
|
|
122
148
|
seg_y : array
|
|
123
149
|
"""
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
150
|
+
if pre is None or post is None:
|
|
151
|
+
half_win = (window if window is not None else 30) / 2
|
|
152
|
+
pre = pre if pre is not None else half_win
|
|
153
|
+
post = post if post is not None else half_win
|
|
154
|
+
|
|
155
|
+
start_idx = np.searchsorted(time_array, center_t - pre)
|
|
156
|
+
end_idx = np.searchsorted(time_array, center_t + post)
|
|
127
157
|
|
|
128
158
|
seg_y = signal[start_idx:end_idx]
|
|
129
159
|
seg_x = time_array[start_idx:end_idx]
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
dataset.py
|
|
3
3
|
----------
|
|
4
4
|
The universal Dataset container returned by every loader, plus format
|
|
5
|
-
detection
|
|
6
|
-
|
|
5
|
+
detection. No parsing logic lives here — that's in loaders/. No GUI
|
|
6
|
+
concerns either — folder/file selection is a caller responsibility.
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
9
|
from __future__ import annotations
|
|
@@ -11,33 +11,11 @@ from __future__ import annotations
|
|
|
11
11
|
import os
|
|
12
12
|
from dataclasses import dataclass, field
|
|
13
13
|
from enum import Enum, auto
|
|
14
|
-
from tkinter import filedialog
|
|
15
14
|
from typing import Optional
|
|
16
15
|
|
|
17
16
|
import numpy as np
|
|
18
17
|
|
|
19
18
|
|
|
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
19
|
# ---------------------------------------------------------------------------
|
|
42
20
|
# Universal output struct
|
|
43
21
|
# ---------------------------------------------------------------------------
|
|
@@ -118,6 +118,30 @@ def get_tdt_struct(path):
|
|
|
118
118
|
"""
|
|
119
119
|
Load a Tucker-Davis Technologies (TDT) recording block.
|
|
120
120
|
|
|
121
|
+
Streams and scalars are read in a single call (unaffected by the issue
|
|
122
|
+
below). Epoc stores are auto-discovered from the block's own headers
|
|
123
|
+
and then read ONE STORE AT A TIME, each with its own fresh
|
|
124
|
+
tdt.read_block() call:
|
|
125
|
+
|
|
126
|
+
- The `tdt` SDK has a bug where reading multiple epoc stores together
|
|
127
|
+
breaks if any store has a mismatched onset/offset count (e.g. a
|
|
128
|
+
strobe/epoch that was still active when the recording was stopped —
|
|
129
|
+
a completely normal way for a session to end). Reading one store per
|
|
130
|
+
call sidesteps it; a store that still fails to a genuine data issue
|
|
131
|
+
is skipped with a warning rather than crashing the entire load.
|
|
132
|
+
- Store names must come from each store's own `.name` attribute, not
|
|
133
|
+
the header dict's key — TDT sanitizes store codes containing `/`
|
|
134
|
+
into a trailing-underscore Python attribute name (e.g. dict key
|
|
135
|
+
"EE1_" but the real store code passed to `store=` is "EE1/").
|
|
136
|
+
- Epoc reads must NOT reuse the cached `headers=` object from the
|
|
137
|
+
initial header scan — doing so carries over the same mismatched
|
|
138
|
+
onset/offset state that causes the crash in the first place. Only
|
|
139
|
+
the streams/scalars read benefits from header reuse.
|
|
140
|
+
|
|
141
|
+
This entirely auto-discovers whatever stores a given recording
|
|
142
|
+
contains — nothing about store names/types is hardcoded, so it works
|
|
143
|
+
for any TDT block regardless of how it was configured in Synapse.
|
|
144
|
+
|
|
121
145
|
Parameters
|
|
122
146
|
----------
|
|
123
147
|
path : str
|
|
@@ -126,11 +150,36 @@ def get_tdt_struct(path):
|
|
|
126
150
|
Returns
|
|
127
151
|
-------
|
|
128
152
|
object
|
|
129
|
-
Parsed TDT data structure.
|
|
153
|
+
Parsed TDT data structure (data.streams, data.epocs, data.scalars).
|
|
130
154
|
"""
|
|
131
|
-
|
|
155
|
+
import warnings
|
|
156
|
+
|
|
157
|
+
heads = tdt.read_block(path, headers=1)
|
|
158
|
+
if heads is None:
|
|
159
|
+
raise Exception("TDT returned an empty object.")
|
|
160
|
+
|
|
161
|
+
data = tdt.read_block(path, headers=heads, evtype=['streams', 'scalars'], verbose=0)
|
|
132
162
|
if data is None:
|
|
133
163
|
raise Exception("TDT returned an empty object.")
|
|
164
|
+
|
|
165
|
+
epoc_stores = [(key, s.name) for key, s in heads.stores.items()
|
|
166
|
+
if getattr(s, 'type_str', None) == 'epocs']
|
|
167
|
+
|
|
168
|
+
for key, real_name in epoc_stores:
|
|
169
|
+
try:
|
|
170
|
+
# No headers= reuse here — see docstring.
|
|
171
|
+
ep = tdt.read_block(path, store=real_name, evtype=['epocs'], verbose=0)
|
|
172
|
+
except Exception as e:
|
|
173
|
+
warnings.warn(
|
|
174
|
+
f"Skipping epoc store '{key}' ({real_name}): {e}",
|
|
175
|
+
RuntimeWarning, stacklevel=2,
|
|
176
|
+
)
|
|
177
|
+
continue
|
|
178
|
+
if not ep.epocs:
|
|
179
|
+
continue
|
|
180
|
+
store_key = next(iter(ep.epocs.keys()))
|
|
181
|
+
setattr(data.epocs, key, ep.epocs[store_key])
|
|
182
|
+
|
|
134
183
|
return data
|
|
135
184
|
|
|
136
185
|
|
|
@@ -220,34 +269,75 @@ def correct_bleaching(y, fs):
|
|
|
220
269
|
|
|
221
270
|
def get_event_markers(data):
|
|
222
271
|
"""
|
|
223
|
-
Extract behavioral event markers from TDT epoc
|
|
272
|
+
Extract behavioral event markers from every populated TDT epoc store.
|
|
273
|
+
|
|
274
|
+
A recording can easily have a dozen+ epoc stores (I/O strobes, Epoch
|
|
275
|
+
Event Storage, free-text notes, a 1-second Tick reference, ...) —
|
|
276
|
+
every one of them becomes its own marker group here, tagged by store
|
|
277
|
+
name via the 'store' key, so a caller (e.g. a GUI) can let the user
|
|
278
|
+
toggle each store's markers independently instead of dumping every
|
|
279
|
+
store onto the plot at once (which overlaps into an unreadable mess).
|
|
280
|
+
|
|
281
|
+
The 'Note' store keeps its original behaviour: each onset becomes a
|
|
282
|
+
marker labeled with the actual note text (from Notes.txt), since
|
|
283
|
+
that's the one store meant for free-text annotations. Every other
|
|
284
|
+
epoc store gets one marker per onset event, labeled with the store
|
|
285
|
+
name.
|
|
286
|
+
|
|
287
|
+
Nothing about which stores exist is hardcoded — this walks whatever
|
|
288
|
+
data.epocs actually contains, so it works for any TDT block.
|
|
224
289
|
|
|
225
290
|
Returns
|
|
226
291
|
-------
|
|
227
292
|
list of dict
|
|
228
293
|
Each dict contains:
|
|
229
|
-
- time
|
|
230
|
-
- label
|
|
231
|
-
- color
|
|
294
|
+
- time : float
|
|
295
|
+
- label : str
|
|
296
|
+
- color : str
|
|
297
|
+
- store : str (source epoc store name, for grouping/toggling)
|
|
232
298
|
"""
|
|
233
|
-
if not hasattr(data
|
|
299
|
+
if not hasattr(data, 'epocs'):
|
|
234
300
|
return []
|
|
235
301
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
302
|
+
# Experiment-specific label→colour mapping for Note text; every other
|
|
303
|
+
# store cycles through a fixed palette so each store gets a distinct,
|
|
304
|
+
# consistent colour across redraws.
|
|
305
|
+
note_color_map = {'Clap': 'red', 'Sucrose': 'green', 'Stop': 'blue'}
|
|
306
|
+
palette = ['black', 'purple', 'orange', 'saddlebrown', 'teal', 'magenta',
|
|
307
|
+
'darkgreen', 'navy', 'gray', 'olive']
|
|
308
|
+
|
|
309
|
+
markers = []
|
|
310
|
+
store_keys = sorted(k for k in data.epocs.keys())
|
|
311
|
+
|
|
312
|
+
for i, store_key in enumerate(store_keys):
|
|
313
|
+
epoc = data.epocs[store_key]
|
|
314
|
+
onsets = getattr(epoc, 'onset', [])
|
|
315
|
+
if len(onsets) == 0:
|
|
316
|
+
continue
|
|
317
|
+
|
|
318
|
+
display_name = store_key.rstrip('_') # cosmetic: TDT pads slash-containing codes with a trailing "_"
|
|
319
|
+
|
|
320
|
+
if store_key == 'Note':
|
|
321
|
+
notes = getattr(epoc, 'notes', [])
|
|
322
|
+
for n, t in zip(notes, onsets):
|
|
323
|
+
note_str = n.decode() if isinstance(n, bytes) else str(n)
|
|
324
|
+
note_str = note_str.strip()
|
|
325
|
+
markers.append({
|
|
326
|
+
'time': float(t),
|
|
327
|
+
'label': note_str,
|
|
328
|
+
'color': note_color_map.get(note_str, 'black'),
|
|
329
|
+
'store': display_name,
|
|
330
|
+
})
|
|
331
|
+
else:
|
|
332
|
+
color = palette[i % len(palette)]
|
|
333
|
+
for t in onsets:
|
|
334
|
+
markers.append({
|
|
335
|
+
'time': float(t),
|
|
336
|
+
'label': display_name,
|
|
337
|
+
'color': color,
|
|
338
|
+
'store': display_name,
|
|
339
|
+
})
|
|
340
|
+
|
|
251
341
|
return markers
|
|
252
342
|
|
|
253
343
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ZaksPhysicsLibrary"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.3.0"
|
|
8
8
|
description = "Data processing and analysis library for TDT, Oxysoft NIRS, and Terranova EFNMR lab data"
|
|
9
9
|
requires-python = ">=3.10"
|
|
10
10
|
readme = "README.md"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{zaksphysicslibrary-1.2.2 → zaksphysicslibrary-1.3.0}/PhysicsLibrary/loaders/oxysoft_loader.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{zaksphysicslibrary-1.2.2 → zaksphysicslibrary-1.3.0}/ZaksPhysicsLibrary.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
{zaksphysicslibrary-1.2.2 → zaksphysicslibrary-1.3.0}/ZaksPhysicsLibrary.egg-info/requires.txt
RENAMED
|
File without changes
|
{zaksphysicslibrary-1.2.2 → zaksphysicslibrary-1.3.0}/ZaksPhysicsLibrary.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|