oscura 0.0.1__py3-none-any.whl → 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.
- 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.0.dist-info/METADATA +300 -0
- oscura-0.1.0.dist-info/RECORD +463 -0
- oscura-0.1.0.dist-info/entry_points.txt +2 -0
- {oscura-0.0.1.dist-info → oscura-0.1.0.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.0.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,725 @@
|
|
|
1
|
+
"""Filter design functions for TraceKit.
|
|
2
|
+
|
|
3
|
+
Provides high-level filter design API with support for Butterworth,
|
|
4
|
+
Chebyshev, Bessel, and Elliptic filter types. Supports automatic
|
|
5
|
+
order calculation from specifications.
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
Example:
|
|
9
|
+
>>> from oscura.filtering.design import LowPassFilter, design_filter
|
|
10
|
+
>>> # Simple filter creation
|
|
11
|
+
>>> lpf = LowPassFilter(cutoff=1e6, sample_rate=10e6, order=4)
|
|
12
|
+
>>> # Spec-based design
|
|
13
|
+
>>> filt = design_filter_spec(
|
|
14
|
+
... passband=1e6, stopband=2e6,
|
|
15
|
+
... passband_ripple=1.0, stopband_atten=40.0,
|
|
16
|
+
... sample_rate=10e6
|
|
17
|
+
... )
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from typing import Any, Literal
|
|
23
|
+
|
|
24
|
+
import numpy as np
|
|
25
|
+
from scipy import signal
|
|
26
|
+
|
|
27
|
+
from oscura.core.exceptions import AnalysisError
|
|
28
|
+
from oscura.filtering.base import IIRFilter
|
|
29
|
+
|
|
30
|
+
FilterType = Literal["butterworth", "chebyshev1", "chebyshev2", "bessel", "elliptic"]
|
|
31
|
+
BandType = Literal["lowpass", "highpass", "bandpass", "bandstop"]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def design_filter(
|
|
35
|
+
filter_type: FilterType,
|
|
36
|
+
cutoff: float | tuple[float, float],
|
|
37
|
+
sample_rate: float,
|
|
38
|
+
order: int,
|
|
39
|
+
btype: BandType = "lowpass",
|
|
40
|
+
*,
|
|
41
|
+
ripple_db: float = 1.0,
|
|
42
|
+
stopband_atten_db: float = 40.0,
|
|
43
|
+
analog: bool = False,
|
|
44
|
+
output: Literal["sos", "ba"] = "sos",
|
|
45
|
+
) -> IIRFilter:
|
|
46
|
+
"""Design an IIR filter with specified parameters.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
filter_type: Type of filter ("butterworth", "chebyshev1", "chebyshev2",
|
|
50
|
+
"bessel", or "elliptic").
|
|
51
|
+
cutoff: Cutoff frequency in Hz. For bandpass/bandstop, tuple of (low, high).
|
|
52
|
+
sample_rate: Sample rate in Hz.
|
|
53
|
+
order: Filter order.
|
|
54
|
+
btype: Band type ("lowpass", "highpass", "bandpass", "bandstop").
|
|
55
|
+
ripple_db: Passband ripple in dB (for Chebyshev and Elliptic).
|
|
56
|
+
stopband_atten_db: Stopband attenuation in dB (for Chebyshev2 and Elliptic).
|
|
57
|
+
analog: If True, design analog filter (s-domain). Default is digital (z-domain).
|
|
58
|
+
output: Output format - "sos" for second-order sections (recommended),
|
|
59
|
+
"ba" for transfer function polynomials.
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
IIRFilter object with designed coefficients.
|
|
63
|
+
|
|
64
|
+
Raises:
|
|
65
|
+
AnalysisError: If cutoff frequency is invalid or filter design fails.
|
|
66
|
+
|
|
67
|
+
Example:
|
|
68
|
+
>>> lpf = design_filter("butterworth", 1e6, 10e6, order=4)
|
|
69
|
+
>>> filtered = lpf.apply(trace)
|
|
70
|
+
|
|
71
|
+
References:
|
|
72
|
+
scipy.signal.iirfilter, butter, cheby1, cheby2, ellip, bessel
|
|
73
|
+
"""
|
|
74
|
+
# Normalize cutoff frequency
|
|
75
|
+
if isinstance(cutoff, tuple):
|
|
76
|
+
Wn = cutoff if analog else (cutoff[0] / (sample_rate / 2), cutoff[1] / (sample_rate / 2))
|
|
77
|
+
else:
|
|
78
|
+
Wn = cutoff if analog else cutoff / (sample_rate / 2) # type: ignore[assignment]
|
|
79
|
+
|
|
80
|
+
# Validate normalized frequency
|
|
81
|
+
if not analog:
|
|
82
|
+
if isinstance(Wn, tuple):
|
|
83
|
+
if not (0 < Wn[0] < 1 and 0 < Wn[1] < 1):
|
|
84
|
+
raise AnalysisError(
|
|
85
|
+
f"Normalized cutoff must be in (0, 1), got {Wn}. "
|
|
86
|
+
f"Cutoff {cutoff} Hz must be less than Nyquist {sample_rate / 2} Hz."
|
|
87
|
+
)
|
|
88
|
+
elif not 0 < Wn < 1: # type: ignore[unreachable]
|
|
89
|
+
raise AnalysisError(
|
|
90
|
+
f"Normalized cutoff must be in (0, 1), got {Wn}. "
|
|
91
|
+
f"Cutoff {cutoff} Hz must be less than Nyquist {sample_rate / 2} Hz."
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
# Design filter
|
|
95
|
+
ftype_map = {
|
|
96
|
+
"butterworth": "butter",
|
|
97
|
+
"chebyshev1": "cheby1",
|
|
98
|
+
"chebyshev2": "cheby2",
|
|
99
|
+
"bessel": "bessel",
|
|
100
|
+
"elliptic": "ellip",
|
|
101
|
+
}
|
|
102
|
+
ftype = ftype_map.get(filter_type)
|
|
103
|
+
if ftype is None:
|
|
104
|
+
raise AnalysisError(f"Unknown filter type: {filter_type}")
|
|
105
|
+
|
|
106
|
+
try:
|
|
107
|
+
if filter_type == "butterworth":
|
|
108
|
+
if output == "sos":
|
|
109
|
+
sos = signal.butter(order, Wn, btype=btype, analog=analog, output="sos")
|
|
110
|
+
return IIRFilter(sample_rate=sample_rate, sos=sos)
|
|
111
|
+
else:
|
|
112
|
+
b, a = signal.butter(order, Wn, btype=btype, analog=analog, output="ba")
|
|
113
|
+
return IIRFilter(sample_rate=sample_rate, ba=(b, a))
|
|
114
|
+
|
|
115
|
+
elif filter_type == "chebyshev1":
|
|
116
|
+
if output == "sos":
|
|
117
|
+
sos = signal.cheby1(order, ripple_db, Wn, btype=btype, analog=analog, output="sos")
|
|
118
|
+
return IIRFilter(sample_rate=sample_rate, sos=sos)
|
|
119
|
+
else:
|
|
120
|
+
b, a = signal.cheby1(order, ripple_db, Wn, btype=btype, analog=analog, output="ba")
|
|
121
|
+
return IIRFilter(sample_rate=sample_rate, ba=(b, a))
|
|
122
|
+
|
|
123
|
+
elif filter_type == "chebyshev2":
|
|
124
|
+
if output == "sos":
|
|
125
|
+
sos = signal.cheby2(
|
|
126
|
+
order,
|
|
127
|
+
stopband_atten_db,
|
|
128
|
+
Wn,
|
|
129
|
+
btype=btype,
|
|
130
|
+
analog=analog,
|
|
131
|
+
output="sos",
|
|
132
|
+
)
|
|
133
|
+
return IIRFilter(sample_rate=sample_rate, sos=sos)
|
|
134
|
+
else:
|
|
135
|
+
b, a = signal.cheby2(
|
|
136
|
+
order,
|
|
137
|
+
stopband_atten_db,
|
|
138
|
+
Wn,
|
|
139
|
+
btype=btype,
|
|
140
|
+
analog=analog,
|
|
141
|
+
output="ba",
|
|
142
|
+
)
|
|
143
|
+
return IIRFilter(sample_rate=sample_rate, ba=(b, a))
|
|
144
|
+
|
|
145
|
+
elif filter_type == "bessel":
|
|
146
|
+
if output == "sos":
|
|
147
|
+
sos = signal.bessel(
|
|
148
|
+
order, Wn, btype=btype, analog=analog, output="sos", norm="phase"
|
|
149
|
+
)
|
|
150
|
+
return IIRFilter(sample_rate=sample_rate, sos=sos)
|
|
151
|
+
else:
|
|
152
|
+
b, a = signal.bessel(
|
|
153
|
+
order, Wn, btype=btype, analog=analog, output="ba", norm="phase"
|
|
154
|
+
)
|
|
155
|
+
return IIRFilter(sample_rate=sample_rate, ba=(b, a))
|
|
156
|
+
|
|
157
|
+
elif filter_type == "elliptic":
|
|
158
|
+
if output == "sos":
|
|
159
|
+
sos = signal.ellip(
|
|
160
|
+
order,
|
|
161
|
+
ripple_db,
|
|
162
|
+
stopband_atten_db,
|
|
163
|
+
Wn,
|
|
164
|
+
btype=btype,
|
|
165
|
+
analog=analog,
|
|
166
|
+
output="sos",
|
|
167
|
+
)
|
|
168
|
+
return IIRFilter(sample_rate=sample_rate, sos=sos)
|
|
169
|
+
else:
|
|
170
|
+
b, a = signal.ellip(
|
|
171
|
+
order,
|
|
172
|
+
ripple_db,
|
|
173
|
+
stopband_atten_db,
|
|
174
|
+
Wn,
|
|
175
|
+
btype=btype,
|
|
176
|
+
analog=analog,
|
|
177
|
+
output="ba",
|
|
178
|
+
)
|
|
179
|
+
return IIRFilter(sample_rate=sample_rate, ba=(b, a))
|
|
180
|
+
|
|
181
|
+
else:
|
|
182
|
+
raise AnalysisError(f"Unsupported filter type: {filter_type}")
|
|
183
|
+
|
|
184
|
+
except Exception as e:
|
|
185
|
+
raise AnalysisError(f"Filter design failed: {e}") from e
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def design_filter_spec(
|
|
189
|
+
passband: float | tuple[float, float],
|
|
190
|
+
stopband: float | tuple[float, float],
|
|
191
|
+
sample_rate: float,
|
|
192
|
+
passband_ripple: float = 1.0,
|
|
193
|
+
stopband_atten: float = 40.0,
|
|
194
|
+
*,
|
|
195
|
+
filter_type: FilterType = "elliptic",
|
|
196
|
+
analog: bool = False,
|
|
197
|
+
) -> IIRFilter:
|
|
198
|
+
"""Design filter from passband/stopband specifications.
|
|
199
|
+
|
|
200
|
+
Automatically computes the minimum filter order required to meet
|
|
201
|
+
the specifications.
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
passband: Passband edge frequency in Hz. Tuple for bandpass/bandstop.
|
|
205
|
+
stopband: Stopband edge frequency in Hz. Tuple for bandpass/bandstop.
|
|
206
|
+
sample_rate: Sample rate in Hz.
|
|
207
|
+
passband_ripple: Maximum passband ripple in dB.
|
|
208
|
+
stopband_atten: Minimum stopband attenuation in dB.
|
|
209
|
+
filter_type: Filter type to design.
|
|
210
|
+
analog: If True, design analog filter.
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
IIRFilter object with minimum-order design.
|
|
214
|
+
|
|
215
|
+
Raises:
|
|
216
|
+
AnalysisError: If filter order cannot be determined.
|
|
217
|
+
|
|
218
|
+
Example:
|
|
219
|
+
>>> # Design a filter with 1MHz passband, 2MHz stopband, 40dB rejection
|
|
220
|
+
>>> filt = design_filter_spec(
|
|
221
|
+
... passband=1e6, stopband=2e6,
|
|
222
|
+
... passband_ripple=1.0, stopband_atten=40.0,
|
|
223
|
+
... sample_rate=10e6
|
|
224
|
+
... )
|
|
225
|
+
"""
|
|
226
|
+
# Normalize frequencies
|
|
227
|
+
if isinstance(passband, tuple):
|
|
228
|
+
wp = (passband[0] / (sample_rate / 2), passband[1] / (sample_rate / 2))
|
|
229
|
+
ws = (stopband[0] / (sample_rate / 2), stopband[1] / (sample_rate / 2)) # type: ignore[index]
|
|
230
|
+
else:
|
|
231
|
+
wp = passband / (sample_rate / 2) # type: ignore[assignment]
|
|
232
|
+
ws = stopband / (sample_rate / 2) # type: ignore[assignment, operator]
|
|
233
|
+
|
|
234
|
+
# Determine band type
|
|
235
|
+
if isinstance(passband, tuple):
|
|
236
|
+
# Bandpass or bandstop
|
|
237
|
+
if passband[0] < stopband[0]: # type: ignore[index]
|
|
238
|
+
btype: BandType = "bandstop"
|
|
239
|
+
else:
|
|
240
|
+
btype = "bandpass"
|
|
241
|
+
# Lowpass or highpass
|
|
242
|
+
elif passband < stopband: # type: ignore[operator]
|
|
243
|
+
btype = "lowpass"
|
|
244
|
+
else:
|
|
245
|
+
btype = "highpass"
|
|
246
|
+
|
|
247
|
+
# Compute minimum order
|
|
248
|
+
try:
|
|
249
|
+
if filter_type == "butterworth":
|
|
250
|
+
order, Wn = signal.buttord(wp, ws, passband_ripple, stopband_atten, analog=analog)
|
|
251
|
+
elif filter_type == "chebyshev1":
|
|
252
|
+
order, Wn = signal.cheb1ord(wp, ws, passband_ripple, stopband_atten, analog=analog)
|
|
253
|
+
elif filter_type == "chebyshev2":
|
|
254
|
+
order, Wn = signal.cheb2ord(wp, ws, passband_ripple, stopband_atten, analog=analog)
|
|
255
|
+
elif filter_type == "elliptic":
|
|
256
|
+
order, Wn = signal.ellipord(wp, ws, passband_ripple, stopband_atten, analog=analog)
|
|
257
|
+
else:
|
|
258
|
+
# Bessel doesn't have an ord function, estimate based on Butterworth
|
|
259
|
+
order, Wn = signal.buttord(wp, ws, passband_ripple, stopband_atten, analog=analog)
|
|
260
|
+
|
|
261
|
+
except Exception as e:
|
|
262
|
+
raise AnalysisError(f"Could not determine filter order: {e}") from e
|
|
263
|
+
|
|
264
|
+
# Design with computed order
|
|
265
|
+
cutoff = (
|
|
266
|
+
tuple(w * sample_rate / 2 for w in Wn)
|
|
267
|
+
if isinstance(Wn, np.ndarray)
|
|
268
|
+
else Wn * sample_rate / 2
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
return design_filter(
|
|
272
|
+
filter_type=filter_type,
|
|
273
|
+
cutoff=cutoff,
|
|
274
|
+
sample_rate=sample_rate,
|
|
275
|
+
order=int(order),
|
|
276
|
+
btype=btype,
|
|
277
|
+
ripple_db=passband_ripple,
|
|
278
|
+
stopband_atten_db=stopband_atten,
|
|
279
|
+
analog=analog,
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
# =============================================================================
|
|
284
|
+
# Convenience Filter Classes
|
|
285
|
+
# =============================================================================
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
class LowPassFilter(IIRFilter):
|
|
289
|
+
"""Low-pass Butterworth filter.
|
|
290
|
+
|
|
291
|
+
Convenient class for creating low-pass filters with sensible defaults.
|
|
292
|
+
|
|
293
|
+
Example:
|
|
294
|
+
>>> lpf = LowPassFilter(cutoff=1e6, sample_rate=10e6, order=4)
|
|
295
|
+
>>> filtered = lpf.apply(trace)
|
|
296
|
+
"""
|
|
297
|
+
|
|
298
|
+
def __init__(
|
|
299
|
+
self,
|
|
300
|
+
cutoff: float,
|
|
301
|
+
sample_rate: float,
|
|
302
|
+
order: int = 4,
|
|
303
|
+
*,
|
|
304
|
+
filter_type: FilterType = "butterworth",
|
|
305
|
+
ripple_db: float = 1.0,
|
|
306
|
+
stopband_atten_db: float = 40.0,
|
|
307
|
+
) -> None:
|
|
308
|
+
"""Initialize low-pass filter.
|
|
309
|
+
|
|
310
|
+
Args:
|
|
311
|
+
cutoff: Cutoff frequency in Hz (-3dB point for Butterworth).
|
|
312
|
+
sample_rate: Sample rate in Hz.
|
|
313
|
+
order: Filter order.
|
|
314
|
+
filter_type: Type of filter to use.
|
|
315
|
+
ripple_db: Passband ripple for Chebyshev/Elliptic.
|
|
316
|
+
stopband_atten_db: Stopband attenuation for Chebyshev2/Elliptic.
|
|
317
|
+
"""
|
|
318
|
+
filt = design_filter(
|
|
319
|
+
filter_type=filter_type,
|
|
320
|
+
cutoff=cutoff,
|
|
321
|
+
sample_rate=sample_rate,
|
|
322
|
+
order=order,
|
|
323
|
+
btype="lowpass",
|
|
324
|
+
ripple_db=ripple_db,
|
|
325
|
+
stopband_atten_db=stopband_atten_db,
|
|
326
|
+
)
|
|
327
|
+
super().__init__(sample_rate=sample_rate, sos=filt.sos)
|
|
328
|
+
self._cutoff = cutoff
|
|
329
|
+
self._filter_type = filter_type
|
|
330
|
+
|
|
331
|
+
@property
|
|
332
|
+
def cutoff(self) -> float:
|
|
333
|
+
"""Cutoff frequency in Hz."""
|
|
334
|
+
return self._cutoff
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
class HighPassFilter(IIRFilter):
|
|
338
|
+
"""High-pass Butterworth filter.
|
|
339
|
+
|
|
340
|
+
Example:
|
|
341
|
+
>>> hpf = HighPassFilter(cutoff=1e3, sample_rate=100e3, order=4)
|
|
342
|
+
>>> filtered = hpf.apply(trace)
|
|
343
|
+
"""
|
|
344
|
+
|
|
345
|
+
def __init__(
|
|
346
|
+
self,
|
|
347
|
+
cutoff: float,
|
|
348
|
+
sample_rate: float,
|
|
349
|
+
order: int = 4,
|
|
350
|
+
*,
|
|
351
|
+
filter_type: FilterType = "butterworth",
|
|
352
|
+
ripple_db: float = 1.0,
|
|
353
|
+
stopband_atten_db: float = 40.0,
|
|
354
|
+
) -> None:
|
|
355
|
+
"""Initialize high-pass filter."""
|
|
356
|
+
filt = design_filter(
|
|
357
|
+
filter_type=filter_type,
|
|
358
|
+
cutoff=cutoff,
|
|
359
|
+
sample_rate=sample_rate,
|
|
360
|
+
order=order,
|
|
361
|
+
btype="highpass",
|
|
362
|
+
ripple_db=ripple_db,
|
|
363
|
+
stopband_atten_db=stopband_atten_db,
|
|
364
|
+
)
|
|
365
|
+
super().__init__(sample_rate=sample_rate, sos=filt.sos)
|
|
366
|
+
self._cutoff = cutoff
|
|
367
|
+
|
|
368
|
+
@property
|
|
369
|
+
def cutoff(self) -> float:
|
|
370
|
+
"""Cutoff frequency in Hz."""
|
|
371
|
+
return self._cutoff
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
class BandPassFilter(IIRFilter):
|
|
375
|
+
"""Band-pass filter.
|
|
376
|
+
|
|
377
|
+
Example:
|
|
378
|
+
>>> bpf = BandPassFilter(low=1e3, high=10e3, sample_rate=100e3, order=4)
|
|
379
|
+
>>> filtered = bpf.apply(trace)
|
|
380
|
+
"""
|
|
381
|
+
|
|
382
|
+
def __init__(
|
|
383
|
+
self,
|
|
384
|
+
low: float,
|
|
385
|
+
high: float,
|
|
386
|
+
sample_rate: float,
|
|
387
|
+
order: int = 4,
|
|
388
|
+
*,
|
|
389
|
+
filter_type: FilterType = "butterworth",
|
|
390
|
+
ripple_db: float = 1.0,
|
|
391
|
+
stopband_atten_db: float = 40.0,
|
|
392
|
+
) -> None:
|
|
393
|
+
"""Initialize band-pass filter."""
|
|
394
|
+
filt = design_filter(
|
|
395
|
+
filter_type=filter_type,
|
|
396
|
+
cutoff=(low, high),
|
|
397
|
+
sample_rate=sample_rate,
|
|
398
|
+
order=order,
|
|
399
|
+
btype="bandpass",
|
|
400
|
+
ripple_db=ripple_db,
|
|
401
|
+
stopband_atten_db=stopband_atten_db,
|
|
402
|
+
)
|
|
403
|
+
super().__init__(sample_rate=sample_rate, sos=filt.sos)
|
|
404
|
+
self._low = low
|
|
405
|
+
self._high = high
|
|
406
|
+
|
|
407
|
+
@property
|
|
408
|
+
def passband(self) -> tuple[float, float]:
|
|
409
|
+
"""Passband frequencies (low, high) in Hz."""
|
|
410
|
+
return (self._low, self._high)
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
class BandStopFilter(IIRFilter):
|
|
414
|
+
"""Band-stop (notch) filter.
|
|
415
|
+
|
|
416
|
+
Example:
|
|
417
|
+
>>> bsf = BandStopFilter(low=50, high=60, sample_rate=1000, order=4)
|
|
418
|
+
>>> filtered = bsf.apply(trace) # Remove 50-60 Hz interference
|
|
419
|
+
"""
|
|
420
|
+
|
|
421
|
+
def __init__(
|
|
422
|
+
self,
|
|
423
|
+
low: float,
|
|
424
|
+
high: float,
|
|
425
|
+
sample_rate: float,
|
|
426
|
+
order: int = 4,
|
|
427
|
+
*,
|
|
428
|
+
filter_type: FilterType = "butterworth",
|
|
429
|
+
ripple_db: float = 1.0,
|
|
430
|
+
stopband_atten_db: float = 40.0,
|
|
431
|
+
) -> None:
|
|
432
|
+
"""Initialize band-stop filter."""
|
|
433
|
+
filt = design_filter(
|
|
434
|
+
filter_type=filter_type,
|
|
435
|
+
cutoff=(low, high),
|
|
436
|
+
sample_rate=sample_rate,
|
|
437
|
+
order=order,
|
|
438
|
+
btype="bandstop",
|
|
439
|
+
ripple_db=ripple_db,
|
|
440
|
+
stopband_atten_db=stopband_atten_db,
|
|
441
|
+
)
|
|
442
|
+
super().__init__(sample_rate=sample_rate, sos=filt.sos)
|
|
443
|
+
self._low = low
|
|
444
|
+
self._high = high
|
|
445
|
+
|
|
446
|
+
@property
|
|
447
|
+
def stopband(self) -> tuple[float, float]:
|
|
448
|
+
"""Stopband frequencies (low, high) in Hz."""
|
|
449
|
+
return (self._low, self._high)
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
# =============================================================================
|
|
453
|
+
# Filter Type Classes
|
|
454
|
+
# =============================================================================
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
class ButterworthFilter(IIRFilter):
|
|
458
|
+
"""Butterworth filter with maximally flat passband.
|
|
459
|
+
|
|
460
|
+
Example:
|
|
461
|
+
>>> filt = ButterworthFilter(cutoff=1e6, sample_rate=10e6, order=4, btype="lowpass")
|
|
462
|
+
"""
|
|
463
|
+
|
|
464
|
+
def __init__(
|
|
465
|
+
self,
|
|
466
|
+
cutoff: float | tuple[float, float],
|
|
467
|
+
sample_rate: float,
|
|
468
|
+
order: int = 4,
|
|
469
|
+
btype: BandType = "lowpass",
|
|
470
|
+
) -> None:
|
|
471
|
+
filt = design_filter("butterworth", cutoff, sample_rate, order, btype)
|
|
472
|
+
super().__init__(sample_rate=sample_rate, sos=filt.sos)
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
class ChebyshevType1Filter(IIRFilter):
|
|
476
|
+
"""Chebyshev Type I filter with passband ripple.
|
|
477
|
+
|
|
478
|
+
Example:
|
|
479
|
+
>>> filt = ChebyshevType1Filter(cutoff=1e6, sample_rate=10e6, order=4, ripple_db=0.5)
|
|
480
|
+
"""
|
|
481
|
+
|
|
482
|
+
def __init__(
|
|
483
|
+
self,
|
|
484
|
+
cutoff: float | tuple[float, float],
|
|
485
|
+
sample_rate: float,
|
|
486
|
+
order: int = 4,
|
|
487
|
+
btype: BandType = "lowpass",
|
|
488
|
+
ripple_db: float = 1.0,
|
|
489
|
+
) -> None:
|
|
490
|
+
filt = design_filter("chebyshev1", cutoff, sample_rate, order, btype, ripple_db=ripple_db)
|
|
491
|
+
super().__init__(sample_rate=sample_rate, sos=filt.sos)
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
class ChebyshevType2Filter(IIRFilter):
|
|
495
|
+
"""Chebyshev Type II filter with stopband ripple.
|
|
496
|
+
|
|
497
|
+
Example:
|
|
498
|
+
>>> filt = ChebyshevType2Filter(cutoff=1e6, sample_rate=10e6, order=4, stopband_atten_db=40)
|
|
499
|
+
"""
|
|
500
|
+
|
|
501
|
+
def __init__(
|
|
502
|
+
self,
|
|
503
|
+
cutoff: float | tuple[float, float],
|
|
504
|
+
sample_rate: float,
|
|
505
|
+
order: int = 4,
|
|
506
|
+
btype: BandType = "lowpass",
|
|
507
|
+
stopband_atten_db: float = 40.0,
|
|
508
|
+
) -> None:
|
|
509
|
+
filt = design_filter(
|
|
510
|
+
"chebyshev2",
|
|
511
|
+
cutoff,
|
|
512
|
+
sample_rate,
|
|
513
|
+
order,
|
|
514
|
+
btype,
|
|
515
|
+
stopband_atten_db=stopband_atten_db,
|
|
516
|
+
)
|
|
517
|
+
super().__init__(sample_rate=sample_rate, sos=filt.sos)
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
class BesselFilter(IIRFilter):
|
|
521
|
+
"""Bessel filter with maximally flat group delay.
|
|
522
|
+
|
|
523
|
+
Best for preserving waveform shape during filtering.
|
|
524
|
+
|
|
525
|
+
Example:
|
|
526
|
+
>>> filt = BesselFilter(cutoff=1e6, sample_rate=10e6, order=4)
|
|
527
|
+
"""
|
|
528
|
+
|
|
529
|
+
def __init__(
|
|
530
|
+
self,
|
|
531
|
+
cutoff: float | tuple[float, float],
|
|
532
|
+
sample_rate: float,
|
|
533
|
+
order: int = 4,
|
|
534
|
+
btype: BandType = "lowpass",
|
|
535
|
+
) -> None:
|
|
536
|
+
filt = design_filter("bessel", cutoff, sample_rate, order, btype)
|
|
537
|
+
super().__init__(sample_rate=sample_rate, sos=filt.sos)
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
class EllipticFilter(IIRFilter):
|
|
541
|
+
"""Elliptic (Cauer) filter with equiripple passband and stopband.
|
|
542
|
+
|
|
543
|
+
Provides the sharpest transition band for a given order.
|
|
544
|
+
|
|
545
|
+
Example:
|
|
546
|
+
>>> filt = EllipticFilter(cutoff=1e6, sample_rate=10e6, order=4,
|
|
547
|
+
... ripple_db=0.5, stopband_atten_db=60)
|
|
548
|
+
"""
|
|
549
|
+
|
|
550
|
+
def __init__(
|
|
551
|
+
self,
|
|
552
|
+
cutoff: float | tuple[float, float],
|
|
553
|
+
sample_rate: float,
|
|
554
|
+
order: int = 4,
|
|
555
|
+
btype: BandType = "lowpass",
|
|
556
|
+
ripple_db: float = 1.0,
|
|
557
|
+
stopband_atten_db: float = 40.0,
|
|
558
|
+
) -> None:
|
|
559
|
+
filt = design_filter(
|
|
560
|
+
"elliptic",
|
|
561
|
+
cutoff,
|
|
562
|
+
sample_rate,
|
|
563
|
+
order,
|
|
564
|
+
btype,
|
|
565
|
+
ripple_db=ripple_db,
|
|
566
|
+
stopband_atten_db=stopband_atten_db,
|
|
567
|
+
)
|
|
568
|
+
super().__init__(sample_rate=sample_rate, sos=filt.sos)
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
def suggest_filter_type(
|
|
572
|
+
transition_bandwidth: float,
|
|
573
|
+
passband_ripple_db: float,
|
|
574
|
+
stopband_atten_db: float,
|
|
575
|
+
) -> FilterType:
|
|
576
|
+
"""Suggest best filter type based on requirements.
|
|
577
|
+
|
|
578
|
+
Recommends filter type based on design tradeoffs:
|
|
579
|
+
- Butterworth: Maximally flat passband, moderate rolloff
|
|
580
|
+
- Chebyshev1: Faster rolloff, passband ripple
|
|
581
|
+
- Chebyshev2: Faster rolloff, stopband ripple
|
|
582
|
+
- Elliptic: Sharpest rolloff, both passband and stopband ripple
|
|
583
|
+
- Bessel: Linear phase, slowest rolloff (for waveform preservation)
|
|
584
|
+
|
|
585
|
+
Args:
|
|
586
|
+
transition_bandwidth: Normalized transition bandwidth (stopband - passband) / sample_rate.
|
|
587
|
+
passband_ripple_db: Acceptable passband ripple in dB.
|
|
588
|
+
stopband_atten_db: Required stopband attenuation in dB.
|
|
589
|
+
|
|
590
|
+
Returns:
|
|
591
|
+
Recommended filter type.
|
|
592
|
+
|
|
593
|
+
Example:
|
|
594
|
+
>>> # Sharp transition, can tolerate some ripple
|
|
595
|
+
>>> ftype = suggest_filter_type(
|
|
596
|
+
... transition_bandwidth=0.1,
|
|
597
|
+
... passband_ripple_db=0.5,
|
|
598
|
+
... stopband_atten_db=60.0
|
|
599
|
+
... )
|
|
600
|
+
>>> print(ftype) # 'elliptic'
|
|
601
|
+
|
|
602
|
+
References:
|
|
603
|
+
API-020: Filter Design Auto-Order
|
|
604
|
+
"""
|
|
605
|
+
# For very sharp transitions with ripple tolerance, use elliptic
|
|
606
|
+
if transition_bandwidth < 0.15 and passband_ripple_db >= 0.1:
|
|
607
|
+
return "elliptic"
|
|
608
|
+
|
|
609
|
+
# For moderate sharpness with low passband ripple, use Chebyshev2
|
|
610
|
+
if transition_bandwidth < 0.2 and passband_ripple_db < 0.1:
|
|
611
|
+
return "chebyshev2"
|
|
612
|
+
|
|
613
|
+
# For moderate sharpness with ripple tolerance, use Chebyshev1
|
|
614
|
+
if transition_bandwidth < 0.3 and passband_ripple_db >= 0.1:
|
|
615
|
+
return "chebyshev1"
|
|
616
|
+
|
|
617
|
+
# For phase linearity (waveform preservation), use Bessel
|
|
618
|
+
if stopband_atten_db < 40.0:
|
|
619
|
+
return "bessel"
|
|
620
|
+
|
|
621
|
+
# Default to Butterworth for balanced performance
|
|
622
|
+
return "butterworth"
|
|
623
|
+
|
|
624
|
+
|
|
625
|
+
def auto_design_filter(
|
|
626
|
+
passband: float | tuple[float, float],
|
|
627
|
+
stopband: float | tuple[float, float],
|
|
628
|
+
sample_rate: float,
|
|
629
|
+
*,
|
|
630
|
+
passband_ripple_db: float = 1.0,
|
|
631
|
+
stopband_atten_db: float = 40.0,
|
|
632
|
+
suggest_type: bool = True,
|
|
633
|
+
) -> tuple[IIRFilter, dict[str, Any]]:
|
|
634
|
+
"""Automatically design optimal filter from specifications.
|
|
635
|
+
|
|
636
|
+
Automatically computes filter order and optionally suggests the best
|
|
637
|
+
filter type based on transition band and ripple requirements.
|
|
638
|
+
|
|
639
|
+
Args:
|
|
640
|
+
passband: Passband edge frequency in Hz. Tuple for bandpass/bandstop.
|
|
641
|
+
stopband: Stopband edge frequency in Hz. Tuple for bandpass/bandstop.
|
|
642
|
+
sample_rate: Sample rate in Hz.
|
|
643
|
+
passband_ripple_db: Maximum passband ripple in dB (default: 1.0).
|
|
644
|
+
stopband_atten_db: Minimum stopband attenuation in dB (default: 40.0).
|
|
645
|
+
suggest_type: If True, automatically suggest filter type (default: True).
|
|
646
|
+
|
|
647
|
+
Returns:
|
|
648
|
+
Tuple of (IIRFilter, design_info_dict).
|
|
649
|
+
design_info_dict contains: filter_type, order, cutoff, transition_bandwidth.
|
|
650
|
+
|
|
651
|
+
Example:
|
|
652
|
+
>>> # Automatic filter design with type suggestion
|
|
653
|
+
>>> filt, info = auto_design_filter(
|
|
654
|
+
... passband=1e6,
|
|
655
|
+
... stopband=1.5e6,
|
|
656
|
+
... sample_rate=10e6,
|
|
657
|
+
... stopband_atten_db=60.0
|
|
658
|
+
... )
|
|
659
|
+
>>> print(f"Designed {info['filter_type']} filter with order {info['order']}")
|
|
660
|
+
>>> filtered = filt.apply(trace)
|
|
661
|
+
|
|
662
|
+
References:
|
|
663
|
+
API-020: Filter Design Auto-Order
|
|
664
|
+
"""
|
|
665
|
+
# Compute transition bandwidth
|
|
666
|
+
if isinstance(passband, tuple):
|
|
667
|
+
# Bandpass/bandstop - use average
|
|
668
|
+
transition_bw = (
|
|
669
|
+
abs(stopband[0] - passband[0]) + abs(stopband[1] - passband[1]) # type: ignore[index]
|
|
670
|
+
) / 2.0
|
|
671
|
+
else:
|
|
672
|
+
transition_bw = abs(stopband - passband) # type: ignore[operator]
|
|
673
|
+
|
|
674
|
+
normalized_transition = transition_bw / sample_rate
|
|
675
|
+
|
|
676
|
+
# Suggest filter type if requested
|
|
677
|
+
if suggest_type:
|
|
678
|
+
filter_type = suggest_filter_type(
|
|
679
|
+
transition_bandwidth=normalized_transition,
|
|
680
|
+
passband_ripple_db=passband_ripple_db,
|
|
681
|
+
stopband_atten_db=stopband_atten_db,
|
|
682
|
+
)
|
|
683
|
+
else:
|
|
684
|
+
filter_type = "butterworth"
|
|
685
|
+
|
|
686
|
+
# Design filter with auto-order computation
|
|
687
|
+
filt = design_filter_spec(
|
|
688
|
+
passband=passband,
|
|
689
|
+
stopband=stopband,
|
|
690
|
+
sample_rate=sample_rate,
|
|
691
|
+
passband_ripple=passband_ripple_db,
|
|
692
|
+
stopband_atten=stopband_atten_db,
|
|
693
|
+
filter_type=filter_type,
|
|
694
|
+
)
|
|
695
|
+
|
|
696
|
+
# Extract design info
|
|
697
|
+
design_info = {
|
|
698
|
+
"filter_type": filter_type,
|
|
699
|
+
"order": filt.sos.shape[0] * 2 if filt.sos is not None else 0,
|
|
700
|
+
"cutoff": passband,
|
|
701
|
+
"transition_bandwidth": transition_bw,
|
|
702
|
+
"passband_ripple_db": passband_ripple_db,
|
|
703
|
+
"stopband_atten_db": stopband_atten_db,
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
return filt, design_info
|
|
707
|
+
|
|
708
|
+
|
|
709
|
+
__all__ = [
|
|
710
|
+
"BandPassFilter",
|
|
711
|
+
"BandStopFilter",
|
|
712
|
+
"BandType",
|
|
713
|
+
"BesselFilter",
|
|
714
|
+
"ButterworthFilter",
|
|
715
|
+
"ChebyshevType1Filter",
|
|
716
|
+
"ChebyshevType2Filter",
|
|
717
|
+
"EllipticFilter",
|
|
718
|
+
"FilterType",
|
|
719
|
+
"HighPassFilter",
|
|
720
|
+
"LowPassFilter",
|
|
721
|
+
"auto_design_filter",
|
|
722
|
+
"design_filter",
|
|
723
|
+
"design_filter_spec",
|
|
724
|
+
"suggest_filter_type",
|
|
725
|
+
]
|