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.
Files changed (84) hide show
  1. twopy/__init__.py +168 -0
  2. twopy/_segments.py +53 -0
  3. twopy/analysis/__init__.py +101 -0
  4. twopy/analysis/background_subtraction.py +672 -0
  5. twopy/analysis/dff.py +524 -0
  6. twopy/analysis/epoch_mapping.py +223 -0
  7. twopy/analysis/motion.py +66 -0
  8. twopy/analysis/persistence.py +975 -0
  9. twopy/analysis/response_processing/__init__.py +55 -0
  10. twopy/analysis/response_processing/apply.py +298 -0
  11. twopy/analysis/response_processing/options.py +192 -0
  12. twopy/analysis/response_processing/qc.py +270 -0
  13. twopy/analysis/response_processing/signals.py +200 -0
  14. twopy/analysis/responses.py +345 -0
  15. twopy/analysis/trials.py +341 -0
  16. twopy/analysis/workflow.py +457 -0
  17. twopy/api.py +78 -0
  18. twopy/config.py +200 -0
  19. twopy/conversion/__init__.py +40 -0
  20. twopy/conversion/alignment_crop.py +329 -0
  21. twopy/conversion/api.py +113 -0
  22. twopy/conversion/frame_ranges.py +35 -0
  23. twopy/conversion/hdf5_writing.py +362 -0
  24. twopy/conversion/matlab_values.py +123 -0
  25. twopy/conversion/source_loading.py +403 -0
  26. twopy/conversion/stimulus_code.py +248 -0
  27. twopy/conversion/types.py +295 -0
  28. twopy/converted.py +681 -0
  29. twopy/database/__init__.py +33 -0
  30. twopy/database/cache.py +166 -0
  31. twopy/database/catalog.py +90 -0
  32. twopy/database/connection.py +27 -0
  33. twopy/database/generic.py +254 -0
  34. twopy/database/modeled.py +419 -0
  35. twopy/database/sql.py +46 -0
  36. twopy/database/types.py +116 -0
  37. twopy/inspection.py +492 -0
  38. twopy/matlab.py +398 -0
  39. twopy/napari/__init__.py +27 -0
  40. twopy/napari/__main__.py +10 -0
  41. twopy/napari/constants.py +13 -0
  42. twopy/napari/controls.py +739 -0
  43. twopy/napari/display.py +181 -0
  44. twopy/napari/display_paths.py +197 -0
  45. twopy/napari/interactive.py +310 -0
  46. twopy/napari/launcher.py +175 -0
  47. twopy/napari/layout.py +147 -0
  48. twopy/napari/loading.py +207 -0
  49. twopy/napari/movie.py +71 -0
  50. twopy/napari/paths.py +181 -0
  51. twopy/napari/plotting/__init__.py +21 -0
  52. twopy/napari/plotting/data.py +266 -0
  53. twopy/napari/plotting/docks.py +1052 -0
  54. twopy/napari/plotting/export.py +803 -0
  55. twopy/napari/plotting/export_controls.py +209 -0
  56. twopy/napari/plotting/label_visibility.py +260 -0
  57. twopy/napari/plotting/options.py +269 -0
  58. twopy/napari/plotting/panels.py +119 -0
  59. twopy/napari/plotting/widgets.py +767 -0
  60. twopy/napari/protocols.py +90 -0
  61. twopy/napari/responses.py +91 -0
  62. twopy/napari/roi.py +238 -0
  63. twopy/napari/session.py +381 -0
  64. twopy/napari/state.py +106 -0
  65. twopy/napari/text.py +26 -0
  66. twopy/napari/types.py +39 -0
  67. twopy/napari/viewer.py +405 -0
  68. twopy/parity/__init__.py +30 -0
  69. twopy/parity/dff.py +399 -0
  70. twopy/parity/roi.py +145 -0
  71. twopy/parity/saved_analysis.py +174 -0
  72. twopy/photodiode.py +375 -0
  73. twopy/photodiode_classification.py +484 -0
  74. twopy/roi.py +462 -0
  75. twopy/session.py +167 -0
  76. twopy/spatial.py +154 -0
  77. twopy/stimulus.py +233 -0
  78. twopy/synchronization.py +235 -0
  79. twopy/typing_guards.py +255 -0
  80. twopy-0.1.0.dist-info/METADATA +311 -0
  81. twopy-0.1.0.dist-info/RECORD +84 -0
  82. twopy-0.1.0.dist-info/WHEEL +5 -0
  83. twopy-0.1.0.dist-info/entry_points.txt +2 -0
  84. 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)