oscura 0.8.0__py3-none-any.whl → 0.10.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.
- oscura/__init__.py +19 -19
- oscura/analyzers/__init__.py +2 -0
- oscura/analyzers/digital/extraction.py +2 -3
- oscura/analyzers/digital/quality.py +1 -1
- oscura/analyzers/digital/timing.py +1 -1
- oscura/analyzers/patterns/__init__.py +66 -0
- oscura/analyzers/power/basic.py +3 -3
- oscura/analyzers/power/soa.py +1 -1
- oscura/analyzers/power/switching.py +3 -3
- oscura/analyzers/signal_classification.py +529 -0
- oscura/analyzers/signal_integrity/sparams.py +3 -3
- oscura/analyzers/statistics/basic.py +10 -7
- oscura/analyzers/validation.py +1 -1
- oscura/analyzers/waveform/measurements.py +200 -156
- oscura/analyzers/waveform/measurements_with_uncertainty.py +91 -35
- oscura/analyzers/waveform/spectral.py +164 -73
- oscura/api/dsl/commands.py +15 -6
- oscura/api/server/templates/base.html +137 -146
- oscura/api/server/templates/export.html +84 -110
- oscura/api/server/templates/home.html +248 -267
- oscura/api/server/templates/protocols.html +44 -48
- oscura/api/server/templates/reports.html +27 -35
- oscura/api/server/templates/session_detail.html +68 -78
- oscura/api/server/templates/sessions.html +62 -72
- oscura/api/server/templates/waveforms.html +54 -64
- oscura/automotive/__init__.py +1 -1
- oscura/automotive/can/session.py +1 -1
- oscura/automotive/dbc/generator.py +638 -23
- oscura/automotive/uds/decoder.py +99 -6
- oscura/cli/analyze.py +8 -2
- oscura/cli/batch.py +36 -5
- oscura/cli/characterize.py +18 -4
- oscura/cli/export.py +47 -5
- oscura/cli/main.py +2 -0
- oscura/cli/onboarding/wizard.py +10 -6
- oscura/cli/pipeline.py +585 -0
- oscura/cli/visualize.py +6 -4
- oscura/convenience.py +400 -32
- oscura/core/measurement_result.py +286 -0
- oscura/core/progress.py +1 -1
- oscura/core/types.py +232 -239
- oscura/correlation/multi_protocol.py +1 -1
- oscura/export/legacy/__init__.py +11 -0
- oscura/export/legacy/wav.py +75 -0
- oscura/exporters/__init__.py +19 -0
- oscura/exporters/wireshark.py +809 -0
- oscura/hardware/acquisition/file.py +5 -19
- oscura/hardware/acquisition/saleae.py +10 -10
- oscura/hardware/acquisition/socketcan.py +4 -6
- oscura/hardware/acquisition/synthetic.py +1 -5
- oscura/hardware/acquisition/visa.py +6 -6
- oscura/hardware/security/side_channel_detector.py +5 -508
- oscura/inference/message_format.py +686 -1
- oscura/jupyter/display.py +2 -2
- oscura/jupyter/magic.py +3 -3
- oscura/loaders/__init__.py +17 -12
- oscura/loaders/binary.py +1 -1
- oscura/loaders/chipwhisperer.py +1 -2
- oscura/loaders/configurable.py +1 -1
- oscura/loaders/csv_loader.py +2 -2
- oscura/loaders/hdf5_loader.py +1 -1
- oscura/loaders/lazy.py +6 -1
- oscura/loaders/mmap_loader.py +0 -1
- oscura/loaders/numpy_loader.py +8 -7
- oscura/loaders/preprocessing.py +3 -5
- oscura/loaders/rigol.py +21 -7
- oscura/loaders/sigrok.py +2 -5
- oscura/loaders/tdms.py +3 -2
- oscura/loaders/tektronix.py +38 -32
- oscura/loaders/tss.py +20 -27
- oscura/loaders/vcd.py +13 -8
- oscura/loaders/wav.py +1 -6
- oscura/pipeline/__init__.py +76 -0
- oscura/pipeline/handlers/__init__.py +165 -0
- oscura/pipeline/handlers/analyzers.py +1045 -0
- oscura/pipeline/handlers/decoders.py +899 -0
- oscura/pipeline/handlers/exporters.py +1103 -0
- oscura/pipeline/handlers/filters.py +891 -0
- oscura/pipeline/handlers/loaders.py +640 -0
- oscura/pipeline/handlers/transforms.py +768 -0
- oscura/reporting/formatting/measurements.py +55 -14
- oscura/reporting/templates/enhanced/protocol_re.html +504 -503
- oscura/side_channel/__init__.py +38 -57
- oscura/utils/builders/signal_builder.py +5 -5
- oscura/utils/comparison/compare.py +7 -9
- oscura/utils/comparison/golden.py +1 -1
- oscura/utils/filtering/convenience.py +2 -2
- oscura/utils/math/arithmetic.py +38 -62
- oscura/utils/math/interpolation.py +20 -20
- oscura/utils/pipeline/__init__.py +4 -17
- oscura/utils/progressive.py +1 -4
- oscura/utils/triggering/edge.py +1 -1
- oscura/utils/triggering/pattern.py +2 -2
- oscura/utils/triggering/pulse.py +2 -2
- oscura/utils/triggering/window.py +3 -3
- oscura/validation/hil_testing.py +11 -11
- oscura/visualization/__init__.py +46 -284
- oscura/visualization/batch.py +72 -433
- oscura/visualization/plot.py +542 -53
- oscura/visualization/styles.py +184 -318
- oscura/workflows/batch/advanced.py +1 -1
- oscura/workflows/batch/aggregate.py +7 -8
- oscura/workflows/complete_re.py +251 -23
- oscura/workflows/digital.py +27 -4
- oscura/workflows/multi_trace.py +136 -17
- oscura/workflows/waveform.py +11 -6
- {oscura-0.8.0.dist-info → oscura-0.10.0.dist-info}/METADATA +59 -79
- {oscura-0.8.0.dist-info → oscura-0.10.0.dist-info}/RECORD +111 -136
- oscura/side_channel/dpa.py +0 -1025
- oscura/utils/optimization/__init__.py +0 -19
- oscura/utils/optimization/parallel.py +0 -443
- oscura/utils/optimization/search.py +0 -532
- oscura/utils/pipeline/base.py +0 -338
- oscura/utils/pipeline/composition.py +0 -248
- oscura/utils/pipeline/parallel.py +0 -449
- oscura/utils/pipeline/pipeline.py +0 -375
- oscura/utils/search/__init__.py +0 -16
- oscura/utils/search/anomaly.py +0 -424
- oscura/utils/search/context.py +0 -294
- oscura/utils/search/pattern.py +0 -288
- oscura/utils/storage/__init__.py +0 -61
- oscura/utils/storage/database.py +0 -1166
- oscura/visualization/accessibility.py +0 -526
- oscura/visualization/annotations.py +0 -371
- oscura/visualization/axis_scaling.py +0 -305
- oscura/visualization/colors.py +0 -451
- oscura/visualization/digital.py +0 -436
- oscura/visualization/eye.py +0 -571
- oscura/visualization/histogram.py +0 -281
- oscura/visualization/interactive.py +0 -1035
- oscura/visualization/jitter.py +0 -1042
- oscura/visualization/keyboard.py +0 -394
- oscura/visualization/layout.py +0 -400
- oscura/visualization/optimization.py +0 -1079
- oscura/visualization/palettes.py +0 -446
- oscura/visualization/power.py +0 -508
- oscura/visualization/power_extended.py +0 -955
- oscura/visualization/presets.py +0 -469
- oscura/visualization/protocols.py +0 -1246
- oscura/visualization/render.py +0 -223
- oscura/visualization/rendering.py +0 -444
- oscura/visualization/reverse_engineering.py +0 -838
- oscura/visualization/signal_integrity.py +0 -989
- oscura/visualization/specialized.py +0 -643
- oscura/visualization/spectral.py +0 -1226
- oscura/visualization/thumbnails.py +0 -340
- oscura/visualization/time_axis.py +0 -351
- oscura/visualization/waveform.py +0 -454
- {oscura-0.8.0.dist-info → oscura-0.10.0.dist-info}/WHEEL +0 -0
- {oscura-0.8.0.dist-info → oscura-0.10.0.dist-info}/entry_points.txt +0 -0
- {oscura-0.8.0.dist-info → oscura-0.10.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,891 @@
|
|
|
1
|
+
"""Filter handlers for pipeline system.
|
|
2
|
+
|
|
3
|
+
This module provides handlers for applying various digital filters to signals.
|
|
4
|
+
All handlers follow the standard signature: (inputs, params, step_name) -> outputs.
|
|
5
|
+
|
|
6
|
+
Available Handlers:
|
|
7
|
+
- filter.low_pass: Apply low-pass filter
|
|
8
|
+
- filter.high_pass: Apply high-pass filter
|
|
9
|
+
- filter.band_pass: Apply band-pass filter
|
|
10
|
+
- filter.band_stop: Apply band-stop/notch filter
|
|
11
|
+
- filter.butterworth: Apply Butterworth filter (generic)
|
|
12
|
+
- filter.chebyshev: Apply Chebyshev Type 1 filter
|
|
13
|
+
- filter.bessel: Apply Bessel filter
|
|
14
|
+
- filter.moving_average: Apply moving average filter
|
|
15
|
+
- filter.savitzky_golay: Apply Savitzky-Golay smoothing
|
|
16
|
+
- filter.median: Apply median filter
|
|
17
|
+
- filter.gaussian: Apply Gaussian smoothing
|
|
18
|
+
- filter.notch: Apply narrow notch filter at specific frequency
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
from oscura.core.config.pipeline import PipelineExecutionError
|
|
26
|
+
from oscura.pipeline.handlers import register_handler
|
|
27
|
+
|
|
28
|
+
# Lazy imports to avoid circular dependencies
|
|
29
|
+
_filtering = None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _get_filtering() -> Any:
|
|
33
|
+
"""Lazy import filtering module."""
|
|
34
|
+
global _filtering
|
|
35
|
+
if _filtering is None:
|
|
36
|
+
import oscura.utils.filtering as _filtering_module
|
|
37
|
+
|
|
38
|
+
_filtering = _filtering_module
|
|
39
|
+
return _filtering
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@register_handler("filter.low_pass")
|
|
43
|
+
def handle_filter_low_pass(
|
|
44
|
+
inputs: dict[str, Any], params: dict[str, Any], step_name: str
|
|
45
|
+
) -> dict[str, Any]:
|
|
46
|
+
"""Apply low-pass filter to remove high-frequency components.
|
|
47
|
+
|
|
48
|
+
Inputs:
|
|
49
|
+
trace: WaveformTrace to filter
|
|
50
|
+
|
|
51
|
+
Parameters:
|
|
52
|
+
cutoff (float): Cutoff frequency in Hz
|
|
53
|
+
order (int, optional): Filter order (default: 4)
|
|
54
|
+
filter_type (str, optional): Filter type - 'butterworth', 'chebyshev1',
|
|
55
|
+
'chebyshev2', 'bessel', or 'elliptic' (default: 'butterworth')
|
|
56
|
+
|
|
57
|
+
Outputs:
|
|
58
|
+
trace: Filtered WaveformTrace
|
|
59
|
+
cutoff: Applied cutoff frequency
|
|
60
|
+
order: Filter order used
|
|
61
|
+
filter_type: Filter type used
|
|
62
|
+
|
|
63
|
+
Example:
|
|
64
|
+
>>> # Remove noise above 1 MHz
|
|
65
|
+
>>> result = handle_filter_low_pass(
|
|
66
|
+
... {"trace": my_trace},
|
|
67
|
+
... {"cutoff": 1e6, "order": 6},
|
|
68
|
+
... "lpf"
|
|
69
|
+
... )
|
|
70
|
+
"""
|
|
71
|
+
filtering = _get_filtering()
|
|
72
|
+
|
|
73
|
+
trace = inputs.get("trace")
|
|
74
|
+
if trace is None:
|
|
75
|
+
raise PipelineExecutionError(
|
|
76
|
+
"Missing required input 'trace' - provide WaveformTrace",
|
|
77
|
+
step_name=step_name,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
cutoff = params.get("cutoff")
|
|
81
|
+
if cutoff is None:
|
|
82
|
+
raise PipelineExecutionError(
|
|
83
|
+
"Missing required parameter 'cutoff' - add 'cutoff: 1e6' (frequency in Hz)",
|
|
84
|
+
step_name=step_name,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
order = params.get("order", 4)
|
|
88
|
+
filter_type = params.get("filter_type", "butterworth")
|
|
89
|
+
|
|
90
|
+
try:
|
|
91
|
+
filtered_trace = filtering.low_pass(
|
|
92
|
+
trace,
|
|
93
|
+
cutoff=cutoff,
|
|
94
|
+
order=order,
|
|
95
|
+
filter_type=filter_type,
|
|
96
|
+
)
|
|
97
|
+
except Exception as e:
|
|
98
|
+
raise PipelineExecutionError(f"Low-pass filtering failed: {e}", step_name=step_name) from e
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
"trace": filtered_trace,
|
|
102
|
+
"cutoff": cutoff,
|
|
103
|
+
"order": order,
|
|
104
|
+
"filter_type": filter_type,
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@register_handler("filter.high_pass")
|
|
109
|
+
def handle_filter_high_pass(
|
|
110
|
+
inputs: dict[str, Any], params: dict[str, Any], step_name: str
|
|
111
|
+
) -> dict[str, Any]:
|
|
112
|
+
"""Apply high-pass filter to remove low-frequency components and DC offset.
|
|
113
|
+
|
|
114
|
+
Inputs:
|
|
115
|
+
trace: WaveformTrace to filter
|
|
116
|
+
|
|
117
|
+
Parameters:
|
|
118
|
+
cutoff (float): Cutoff frequency in Hz
|
|
119
|
+
order (int, optional): Filter order (default: 4)
|
|
120
|
+
filter_type (str, optional): Filter type - 'butterworth', 'chebyshev1',
|
|
121
|
+
'chebyshev2', 'bessel', or 'elliptic' (default: 'butterworth')
|
|
122
|
+
|
|
123
|
+
Outputs:
|
|
124
|
+
trace: Filtered WaveformTrace
|
|
125
|
+
cutoff: Applied cutoff frequency
|
|
126
|
+
order: Filter order used
|
|
127
|
+
filter_type: Filter type used
|
|
128
|
+
|
|
129
|
+
Example:
|
|
130
|
+
>>> # Remove DC and frequencies below 100 Hz
|
|
131
|
+
>>> result = handle_filter_high_pass(
|
|
132
|
+
... {"trace": my_trace},
|
|
133
|
+
... {"cutoff": 100, "order": 4},
|
|
134
|
+
... "hpf"
|
|
135
|
+
... )
|
|
136
|
+
"""
|
|
137
|
+
filtering = _get_filtering()
|
|
138
|
+
|
|
139
|
+
trace = inputs.get("trace")
|
|
140
|
+
if trace is None:
|
|
141
|
+
raise PipelineExecutionError(
|
|
142
|
+
"Missing required input 'trace' - provide WaveformTrace",
|
|
143
|
+
step_name=step_name,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
cutoff = params.get("cutoff")
|
|
147
|
+
if cutoff is None:
|
|
148
|
+
raise PipelineExecutionError(
|
|
149
|
+
"Missing required parameter 'cutoff' - add 'cutoff: 100' (frequency in Hz)",
|
|
150
|
+
step_name=step_name,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
order = params.get("order", 4)
|
|
154
|
+
filter_type = params.get("filter_type", "butterworth")
|
|
155
|
+
|
|
156
|
+
try:
|
|
157
|
+
filtered_trace = filtering.high_pass(
|
|
158
|
+
trace,
|
|
159
|
+
cutoff=cutoff,
|
|
160
|
+
order=order,
|
|
161
|
+
filter_type=filter_type,
|
|
162
|
+
)
|
|
163
|
+
except Exception as e:
|
|
164
|
+
raise PipelineExecutionError(f"High-pass filtering failed: {e}", step_name=step_name) from e
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
"trace": filtered_trace,
|
|
168
|
+
"cutoff": cutoff,
|
|
169
|
+
"order": order,
|
|
170
|
+
"filter_type": filter_type,
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
@register_handler("filter.band_pass")
|
|
175
|
+
def handle_filter_band_pass(
|
|
176
|
+
inputs: dict[str, Any], params: dict[str, Any], step_name: str
|
|
177
|
+
) -> dict[str, Any]:
|
|
178
|
+
"""Apply band-pass filter to pass frequencies in a specified band.
|
|
179
|
+
|
|
180
|
+
Inputs:
|
|
181
|
+
trace: WaveformTrace to filter
|
|
182
|
+
|
|
183
|
+
Parameters:
|
|
184
|
+
low (float): Lower cutoff frequency in Hz
|
|
185
|
+
high (float): Upper cutoff frequency in Hz
|
|
186
|
+
order (int, optional): Filter order (default: 4)
|
|
187
|
+
filter_type (str, optional): Filter type - 'butterworth', 'chebyshev1',
|
|
188
|
+
'chebyshev2', 'bessel', or 'elliptic' (default: 'butterworth')
|
|
189
|
+
|
|
190
|
+
Outputs:
|
|
191
|
+
trace: Filtered WaveformTrace
|
|
192
|
+
low: Lower cutoff frequency
|
|
193
|
+
high: Upper cutoff frequency
|
|
194
|
+
order: Filter order used
|
|
195
|
+
filter_type: Filter type used
|
|
196
|
+
|
|
197
|
+
Example:
|
|
198
|
+
>>> # Pass only 1 kHz to 10 kHz
|
|
199
|
+
>>> result = handle_filter_band_pass(
|
|
200
|
+
... {"trace": my_trace},
|
|
201
|
+
... {"low": 1e3, "high": 10e3, "order": 6},
|
|
202
|
+
... "bpf"
|
|
203
|
+
... )
|
|
204
|
+
"""
|
|
205
|
+
filtering = _get_filtering()
|
|
206
|
+
|
|
207
|
+
trace = inputs.get("trace")
|
|
208
|
+
if trace is None:
|
|
209
|
+
raise PipelineExecutionError(
|
|
210
|
+
"Missing required input 'trace' - provide WaveformTrace",
|
|
211
|
+
step_name=step_name,
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
low = params.get("low")
|
|
215
|
+
high = params.get("high")
|
|
216
|
+
|
|
217
|
+
if low is None or high is None:
|
|
218
|
+
raise PipelineExecutionError(
|
|
219
|
+
"Missing required parameters 'low' and 'high' - add frequencies in Hz",
|
|
220
|
+
step_name=step_name,
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
if low >= high:
|
|
224
|
+
raise PipelineExecutionError(
|
|
225
|
+
f"Low cutoff ({low}) must be less than high cutoff ({high})",
|
|
226
|
+
step_name=step_name,
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
order = params.get("order", 4)
|
|
230
|
+
filter_type = params.get("filter_type", "butterworth")
|
|
231
|
+
|
|
232
|
+
try:
|
|
233
|
+
filtered_trace = filtering.band_pass(
|
|
234
|
+
trace,
|
|
235
|
+
low=low,
|
|
236
|
+
high=high,
|
|
237
|
+
order=order,
|
|
238
|
+
filter_type=filter_type,
|
|
239
|
+
)
|
|
240
|
+
except Exception as e:
|
|
241
|
+
raise PipelineExecutionError(f"Band-pass filtering failed: {e}", step_name=step_name) from e
|
|
242
|
+
|
|
243
|
+
return {
|
|
244
|
+
"trace": filtered_trace,
|
|
245
|
+
"low": low,
|
|
246
|
+
"high": high,
|
|
247
|
+
"order": order,
|
|
248
|
+
"filter_type": filter_type,
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
@register_handler("filter.band_stop")
|
|
253
|
+
def handle_filter_band_stop(
|
|
254
|
+
inputs: dict[str, Any], params: dict[str, Any], step_name: str
|
|
255
|
+
) -> dict[str, Any]:
|
|
256
|
+
"""Apply band-stop (notch) filter to reject frequencies in a specified band.
|
|
257
|
+
|
|
258
|
+
Inputs:
|
|
259
|
+
trace: WaveformTrace to filter
|
|
260
|
+
|
|
261
|
+
Parameters:
|
|
262
|
+
low (float): Lower cutoff frequency in Hz
|
|
263
|
+
high (float): Upper cutoff frequency in Hz
|
|
264
|
+
order (int, optional): Filter order (default: 4)
|
|
265
|
+
filter_type (str, optional): Filter type - 'butterworth', 'chebyshev1',
|
|
266
|
+
'chebyshev2', 'bessel', or 'elliptic' (default: 'butterworth')
|
|
267
|
+
|
|
268
|
+
Outputs:
|
|
269
|
+
trace: Filtered WaveformTrace
|
|
270
|
+
low: Lower cutoff frequency
|
|
271
|
+
high: Upper cutoff frequency
|
|
272
|
+
order: Filter order used
|
|
273
|
+
filter_type: Filter type used
|
|
274
|
+
|
|
275
|
+
Example:
|
|
276
|
+
>>> # Remove 50 Hz power line interference
|
|
277
|
+
>>> result = handle_filter_band_stop(
|
|
278
|
+
... {"trace": my_trace},
|
|
279
|
+
... {"low": 49, "high": 51, "order": 4},
|
|
280
|
+
... "bsf"
|
|
281
|
+
... )
|
|
282
|
+
"""
|
|
283
|
+
filtering = _get_filtering()
|
|
284
|
+
|
|
285
|
+
trace = inputs.get("trace")
|
|
286
|
+
if trace is None:
|
|
287
|
+
raise PipelineExecutionError(
|
|
288
|
+
"Missing required input 'trace' - provide WaveformTrace",
|
|
289
|
+
step_name=step_name,
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
low = params.get("low")
|
|
293
|
+
high = params.get("high")
|
|
294
|
+
|
|
295
|
+
if low is None or high is None:
|
|
296
|
+
raise PipelineExecutionError(
|
|
297
|
+
"Missing required parameters 'low' and 'high' - add frequencies in Hz",
|
|
298
|
+
step_name=step_name,
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
if low >= high:
|
|
302
|
+
raise PipelineExecutionError(
|
|
303
|
+
f"Low cutoff ({low}) must be less than high cutoff ({high})",
|
|
304
|
+
step_name=step_name,
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
order = params.get("order", 4)
|
|
308
|
+
filter_type = params.get("filter_type", "butterworth")
|
|
309
|
+
|
|
310
|
+
try:
|
|
311
|
+
filtered_trace = filtering.band_stop(
|
|
312
|
+
trace,
|
|
313
|
+
low=low,
|
|
314
|
+
high=high,
|
|
315
|
+
order=order,
|
|
316
|
+
filter_type=filter_type,
|
|
317
|
+
)
|
|
318
|
+
except Exception as e:
|
|
319
|
+
raise PipelineExecutionError(f"Band-stop filtering failed: {e}", step_name=step_name) from e
|
|
320
|
+
|
|
321
|
+
return {
|
|
322
|
+
"trace": filtered_trace,
|
|
323
|
+
"low": low,
|
|
324
|
+
"high": high,
|
|
325
|
+
"order": order,
|
|
326
|
+
"filter_type": filter_type,
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
@register_handler("filter.butterworth")
|
|
331
|
+
def handle_filter_butterworth(
|
|
332
|
+
inputs: dict[str, Any], params: dict[str, Any], step_name: str
|
|
333
|
+
) -> dict[str, Any]:
|
|
334
|
+
"""Apply generic Butterworth filter with specified band type.
|
|
335
|
+
|
|
336
|
+
Butterworth filters have maximally flat passband response.
|
|
337
|
+
|
|
338
|
+
Inputs:
|
|
339
|
+
trace: WaveformTrace to filter
|
|
340
|
+
|
|
341
|
+
Parameters:
|
|
342
|
+
cutoff (float|tuple): Cutoff frequency in Hz (scalar for low/high-pass,
|
|
343
|
+
tuple (low, high) for band-pass/band-stop)
|
|
344
|
+
band_type (str): Filter band type - 'lowpass', 'highpass', 'bandpass', or 'bandstop'
|
|
345
|
+
order (int, optional): Filter order (default: 4)
|
|
346
|
+
|
|
347
|
+
Outputs:
|
|
348
|
+
trace: Filtered WaveformTrace
|
|
349
|
+
cutoff: Applied cutoff frequency
|
|
350
|
+
band_type: Filter band type used
|
|
351
|
+
order: Filter order used
|
|
352
|
+
|
|
353
|
+
Example:
|
|
354
|
+
>>> # Generic low-pass Butterworth
|
|
355
|
+
>>> result = handle_filter_butterworth(
|
|
356
|
+
... {"trace": my_trace},
|
|
357
|
+
... {"cutoff": 1e6, "band_type": "lowpass", "order": 6},
|
|
358
|
+
... "butter"
|
|
359
|
+
... )
|
|
360
|
+
"""
|
|
361
|
+
filtering = _get_filtering()
|
|
362
|
+
|
|
363
|
+
trace = inputs.get("trace")
|
|
364
|
+
if trace is None:
|
|
365
|
+
raise PipelineExecutionError("Missing required input 'trace'", step_name=step_name)
|
|
366
|
+
|
|
367
|
+
cutoff = params.get("cutoff")
|
|
368
|
+
if cutoff is None:
|
|
369
|
+
raise PipelineExecutionError(
|
|
370
|
+
"Missing required parameter 'cutoff'",
|
|
371
|
+
step_name=step_name,
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
band_type = params.get("band_type", "lowpass")
|
|
375
|
+
if band_type not in ["lowpass", "highpass", "bandpass", "bandstop"]:
|
|
376
|
+
raise PipelineExecutionError(
|
|
377
|
+
f"Invalid band_type '{band_type}' - use lowpass/highpass/bandpass/bandstop",
|
|
378
|
+
step_name=step_name,
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
order = params.get("order", 4)
|
|
382
|
+
|
|
383
|
+
try:
|
|
384
|
+
# Create Butterworth filter
|
|
385
|
+
sample_rate = trace.metadata.sample_rate
|
|
386
|
+
filt = filtering.ButterworthFilter(
|
|
387
|
+
cutoff=cutoff,
|
|
388
|
+
sample_rate=sample_rate,
|
|
389
|
+
order=order,
|
|
390
|
+
band_type=band_type,
|
|
391
|
+
)
|
|
392
|
+
filtered_trace = filt.apply(trace)
|
|
393
|
+
|
|
394
|
+
except Exception as e:
|
|
395
|
+
raise PipelineExecutionError(
|
|
396
|
+
f"Butterworth filtering failed: {e}", step_name=step_name
|
|
397
|
+
) from e
|
|
398
|
+
|
|
399
|
+
return {
|
|
400
|
+
"trace": filtered_trace,
|
|
401
|
+
"cutoff": cutoff,
|
|
402
|
+
"band_type": band_type,
|
|
403
|
+
"order": order,
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
@register_handler("filter.chebyshev")
|
|
408
|
+
def handle_filter_chebyshev(
|
|
409
|
+
inputs: dict[str, Any], params: dict[str, Any], step_name: str
|
|
410
|
+
) -> dict[str, Any]:
|
|
411
|
+
"""Apply Chebyshev Type 1 filter with specified band type.
|
|
412
|
+
|
|
413
|
+
Chebyshev Type 1 filters have ripple in the passband but steeper rolloff
|
|
414
|
+
than Butterworth filters.
|
|
415
|
+
|
|
416
|
+
Inputs:
|
|
417
|
+
trace: WaveformTrace to filter
|
|
418
|
+
|
|
419
|
+
Parameters:
|
|
420
|
+
cutoff (float|tuple): Cutoff frequency in Hz (scalar for low/high-pass,
|
|
421
|
+
tuple (low, high) for band-pass/band-stop)
|
|
422
|
+
band_type (str): Filter band type - 'lowpass', 'highpass', 'bandpass', or 'bandstop'
|
|
423
|
+
order (int, optional): Filter order (default: 4)
|
|
424
|
+
ripple (float, optional): Passband ripple in dB (default: 1.0)
|
|
425
|
+
|
|
426
|
+
Outputs:
|
|
427
|
+
trace: Filtered WaveformTrace
|
|
428
|
+
cutoff: Applied cutoff frequency
|
|
429
|
+
band_type: Filter band type used
|
|
430
|
+
order: Filter order used
|
|
431
|
+
ripple: Passband ripple in dB
|
|
432
|
+
|
|
433
|
+
Example:
|
|
434
|
+
>>> # Chebyshev low-pass with 0.5 dB ripple
|
|
435
|
+
>>> result = handle_filter_chebyshev(
|
|
436
|
+
... {"trace": my_trace},
|
|
437
|
+
... {"cutoff": 1e6, "band_type": "lowpass", "ripple": 0.5},
|
|
438
|
+
... "cheby"
|
|
439
|
+
... )
|
|
440
|
+
"""
|
|
441
|
+
filtering = _get_filtering()
|
|
442
|
+
|
|
443
|
+
trace = inputs.get("trace")
|
|
444
|
+
if trace is None:
|
|
445
|
+
raise PipelineExecutionError("Missing required input 'trace'", step_name=step_name)
|
|
446
|
+
|
|
447
|
+
cutoff = params.get("cutoff")
|
|
448
|
+
if cutoff is None:
|
|
449
|
+
raise PipelineExecutionError("Missing required parameter 'cutoff'", step_name=step_name)
|
|
450
|
+
|
|
451
|
+
band_type = params.get("band_type", "lowpass")
|
|
452
|
+
if band_type not in ["lowpass", "highpass", "bandpass", "bandstop"]:
|
|
453
|
+
raise PipelineExecutionError(
|
|
454
|
+
f"Invalid band_type '{band_type}' - use lowpass/highpass/bandpass/bandstop",
|
|
455
|
+
step_name=step_name,
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
order = params.get("order", 4)
|
|
459
|
+
ripple = params.get("ripple", 1.0)
|
|
460
|
+
|
|
461
|
+
try:
|
|
462
|
+
sample_rate = trace.metadata.sample_rate
|
|
463
|
+
filt = filtering.ChebyshevType1Filter(
|
|
464
|
+
cutoff=cutoff,
|
|
465
|
+
sample_rate=sample_rate,
|
|
466
|
+
order=order,
|
|
467
|
+
band_type=band_type,
|
|
468
|
+
ripple=ripple,
|
|
469
|
+
)
|
|
470
|
+
filtered_trace = filt.apply(trace)
|
|
471
|
+
|
|
472
|
+
except Exception as e:
|
|
473
|
+
raise PipelineExecutionError(f"Chebyshev filtering failed: {e}", step_name=step_name) from e
|
|
474
|
+
|
|
475
|
+
return {
|
|
476
|
+
"trace": filtered_trace,
|
|
477
|
+
"cutoff": cutoff,
|
|
478
|
+
"band_type": band_type,
|
|
479
|
+
"order": order,
|
|
480
|
+
"ripple": ripple,
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
@register_handler("filter.bessel")
|
|
485
|
+
def handle_filter_bessel(
|
|
486
|
+
inputs: dict[str, Any], params: dict[str, Any], step_name: str
|
|
487
|
+
) -> dict[str, Any]:
|
|
488
|
+
"""Apply Bessel filter with specified band type.
|
|
489
|
+
|
|
490
|
+
Bessel filters have maximally flat group delay (linear phase response),
|
|
491
|
+
preserving pulse shapes better than other filter types.
|
|
492
|
+
|
|
493
|
+
Inputs:
|
|
494
|
+
trace: WaveformTrace to filter
|
|
495
|
+
|
|
496
|
+
Parameters:
|
|
497
|
+
cutoff (float|tuple): Cutoff frequency in Hz (scalar for low/high-pass,
|
|
498
|
+
tuple (low, high) for band-pass/band-stop)
|
|
499
|
+
band_type (str): Filter band type - 'lowpass', 'highpass', 'bandpass', or 'bandstop'
|
|
500
|
+
order (int, optional): Filter order (default: 4)
|
|
501
|
+
|
|
502
|
+
Outputs:
|
|
503
|
+
trace: Filtered WaveformTrace
|
|
504
|
+
cutoff: Applied cutoff frequency
|
|
505
|
+
band_type: Filter band type used
|
|
506
|
+
order: Filter order used
|
|
507
|
+
|
|
508
|
+
Example:
|
|
509
|
+
>>> # Bessel low-pass for minimal pulse distortion
|
|
510
|
+
>>> result = handle_filter_bessel(
|
|
511
|
+
... {"trace": my_trace},
|
|
512
|
+
... {"cutoff": 1e6, "band_type": "lowpass", "order": 5},
|
|
513
|
+
... "bessel"
|
|
514
|
+
... )
|
|
515
|
+
"""
|
|
516
|
+
filtering = _get_filtering()
|
|
517
|
+
|
|
518
|
+
trace = inputs.get("trace")
|
|
519
|
+
if trace is None:
|
|
520
|
+
raise PipelineExecutionError("Missing required input 'trace'", step_name=step_name)
|
|
521
|
+
|
|
522
|
+
cutoff = params.get("cutoff")
|
|
523
|
+
if cutoff is None:
|
|
524
|
+
raise PipelineExecutionError("Missing required parameter 'cutoff'", step_name=step_name)
|
|
525
|
+
|
|
526
|
+
band_type = params.get("band_type", "lowpass")
|
|
527
|
+
if band_type not in ["lowpass", "highpass", "bandpass", "bandstop"]:
|
|
528
|
+
raise PipelineExecutionError(
|
|
529
|
+
f"Invalid band_type '{band_type}' - use lowpass/highpass/bandpass/bandstop",
|
|
530
|
+
step_name=step_name,
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
order = params.get("order", 4)
|
|
534
|
+
|
|
535
|
+
try:
|
|
536
|
+
sample_rate = trace.metadata.sample_rate
|
|
537
|
+
filt = filtering.BesselFilter(
|
|
538
|
+
cutoff=cutoff,
|
|
539
|
+
sample_rate=sample_rate,
|
|
540
|
+
order=order,
|
|
541
|
+
band_type=band_type,
|
|
542
|
+
)
|
|
543
|
+
filtered_trace = filt.apply(trace)
|
|
544
|
+
|
|
545
|
+
except Exception as e:
|
|
546
|
+
raise PipelineExecutionError(f"Bessel filtering failed: {e}", step_name=step_name) from e
|
|
547
|
+
|
|
548
|
+
return {
|
|
549
|
+
"trace": filtered_trace,
|
|
550
|
+
"cutoff": cutoff,
|
|
551
|
+
"band_type": band_type,
|
|
552
|
+
"order": order,
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
@register_handler("filter.moving_average")
|
|
557
|
+
def handle_filter_moving_average(
|
|
558
|
+
inputs: dict[str, Any], params: dict[str, Any], step_name: str
|
|
559
|
+
) -> dict[str, Any]:
|
|
560
|
+
"""Apply moving average (FIR) filter for simple smoothing.
|
|
561
|
+
|
|
562
|
+
Inputs:
|
|
563
|
+
trace: WaveformTrace to filter
|
|
564
|
+
|
|
565
|
+
Parameters:
|
|
566
|
+
window_size (int): Number of samples in averaging window (must be odd for 'same' mode)
|
|
567
|
+
mode (str, optional): Convolution mode - 'same' (default), 'valid', or 'full'
|
|
568
|
+
|
|
569
|
+
Outputs:
|
|
570
|
+
trace: Filtered WaveformTrace
|
|
571
|
+
window_size: Window size used
|
|
572
|
+
mode: Convolution mode used
|
|
573
|
+
|
|
574
|
+
Example:
|
|
575
|
+
>>> # Simple 11-sample moving average
|
|
576
|
+
>>> result = handle_filter_moving_average(
|
|
577
|
+
... {"trace": my_trace},
|
|
578
|
+
... {"window_size": 11},
|
|
579
|
+
... "mavg"
|
|
580
|
+
... )
|
|
581
|
+
"""
|
|
582
|
+
filtering = _get_filtering()
|
|
583
|
+
|
|
584
|
+
trace = inputs.get("trace")
|
|
585
|
+
if trace is None:
|
|
586
|
+
raise PipelineExecutionError("Missing required input 'trace'", step_name=step_name)
|
|
587
|
+
|
|
588
|
+
window_size = params.get("window_size")
|
|
589
|
+
if window_size is None:
|
|
590
|
+
raise PipelineExecutionError(
|
|
591
|
+
"Missing required parameter 'window_size'",
|
|
592
|
+
step_name=step_name,
|
|
593
|
+
)
|
|
594
|
+
|
|
595
|
+
if not isinstance(window_size, int) or window_size < 1:
|
|
596
|
+
raise PipelineExecutionError(
|
|
597
|
+
f"Window size must be a positive integer, got {window_size}",
|
|
598
|
+
step_name=step_name,
|
|
599
|
+
)
|
|
600
|
+
|
|
601
|
+
mode = params.get("mode", "same")
|
|
602
|
+
|
|
603
|
+
try:
|
|
604
|
+
filtered_trace = filtering.moving_average(
|
|
605
|
+
trace,
|
|
606
|
+
window_size=window_size,
|
|
607
|
+
mode=mode,
|
|
608
|
+
)
|
|
609
|
+
except Exception as e:
|
|
610
|
+
raise PipelineExecutionError(
|
|
611
|
+
f"Moving average filtering failed: {e}", step_name=step_name
|
|
612
|
+
) from e
|
|
613
|
+
|
|
614
|
+
return {
|
|
615
|
+
"trace": filtered_trace,
|
|
616
|
+
"window_size": window_size,
|
|
617
|
+
"mode": mode,
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
@register_handler("filter.savitzky_golay")
|
|
622
|
+
def handle_filter_savitzky_golay(
|
|
623
|
+
inputs: dict[str, Any], params: dict[str, Any], step_name: str
|
|
624
|
+
) -> dict[str, Any]:
|
|
625
|
+
"""Apply Savitzky-Golay smoothing filter.
|
|
626
|
+
|
|
627
|
+
Smooths data while preserving higher moments (peaks, etc.) better than
|
|
628
|
+
simple moving average. Uses polynomial fitting within a moving window.
|
|
629
|
+
|
|
630
|
+
Inputs:
|
|
631
|
+
trace: WaveformTrace to filter
|
|
632
|
+
|
|
633
|
+
Parameters:
|
|
634
|
+
window_length (int): Length of filter window (must be odd and > polyorder)
|
|
635
|
+
polyorder (int): Order of polynomial used in fitting
|
|
636
|
+
deriv (int, optional): Derivative order (0 = smoothing, 1 = first derivative, etc.)
|
|
637
|
+
(default: 0)
|
|
638
|
+
|
|
639
|
+
Outputs:
|
|
640
|
+
trace: Filtered WaveformTrace
|
|
641
|
+
window_length: Window length used
|
|
642
|
+
polyorder: Polynomial order used
|
|
643
|
+
deriv: Derivative order
|
|
644
|
+
|
|
645
|
+
Example:
|
|
646
|
+
>>> # Savitzky-Golay smoothing with cubic polynomial
|
|
647
|
+
>>> result = handle_filter_savitzky_golay(
|
|
648
|
+
... {"trace": my_trace},
|
|
649
|
+
... {"window_length": 11, "polyorder": 3},
|
|
650
|
+
... "savgol"
|
|
651
|
+
... )
|
|
652
|
+
"""
|
|
653
|
+
filtering = _get_filtering()
|
|
654
|
+
|
|
655
|
+
trace = inputs.get("trace")
|
|
656
|
+
if trace is None:
|
|
657
|
+
raise PipelineExecutionError("Missing required input 'trace'", step_name=step_name)
|
|
658
|
+
|
|
659
|
+
window_length = params.get("window_length")
|
|
660
|
+
polyorder = params.get("polyorder")
|
|
661
|
+
|
|
662
|
+
if window_length is None or polyorder is None:
|
|
663
|
+
raise PipelineExecutionError(
|
|
664
|
+
"Missing required parameters 'window_length' and 'polyorder'",
|
|
665
|
+
step_name=step_name,
|
|
666
|
+
)
|
|
667
|
+
|
|
668
|
+
if not isinstance(window_length, int) or window_length < 1:
|
|
669
|
+
raise PipelineExecutionError(
|
|
670
|
+
f"Window length must be a positive integer, got {window_length}",
|
|
671
|
+
step_name=step_name,
|
|
672
|
+
)
|
|
673
|
+
|
|
674
|
+
if not isinstance(polyorder, int) or polyorder < 0:
|
|
675
|
+
raise PipelineExecutionError(
|
|
676
|
+
f"Polynomial order must be a non-negative integer, got {polyorder}",
|
|
677
|
+
step_name=step_name,
|
|
678
|
+
)
|
|
679
|
+
|
|
680
|
+
deriv = params.get("deriv", 0)
|
|
681
|
+
|
|
682
|
+
try:
|
|
683
|
+
filtered_trace = filtering.savgol_filter(
|
|
684
|
+
trace,
|
|
685
|
+
window_length=window_length,
|
|
686
|
+
polyorder=polyorder,
|
|
687
|
+
deriv=deriv,
|
|
688
|
+
)
|
|
689
|
+
except Exception as e:
|
|
690
|
+
raise PipelineExecutionError(
|
|
691
|
+
f"Savitzky-Golay filtering failed: {e}", step_name=step_name
|
|
692
|
+
) from e
|
|
693
|
+
|
|
694
|
+
return {
|
|
695
|
+
"trace": filtered_trace,
|
|
696
|
+
"window_length": window_length,
|
|
697
|
+
"polyorder": polyorder,
|
|
698
|
+
"deriv": deriv,
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
|
|
702
|
+
@register_handler("filter.median")
|
|
703
|
+
def handle_filter_median(
|
|
704
|
+
inputs: dict[str, Any], params: dict[str, Any], step_name: str
|
|
705
|
+
) -> dict[str, Any]:
|
|
706
|
+
"""Apply median filter for spike/impulse noise removal.
|
|
707
|
+
|
|
708
|
+
Non-linear filter that preserves edges while removing outliers.
|
|
709
|
+
Excellent for removing salt-and-pepper noise or voltage spikes.
|
|
710
|
+
|
|
711
|
+
Inputs:
|
|
712
|
+
trace: WaveformTrace to filter
|
|
713
|
+
|
|
714
|
+
Parameters:
|
|
715
|
+
kernel_size (int): Size of the median filter kernel (must be odd)
|
|
716
|
+
|
|
717
|
+
Outputs:
|
|
718
|
+
trace: Filtered WaveformTrace
|
|
719
|
+
kernel_size: Kernel size used
|
|
720
|
+
|
|
721
|
+
Example:
|
|
722
|
+
>>> # Remove impulse noise with 5-sample median filter
|
|
723
|
+
>>> result = handle_filter_median(
|
|
724
|
+
... {"trace": my_trace},
|
|
725
|
+
... {"kernel_size": 5},
|
|
726
|
+
... "median"
|
|
727
|
+
... )
|
|
728
|
+
"""
|
|
729
|
+
filtering = _get_filtering()
|
|
730
|
+
|
|
731
|
+
trace = inputs.get("trace")
|
|
732
|
+
if trace is None:
|
|
733
|
+
raise PipelineExecutionError("Missing required input 'trace'", step_name=step_name)
|
|
734
|
+
|
|
735
|
+
kernel_size = params.get("kernel_size")
|
|
736
|
+
if kernel_size is None:
|
|
737
|
+
raise PipelineExecutionError(
|
|
738
|
+
"Missing required parameter 'kernel_size'",
|
|
739
|
+
step_name=step_name,
|
|
740
|
+
)
|
|
741
|
+
|
|
742
|
+
if not isinstance(kernel_size, int) or kernel_size < 1:
|
|
743
|
+
raise PipelineExecutionError(
|
|
744
|
+
f"Kernel size must be a positive integer, got {kernel_size}",
|
|
745
|
+
step_name=step_name,
|
|
746
|
+
)
|
|
747
|
+
|
|
748
|
+
try:
|
|
749
|
+
filtered_trace = filtering.median_filter(
|
|
750
|
+
trace,
|
|
751
|
+
kernel_size=kernel_size,
|
|
752
|
+
)
|
|
753
|
+
except Exception as e:
|
|
754
|
+
raise PipelineExecutionError(f"Median filtering failed: {e}", step_name=step_name) from e
|
|
755
|
+
|
|
756
|
+
return {
|
|
757
|
+
"trace": filtered_trace,
|
|
758
|
+
"kernel_size": kernel_size,
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
|
|
762
|
+
@register_handler("filter.gaussian")
|
|
763
|
+
def handle_filter_gaussian(
|
|
764
|
+
inputs: dict[str, Any], params: dict[str, Any], step_name: str
|
|
765
|
+
) -> dict[str, Any]:
|
|
766
|
+
"""Apply Gaussian smoothing filter.
|
|
767
|
+
|
|
768
|
+
Smooths with Gaussian kernel of specified standard deviation.
|
|
769
|
+
Provides optimal time-frequency localization.
|
|
770
|
+
|
|
771
|
+
Inputs:
|
|
772
|
+
trace: WaveformTrace to filter
|
|
773
|
+
|
|
774
|
+
Parameters:
|
|
775
|
+
sigma (float): Standard deviation of Gaussian kernel in samples
|
|
776
|
+
|
|
777
|
+
Outputs:
|
|
778
|
+
trace: Filtered WaveformTrace
|
|
779
|
+
sigma: Standard deviation used
|
|
780
|
+
|
|
781
|
+
Example:
|
|
782
|
+
>>> # Gaussian smoothing with sigma=3
|
|
783
|
+
>>> result = handle_filter_gaussian(
|
|
784
|
+
... {"trace": my_trace},
|
|
785
|
+
... {"sigma": 3.0},
|
|
786
|
+
... "gaussian"
|
|
787
|
+
... )
|
|
788
|
+
"""
|
|
789
|
+
filtering = _get_filtering()
|
|
790
|
+
|
|
791
|
+
trace = inputs.get("trace")
|
|
792
|
+
if trace is None:
|
|
793
|
+
raise PipelineExecutionError("Missing required input 'trace'", step_name=step_name)
|
|
794
|
+
|
|
795
|
+
sigma = params.get("sigma")
|
|
796
|
+
if sigma is None:
|
|
797
|
+
raise PipelineExecutionError(
|
|
798
|
+
"Missing required parameter 'sigma'",
|
|
799
|
+
step_name=step_name,
|
|
800
|
+
)
|
|
801
|
+
|
|
802
|
+
if not isinstance(sigma, (int, float)) or sigma <= 0:
|
|
803
|
+
raise PipelineExecutionError(
|
|
804
|
+
f"Sigma must be a positive number, got {sigma}",
|
|
805
|
+
step_name=step_name,
|
|
806
|
+
)
|
|
807
|
+
|
|
808
|
+
try:
|
|
809
|
+
filtered_trace = filtering.gaussian_filter(
|
|
810
|
+
trace,
|
|
811
|
+
sigma=sigma,
|
|
812
|
+
)
|
|
813
|
+
except Exception as e:
|
|
814
|
+
raise PipelineExecutionError(f"Gaussian filtering failed: {e}", step_name=step_name) from e
|
|
815
|
+
|
|
816
|
+
return {
|
|
817
|
+
"trace": filtered_trace,
|
|
818
|
+
"sigma": sigma,
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
|
|
822
|
+
@register_handler("filter.notch")
|
|
823
|
+
def handle_filter_notch(
|
|
824
|
+
inputs: dict[str, Any], params: dict[str, Any], step_name: str
|
|
825
|
+
) -> dict[str, Any]:
|
|
826
|
+
"""Apply narrow notch filter at specific frequency.
|
|
827
|
+
|
|
828
|
+
Uses band-stop Butterworth filter with bandwidth determined by Q factor.
|
|
829
|
+
Ideal for removing specific interference (e.g., 50/60 Hz power line noise).
|
|
830
|
+
|
|
831
|
+
Inputs:
|
|
832
|
+
trace: WaveformTrace to filter
|
|
833
|
+
|
|
834
|
+
Parameters:
|
|
835
|
+
freq (float): Center frequency to notch out in Hz
|
|
836
|
+
q_factor (float, optional): Quality factor (higher = narrower notch) (default: 30.0)
|
|
837
|
+
|
|
838
|
+
Outputs:
|
|
839
|
+
trace: Filtered WaveformTrace
|
|
840
|
+
freq: Notch frequency
|
|
841
|
+
q_factor: Quality factor used
|
|
842
|
+
bandwidth: Actual bandwidth in Hz (freq / q_factor)
|
|
843
|
+
|
|
844
|
+
Example:
|
|
845
|
+
>>> # Remove 60 Hz power line noise
|
|
846
|
+
>>> result = handle_filter_notch(
|
|
847
|
+
... {"trace": my_trace},
|
|
848
|
+
... {"freq": 60, "q_factor": 30},
|
|
849
|
+
... "notch_60hz"
|
|
850
|
+
... )
|
|
851
|
+
"""
|
|
852
|
+
filtering = _get_filtering()
|
|
853
|
+
|
|
854
|
+
trace = inputs.get("trace")
|
|
855
|
+
if trace is None:
|
|
856
|
+
raise PipelineExecutionError("Missing required input 'trace'", step_name=step_name)
|
|
857
|
+
|
|
858
|
+
freq = params.get("freq")
|
|
859
|
+
if freq is None:
|
|
860
|
+
raise PipelineExecutionError(
|
|
861
|
+
"Missing required parameter 'freq'",
|
|
862
|
+
step_name=step_name,
|
|
863
|
+
)
|
|
864
|
+
|
|
865
|
+
if not isinstance(freq, (int, float)) or freq <= 0:
|
|
866
|
+
raise PipelineExecutionError(
|
|
867
|
+
f"Frequency must be a positive number, got {freq}",
|
|
868
|
+
step_name=step_name,
|
|
869
|
+
)
|
|
870
|
+
|
|
871
|
+
q_factor = params.get("q_factor", 30.0)
|
|
872
|
+
|
|
873
|
+
try:
|
|
874
|
+
filtered_trace = filtering.notch_filter(
|
|
875
|
+
trace,
|
|
876
|
+
freq=freq,
|
|
877
|
+
q_factor=q_factor,
|
|
878
|
+
)
|
|
879
|
+
|
|
880
|
+
# Calculate bandwidth for output
|
|
881
|
+
bandwidth = freq / q_factor
|
|
882
|
+
|
|
883
|
+
except Exception as e:
|
|
884
|
+
raise PipelineExecutionError(f"Notch filtering failed: {e}", step_name=step_name) from e
|
|
885
|
+
|
|
886
|
+
return {
|
|
887
|
+
"trace": filtered_trace,
|
|
888
|
+
"freq": freq,
|
|
889
|
+
"q_factor": q_factor,
|
|
890
|
+
"bandwidth": bandwidth,
|
|
891
|
+
}
|