TwoPy 0.1.0__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.
- twopy/__init__.py +168 -0
- twopy/_segments.py +53 -0
- twopy/analysis/__init__.py +101 -0
- twopy/analysis/background_subtraction.py +672 -0
- twopy/analysis/dff.py +524 -0
- twopy/analysis/epoch_mapping.py +223 -0
- twopy/analysis/motion.py +66 -0
- twopy/analysis/persistence.py +975 -0
- twopy/analysis/response_processing/__init__.py +55 -0
- twopy/analysis/response_processing/apply.py +298 -0
- twopy/analysis/response_processing/options.py +192 -0
- twopy/analysis/response_processing/qc.py +270 -0
- twopy/analysis/response_processing/signals.py +200 -0
- twopy/analysis/responses.py +345 -0
- twopy/analysis/trials.py +341 -0
- twopy/analysis/workflow.py +457 -0
- twopy/api.py +78 -0
- twopy/config.py +200 -0
- twopy/conversion/__init__.py +40 -0
- twopy/conversion/alignment_crop.py +329 -0
- twopy/conversion/api.py +113 -0
- twopy/conversion/frame_ranges.py +35 -0
- twopy/conversion/hdf5_writing.py +362 -0
- twopy/conversion/matlab_values.py +123 -0
- twopy/conversion/source_loading.py +403 -0
- twopy/conversion/stimulus_code.py +248 -0
- twopy/conversion/types.py +295 -0
- twopy/converted.py +681 -0
- twopy/database/__init__.py +33 -0
- twopy/database/cache.py +166 -0
- twopy/database/catalog.py +90 -0
- twopy/database/connection.py +27 -0
- twopy/database/generic.py +254 -0
- twopy/database/modeled.py +419 -0
- twopy/database/sql.py +46 -0
- twopy/database/types.py +116 -0
- twopy/inspection.py +492 -0
- twopy/matlab.py +398 -0
- twopy/napari/__init__.py +27 -0
- twopy/napari/__main__.py +10 -0
- twopy/napari/constants.py +13 -0
- twopy/napari/controls.py +739 -0
- twopy/napari/display.py +181 -0
- twopy/napari/display_paths.py +197 -0
- twopy/napari/interactive.py +310 -0
- twopy/napari/launcher.py +175 -0
- twopy/napari/layout.py +147 -0
- twopy/napari/loading.py +207 -0
- twopy/napari/movie.py +71 -0
- twopy/napari/paths.py +181 -0
- twopy/napari/plotting/__init__.py +21 -0
- twopy/napari/plotting/data.py +266 -0
- twopy/napari/plotting/docks.py +1052 -0
- twopy/napari/plotting/export.py +803 -0
- twopy/napari/plotting/export_controls.py +209 -0
- twopy/napari/plotting/label_visibility.py +260 -0
- twopy/napari/plotting/options.py +269 -0
- twopy/napari/plotting/panels.py +119 -0
- twopy/napari/plotting/widgets.py +767 -0
- twopy/napari/protocols.py +90 -0
- twopy/napari/responses.py +91 -0
- twopy/napari/roi.py +238 -0
- twopy/napari/session.py +381 -0
- twopy/napari/state.py +106 -0
- twopy/napari/text.py +26 -0
- twopy/napari/types.py +39 -0
- twopy/napari/viewer.py +405 -0
- twopy/parity/__init__.py +30 -0
- twopy/parity/dff.py +399 -0
- twopy/parity/roi.py +145 -0
- twopy/parity/saved_analysis.py +174 -0
- twopy/photodiode.py +375 -0
- twopy/photodiode_classification.py +484 -0
- twopy/roi.py +462 -0
- twopy/session.py +167 -0
- twopy/spatial.py +154 -0
- twopy/stimulus.py +233 -0
- twopy/synchronization.py +235 -0
- twopy/typing_guards.py +255 -0
- twopy-0.1.0.dist-info/METADATA +311 -0
- twopy-0.1.0.dist-info/RECORD +84 -0
- twopy-0.1.0.dist-info/WHEEL +5 -0
- twopy-0.1.0.dist-info/entry_points.txt +2 -0
- twopy-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Core response post-processing helpers.
|
|
2
|
+
|
|
3
|
+
Inputs: ROI dF/F traces, grouped trial responses, and explicit processing
|
|
4
|
+
options.
|
|
5
|
+
Outputs: processed response objects plus optional quality-control scores.
|
|
6
|
+
|
|
7
|
+
This package is GUI-independent. It owns the math and typed option contracts
|
|
8
|
+
for response smoothing, low-pass filtering, and correlation-based response
|
|
9
|
+
quality control. Workflow code decides when to call it; persistence code decides
|
|
10
|
+
how to save the resulting audit trail.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from twopy.analysis.response_processing.apply import (
|
|
14
|
+
ResponseProcessingResult,
|
|
15
|
+
process_grouped_roi_responses,
|
|
16
|
+
process_roi_delta_f_over_f,
|
|
17
|
+
)
|
|
18
|
+
from twopy.analysis.response_processing.options import (
|
|
19
|
+
CorrelationFilterOptions,
|
|
20
|
+
CorrelationFilterReference,
|
|
21
|
+
CorrelationWindowSeconds,
|
|
22
|
+
LowPassFilterMethod,
|
|
23
|
+
LowPassFilterOptions,
|
|
24
|
+
ResponseProcessingOptions,
|
|
25
|
+
SmoothingMethod,
|
|
26
|
+
SmoothingOptions,
|
|
27
|
+
validate_response_processing_options,
|
|
28
|
+
)
|
|
29
|
+
from twopy.analysis.response_processing.qc import (
|
|
30
|
+
RoiCorrelationScores,
|
|
31
|
+
score_roi_correlations,
|
|
32
|
+
)
|
|
33
|
+
from twopy.analysis.response_processing.signals import (
|
|
34
|
+
butterworth_low_pass,
|
|
35
|
+
nan_aware_moving_average,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
__all__ = [
|
|
39
|
+
"butterworth_low_pass",
|
|
40
|
+
"CorrelationFilterOptions",
|
|
41
|
+
"CorrelationFilterReference",
|
|
42
|
+
"CorrelationWindowSeconds",
|
|
43
|
+
"LowPassFilterMethod",
|
|
44
|
+
"LowPassFilterOptions",
|
|
45
|
+
"nan_aware_moving_average",
|
|
46
|
+
"process_grouped_roi_responses",
|
|
47
|
+
"process_roi_delta_f_over_f",
|
|
48
|
+
"ResponseProcessingOptions",
|
|
49
|
+
"ResponseProcessingResult",
|
|
50
|
+
"RoiCorrelationScores",
|
|
51
|
+
"score_roi_correlations",
|
|
52
|
+
"SmoothingMethod",
|
|
53
|
+
"SmoothingOptions",
|
|
54
|
+
"validate_response_processing_options",
|
|
55
|
+
]
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
"""Apply response post-processing options to core analysis objects.
|
|
2
|
+
|
|
3
|
+
Inputs: ROI dF/F traces, grouped trial responses, and typed processing options.
|
|
4
|
+
Outputs: new analysis objects with processed values plus optional QC scores.
|
|
5
|
+
|
|
6
|
+
This module coordinates the pure signal kernels and QC scoring helpers. It does
|
|
7
|
+
not know how options were selected, where outputs are saved, or how plots are
|
|
8
|
+
drawn.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
import numpy.typing as npt
|
|
15
|
+
|
|
16
|
+
from twopy.analysis.dff import RoiDeltaFOverF
|
|
17
|
+
from twopy.analysis.response_processing.options import (
|
|
18
|
+
ResponseProcessingOptions,
|
|
19
|
+
validate_response_processing_options,
|
|
20
|
+
)
|
|
21
|
+
from twopy.analysis.response_processing.qc import (
|
|
22
|
+
RoiCorrelationScores,
|
|
23
|
+
score_roi_correlations,
|
|
24
|
+
)
|
|
25
|
+
from twopy.analysis.response_processing.signals import (
|
|
26
|
+
butterworth_low_pass,
|
|
27
|
+
nan_aware_moving_average,
|
|
28
|
+
)
|
|
29
|
+
from twopy.analysis.responses import GroupedRoiResponses, RoiResponseTrial
|
|
30
|
+
|
|
31
|
+
__all__ = [
|
|
32
|
+
"ResponseProcessingResult",
|
|
33
|
+
"process_grouped_roi_responses",
|
|
34
|
+
"process_roi_delta_f_over_f",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True)
|
|
39
|
+
class ResponseProcessingResult:
|
|
40
|
+
"""Processed grouped responses plus optional QC scores.
|
|
41
|
+
|
|
42
|
+
Inputs: grouped trial responses and one processing option object.
|
|
43
|
+
Outputs: grouped responses with derived values, the options used to create
|
|
44
|
+
them, and optional ROI correlation scores.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
grouped_responses: GroupedRoiResponses
|
|
48
|
+
options: ResponseProcessingOptions
|
|
49
|
+
correlation_scores: RoiCorrelationScores | None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def process_roi_delta_f_over_f(
|
|
53
|
+
dff: RoiDeltaFOverF,
|
|
54
|
+
*,
|
|
55
|
+
options: ResponseProcessingOptions,
|
|
56
|
+
data_rate_hz: float,
|
|
57
|
+
) -> RoiDeltaFOverF:
|
|
58
|
+
"""Apply smoothing and low-pass filtering to continuous ROI dF/F.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
dff: Continuous ROI dF/F result.
|
|
62
|
+
options: Processing options. Correlation filtering is ignored here
|
|
63
|
+
because it needs grouped repeated-trial responses.
|
|
64
|
+
data_rate_hz: Sampling rate in hertz.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
New ``RoiDeltaFOverF`` with processed ``values`` and original raw audit
|
|
68
|
+
arrays preserved.
|
|
69
|
+
|
|
70
|
+
Raises:
|
|
71
|
+
ValueError: If options or dF/F shapes are invalid.
|
|
72
|
+
|
|
73
|
+
This is the preferred stage for smoothing and low-pass filtering because it
|
|
74
|
+
operates on continuous traces before trial slicing, avoiding per-trial edge
|
|
75
|
+
artifacts.
|
|
76
|
+
"""
|
|
77
|
+
_validate_dff_for_processing(dff)
|
|
78
|
+
validate_response_processing_options(options, data_rate_hz=data_rate_hz)
|
|
79
|
+
processed_values = _process_values(
|
|
80
|
+
dff.values,
|
|
81
|
+
options=options,
|
|
82
|
+
data_rate_hz=data_rate_hz,
|
|
83
|
+
)
|
|
84
|
+
metadata = dict(dff.metadata)
|
|
85
|
+
metadata.update(_processing_metadata(options))
|
|
86
|
+
return RoiDeltaFOverF(
|
|
87
|
+
fluorescence=dff.fluorescence,
|
|
88
|
+
baseline=dff.baseline,
|
|
89
|
+
values=processed_values,
|
|
90
|
+
labels=dff.labels,
|
|
91
|
+
start_frame=dff.start_frame,
|
|
92
|
+
stop_frame=dff.stop_frame,
|
|
93
|
+
tau=dff.tau,
|
|
94
|
+
amplitudes=dff.amplitudes,
|
|
95
|
+
interleave_frame_numbers=dff.interleave_frame_numbers,
|
|
96
|
+
interleave_fluorescence=dff.interleave_fluorescence,
|
|
97
|
+
metadata=metadata,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def process_grouped_roi_responses(
|
|
102
|
+
grouped: GroupedRoiResponses,
|
|
103
|
+
*,
|
|
104
|
+
options: ResponseProcessingOptions,
|
|
105
|
+
) -> ResponseProcessingResult:
|
|
106
|
+
"""Apply response post-processing to grouped trial responses.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
grouped: Grouped ROI responses.
|
|
110
|
+
options: Processing options.
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
Processed grouped responses plus optional correlation scores.
|
|
114
|
+
|
|
115
|
+
Raises:
|
|
116
|
+
ValueError: If options or grouped response shapes are invalid.
|
|
117
|
+
|
|
118
|
+
This grouped-response path is useful for saved responses or plot-only
|
|
119
|
+
previews. New full workflow wiring should prefer continuous dF/F processing
|
|
120
|
+
before grouping when smoothing or low-pass filtering are enabled.
|
|
121
|
+
"""
|
|
122
|
+
validate_response_processing_options(options, data_rate_hz=grouped.data_rate_hz)
|
|
123
|
+
_validate_grouped_for_processing(grouped)
|
|
124
|
+
processed = _copy_grouped_with_processed_values(grouped, options=options)
|
|
125
|
+
correlation_scores = None
|
|
126
|
+
if options.correlation_filter.reference in {"epoch_mean", "epoch_peak"}:
|
|
127
|
+
correlation_scores = score_roi_correlations(
|
|
128
|
+
processed,
|
|
129
|
+
minimum_correlation=options.correlation_filter.minimum_correlation,
|
|
130
|
+
reference=options.correlation_filter.reference,
|
|
131
|
+
window_seconds=options.correlation_filter.window_seconds,
|
|
132
|
+
)
|
|
133
|
+
processed = _mask_excluded_rois(
|
|
134
|
+
processed,
|
|
135
|
+
included_mask=correlation_scores.included_mask,
|
|
136
|
+
)
|
|
137
|
+
return ResponseProcessingResult(
|
|
138
|
+
grouped_responses=processed,
|
|
139
|
+
options=options,
|
|
140
|
+
correlation_scores=correlation_scores,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _process_values(
|
|
145
|
+
values: npt.NDArray[np.float64],
|
|
146
|
+
*,
|
|
147
|
+
options: ResponseProcessingOptions,
|
|
148
|
+
data_rate_hz: float,
|
|
149
|
+
) -> npt.NDArray[np.float64]:
|
|
150
|
+
"""Apply enabled signal transforms to frame-by-ROI values."""
|
|
151
|
+
processed = values.astype(np.float64, copy=True)
|
|
152
|
+
if options.smoothing.method == "moving_average":
|
|
153
|
+
processed = nan_aware_moving_average(
|
|
154
|
+
processed,
|
|
155
|
+
window_frames=options.smoothing.window_frames,
|
|
156
|
+
)
|
|
157
|
+
if options.low_pass.method == "butterworth":
|
|
158
|
+
if options.low_pass.cutoff_hz is None:
|
|
159
|
+
msg = "enabled low-pass filtering requires cutoff_hz"
|
|
160
|
+
raise ValueError(msg)
|
|
161
|
+
processed = butterworth_low_pass(
|
|
162
|
+
processed,
|
|
163
|
+
sample_rate_hz=data_rate_hz,
|
|
164
|
+
cutoff_hz=options.low_pass.cutoff_hz,
|
|
165
|
+
order=options.low_pass.order,
|
|
166
|
+
)
|
|
167
|
+
return processed
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _copy_grouped_with_processed_values(
|
|
171
|
+
grouped: GroupedRoiResponses,
|
|
172
|
+
*,
|
|
173
|
+
options: ResponseProcessingOptions,
|
|
174
|
+
) -> GroupedRoiResponses:
|
|
175
|
+
"""Return grouped responses with each trial value matrix processed."""
|
|
176
|
+
trials = tuple(
|
|
177
|
+
_copy_trial_with_values(
|
|
178
|
+
trial,
|
|
179
|
+
_process_values(
|
|
180
|
+
trial.values,
|
|
181
|
+
options=options,
|
|
182
|
+
data_rate_hz=grouped.data_rate_hz,
|
|
183
|
+
),
|
|
184
|
+
)
|
|
185
|
+
for trial in grouped.trials
|
|
186
|
+
)
|
|
187
|
+
return GroupedRoiResponses(
|
|
188
|
+
roi_labels=grouped.roi_labels,
|
|
189
|
+
data_rate_hz=grouped.data_rate_hz,
|
|
190
|
+
trials=trials,
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _mask_excluded_rois(
|
|
195
|
+
grouped: GroupedRoiResponses,
|
|
196
|
+
*,
|
|
197
|
+
included_mask: npt.NDArray[np.bool_],
|
|
198
|
+
) -> GroupedRoiResponses:
|
|
199
|
+
"""Return grouped responses with excluded ROI columns set to NaN."""
|
|
200
|
+
if included_mask.shape != (len(grouped.roi_labels),):
|
|
201
|
+
msg = (
|
|
202
|
+
"included_mask must match ROI label count; "
|
|
203
|
+
f"got {included_mask.shape} for {len(grouped.roi_labels)} labels"
|
|
204
|
+
)
|
|
205
|
+
raise ValueError(msg)
|
|
206
|
+
trials: list[RoiResponseTrial] = []
|
|
207
|
+
for trial in grouped.trials:
|
|
208
|
+
values = trial.values.copy()
|
|
209
|
+
values[:, ~included_mask] = np.nan
|
|
210
|
+
trials.append(_copy_trial_with_values(trial, values))
|
|
211
|
+
return GroupedRoiResponses(
|
|
212
|
+
roi_labels=grouped.roi_labels,
|
|
213
|
+
data_rate_hz=grouped.data_rate_hz,
|
|
214
|
+
trials=tuple(trials),
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _copy_trial_with_values(
|
|
219
|
+
trial: RoiResponseTrial,
|
|
220
|
+
values: npt.NDArray[np.float64],
|
|
221
|
+
) -> RoiResponseTrial:
|
|
222
|
+
"""Return one trial response with replacement values."""
|
|
223
|
+
return RoiResponseTrial(
|
|
224
|
+
epoch_number=trial.epoch_number,
|
|
225
|
+
epoch_name=trial.epoch_name,
|
|
226
|
+
trial_index=trial.trial_index,
|
|
227
|
+
window_index=trial.window_index,
|
|
228
|
+
start_frame=trial.start_frame,
|
|
229
|
+
stop_frame=trial.stop_frame,
|
|
230
|
+
frame_numbers=trial.frame_numbers,
|
|
231
|
+
time_seconds=trial.time_seconds,
|
|
232
|
+
values=values,
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _validate_dff_for_processing(dff: RoiDeltaFOverF) -> None:
|
|
237
|
+
"""Validate a dF/F object before value-only processing."""
|
|
238
|
+
if dff.values.ndim != 2:
|
|
239
|
+
msg = f"dF/F values must have shape (frames, rois); got {dff.values.shape}"
|
|
240
|
+
raise ValueError(msg)
|
|
241
|
+
if dff.values.shape[1] != len(dff.labels):
|
|
242
|
+
msg = (
|
|
243
|
+
"dF/F values width does not match labels: "
|
|
244
|
+
f"{dff.values.shape[1]} values, {len(dff.labels)} labels"
|
|
245
|
+
)
|
|
246
|
+
raise ValueError(msg)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _validate_grouped_for_processing(grouped: GroupedRoiResponses) -> None:
|
|
250
|
+
"""Validate grouped responses before value-only processing."""
|
|
251
|
+
if grouped.data_rate_hz <= 0:
|
|
252
|
+
msg = f"data_rate_hz must be positive; got {grouped.data_rate_hz}"
|
|
253
|
+
raise ValueError(msg)
|
|
254
|
+
for trial in grouped.trials:
|
|
255
|
+
if trial.values.ndim != 2:
|
|
256
|
+
msg = (
|
|
257
|
+
f"trial values must have shape (frames, rois); got {trial.values.shape}"
|
|
258
|
+
)
|
|
259
|
+
raise ValueError(msg)
|
|
260
|
+
if trial.values.shape[1] != len(grouped.roi_labels):
|
|
261
|
+
msg = (
|
|
262
|
+
"trial response width does not match ROI labels: "
|
|
263
|
+
f"{trial.values.shape[1]} values, {len(grouped.roi_labels)} labels"
|
|
264
|
+
)
|
|
265
|
+
raise ValueError(msg)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _processing_metadata(
|
|
269
|
+
options: ResponseProcessingOptions,
|
|
270
|
+
) -> dict[str, str | int | float | bool]:
|
|
271
|
+
"""Return flat metadata for a processed dF/F result."""
|
|
272
|
+
return {
|
|
273
|
+
"response_processing_smoothing_method": options.smoothing.method,
|
|
274
|
+
"response_processing_smoothing_window_frames": options.smoothing.window_frames,
|
|
275
|
+
"response_processing_low_pass_method": options.low_pass.method,
|
|
276
|
+
"response_processing_low_pass_cutoff_hz": (
|
|
277
|
+
"none"
|
|
278
|
+
if options.low_pass.cutoff_hz is None
|
|
279
|
+
else float(options.low_pass.cutoff_hz)
|
|
280
|
+
),
|
|
281
|
+
"response_processing_low_pass_order": options.low_pass.order,
|
|
282
|
+
"response_processing_correlation_reference": (
|
|
283
|
+
options.correlation_filter.reference
|
|
284
|
+
),
|
|
285
|
+
"response_processing_minimum_correlation": (
|
|
286
|
+
options.correlation_filter.minimum_correlation
|
|
287
|
+
),
|
|
288
|
+
"response_processing_correlation_window_start_seconds": (
|
|
289
|
+
"none"
|
|
290
|
+
if options.correlation_filter.window_seconds[0] is None
|
|
291
|
+
else float(options.correlation_filter.window_seconds[0])
|
|
292
|
+
),
|
|
293
|
+
"response_processing_correlation_window_stop_seconds": (
|
|
294
|
+
"none"
|
|
295
|
+
if options.correlation_filter.window_seconds[1] is None
|
|
296
|
+
else float(options.correlation_filter.window_seconds[1])
|
|
297
|
+
),
|
|
298
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""Typed options for response post-processing.
|
|
2
|
+
|
|
3
|
+
Inputs: user or script-selected processing parameters.
|
|
4
|
+
Outputs: immutable option objects validated before math is run.
|
|
5
|
+
|
|
6
|
+
These objects are intentionally plain dataclasses so GUI controls, scripts, and
|
|
7
|
+
future persistence code can all pass the same explicit contract without pulling
|
|
8
|
+
Qt or HDF5 concerns into the analysis layer.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from typing import Literal
|
|
13
|
+
|
|
14
|
+
import numpy as np
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"CorrelationFilterOptions",
|
|
18
|
+
"CorrelationFilterReference",
|
|
19
|
+
"CorrelationWindowSeconds",
|
|
20
|
+
"LowPassFilterMethod",
|
|
21
|
+
"LowPassFilterOptions",
|
|
22
|
+
"ResponseProcessingOptions",
|
|
23
|
+
"SmoothingMethod",
|
|
24
|
+
"SmoothingOptions",
|
|
25
|
+
"validate_response_processing_options",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
SmoothingMethod = Literal["none", "moving_average"]
|
|
29
|
+
LowPassFilterMethod = Literal["none", "butterworth"]
|
|
30
|
+
CorrelationFilterReference = Literal["none", "epoch_mean", "epoch_peak"]
|
|
31
|
+
CorrelationWindowSeconds = tuple[float | None, float | None]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True)
|
|
35
|
+
class SmoothingOptions:
|
|
36
|
+
"""Moving-average smoothing settings.
|
|
37
|
+
|
|
38
|
+
Inputs: a smoothing method and window length in imaging frames.
|
|
39
|
+
Outputs: a typed contract consumed by signal-processing kernels.
|
|
40
|
+
|
|
41
|
+
``method="none"`` leaves values unchanged. ``window_frames`` is still kept
|
|
42
|
+
explicit so saved options can report exactly what the user selected.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
method: SmoothingMethod = "none"
|
|
46
|
+
window_frames: int = 1
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass(frozen=True)
|
|
50
|
+
class LowPassFilterOptions:
|
|
51
|
+
"""Low-pass filter settings for ROI response traces.
|
|
52
|
+
|
|
53
|
+
Inputs: filter method, cutoff frequency, and Butterworth order.
|
|
54
|
+
Outputs: a typed contract consumed by low-pass kernels.
|
|
55
|
+
|
|
56
|
+
``cutoff_hz`` is optional because disabled filters do not need a cutoff.
|
|
57
|
+
Enabled Butterworth filters must provide a positive cutoff below Nyquist.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
method: LowPassFilterMethod = "none"
|
|
61
|
+
cutoff_hz: float | None = None
|
|
62
|
+
order: int = 2
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass(frozen=True)
|
|
66
|
+
class CorrelationFilterOptions:
|
|
67
|
+
"""Correlation-based response quality-control settings.
|
|
68
|
+
|
|
69
|
+
Inputs: reference strategy, accepted correlation, and relative time window.
|
|
70
|
+
Outputs: ROI-level scores and inclusion masks when enabled.
|
|
71
|
+
|
|
72
|
+
Correlation filtering should produce auditable scores and masks. Downstream
|
|
73
|
+
callers can hide, mark, or exclude responses, but raw data should remain
|
|
74
|
+
available elsewhere in the analysis record.
|
|
75
|
+
|
|
76
|
+
``window_seconds`` is a half-open ``(start, stop)`` interval on each trial's
|
|
77
|
+
epoch-relative time axis. ``None`` on the stop side leaves it unbounded, so
|
|
78
|
+
the default starts at epoch onset and uses the full stored epoch response.
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
reference: CorrelationFilterReference = "none"
|
|
82
|
+
minimum_correlation: float = 0.0
|
|
83
|
+
window_seconds: CorrelationWindowSeconds = (0.0, None)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@dataclass(frozen=True)
|
|
87
|
+
class ResponseProcessingOptions:
|
|
88
|
+
"""Complete response post-processing configuration.
|
|
89
|
+
|
|
90
|
+
Inputs: optional smoothing, low-pass filtering, and correlation QC settings.
|
|
91
|
+
Outputs: one immutable object passed through workflow, plotting, and future
|
|
92
|
+
persistence boundaries.
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
smoothing: SmoothingOptions = field(default_factory=SmoothingOptions)
|
|
96
|
+
low_pass: LowPassFilterOptions = field(default_factory=LowPassFilterOptions)
|
|
97
|
+
correlation_filter: CorrelationFilterOptions = field(
|
|
98
|
+
default_factory=CorrelationFilterOptions
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def validate_response_processing_options(
|
|
103
|
+
options: ResponseProcessingOptions,
|
|
104
|
+
*,
|
|
105
|
+
data_rate_hz: float | None = None,
|
|
106
|
+
) -> None:
|
|
107
|
+
"""Validate one response-processing option object.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
options: Candidate processing configuration.
|
|
111
|
+
data_rate_hz: Optional sampling rate used to validate low-pass Nyquist
|
|
112
|
+
bounds when the filter is enabled.
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
None.
|
|
116
|
+
|
|
117
|
+
Raises:
|
|
118
|
+
ValueError: If any enabled option is invalid.
|
|
119
|
+
"""
|
|
120
|
+
_validate_smoothing_options(options.smoothing)
|
|
121
|
+
_validate_low_pass_options(options.low_pass, data_rate_hz=data_rate_hz)
|
|
122
|
+
_validate_correlation_options(options.correlation_filter)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _validate_smoothing_options(options: SmoothingOptions) -> None:
|
|
126
|
+
"""Validate smoothing options."""
|
|
127
|
+
if options.method not in {"none", "moving_average"}:
|
|
128
|
+
msg = f"Unknown smoothing method {options.method!r}"
|
|
129
|
+
raise ValueError(msg)
|
|
130
|
+
if options.window_frames < 1:
|
|
131
|
+
msg = f"smoothing window_frames must be at least 1; got {options.window_frames}"
|
|
132
|
+
raise ValueError(msg)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _validate_low_pass_options(
|
|
136
|
+
options: LowPassFilterOptions,
|
|
137
|
+
*,
|
|
138
|
+
data_rate_hz: float | None,
|
|
139
|
+
) -> None:
|
|
140
|
+
"""Validate low-pass filter options."""
|
|
141
|
+
if options.method not in {"none", "butterworth"}:
|
|
142
|
+
msg = f"Unknown low-pass method {options.method!r}"
|
|
143
|
+
raise ValueError(msg)
|
|
144
|
+
if options.order < 1:
|
|
145
|
+
msg = f"low-pass order must be at least 1; got {options.order}"
|
|
146
|
+
raise ValueError(msg)
|
|
147
|
+
if options.method == "none":
|
|
148
|
+
return
|
|
149
|
+
if options.cutoff_hz is None or options.cutoff_hz <= 0:
|
|
150
|
+
msg = "enabled low-pass filtering requires a positive cutoff_hz"
|
|
151
|
+
raise ValueError(msg)
|
|
152
|
+
if data_rate_hz is not None:
|
|
153
|
+
if data_rate_hz <= 0:
|
|
154
|
+
msg = f"data_rate_hz must be positive; got {data_rate_hz}"
|
|
155
|
+
raise ValueError(msg)
|
|
156
|
+
nyquist_hz = data_rate_hz / 2.0
|
|
157
|
+
if options.cutoff_hz >= nyquist_hz:
|
|
158
|
+
msg = (
|
|
159
|
+
"low-pass cutoff_hz must be below Nyquist; "
|
|
160
|
+
f"got {options.cutoff_hz} Hz for {data_rate_hz} Hz data"
|
|
161
|
+
)
|
|
162
|
+
raise ValueError(msg)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _validate_correlation_options(options: CorrelationFilterOptions) -> None:
|
|
166
|
+
"""Validate correlation-filter options."""
|
|
167
|
+
if options.reference not in {"none", "epoch_mean", "epoch_peak"}:
|
|
168
|
+
msg = f"Unknown correlation reference {options.reference!r}"
|
|
169
|
+
raise ValueError(msg)
|
|
170
|
+
if not -1.0 <= options.minimum_correlation <= 1.0:
|
|
171
|
+
msg = (
|
|
172
|
+
"minimum_correlation must be between -1 and 1; "
|
|
173
|
+
f"got {options.minimum_correlation}"
|
|
174
|
+
)
|
|
175
|
+
raise ValueError(msg)
|
|
176
|
+
start_seconds, stop_seconds = options.window_seconds
|
|
177
|
+
if start_seconds is not None and not np.isfinite(start_seconds):
|
|
178
|
+
msg = f"correlation window start must be finite or None; got {start_seconds}"
|
|
179
|
+
raise ValueError(msg)
|
|
180
|
+
if stop_seconds is not None and not np.isfinite(stop_seconds):
|
|
181
|
+
msg = f"correlation window stop must be finite or None; got {stop_seconds}"
|
|
182
|
+
raise ValueError(msg)
|
|
183
|
+
if (
|
|
184
|
+
start_seconds is not None
|
|
185
|
+
and stop_seconds is not None
|
|
186
|
+
and start_seconds >= stop_seconds
|
|
187
|
+
):
|
|
188
|
+
msg = (
|
|
189
|
+
"correlation window start must be before stop; "
|
|
190
|
+
f"got ({start_seconds}, {stop_seconds})"
|
|
191
|
+
)
|
|
192
|
+
raise ValueError(msg)
|