oscura 0.0.1__py3-none-any.whl → 0.1.1__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 +813 -8
- oscura/__main__.py +392 -0
- oscura/analyzers/__init__.py +37 -0
- oscura/analyzers/digital/__init__.py +177 -0
- oscura/analyzers/digital/bus.py +691 -0
- oscura/analyzers/digital/clock.py +805 -0
- oscura/analyzers/digital/correlation.py +720 -0
- oscura/analyzers/digital/edges.py +632 -0
- oscura/analyzers/digital/extraction.py +413 -0
- oscura/analyzers/digital/quality.py +878 -0
- oscura/analyzers/digital/signal_quality.py +877 -0
- oscura/analyzers/digital/thresholds.py +708 -0
- oscura/analyzers/digital/timing.py +1104 -0
- oscura/analyzers/eye/__init__.py +46 -0
- oscura/analyzers/eye/diagram.py +434 -0
- oscura/analyzers/eye/metrics.py +555 -0
- oscura/analyzers/jitter/__init__.py +83 -0
- oscura/analyzers/jitter/ber.py +333 -0
- oscura/analyzers/jitter/decomposition.py +759 -0
- oscura/analyzers/jitter/measurements.py +413 -0
- oscura/analyzers/jitter/spectrum.py +220 -0
- oscura/analyzers/measurements.py +40 -0
- oscura/analyzers/packet/__init__.py +171 -0
- oscura/analyzers/packet/daq.py +1077 -0
- oscura/analyzers/packet/metrics.py +437 -0
- oscura/analyzers/packet/parser.py +327 -0
- oscura/analyzers/packet/payload.py +2156 -0
- oscura/analyzers/packet/payload_analysis.py +1312 -0
- oscura/analyzers/packet/payload_extraction.py +236 -0
- oscura/analyzers/packet/payload_patterns.py +670 -0
- oscura/analyzers/packet/stream.py +359 -0
- oscura/analyzers/patterns/__init__.py +266 -0
- oscura/analyzers/patterns/clustering.py +1036 -0
- oscura/analyzers/patterns/discovery.py +539 -0
- oscura/analyzers/patterns/learning.py +797 -0
- oscura/analyzers/patterns/matching.py +1091 -0
- oscura/analyzers/patterns/periodic.py +650 -0
- oscura/analyzers/patterns/sequences.py +767 -0
- oscura/analyzers/power/__init__.py +116 -0
- oscura/analyzers/power/ac_power.py +391 -0
- oscura/analyzers/power/basic.py +383 -0
- oscura/analyzers/power/conduction.py +314 -0
- oscura/analyzers/power/efficiency.py +297 -0
- oscura/analyzers/power/ripple.py +356 -0
- oscura/analyzers/power/soa.py +372 -0
- oscura/analyzers/power/switching.py +479 -0
- oscura/analyzers/protocol/__init__.py +150 -0
- oscura/analyzers/protocols/__init__.py +150 -0
- oscura/analyzers/protocols/base.py +500 -0
- oscura/analyzers/protocols/can.py +620 -0
- oscura/analyzers/protocols/can_fd.py +448 -0
- oscura/analyzers/protocols/flexray.py +405 -0
- oscura/analyzers/protocols/hdlc.py +399 -0
- oscura/analyzers/protocols/i2c.py +368 -0
- oscura/analyzers/protocols/i2s.py +296 -0
- oscura/analyzers/protocols/jtag.py +393 -0
- oscura/analyzers/protocols/lin.py +445 -0
- oscura/analyzers/protocols/manchester.py +333 -0
- oscura/analyzers/protocols/onewire.py +501 -0
- oscura/analyzers/protocols/spi.py +334 -0
- oscura/analyzers/protocols/swd.py +325 -0
- oscura/analyzers/protocols/uart.py +393 -0
- oscura/analyzers/protocols/usb.py +495 -0
- oscura/analyzers/signal_integrity/__init__.py +63 -0
- oscura/analyzers/signal_integrity/embedding.py +294 -0
- oscura/analyzers/signal_integrity/equalization.py +370 -0
- oscura/analyzers/signal_integrity/sparams.py +484 -0
- oscura/analyzers/spectral/__init__.py +53 -0
- oscura/analyzers/spectral/chunked.py +273 -0
- oscura/analyzers/spectral/chunked_fft.py +571 -0
- oscura/analyzers/spectral/chunked_wavelet.py +391 -0
- oscura/analyzers/spectral/fft.py +92 -0
- oscura/analyzers/statistical/__init__.py +250 -0
- oscura/analyzers/statistical/checksum.py +923 -0
- oscura/analyzers/statistical/chunked_corr.py +228 -0
- oscura/analyzers/statistical/classification.py +778 -0
- oscura/analyzers/statistical/entropy.py +1113 -0
- oscura/analyzers/statistical/ngrams.py +614 -0
- oscura/analyzers/statistics/__init__.py +119 -0
- oscura/analyzers/statistics/advanced.py +885 -0
- oscura/analyzers/statistics/basic.py +263 -0
- oscura/analyzers/statistics/correlation.py +630 -0
- oscura/analyzers/statistics/distribution.py +298 -0
- oscura/analyzers/statistics/outliers.py +463 -0
- oscura/analyzers/statistics/streaming.py +93 -0
- oscura/analyzers/statistics/trend.py +520 -0
- oscura/analyzers/validation.py +598 -0
- oscura/analyzers/waveform/__init__.py +36 -0
- oscura/analyzers/waveform/measurements.py +943 -0
- oscura/analyzers/waveform/measurements_with_uncertainty.py +371 -0
- oscura/analyzers/waveform/spectral.py +1689 -0
- oscura/analyzers/waveform/wavelets.py +298 -0
- oscura/api/__init__.py +62 -0
- oscura/api/dsl.py +538 -0
- oscura/api/fluent.py +571 -0
- oscura/api/operators.py +498 -0
- oscura/api/optimization.py +392 -0
- oscura/api/profiling.py +396 -0
- oscura/automotive/__init__.py +73 -0
- oscura/automotive/can/__init__.py +52 -0
- oscura/automotive/can/analysis.py +356 -0
- oscura/automotive/can/checksum.py +250 -0
- oscura/automotive/can/correlation.py +212 -0
- oscura/automotive/can/discovery.py +355 -0
- oscura/automotive/can/message_wrapper.py +375 -0
- oscura/automotive/can/models.py +385 -0
- oscura/automotive/can/patterns.py +381 -0
- oscura/automotive/can/session.py +452 -0
- oscura/automotive/can/state_machine.py +300 -0
- oscura/automotive/can/stimulus_response.py +461 -0
- oscura/automotive/dbc/__init__.py +15 -0
- oscura/automotive/dbc/generator.py +156 -0
- oscura/automotive/dbc/parser.py +146 -0
- oscura/automotive/dtc/__init__.py +30 -0
- oscura/automotive/dtc/database.py +3036 -0
- oscura/automotive/j1939/__init__.py +14 -0
- oscura/automotive/j1939/decoder.py +745 -0
- oscura/automotive/loaders/__init__.py +35 -0
- oscura/automotive/loaders/asc.py +98 -0
- oscura/automotive/loaders/blf.py +77 -0
- oscura/automotive/loaders/csv_can.py +136 -0
- oscura/automotive/loaders/dispatcher.py +136 -0
- oscura/automotive/loaders/mdf.py +331 -0
- oscura/automotive/loaders/pcap.py +132 -0
- oscura/automotive/obd/__init__.py +14 -0
- oscura/automotive/obd/decoder.py +707 -0
- oscura/automotive/uds/__init__.py +48 -0
- oscura/automotive/uds/decoder.py +265 -0
- oscura/automotive/uds/models.py +64 -0
- oscura/automotive/visualization.py +369 -0
- oscura/batch/__init__.py +55 -0
- oscura/batch/advanced.py +627 -0
- oscura/batch/aggregate.py +300 -0
- oscura/batch/analyze.py +139 -0
- oscura/batch/logging.py +487 -0
- oscura/batch/metrics.py +556 -0
- oscura/builders/__init__.py +41 -0
- oscura/builders/signal_builder.py +1131 -0
- oscura/cli/__init__.py +14 -0
- oscura/cli/batch.py +339 -0
- oscura/cli/characterize.py +273 -0
- oscura/cli/compare.py +775 -0
- oscura/cli/decode.py +551 -0
- oscura/cli/main.py +247 -0
- oscura/cli/shell.py +350 -0
- oscura/comparison/__init__.py +66 -0
- oscura/comparison/compare.py +397 -0
- oscura/comparison/golden.py +487 -0
- oscura/comparison/limits.py +391 -0
- oscura/comparison/mask.py +434 -0
- oscura/comparison/trace_diff.py +30 -0
- oscura/comparison/visualization.py +481 -0
- oscura/compliance/__init__.py +70 -0
- oscura/compliance/advanced.py +756 -0
- oscura/compliance/masks.py +363 -0
- oscura/compliance/reporting.py +483 -0
- oscura/compliance/testing.py +298 -0
- oscura/component/__init__.py +38 -0
- oscura/component/impedance.py +365 -0
- oscura/component/reactive.py +598 -0
- oscura/component/transmission_line.py +312 -0
- oscura/config/__init__.py +191 -0
- oscura/config/defaults.py +254 -0
- oscura/config/loader.py +348 -0
- oscura/config/memory.py +271 -0
- oscura/config/migration.py +458 -0
- oscura/config/pipeline.py +1077 -0
- oscura/config/preferences.py +530 -0
- oscura/config/protocol.py +875 -0
- oscura/config/schema.py +713 -0
- oscura/config/settings.py +420 -0
- oscura/config/thresholds.py +599 -0
- oscura/convenience.py +457 -0
- oscura/core/__init__.py +299 -0
- oscura/core/audit.py +457 -0
- oscura/core/backend_selector.py +405 -0
- oscura/core/cache.py +590 -0
- oscura/core/cancellation.py +439 -0
- oscura/core/confidence.py +225 -0
- oscura/core/config.py +506 -0
- oscura/core/correlation.py +216 -0
- oscura/core/cross_domain.py +422 -0
- oscura/core/debug.py +301 -0
- oscura/core/edge_cases.py +541 -0
- oscura/core/exceptions.py +535 -0
- oscura/core/gpu_backend.py +523 -0
- oscura/core/lazy.py +832 -0
- oscura/core/log_query.py +540 -0
- oscura/core/logging.py +931 -0
- oscura/core/logging_advanced.py +952 -0
- oscura/core/memoize.py +171 -0
- oscura/core/memory_check.py +274 -0
- oscura/core/memory_guard.py +290 -0
- oscura/core/memory_limits.py +336 -0
- oscura/core/memory_monitor.py +453 -0
- oscura/core/memory_progress.py +465 -0
- oscura/core/memory_warnings.py +315 -0
- oscura/core/numba_backend.py +362 -0
- oscura/core/performance.py +352 -0
- oscura/core/progress.py +524 -0
- oscura/core/provenance.py +358 -0
- oscura/core/results.py +331 -0
- oscura/core/types.py +504 -0
- oscura/core/uncertainty.py +383 -0
- oscura/discovery/__init__.py +52 -0
- oscura/discovery/anomaly_detector.py +672 -0
- oscura/discovery/auto_decoder.py +415 -0
- oscura/discovery/comparison.py +497 -0
- oscura/discovery/quality_validator.py +528 -0
- oscura/discovery/signal_detector.py +769 -0
- oscura/dsl/__init__.py +73 -0
- oscura/dsl/commands.py +246 -0
- oscura/dsl/interpreter.py +455 -0
- oscura/dsl/parser.py +689 -0
- oscura/dsl/repl.py +172 -0
- oscura/exceptions.py +59 -0
- oscura/exploratory/__init__.py +111 -0
- oscura/exploratory/error_recovery.py +642 -0
- oscura/exploratory/fuzzy.py +513 -0
- oscura/exploratory/fuzzy_advanced.py +786 -0
- oscura/exploratory/legacy.py +831 -0
- oscura/exploratory/parse.py +358 -0
- oscura/exploratory/recovery.py +275 -0
- oscura/exploratory/sync.py +382 -0
- oscura/exploratory/unknown.py +707 -0
- oscura/export/__init__.py +25 -0
- oscura/export/wireshark/README.md +265 -0
- oscura/export/wireshark/__init__.py +47 -0
- oscura/export/wireshark/generator.py +312 -0
- oscura/export/wireshark/lua_builder.py +159 -0
- oscura/export/wireshark/templates/dissector.lua.j2 +92 -0
- oscura/export/wireshark/type_mapping.py +165 -0
- oscura/export/wireshark/validator.py +105 -0
- oscura/exporters/__init__.py +94 -0
- oscura/exporters/csv.py +303 -0
- oscura/exporters/exporters.py +44 -0
- oscura/exporters/hdf5.py +219 -0
- oscura/exporters/html_export.py +701 -0
- oscura/exporters/json_export.py +291 -0
- oscura/exporters/markdown_export.py +367 -0
- oscura/exporters/matlab_export.py +354 -0
- oscura/exporters/npz_export.py +219 -0
- oscura/exporters/spice_export.py +210 -0
- oscura/extensibility/__init__.py +131 -0
- oscura/extensibility/docs.py +752 -0
- oscura/extensibility/extensions.py +1125 -0
- oscura/extensibility/logging.py +259 -0
- oscura/extensibility/measurements.py +485 -0
- oscura/extensibility/plugins.py +414 -0
- oscura/extensibility/registry.py +346 -0
- oscura/extensibility/templates.py +913 -0
- oscura/extensibility/validation.py +651 -0
- oscura/filtering/__init__.py +89 -0
- oscura/filtering/base.py +563 -0
- oscura/filtering/convenience.py +564 -0
- oscura/filtering/design.py +725 -0
- oscura/filtering/filters.py +32 -0
- oscura/filtering/introspection.py +605 -0
- oscura/guidance/__init__.py +24 -0
- oscura/guidance/recommender.py +429 -0
- oscura/guidance/wizard.py +518 -0
- oscura/inference/__init__.py +251 -0
- oscura/inference/active_learning/README.md +153 -0
- oscura/inference/active_learning/__init__.py +38 -0
- oscura/inference/active_learning/lstar.py +257 -0
- oscura/inference/active_learning/observation_table.py +230 -0
- oscura/inference/active_learning/oracle.py +78 -0
- oscura/inference/active_learning/teachers/__init__.py +15 -0
- oscura/inference/active_learning/teachers/simulator.py +192 -0
- oscura/inference/adaptive_tuning.py +453 -0
- oscura/inference/alignment.py +653 -0
- oscura/inference/bayesian.py +943 -0
- oscura/inference/binary.py +1016 -0
- oscura/inference/crc_reverse.py +711 -0
- oscura/inference/logic.py +288 -0
- oscura/inference/message_format.py +1305 -0
- oscura/inference/protocol.py +417 -0
- oscura/inference/protocol_dsl.py +1084 -0
- oscura/inference/protocol_library.py +1230 -0
- oscura/inference/sequences.py +809 -0
- oscura/inference/signal_intelligence.py +1509 -0
- oscura/inference/spectral.py +215 -0
- oscura/inference/state_machine.py +634 -0
- oscura/inference/stream.py +918 -0
- oscura/integrations/__init__.py +59 -0
- oscura/integrations/llm.py +1827 -0
- oscura/jupyter/__init__.py +32 -0
- oscura/jupyter/display.py +268 -0
- oscura/jupyter/magic.py +334 -0
- oscura/loaders/__init__.py +526 -0
- oscura/loaders/binary.py +69 -0
- oscura/loaders/configurable.py +1255 -0
- oscura/loaders/csv.py +26 -0
- oscura/loaders/csv_loader.py +473 -0
- oscura/loaders/hdf5.py +9 -0
- oscura/loaders/hdf5_loader.py +510 -0
- oscura/loaders/lazy.py +370 -0
- oscura/loaders/mmap_loader.py +583 -0
- oscura/loaders/numpy_loader.py +436 -0
- oscura/loaders/pcap.py +432 -0
- oscura/loaders/preprocessing.py +368 -0
- oscura/loaders/rigol.py +287 -0
- oscura/loaders/sigrok.py +321 -0
- oscura/loaders/tdms.py +367 -0
- oscura/loaders/tektronix.py +711 -0
- oscura/loaders/validation.py +584 -0
- oscura/loaders/vcd.py +464 -0
- oscura/loaders/wav.py +233 -0
- oscura/math/__init__.py +45 -0
- oscura/math/arithmetic.py +824 -0
- oscura/math/interpolation.py +413 -0
- oscura/onboarding/__init__.py +39 -0
- oscura/onboarding/help.py +498 -0
- oscura/onboarding/tutorials.py +405 -0
- oscura/onboarding/wizard.py +466 -0
- oscura/optimization/__init__.py +19 -0
- oscura/optimization/parallel.py +440 -0
- oscura/optimization/search.py +532 -0
- oscura/pipeline/__init__.py +43 -0
- oscura/pipeline/base.py +338 -0
- oscura/pipeline/composition.py +242 -0
- oscura/pipeline/parallel.py +448 -0
- oscura/pipeline/pipeline.py +375 -0
- oscura/pipeline/reverse_engineering.py +1119 -0
- oscura/plugins/__init__.py +122 -0
- oscura/plugins/base.py +272 -0
- oscura/plugins/cli.py +497 -0
- oscura/plugins/discovery.py +411 -0
- oscura/plugins/isolation.py +418 -0
- oscura/plugins/lifecycle.py +959 -0
- oscura/plugins/manager.py +493 -0
- oscura/plugins/registry.py +421 -0
- oscura/plugins/versioning.py +372 -0
- oscura/py.typed +0 -0
- oscura/quality/__init__.py +65 -0
- oscura/quality/ensemble.py +740 -0
- oscura/quality/explainer.py +338 -0
- oscura/quality/scoring.py +616 -0
- oscura/quality/warnings.py +456 -0
- oscura/reporting/__init__.py +248 -0
- oscura/reporting/advanced.py +1234 -0
- oscura/reporting/analyze.py +448 -0
- oscura/reporting/argument_preparer.py +596 -0
- oscura/reporting/auto_report.py +507 -0
- oscura/reporting/batch.py +615 -0
- oscura/reporting/chart_selection.py +223 -0
- oscura/reporting/comparison.py +330 -0
- oscura/reporting/config.py +615 -0
- oscura/reporting/content/__init__.py +39 -0
- oscura/reporting/content/executive.py +127 -0
- oscura/reporting/content/filtering.py +191 -0
- oscura/reporting/content/minimal.py +257 -0
- oscura/reporting/content/verbosity.py +162 -0
- oscura/reporting/core.py +508 -0
- oscura/reporting/core_formats/__init__.py +17 -0
- oscura/reporting/core_formats/multi_format.py +210 -0
- oscura/reporting/engine.py +836 -0
- oscura/reporting/export.py +366 -0
- oscura/reporting/formatting/__init__.py +129 -0
- oscura/reporting/formatting/emphasis.py +81 -0
- oscura/reporting/formatting/numbers.py +403 -0
- oscura/reporting/formatting/standards.py +55 -0
- oscura/reporting/formatting.py +466 -0
- oscura/reporting/html.py +578 -0
- oscura/reporting/index.py +590 -0
- oscura/reporting/multichannel.py +296 -0
- oscura/reporting/output.py +379 -0
- oscura/reporting/pdf.py +373 -0
- oscura/reporting/plots.py +731 -0
- oscura/reporting/pptx_export.py +360 -0
- oscura/reporting/renderers/__init__.py +11 -0
- oscura/reporting/renderers/pdf.py +94 -0
- oscura/reporting/sections.py +471 -0
- oscura/reporting/standards.py +680 -0
- oscura/reporting/summary_generator.py +368 -0
- oscura/reporting/tables.py +397 -0
- oscura/reporting/template_system.py +724 -0
- oscura/reporting/templates/__init__.py +15 -0
- oscura/reporting/templates/definition.py +205 -0
- oscura/reporting/templates/index.html +649 -0
- oscura/reporting/templates/index.md +173 -0
- oscura/schemas/__init__.py +158 -0
- oscura/schemas/bus_configuration.json +322 -0
- oscura/schemas/device_mapping.json +182 -0
- oscura/schemas/packet_format.json +418 -0
- oscura/schemas/protocol_definition.json +363 -0
- oscura/search/__init__.py +16 -0
- oscura/search/anomaly.py +292 -0
- oscura/search/context.py +149 -0
- oscura/search/pattern.py +160 -0
- oscura/session/__init__.py +34 -0
- oscura/session/annotations.py +289 -0
- oscura/session/history.py +313 -0
- oscura/session/session.py +445 -0
- oscura/streaming/__init__.py +43 -0
- oscura/streaming/chunked.py +611 -0
- oscura/streaming/progressive.py +393 -0
- oscura/streaming/realtime.py +622 -0
- oscura/testing/__init__.py +54 -0
- oscura/testing/synthetic.py +808 -0
- oscura/triggering/__init__.py +68 -0
- oscura/triggering/base.py +229 -0
- oscura/triggering/edge.py +353 -0
- oscura/triggering/pattern.py +344 -0
- oscura/triggering/pulse.py +581 -0
- oscura/triggering/window.py +453 -0
- oscura/ui/__init__.py +48 -0
- oscura/ui/formatters.py +526 -0
- oscura/ui/progressive_display.py +340 -0
- oscura/utils/__init__.py +99 -0
- oscura/utils/autodetect.py +338 -0
- oscura/utils/buffer.py +389 -0
- oscura/utils/lazy.py +407 -0
- oscura/utils/lazy_imports.py +147 -0
- oscura/utils/memory.py +836 -0
- oscura/utils/memory_advanced.py +1326 -0
- oscura/utils/memory_extensions.py +465 -0
- oscura/utils/progressive.py +352 -0
- oscura/utils/windowing.py +362 -0
- oscura/visualization/__init__.py +321 -0
- oscura/visualization/accessibility.py +526 -0
- oscura/visualization/annotations.py +374 -0
- oscura/visualization/axis_scaling.py +305 -0
- oscura/visualization/colors.py +453 -0
- oscura/visualization/digital.py +337 -0
- oscura/visualization/eye.py +420 -0
- oscura/visualization/histogram.py +281 -0
- oscura/visualization/interactive.py +858 -0
- oscura/visualization/jitter.py +702 -0
- oscura/visualization/keyboard.py +394 -0
- oscura/visualization/layout.py +365 -0
- oscura/visualization/optimization.py +1028 -0
- oscura/visualization/palettes.py +446 -0
- oscura/visualization/plot.py +92 -0
- oscura/visualization/power.py +290 -0
- oscura/visualization/power_extended.py +626 -0
- oscura/visualization/presets.py +467 -0
- oscura/visualization/protocols.py +932 -0
- oscura/visualization/render.py +207 -0
- oscura/visualization/rendering.py +444 -0
- oscura/visualization/reverse_engineering.py +791 -0
- oscura/visualization/signal_integrity.py +808 -0
- oscura/visualization/specialized.py +553 -0
- oscura/visualization/spectral.py +811 -0
- oscura/visualization/styles.py +381 -0
- oscura/visualization/thumbnails.py +311 -0
- oscura/visualization/time_axis.py +351 -0
- oscura/visualization/waveform.py +367 -0
- oscura/workflow/__init__.py +13 -0
- oscura/workflow/dag.py +377 -0
- oscura/workflows/__init__.py +58 -0
- oscura/workflows/compliance.py +280 -0
- oscura/workflows/digital.py +272 -0
- oscura/workflows/multi_trace.py +502 -0
- oscura/workflows/power.py +178 -0
- oscura/workflows/protocol.py +492 -0
- oscura/workflows/reverse_engineering.py +639 -0
- oscura/workflows/signal_integrity.py +227 -0
- oscura-0.1.1.dist-info/METADATA +300 -0
- oscura-0.1.1.dist-info/RECORD +463 -0
- oscura-0.1.1.dist-info/entry_points.txt +2 -0
- {oscura-0.0.1.dist-info → oscura-0.1.1.dist-info}/licenses/LICENSE +1 -1
- oscura-0.0.1.dist-info/METADATA +0 -63
- oscura-0.0.1.dist-info/RECORD +0 -5
- {oscura-0.0.1.dist-info → oscura-0.1.1.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Signal filtering module for Oscura.
|
|
2
|
+
|
|
3
|
+
Provides digital filter design, application, and introspection capabilities
|
|
4
|
+
including IIR and FIR filters, various filter types (Butterworth, Chebyshev,
|
|
5
|
+
Bessel, Elliptic), and convenience filters (moving average, median).
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
Example:
|
|
9
|
+
>>> from oscura.filtering import LowPassFilter, design_filter
|
|
10
|
+
>>> lpf = LowPassFilter(cutoff=1e6, sample_rate=10e6, order=4)
|
|
11
|
+
>>> filtered_trace = lpf.apply(trace)
|
|
12
|
+
>>> w, h = lpf.get_frequency_response()
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
# Import filters module as namespace for DSL compatibility
|
|
16
|
+
from oscura.filtering import filters
|
|
17
|
+
from oscura.filtering.base import (
|
|
18
|
+
Filter,
|
|
19
|
+
FIRFilter,
|
|
20
|
+
IIRFilter,
|
|
21
|
+
)
|
|
22
|
+
from oscura.filtering.convenience import (
|
|
23
|
+
band_pass,
|
|
24
|
+
band_stop,
|
|
25
|
+
high_pass,
|
|
26
|
+
low_pass,
|
|
27
|
+
matched_filter,
|
|
28
|
+
median_filter,
|
|
29
|
+
moving_average,
|
|
30
|
+
notch_filter,
|
|
31
|
+
savgol_filter,
|
|
32
|
+
)
|
|
33
|
+
from oscura.filtering.design import (
|
|
34
|
+
BandPassFilter,
|
|
35
|
+
BandStopFilter,
|
|
36
|
+
BesselFilter,
|
|
37
|
+
ButterworthFilter,
|
|
38
|
+
ChebyshevType1Filter,
|
|
39
|
+
ChebyshevType2Filter,
|
|
40
|
+
EllipticFilter,
|
|
41
|
+
HighPassFilter,
|
|
42
|
+
LowPassFilter,
|
|
43
|
+
design_filter,
|
|
44
|
+
design_filter_spec,
|
|
45
|
+
)
|
|
46
|
+
from oscura.filtering.introspection import (
|
|
47
|
+
FilterIntrospection,
|
|
48
|
+
plot_bode,
|
|
49
|
+
plot_impulse,
|
|
50
|
+
plot_poles_zeros,
|
|
51
|
+
plot_step,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
__all__ = [
|
|
55
|
+
"BandPassFilter",
|
|
56
|
+
"BandStopFilter",
|
|
57
|
+
"BesselFilter",
|
|
58
|
+
"ButterworthFilter",
|
|
59
|
+
"ChebyshevType1Filter",
|
|
60
|
+
"ChebyshevType2Filter",
|
|
61
|
+
"EllipticFilter",
|
|
62
|
+
"FIRFilter",
|
|
63
|
+
# Base classes
|
|
64
|
+
"Filter",
|
|
65
|
+
# Introspection
|
|
66
|
+
"FilterIntrospection",
|
|
67
|
+
"HighPassFilter",
|
|
68
|
+
"IIRFilter",
|
|
69
|
+
# Filter types
|
|
70
|
+
"LowPassFilter",
|
|
71
|
+
"band_pass",
|
|
72
|
+
"band_stop",
|
|
73
|
+
# Design functions
|
|
74
|
+
"design_filter",
|
|
75
|
+
"design_filter_spec",
|
|
76
|
+
"filters",
|
|
77
|
+
"high_pass",
|
|
78
|
+
"low_pass",
|
|
79
|
+
"matched_filter",
|
|
80
|
+
"median_filter",
|
|
81
|
+
# Convenience functions
|
|
82
|
+
"moving_average",
|
|
83
|
+
"notch_filter",
|
|
84
|
+
"plot_bode",
|
|
85
|
+
"plot_impulse",
|
|
86
|
+
"plot_poles_zeros",
|
|
87
|
+
"plot_step",
|
|
88
|
+
"savgol_filter",
|
|
89
|
+
]
|
oscura/filtering/base.py
ADDED
|
@@ -0,0 +1,563 @@
|
|
|
1
|
+
"""Base filter classes for Oscura filtering module.
|
|
2
|
+
|
|
3
|
+
Provides abstract base classes for IIR and FIR filter implementations
|
|
4
|
+
with common interface for filter application and introspection.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from abc import ABC, abstractmethod
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from typing import TYPE_CHECKING, Literal
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
from scipy import signal
|
|
15
|
+
|
|
16
|
+
from oscura.core.exceptions import AnalysisError
|
|
17
|
+
from oscura.core.types import WaveformTrace
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from numpy.typing import NDArray
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class FilterResult:
|
|
25
|
+
"""Result of filter application with optional introspection data.
|
|
26
|
+
|
|
27
|
+
Attributes:
|
|
28
|
+
trace: Filtered waveform trace.
|
|
29
|
+
transfer_function: Optional frequency response H(f).
|
|
30
|
+
impulse_response: Optional impulse response h[n].
|
|
31
|
+
group_delay: Optional group delay in samples.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
trace: WaveformTrace
|
|
35
|
+
transfer_function: NDArray[np.complex128] | None = None
|
|
36
|
+
impulse_response: NDArray[np.float64] | None = None
|
|
37
|
+
group_delay: NDArray[np.float64] | None = None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class Filter(ABC):
|
|
41
|
+
"""Abstract base class for all filters.
|
|
42
|
+
|
|
43
|
+
Defines the common interface for filter application and introspection.
|
|
44
|
+
All filter implementations must inherit from this class.
|
|
45
|
+
|
|
46
|
+
Attributes:
|
|
47
|
+
sample_rate: Sample rate in Hz for digital filter design.
|
|
48
|
+
is_stable: Whether the filter is stable (for IIR filters).
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
def __init__(self, sample_rate: float | None = None) -> None:
|
|
52
|
+
"""Initialize filter.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
sample_rate: Sample rate in Hz. If None, must be provided at apply time.
|
|
56
|
+
"""
|
|
57
|
+
self._sample_rate = sample_rate
|
|
58
|
+
self._is_designed = False
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def sample_rate(self) -> float | None:
|
|
62
|
+
"""Sample rate in Hz."""
|
|
63
|
+
return self._sample_rate
|
|
64
|
+
|
|
65
|
+
@sample_rate.setter
|
|
66
|
+
def sample_rate(self, value: float) -> None:
|
|
67
|
+
"""Set sample rate and mark filter for redesign."""
|
|
68
|
+
if value != self._sample_rate:
|
|
69
|
+
self._sample_rate = value
|
|
70
|
+
self._is_designed = False
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
@abstractmethod
|
|
74
|
+
def is_stable(self) -> bool:
|
|
75
|
+
"""Check if filter is stable."""
|
|
76
|
+
...
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
@abstractmethod
|
|
80
|
+
def order(self) -> int:
|
|
81
|
+
"""Filter order."""
|
|
82
|
+
...
|
|
83
|
+
|
|
84
|
+
@abstractmethod
|
|
85
|
+
def apply(
|
|
86
|
+
self,
|
|
87
|
+
trace: WaveformTrace,
|
|
88
|
+
*,
|
|
89
|
+
return_details: bool = False,
|
|
90
|
+
) -> WaveformTrace | FilterResult:
|
|
91
|
+
"""Apply filter to a waveform trace.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
trace: Input waveform trace.
|
|
95
|
+
return_details: If True, return FilterResult with introspection data.
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
Filtered trace, or FilterResult if return_details=True.
|
|
99
|
+
"""
|
|
100
|
+
...
|
|
101
|
+
|
|
102
|
+
@abstractmethod
|
|
103
|
+
def get_frequency_response(
|
|
104
|
+
self,
|
|
105
|
+
worN: int | NDArray[np.float64] | None = None,
|
|
106
|
+
) -> tuple[NDArray[np.float64], NDArray[np.complex128]]:
|
|
107
|
+
"""Get frequency response of the filter.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
worN: Frequencies at which to evaluate. If int, that many frequencies
|
|
111
|
+
from 0 to pi (Nyquist). If array, specific frequencies in rad/s.
|
|
112
|
+
If None, uses 512 points.
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
Tuple of (frequencies, complex response H(f)).
|
|
116
|
+
"""
|
|
117
|
+
...
|
|
118
|
+
|
|
119
|
+
@abstractmethod
|
|
120
|
+
def get_impulse_response(
|
|
121
|
+
self,
|
|
122
|
+
n_samples: int = 256,
|
|
123
|
+
) -> NDArray[np.float64]:
|
|
124
|
+
"""Get impulse response of the filter.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
n_samples: Number of samples in impulse response.
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
Impulse response h[n].
|
|
131
|
+
"""
|
|
132
|
+
...
|
|
133
|
+
|
|
134
|
+
@abstractmethod
|
|
135
|
+
def get_step_response(
|
|
136
|
+
self,
|
|
137
|
+
n_samples: int = 256,
|
|
138
|
+
) -> NDArray[np.float64]:
|
|
139
|
+
"""Get step response of the filter.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
n_samples: Number of samples in step response.
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
Step response s[n].
|
|
146
|
+
"""
|
|
147
|
+
...
|
|
148
|
+
|
|
149
|
+
def get_transfer_function(
|
|
150
|
+
self,
|
|
151
|
+
freqs: NDArray[np.float64] | None = None,
|
|
152
|
+
) -> NDArray[np.complex128]:
|
|
153
|
+
"""Get transfer function H(f) at specified frequencies.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
freqs: Frequencies in Hz. If None, uses 512 points from 0 to Nyquist.
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
Complex transfer function values.
|
|
160
|
+
|
|
161
|
+
Raises:
|
|
162
|
+
AnalysisError: If sample rate is not set.
|
|
163
|
+
"""
|
|
164
|
+
if self._sample_rate is None:
|
|
165
|
+
raise AnalysisError("Sample rate must be set to compute transfer function")
|
|
166
|
+
|
|
167
|
+
if freqs is None:
|
|
168
|
+
freqs = np.linspace(0, self._sample_rate / 2, 512)
|
|
169
|
+
|
|
170
|
+
# Convert Hz to normalized frequency
|
|
171
|
+
w = 2 * np.pi * freqs / self._sample_rate
|
|
172
|
+
_, h = self.get_frequency_response(w)
|
|
173
|
+
return h
|
|
174
|
+
|
|
175
|
+
def get_group_delay(
|
|
176
|
+
self,
|
|
177
|
+
worN: int | NDArray[np.float64] | None = None,
|
|
178
|
+
) -> tuple[NDArray[np.float64], NDArray[np.float64]]:
|
|
179
|
+
"""Get group delay of the filter.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
worN: Frequencies at which to evaluate. If int, that many frequencies.
|
|
183
|
+
If None, uses 512 points.
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
Tuple of (frequencies, group delay in samples).
|
|
187
|
+
"""
|
|
188
|
+
if worN is None:
|
|
189
|
+
worN = 512
|
|
190
|
+
# Default implementation using phase derivative
|
|
191
|
+
w, h = self.get_frequency_response(worN)
|
|
192
|
+
phase = np.unwrap(np.angle(h))
|
|
193
|
+
dw = np.diff(w)
|
|
194
|
+
dphi = np.diff(phase)
|
|
195
|
+
# Avoid division by zero
|
|
196
|
+
gd = np.zeros_like(w)
|
|
197
|
+
gd[:-1] = -dphi / dw
|
|
198
|
+
gd[-1] = gd[-2] if len(gd) > 1 else 0
|
|
199
|
+
return w, gd
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
class IIRFilter(Filter):
|
|
203
|
+
"""Infinite Impulse Response filter base class.
|
|
204
|
+
|
|
205
|
+
Stores filter coefficients in Second-Order Sections (SOS) format
|
|
206
|
+
for numerical stability, with optional B/A polynomial format.
|
|
207
|
+
|
|
208
|
+
Attributes:
|
|
209
|
+
sos: Second-order sections coefficients (preferred format).
|
|
210
|
+
ba: Numerator/denominator polynomial coefficients (optional).
|
|
211
|
+
"""
|
|
212
|
+
|
|
213
|
+
def __init__(
|
|
214
|
+
self,
|
|
215
|
+
sample_rate: float | None = None,
|
|
216
|
+
sos: NDArray[np.float64] | None = None,
|
|
217
|
+
ba: tuple[NDArray[np.float64], NDArray[np.float64]] | None = None,
|
|
218
|
+
) -> None:
|
|
219
|
+
"""Initialize IIR filter.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
sample_rate: Sample rate in Hz.
|
|
223
|
+
sos: Second-order sections array (n_sections, 6).
|
|
224
|
+
ba: Tuple of (b, a) polynomial coefficients.
|
|
225
|
+
"""
|
|
226
|
+
super().__init__(sample_rate)
|
|
227
|
+
self._sos = sos
|
|
228
|
+
self._ba = ba
|
|
229
|
+
self._is_designed = sos is not None or ba is not None
|
|
230
|
+
|
|
231
|
+
@property
|
|
232
|
+
def sos(self) -> NDArray[np.float64] | None:
|
|
233
|
+
"""Second-order sections coefficients."""
|
|
234
|
+
return self._sos
|
|
235
|
+
|
|
236
|
+
@property
|
|
237
|
+
def ba(self) -> tuple[NDArray[np.float64], NDArray[np.float64]] | None:
|
|
238
|
+
"""B/A polynomial coefficients."""
|
|
239
|
+
if self._ba is not None:
|
|
240
|
+
return self._ba
|
|
241
|
+
if self._sos is not None:
|
|
242
|
+
# Convert SOS to BA
|
|
243
|
+
b, a = signal.sos2tf(self._sos)
|
|
244
|
+
return (b, a)
|
|
245
|
+
return None
|
|
246
|
+
|
|
247
|
+
@property
|
|
248
|
+
def is_stable(self) -> bool:
|
|
249
|
+
"""Check if filter is stable (all poles inside unit circle)."""
|
|
250
|
+
if self._sos is None and self._ba is None:
|
|
251
|
+
return True # Not designed yet
|
|
252
|
+
|
|
253
|
+
ba = self.ba
|
|
254
|
+
if ba is None:
|
|
255
|
+
return True
|
|
256
|
+
|
|
257
|
+
_, a = ba
|
|
258
|
+
poles = np.roots(a)
|
|
259
|
+
return bool(np.all(np.abs(poles) < 1.0))
|
|
260
|
+
|
|
261
|
+
@property
|
|
262
|
+
def order(self) -> int:
|
|
263
|
+
"""Filter order."""
|
|
264
|
+
if self._sos is not None:
|
|
265
|
+
return 2 * len(self._sos)
|
|
266
|
+
if self._ba is not None:
|
|
267
|
+
return len(self._ba[1]) - 1
|
|
268
|
+
return 0
|
|
269
|
+
|
|
270
|
+
@property
|
|
271
|
+
def poles(self) -> NDArray[np.complex128]:
|
|
272
|
+
"""Filter poles in z-domain."""
|
|
273
|
+
ba = self.ba
|
|
274
|
+
if ba is None:
|
|
275
|
+
return np.array([], dtype=np.complex128)
|
|
276
|
+
_, a = ba
|
|
277
|
+
return np.roots(a).astype(np.complex128)
|
|
278
|
+
|
|
279
|
+
@property
|
|
280
|
+
def zeros(self) -> NDArray[np.complex128]:
|
|
281
|
+
"""Filter zeros in z-domain."""
|
|
282
|
+
ba = self.ba
|
|
283
|
+
if ba is None:
|
|
284
|
+
return np.array([], dtype=np.complex128)
|
|
285
|
+
b, _ = ba
|
|
286
|
+
return np.roots(b).astype(np.complex128)
|
|
287
|
+
|
|
288
|
+
def apply(
|
|
289
|
+
self,
|
|
290
|
+
trace: WaveformTrace,
|
|
291
|
+
*,
|
|
292
|
+
return_details: bool = False,
|
|
293
|
+
filtfilt: bool = True,
|
|
294
|
+
) -> WaveformTrace | FilterResult:
|
|
295
|
+
"""Apply IIR filter to waveform.
|
|
296
|
+
|
|
297
|
+
Args:
|
|
298
|
+
trace: Input waveform trace.
|
|
299
|
+
return_details: If True, return FilterResult with introspection data.
|
|
300
|
+
filtfilt: If True, use zero-phase filtering (forward-backward).
|
|
301
|
+
If False, use causal filtering.
|
|
302
|
+
|
|
303
|
+
Returns:
|
|
304
|
+
Filtered trace, or FilterResult if return_details=True.
|
|
305
|
+
|
|
306
|
+
Raises:
|
|
307
|
+
AnalysisError: If filter not designed or is unstable.
|
|
308
|
+
"""
|
|
309
|
+
if self._sos is None and self._ba is None:
|
|
310
|
+
raise AnalysisError("Filter not designed - no coefficients available")
|
|
311
|
+
|
|
312
|
+
if not self.is_stable:
|
|
313
|
+
raise AnalysisError("Cannot apply unstable filter")
|
|
314
|
+
|
|
315
|
+
# Apply filter
|
|
316
|
+
if self._sos is not None:
|
|
317
|
+
if filtfilt:
|
|
318
|
+
filtered_data = signal.sosfiltfilt(self._sos, trace.data)
|
|
319
|
+
else:
|
|
320
|
+
filtered_data = signal.sosfilt(self._sos, trace.data)
|
|
321
|
+
else:
|
|
322
|
+
b, a = self._ba # type: ignore[misc]
|
|
323
|
+
if filtfilt:
|
|
324
|
+
filtered_data = signal.filtfilt(b, a, trace.data)
|
|
325
|
+
else:
|
|
326
|
+
filtered_data = signal.lfilter(b, a, trace.data)
|
|
327
|
+
|
|
328
|
+
filtered_trace = WaveformTrace(
|
|
329
|
+
data=filtered_data.astype(np.float64),
|
|
330
|
+
metadata=trace.metadata,
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
if return_details:
|
|
334
|
+
_w, h = self.get_frequency_response()
|
|
335
|
+
impulse = self.get_impulse_response()
|
|
336
|
+
_, gd = self.get_group_delay()
|
|
337
|
+
return FilterResult(
|
|
338
|
+
trace=filtered_trace,
|
|
339
|
+
transfer_function=h,
|
|
340
|
+
impulse_response=impulse,
|
|
341
|
+
group_delay=gd,
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
return filtered_trace
|
|
345
|
+
|
|
346
|
+
def get_frequency_response(
|
|
347
|
+
self,
|
|
348
|
+
worN: int | NDArray[np.float64] | None = None,
|
|
349
|
+
) -> tuple[NDArray[np.float64], NDArray[np.complex128]]:
|
|
350
|
+
"""Get frequency response."""
|
|
351
|
+
if worN is None:
|
|
352
|
+
worN = 512
|
|
353
|
+
|
|
354
|
+
if self._sos is not None:
|
|
355
|
+
w, h = signal.sosfreqz(self._sos, worN=worN)
|
|
356
|
+
elif self._ba is not None:
|
|
357
|
+
w, h = signal.freqz(self._ba[0], self._ba[1], worN=worN)
|
|
358
|
+
else:
|
|
359
|
+
raise AnalysisError("Filter not designed")
|
|
360
|
+
|
|
361
|
+
return w.astype(np.float64), h.astype(np.complex128)
|
|
362
|
+
|
|
363
|
+
def get_impulse_response(
|
|
364
|
+
self,
|
|
365
|
+
n_samples: int = 256,
|
|
366
|
+
) -> NDArray[np.float64]:
|
|
367
|
+
"""Get impulse response."""
|
|
368
|
+
impulse = np.zeros(n_samples)
|
|
369
|
+
impulse[0] = 1.0
|
|
370
|
+
|
|
371
|
+
response: NDArray[np.float64]
|
|
372
|
+
if self._sos is not None:
|
|
373
|
+
response = signal.sosfilt(self._sos, impulse).astype(np.float64)
|
|
374
|
+
elif self._ba is not None:
|
|
375
|
+
response = signal.lfilter(self._ba[0], self._ba[1], impulse).astype(np.float64)
|
|
376
|
+
else:
|
|
377
|
+
raise AnalysisError("Filter not designed")
|
|
378
|
+
|
|
379
|
+
return response
|
|
380
|
+
|
|
381
|
+
def get_step_response(
|
|
382
|
+
self,
|
|
383
|
+
n_samples: int = 256,
|
|
384
|
+
) -> NDArray[np.float64]:
|
|
385
|
+
"""Get step response."""
|
|
386
|
+
step = np.ones(n_samples)
|
|
387
|
+
|
|
388
|
+
response: NDArray[np.float64]
|
|
389
|
+
if self._sos is not None:
|
|
390
|
+
response = signal.sosfilt(self._sos, step).astype(np.float64)
|
|
391
|
+
elif self._ba is not None:
|
|
392
|
+
response = signal.lfilter(self._ba[0], self._ba[1], step).astype(np.float64)
|
|
393
|
+
else:
|
|
394
|
+
raise AnalysisError("Filter not designed")
|
|
395
|
+
|
|
396
|
+
return response
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
class FIRFilter(Filter):
|
|
400
|
+
"""Finite Impulse Response filter base class.
|
|
401
|
+
|
|
402
|
+
Stores filter coefficients as a single array of tap weights.
|
|
403
|
+
FIR filters are always stable and can achieve linear phase.
|
|
404
|
+
|
|
405
|
+
Attributes:
|
|
406
|
+
coeffs: Filter tap coefficients.
|
|
407
|
+
"""
|
|
408
|
+
|
|
409
|
+
def __init__(
|
|
410
|
+
self,
|
|
411
|
+
sample_rate: float | None = None,
|
|
412
|
+
coeffs: NDArray[np.float64] | None = None,
|
|
413
|
+
) -> None:
|
|
414
|
+
"""Initialize FIR filter.
|
|
415
|
+
|
|
416
|
+
Args:
|
|
417
|
+
sample_rate: Sample rate in Hz.
|
|
418
|
+
coeffs: Filter coefficients (tap weights).
|
|
419
|
+
"""
|
|
420
|
+
super().__init__(sample_rate)
|
|
421
|
+
self._coeffs = coeffs
|
|
422
|
+
self._is_designed = coeffs is not None
|
|
423
|
+
|
|
424
|
+
@property
|
|
425
|
+
def coeffs(self) -> NDArray[np.float64] | None:
|
|
426
|
+
"""Filter coefficients."""
|
|
427
|
+
return self._coeffs
|
|
428
|
+
|
|
429
|
+
@coeffs.setter
|
|
430
|
+
def coeffs(self, value: NDArray[np.float64]) -> None:
|
|
431
|
+
"""Set filter coefficients."""
|
|
432
|
+
self._coeffs = value
|
|
433
|
+
self._is_designed = True
|
|
434
|
+
|
|
435
|
+
@property
|
|
436
|
+
def is_stable(self) -> bool:
|
|
437
|
+
"""FIR filters are always stable."""
|
|
438
|
+
return True
|
|
439
|
+
|
|
440
|
+
@property
|
|
441
|
+
def order(self) -> int:
|
|
442
|
+
"""Filter order (number of taps - 1)."""
|
|
443
|
+
if self._coeffs is not None:
|
|
444
|
+
return len(self._coeffs) - 1
|
|
445
|
+
return 0
|
|
446
|
+
|
|
447
|
+
@property
|
|
448
|
+
def is_linear_phase(self) -> bool:
|
|
449
|
+
"""Check if filter has linear phase (symmetric or antisymmetric coefficients)."""
|
|
450
|
+
if self._coeffs is None:
|
|
451
|
+
return False
|
|
452
|
+
len(self._coeffs)
|
|
453
|
+
# Check symmetry
|
|
454
|
+
symmetric = np.allclose(self._coeffs, self._coeffs[::-1])
|
|
455
|
+
antisymmetric = np.allclose(self._coeffs, -self._coeffs[::-1])
|
|
456
|
+
return symmetric or antisymmetric # type: ignore[no-any-return]
|
|
457
|
+
|
|
458
|
+
def apply(
|
|
459
|
+
self,
|
|
460
|
+
trace: WaveformTrace,
|
|
461
|
+
*,
|
|
462
|
+
return_details: bool = False,
|
|
463
|
+
mode: Literal["full", "same", "valid"] = "same",
|
|
464
|
+
) -> WaveformTrace | FilterResult:
|
|
465
|
+
"""Apply FIR filter to waveform.
|
|
466
|
+
|
|
467
|
+
Args:
|
|
468
|
+
trace: Input waveform trace.
|
|
469
|
+
return_details: If True, return FilterResult with introspection data.
|
|
470
|
+
mode: Convolution mode - "same" preserves length.
|
|
471
|
+
|
|
472
|
+
Returns:
|
|
473
|
+
Filtered trace, or FilterResult if return_details=True.
|
|
474
|
+
|
|
475
|
+
Raises:
|
|
476
|
+
AnalysisError: If filter not designed.
|
|
477
|
+
"""
|
|
478
|
+
if self._coeffs is None:
|
|
479
|
+
raise AnalysisError("Filter not designed - no coefficients available")
|
|
480
|
+
|
|
481
|
+
# Apply filter using convolution
|
|
482
|
+
filtered_data = np.convolve(trace.data, self._coeffs, mode=mode)
|
|
483
|
+
|
|
484
|
+
filtered_trace = WaveformTrace(
|
|
485
|
+
data=filtered_data.astype(np.float64),
|
|
486
|
+
metadata=trace.metadata,
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
if return_details:
|
|
490
|
+
_w, h = self.get_frequency_response()
|
|
491
|
+
impulse = self.get_impulse_response()
|
|
492
|
+
_, gd = self.get_group_delay()
|
|
493
|
+
return FilterResult(
|
|
494
|
+
trace=filtered_trace,
|
|
495
|
+
transfer_function=h,
|
|
496
|
+
impulse_response=impulse,
|
|
497
|
+
group_delay=gd,
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
return filtered_trace
|
|
501
|
+
|
|
502
|
+
def get_frequency_response(
|
|
503
|
+
self,
|
|
504
|
+
worN: int | NDArray[np.float64] | None = None,
|
|
505
|
+
) -> tuple[NDArray[np.float64], NDArray[np.complex128]]:
|
|
506
|
+
"""Get frequency response."""
|
|
507
|
+
if self._coeffs is None:
|
|
508
|
+
raise AnalysisError("Filter not designed")
|
|
509
|
+
|
|
510
|
+
if worN is None:
|
|
511
|
+
worN = 512
|
|
512
|
+
|
|
513
|
+
w, h = signal.freqz(self._coeffs, 1, worN=worN)
|
|
514
|
+
return w.astype(np.float64), h.astype(np.complex128)
|
|
515
|
+
|
|
516
|
+
def get_impulse_response(
|
|
517
|
+
self,
|
|
518
|
+
n_samples: int = 256,
|
|
519
|
+
) -> NDArray[np.float64]:
|
|
520
|
+
"""Get impulse response (just the coefficients, zero-padded)."""
|
|
521
|
+
if self._coeffs is None:
|
|
522
|
+
raise AnalysisError("Filter not designed")
|
|
523
|
+
|
|
524
|
+
if len(self._coeffs) >= n_samples:
|
|
525
|
+
return self._coeffs[:n_samples].astype(np.float64)
|
|
526
|
+
|
|
527
|
+
response = np.zeros(n_samples)
|
|
528
|
+
response[: len(self._coeffs)] = self._coeffs
|
|
529
|
+
return response.astype(np.float64)
|
|
530
|
+
|
|
531
|
+
def get_step_response(
|
|
532
|
+
self,
|
|
533
|
+
n_samples: int = 256,
|
|
534
|
+
) -> NDArray[np.float64]:
|
|
535
|
+
"""Get step response."""
|
|
536
|
+
if self._coeffs is None:
|
|
537
|
+
raise AnalysisError("Filter not designed")
|
|
538
|
+
|
|
539
|
+
step = np.ones(n_samples)
|
|
540
|
+
response = np.convolve(step, self._coeffs, mode="full")[:n_samples]
|
|
541
|
+
return response.astype(np.float64)
|
|
542
|
+
|
|
543
|
+
def get_group_delay(
|
|
544
|
+
self,
|
|
545
|
+
worN: int | NDArray[np.float64] | None = None,
|
|
546
|
+
) -> tuple[NDArray[np.float64], NDArray[np.float64]]:
|
|
547
|
+
"""Get group delay."""
|
|
548
|
+
if self._coeffs is None:
|
|
549
|
+
raise AnalysisError("Filter not designed")
|
|
550
|
+
|
|
551
|
+
if worN is None:
|
|
552
|
+
worN = 512
|
|
553
|
+
|
|
554
|
+
w, gd = signal.group_delay((self._coeffs, 1), w=worN)
|
|
555
|
+
return w.astype(np.float64), gd.astype(np.float64)
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
__all__ = [
|
|
559
|
+
"FIRFilter",
|
|
560
|
+
"Filter",
|
|
561
|
+
"FilterResult",
|
|
562
|
+
"IIRFilter",
|
|
563
|
+
]
|