ZaksPhysicsLibrary 1.4.0__tar.gz → 1.7.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.4.0/ZaksPhysicsLibrary.egg-info → zaksphysicslibrary-1.7.0}/PKG-INFO +43 -5
- {zaksphysicslibrary-1.4.0 → zaksphysicslibrary-1.7.0}/PhysicsLibrary/__init__.py +30 -0
- {zaksphysicslibrary-1.4.0 → zaksphysicslibrary-1.7.0}/PhysicsLibrary/analysis.py +193 -0
- zaksphysicslibrary-1.7.0/PhysicsLibrary/field_study_validation.py +433 -0
- {zaksphysicslibrary-1.4.0 → zaksphysicslibrary-1.7.0}/PhysicsLibrary/loaders/oxysoft_loader.py +7 -0
- {zaksphysicslibrary-1.4.0 → zaksphysicslibrary-1.7.0}/PhysicsLibrary/processing_TDT.py +10 -0
- zaksphysicslibrary-1.7.0/PhysicsLibrary/splice.py +113 -0
- zaksphysicslibrary-1.7.0/PhysicsLibrary/text_field_study.py +405 -0
- {zaksphysicslibrary-1.4.0 → zaksphysicslibrary-1.7.0}/README.md +37 -2
- {zaksphysicslibrary-1.4.0 → zaksphysicslibrary-1.7.0/ZaksPhysicsLibrary.egg-info}/PKG-INFO +43 -5
- {zaksphysicslibrary-1.4.0 → zaksphysicslibrary-1.7.0}/ZaksPhysicsLibrary.egg-info/SOURCES.txt +3 -0
- zaksphysicslibrary-1.7.0/ZaksPhysicsLibrary.egg-info/requires.txt +6 -0
- {zaksphysicslibrary-1.4.0 → zaksphysicslibrary-1.7.0}/pyproject.toml +6 -3
- zaksphysicslibrary-1.4.0/ZaksPhysicsLibrary.egg-info/requires.txt +0 -3
- {zaksphysicslibrary-1.4.0 → zaksphysicslibrary-1.7.0}/LICENSE +0 -0
- {zaksphysicslibrary-1.4.0 → zaksphysicslibrary-1.7.0}/PhysicsLibrary/dataset.py +0 -0
- {zaksphysicslibrary-1.4.0 → zaksphysicslibrary-1.7.0}/PhysicsLibrary/file_parser.py +0 -0
- {zaksphysicslibrary-1.4.0 → zaksphysicslibrary-1.7.0}/PhysicsLibrary/file_parser_generic.py +0 -0
- {zaksphysicslibrary-1.4.0 → zaksphysicslibrary-1.7.0}/PhysicsLibrary/loaders/__init__.py +0 -0
- {zaksphysicslibrary-1.4.0 → zaksphysicslibrary-1.7.0}/PhysicsLibrary/loaders/pt2_loader.py +0 -0
- {zaksphysicslibrary-1.4.0 → zaksphysicslibrary-1.7.0}/PhysicsLibrary/loaders/tdt_loader.py +0 -0
- {zaksphysicslibrary-1.4.0 → zaksphysicslibrary-1.7.0}/PhysicsLibrary/models.py +0 -0
- {zaksphysicslibrary-1.4.0 → zaksphysicslibrary-1.7.0}/ZaksPhysicsLibrary.egg-info/dependency_links.txt +0 -0
- {zaksphysicslibrary-1.4.0 → zaksphysicslibrary-1.7.0}/ZaksPhysicsLibrary.egg-info/top_level.txt +0 -0
- {zaksphysicslibrary-1.4.0 → zaksphysicslibrary-1.7.0}/setup.cfg +0 -0
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ZaksPhysicsLibrary
|
|
3
|
-
Version: 1.
|
|
4
|
-
Summary: Data processing and analysis library for TDT, Oxysoft NIRS,
|
|
3
|
+
Version: 1.7.0
|
|
4
|
+
Summary: Data processing and analysis library for TDT, Oxysoft NIRS, Terranova EFNMR lab data, and text-field studies
|
|
5
5
|
Author: zakgm2
|
|
6
6
|
License-Expression: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/zakgm2/PhysicsLibrary
|
|
8
8
|
Project-URL: Repository, https://github.com/zakgm2/PhysicsLibrary
|
|
9
9
|
Project-URL: Changelog, https://github.com/zakgm2/PhysicsLibrary/blob/main/CHANGELOG.md
|
|
10
|
-
Keywords: physics,neuroscience,fibre-photometry,NIRS,TDT,EFNMR,signal-processing
|
|
10
|
+
Keywords: physics,neuroscience,fibre-photometry,NIRS,TDT,EFNMR,signal-processing,text-analysis,nlp
|
|
11
11
|
Classifier: Programming Language :: Python :: 3
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.10
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.11
|
|
@@ -21,6 +21,9 @@ License-File: LICENSE
|
|
|
21
21
|
Requires-Dist: numpy
|
|
22
22
|
Requires-Dist: scipy
|
|
23
23
|
Requires-Dist: tdt
|
|
24
|
+
Requires-Dist: pandas
|
|
25
|
+
Requires-Dist: sentence-transformers
|
|
26
|
+
Requires-Dist: statsmodels
|
|
24
27
|
Dynamic: license-file
|
|
25
28
|
|
|
26
29
|
# PhysicsLibrary
|
|
@@ -38,6 +41,8 @@ Data processing and analysis library backing [PhysicsAnalysis](https://github.co
|
|
|
38
41
|
- Generic **Excel / CSV / TSV / plain text**, with automatic sub-table detection for side-by-side data layouts on one sheet
|
|
39
42
|
- **Processes signals** — bleach correction, denoising, Z-score PETH slicing, FFT with peak annotation, slope/segment analysis
|
|
40
43
|
- **Fits curves** — linear, single/double exponential, exponential rise, Gaussian, sinusoidal, and a photon-entanglement visibility model, all via `scipy.optimize.curve_fit`
|
|
44
|
+
- **Analyses text field studies** — one JSON file per subject with several free-text fields; pick any pair of fields to compare directly (e.g. does the answer to one question track another for the same subject). Word counts, data-quality flagging, sentence-transformers embeddings, an optional delta-vector magnitude between two fields, and an optional paired-similarity metric per pair with a permutation test and a word-count confound check. Domain-agnostic — field names and which pairs to compare are supplied by the caller, nothing is hardcoded to one study
|
|
45
|
+
- **Validates the similarity metric statistically** — Benjamini-Hochberg FDR-corrected permutation-test p-values, Cohen's d effect size, a word-count-controlled OLS regression (statsmodels), a bootstrap confidence interval on the mean, and a leave-one-out sensitivity check, one row per field pair, with docstrings explaining what each statistic means
|
|
41
46
|
|
|
42
47
|
---
|
|
43
48
|
|
|
@@ -52,6 +57,8 @@ PhysicsLibrary/
|
|
|
52
57
|
processing_TDT.py TDT tank reading, bleach correction, denoising, event markers
|
|
53
58
|
analysis.py PETH/Z-score, FFT, slope segments, curve-fit runner
|
|
54
59
|
models.py Parametric model functions for curve fitting
|
|
60
|
+
text_field_study.py Grouped-text-field study pipeline (embeddings, delta vector, paired similarity)
|
|
61
|
+
field_study_validation.py Statistical validation for the paired-similarity metric (permutation test, Cohen's d, regression, bootstrap CI, leave-one-out)
|
|
55
62
|
loaders/
|
|
56
63
|
tdt_loader.py Wraps processing_TDT into a Dataset
|
|
57
64
|
oxysoft_loader.py Oxysoft .txt parsing (folder + single-file) into a Dataset
|
|
@@ -77,7 +84,8 @@ git+https://github.com/zakgm2/PhysicsLibrary.git
|
|
|
77
84
|
### Requirements
|
|
78
85
|
|
|
79
86
|
- Python 3.10+
|
|
80
|
-
- `numpy`, `scipy`, `tdt` (installed automatically)
|
|
87
|
+
- `numpy`, `scipy`, `tdt`, `pandas`, `sentence-transformers`, `statsmodels` (installed automatically)
|
|
88
|
+
- `sentence-transformers` pulls in `torch`/`transformers` as transitive dependencies — a genuinely heavy install (hundreds of MB) if you only need the signal-processing side; only actually loaded when you call `embed_text_fields`/`run_field_study_pipeline`
|
|
81
89
|
- `openpyxl` — only needed for `.xlsx`/`.xls` files; imported lazily with a clear error if missing when you actually try to load Excel
|
|
82
90
|
|
|
83
91
|
---
|
|
@@ -125,6 +133,34 @@ result = pl.fit_model_to_segment(x_seg, y_seg, pl.single_exponential_model, p0_f
|
|
|
125
133
|
result["popt"], result["r2"], result["y_fit"]
|
|
126
134
|
```
|
|
127
135
|
|
|
136
|
+
```python
|
|
137
|
+
# Text field study — one JSON file per subject, e.g. P-0001.json. Pick
|
|
138
|
+
# pairs of fields to compare directly; no grouping concept needed.
|
|
139
|
+
fields = pl.peek_fields(folder_path) # see what fields exist before picking pairs
|
|
140
|
+
df = pl.run_field_study_pipeline(
|
|
141
|
+
folder_path,
|
|
142
|
+
text_fields=["q1", "q2", "q3", "q4"],
|
|
143
|
+
delta_pair=("q1", "q2"), # optional: how much did q2 change from q1
|
|
144
|
+
paired_fields=[("q1", "q3", "pair1")], # optional: does q1 track q3
|
|
145
|
+
)
|
|
146
|
+
# df has one row per subject: wordcount_<field>, low_quality_<field>, delta_magnitude,
|
|
147
|
+
# sim_<pair>, pvalue_<pair>, effect_size_<pair>, wc_confound_r_<pair>, ...
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
# Statistical validation of the paired-similarity metric — one row per pair
|
|
152
|
+
summary = pl.run_validation_pipeline(
|
|
153
|
+
folder_path,
|
|
154
|
+
text_fields=["q1", "q3"],
|
|
155
|
+
paired_fields=[("q1", "q3", "pair1")],
|
|
156
|
+
)
|
|
157
|
+
# summary: p_value, p_value_fdr, cohens_d, wc_coef_a/b + wc_pvalue_a/b,
|
|
158
|
+
# regression_r_squared, ci_lower/ci_upper, n_flagged_loo, flagged_participant_ids
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
See [FIELD_STUDY_METHODOLOGY.md](FIELD_STUDY_METHODOLOGY.md) for why each statistic in the
|
|
162
|
+
validation step is a sound, standard technique — useful if anyone asks.
|
|
163
|
+
|
|
128
164
|
See [CHANGELOG.md](CHANGELOG.md) for the version history.
|
|
129
165
|
|
|
130
166
|
---
|
|
@@ -135,8 +171,10 @@ Everything importable from `PhysicsLibrary` directly:
|
|
|
135
171
|
|
|
136
172
|
| Category | Names |
|
|
137
173
|
|----------|-------|
|
|
138
|
-
| Format detection | `
|
|
174
|
+
| Format detection | `detect_format`, `detect_format_file`, `DataFormat`, `Dataset` |
|
|
139
175
|
| Loading | `load_dataset`, `load_dataset_file`, `load_any_file`, `load_pt2` |
|
|
140
176
|
| TDT processing | `process_tdt_folder`, `validate_tdt_folder`, `get_tdt_struct`, `get_plot_data`, `correct_bleaching`, `denoise_signal`, `get_event_markers` |
|
|
141
177
|
| Analysis | `get_zscore_slice`, `smooth_signal`, `bin_for_heatmap`, `compute_fft_slice`, `annotate_fft_peaks`, `compute_slope_segment`, `fit_model_to_segment` |
|
|
142
178
|
| Curve fit models | `linear_model`, `single_exponential_model`, `exponential_rise_model`, `double_exponential_model`, `gaussian_model`, `sinusoidal_model`, `visibility_model` |
|
|
179
|
+
| Text field study | `run_field_study_pipeline`, `load_field_study_folder`, `peek_fields`, `flag_low_quality`, `embed_text_fields`, `compute_delta_vector`, `compute_paired_similarity`, `permutation_test_similarity`, `wordcount_confound_check` |
|
|
180
|
+
| Field study validation | `run_validation_pipeline`, `build_validation_summary`, `cohens_d`, `benjamini_hochberg`, `wordcount_controlled_regression`, `bootstrap_mean_ci`, `leave_one_out_sensitivity` |
|
|
@@ -45,6 +45,14 @@ from .analysis import (
|
|
|
45
45
|
annotate_fft_peaks,
|
|
46
46
|
compute_slope_segment,
|
|
47
47
|
fit_model_to_segment,
|
|
48
|
+
compute_event_zscore_peth,
|
|
49
|
+
find_significant_peaks,
|
|
50
|
+
find_peak_near_events,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
from .splice import (
|
|
54
|
+
splice_keep_inside,
|
|
55
|
+
splice_cut_out,
|
|
48
56
|
)
|
|
49
57
|
|
|
50
58
|
from .models import (
|
|
@@ -55,4 +63,26 @@ from .models import (
|
|
|
55
63
|
exponential_rise_model,
|
|
56
64
|
gaussian_model,
|
|
57
65
|
sinusoidal_model,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
from .text_field_study import (
|
|
69
|
+
run_field_study_pipeline,
|
|
70
|
+
load_field_study_folder,
|
|
71
|
+
peek_fields,
|
|
72
|
+
flag_low_quality,
|
|
73
|
+
embed_text_fields,
|
|
74
|
+
compute_delta_vector,
|
|
75
|
+
compute_paired_similarity,
|
|
76
|
+
permutation_test_similarity,
|
|
77
|
+
wordcount_confound_check,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
from .field_study_validation import (
|
|
81
|
+
run_validation_pipeline,
|
|
82
|
+
build_validation_summary,
|
|
83
|
+
cohens_d,
|
|
84
|
+
benjamini_hochberg,
|
|
85
|
+
wordcount_controlled_regression,
|
|
86
|
+
bootstrap_mean_ci,
|
|
87
|
+
leave_one_out_sensitivity,
|
|
58
88
|
)
|
|
@@ -69,6 +69,199 @@ def get_zscore_slice(time_array, signal, center_t, window=None, pre=None, post=N
|
|
|
69
69
|
return seg_x, (seg_y - mu) / std
|
|
70
70
|
|
|
71
71
|
|
|
72
|
+
def compute_event_zscore_peth(time_array, signal, event_times, pre, post, num_bins=300):
|
|
73
|
+
"""
|
|
74
|
+
Z-score and align every occurrence of one event type into a
|
|
75
|
+
trial x time matrix, for a stacked-heatmap + trial-average PETH view
|
|
76
|
+
(GuPPy-style) rather than a single click-triggered PETH.
|
|
77
|
+
|
|
78
|
+
Each trial is z-scored independently against its own pre-event
|
|
79
|
+
baseline (see get_zscore_slice) — that's what makes a trial's
|
|
80
|
+
response comparable regardless of the signal's absolute level at
|
|
81
|
+
that point in the recording. Trials are then resampled onto one
|
|
82
|
+
shared relative-time axis (num_bins points spanning -pre..+post) so
|
|
83
|
+
they can be stacked into a single matrix despite each trial's raw
|
|
84
|
+
segment having a slightly different sample count from indexing
|
|
85
|
+
rounding.
|
|
86
|
+
|
|
87
|
+
Parameters
|
|
88
|
+
----------
|
|
89
|
+
time_array : array
|
|
90
|
+
signal : array
|
|
91
|
+
event_times : list of float
|
|
92
|
+
Timestamps (same units as time_array) for every occurrence of
|
|
93
|
+
the event being analyzed.
|
|
94
|
+
pre, post : float
|
|
95
|
+
Seconds before/after each event to include.
|
|
96
|
+
num_bins : int
|
|
97
|
+
Number of points each trial is resampled to.
|
|
98
|
+
|
|
99
|
+
Returns
|
|
100
|
+
-------
|
|
101
|
+
dict with:
|
|
102
|
+
time_axis : array, shape (num_bins,) — relative time, -pre..+post
|
|
103
|
+
trial_matrix : array, shape (n_valid_trials, num_bins)
|
|
104
|
+
trial_event_times : list of the event_times that produced a
|
|
105
|
+
usable trial (too-short/edge-of-recording events are skipped)
|
|
106
|
+
mean_trace : array, shape (num_bins,)
|
|
107
|
+
sem_trace : array, shape (num_bins,) — standard error of the mean
|
|
108
|
+
across trials, zero if fewer than 2 trials
|
|
109
|
+
"""
|
|
110
|
+
time_axis = np.linspace(-pre, post, num_bins)
|
|
111
|
+
rows = []
|
|
112
|
+
valid_times = []
|
|
113
|
+
for t in event_times:
|
|
114
|
+
seg_x, seg_z = get_zscore_slice(time_array, signal, t, pre=pre, post=post)
|
|
115
|
+
if seg_x is None or len(seg_x) < 2:
|
|
116
|
+
continue
|
|
117
|
+
rel_x = seg_x - t
|
|
118
|
+
rows.append(np.interp(time_axis, rel_x, seg_z))
|
|
119
|
+
valid_times.append(t)
|
|
120
|
+
|
|
121
|
+
if not rows:
|
|
122
|
+
empty = np.zeros(num_bins)
|
|
123
|
+
return {
|
|
124
|
+
"time_axis": time_axis, "trial_matrix": np.zeros((0, num_bins)),
|
|
125
|
+
"trial_event_times": [], "mean_trace": empty, "sem_trace": empty,
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
trial_matrix = np.array(rows)
|
|
129
|
+
mean_trace = trial_matrix.mean(axis=0)
|
|
130
|
+
if trial_matrix.shape[0] > 1:
|
|
131
|
+
sem_trace = trial_matrix.std(axis=0, ddof=1) / np.sqrt(trial_matrix.shape[0])
|
|
132
|
+
else:
|
|
133
|
+
sem_trace = np.zeros(num_bins)
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
"time_axis": time_axis, "trial_matrix": trial_matrix,
|
|
137
|
+
"trial_event_times": valid_times, "mean_trace": mean_trace, "sem_trace": sem_trace,
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def find_significant_peaks(time_array, signal, z_threshold=2.5, min_distance_sec=1.0,
|
|
142
|
+
include_troughs=False):
|
|
143
|
+
"""
|
|
144
|
+
Auto-detect statistically significant transients directly from the
|
|
145
|
+
signal, rather than relying on externally-supplied event markers
|
|
146
|
+
(TDT epocs, manual markers, ...) that may not actually line up with
|
|
147
|
+
where the neural signal itself is doing something.
|
|
148
|
+
|
|
149
|
+
The whole recording is z-scored against its own global mean/std
|
|
150
|
+
(not a local baseline — this is a single-pass "how unusual is this
|
|
151
|
+
point relative to the entire recording" measure, not per-event), and
|
|
152
|
+
scipy.signal.find_peaks picks local maxima at or above z_threshold,
|
|
153
|
+
at least min_distance_sec apart so a single transient's rising edge
|
|
154
|
+
doesn't get counted as several peaks.
|
|
155
|
+
|
|
156
|
+
Parameters
|
|
157
|
+
----------
|
|
158
|
+
time_array : array
|
|
159
|
+
signal : array
|
|
160
|
+
Already-processed signal (e.g. bleach-corrected + smoothed) —
|
|
161
|
+
this function does no filtering of its own.
|
|
162
|
+
z_threshold : float
|
|
163
|
+
Minimum z-score (standard deviations above the recording's own
|
|
164
|
+
mean) for a peak to count as "statistically significant".
|
|
165
|
+
min_distance_sec : float
|
|
166
|
+
Minimum spacing between detected peaks, in seconds.
|
|
167
|
+
include_troughs : bool
|
|
168
|
+
Also detect significant negative-going deflections (z <=
|
|
169
|
+
-z_threshold) — off by default since most fibre-photometry
|
|
170
|
+
analyses care about excitatory transients specifically.
|
|
171
|
+
|
|
172
|
+
Returns
|
|
173
|
+
-------
|
|
174
|
+
list of dict, each {"time": float, "z_score": float, "kind": "peak"|"trough"},
|
|
175
|
+
sorted by time.
|
|
176
|
+
"""
|
|
177
|
+
fs = 1.0 / np.median(np.diff(time_array))
|
|
178
|
+
distance = max(1, int(min_distance_sec * fs))
|
|
179
|
+
|
|
180
|
+
mu, std = np.mean(signal), np.std(signal)
|
|
181
|
+
if std < 1e-9:
|
|
182
|
+
return []
|
|
183
|
+
z = (signal - mu) / std
|
|
184
|
+
|
|
185
|
+
results = []
|
|
186
|
+
peak_idx, _ = find_peaks(z, height=z_threshold, distance=distance)
|
|
187
|
+
for i in peak_idx:
|
|
188
|
+
results.append({"time": float(time_array[i]), "z_score": float(z[i]), "kind": "peak"})
|
|
189
|
+
|
|
190
|
+
if include_troughs:
|
|
191
|
+
trough_idx, _ = find_peaks(-z, height=z_threshold, distance=distance)
|
|
192
|
+
for i in trough_idx:
|
|
193
|
+
results.append({"time": float(time_array[i]), "z_score": float(z[i]), "kind": "trough"})
|
|
194
|
+
|
|
195
|
+
results.sort(key=lambda r: r["time"])
|
|
196
|
+
return results
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def find_peak_near_events(time_array, signal, event_times, pre, post,
|
|
200
|
+
z_threshold=2.5, include_troughs=False):
|
|
201
|
+
"""
|
|
202
|
+
Check whether a statistically significant peak actually shows up near
|
|
203
|
+
each given event time, rather than assuming the event marker itself
|
|
204
|
+
marks where the neural signal responds. Works for a single event
|
|
205
|
+
(event_times of length 1) or many occurrences of the same event type
|
|
206
|
+
(checking consistency across all of them).
|
|
207
|
+
|
|
208
|
+
Each event's window is baselined the same way as get_zscore_slice
|
|
209
|
+
(pre-event portion), so "significant" means relative to that event's
|
|
210
|
+
own local baseline, not the whole recording's.
|
|
211
|
+
|
|
212
|
+
Parameters
|
|
213
|
+
----------
|
|
214
|
+
time_array : array
|
|
215
|
+
signal : array
|
|
216
|
+
event_times : list of float
|
|
217
|
+
pre, post : float
|
|
218
|
+
Seconds before/after each event to search within.
|
|
219
|
+
z_threshold : float
|
|
220
|
+
Minimum |z-score| within the window for a peak to count as found.
|
|
221
|
+
include_troughs : bool
|
|
222
|
+
Also consider negative-going deflections as candidate "peaks",
|
|
223
|
+
keeping whichever (peak or trough) is more extreme.
|
|
224
|
+
|
|
225
|
+
Returns
|
|
226
|
+
-------
|
|
227
|
+
list of dict, one per event_time (same order), each:
|
|
228
|
+
{"event_time": float, "found": bool, "peak_time": float or None,
|
|
229
|
+
"latency": float or None (peak_time - event_time),
|
|
230
|
+
"z_score": float or None, "kind": "peak"|"trough"|None}
|
|
231
|
+
"found" is False when the window was unusable (too close to the
|
|
232
|
+
recording's edges) or nothing in it reached z_threshold.
|
|
233
|
+
"""
|
|
234
|
+
results = []
|
|
235
|
+
for t in event_times:
|
|
236
|
+
seg_x, seg_z = get_zscore_slice(time_array, signal, t, pre=pre, post=post)
|
|
237
|
+
if seg_x is None or len(seg_x) == 0:
|
|
238
|
+
results.append({"event_time": t, "found": False, "peak_time": None,
|
|
239
|
+
"latency": None, "z_score": None, "kind": None})
|
|
240
|
+
continue
|
|
241
|
+
|
|
242
|
+
idx_max = int(np.argmax(seg_z))
|
|
243
|
+
if include_troughs:
|
|
244
|
+
idx_min = int(np.argmin(seg_z))
|
|
245
|
+
if abs(seg_z[idx_min]) > seg_z[idx_max]:
|
|
246
|
+
best_idx, kind = idx_min, "trough"
|
|
247
|
+
else:
|
|
248
|
+
best_idx, kind = idx_max, "peak"
|
|
249
|
+
else:
|
|
250
|
+
best_idx, kind = idx_max, "peak"
|
|
251
|
+
|
|
252
|
+
best_z = float(seg_z[best_idx])
|
|
253
|
+
found = abs(best_z) >= z_threshold
|
|
254
|
+
peak_time = float(seg_x[best_idx]) if found else None
|
|
255
|
+
results.append({
|
|
256
|
+
"event_time": t, "found": found,
|
|
257
|
+
"peak_time": peak_time,
|
|
258
|
+
"latency": (peak_time - t) if found else None,
|
|
259
|
+
"z_score": best_z if found else None,
|
|
260
|
+
"kind": kind if found else None,
|
|
261
|
+
})
|
|
262
|
+
return results
|
|
263
|
+
|
|
264
|
+
|
|
72
265
|
def smooth_signal(data, fs, window_sec=0.5):
|
|
73
266
|
"""
|
|
74
267
|
Moving average smoothing filter.
|