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,32 @@
|
|
|
1
|
+
"""Filter convenience functions namespace.
|
|
2
|
+
|
|
3
|
+
This module provides a namespace for filter functions to support:
|
|
4
|
+
from oscura.filtering import filters
|
|
5
|
+
filters.low_pass(trace, cutoff=1000)
|
|
6
|
+
|
|
7
|
+
Re-exports convenience functions from the filtering package.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from oscura.filtering.convenience import (
|
|
11
|
+
band_pass,
|
|
12
|
+
band_stop,
|
|
13
|
+
high_pass,
|
|
14
|
+
low_pass,
|
|
15
|
+
matched_filter,
|
|
16
|
+
median_filter,
|
|
17
|
+
moving_average,
|
|
18
|
+
notch_filter,
|
|
19
|
+
savgol_filter,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
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
|
+
]
|
|
@@ -0,0 +1,605 @@
|
|
|
1
|
+
"""Filter introspection and visualization for TraceKit.
|
|
2
|
+
|
|
3
|
+
Provides filter analysis tools including Bode plots, impulse response,
|
|
4
|
+
step response, and pole-zero diagrams.
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
>>> from oscura.filtering import LowPassFilter, plot_bode
|
|
9
|
+
>>> filt = LowPassFilter(cutoff=1e6, sample_rate=10e6, order=4)
|
|
10
|
+
>>> fig = plot_bode(filt)
|
|
11
|
+
>>> plt.show()
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from typing import TYPE_CHECKING
|
|
17
|
+
|
|
18
|
+
import numpy as np
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from matplotlib.figure import Figure
|
|
22
|
+
from numpy.typing import NDArray
|
|
23
|
+
|
|
24
|
+
from oscura.filtering.base import Filter, IIRFilter
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class FilterIntrospection:
|
|
28
|
+
"""Mixin class providing filter introspection methods.
|
|
29
|
+
|
|
30
|
+
Provides methods for analyzing filter characteristics including
|
|
31
|
+
frequency response, impulse response, step response, and stability.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self, filter_obj: Filter) -> None:
|
|
35
|
+
"""Initialize with a filter object.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
filter_obj: Filter to introspect.
|
|
39
|
+
"""
|
|
40
|
+
self._filter = filter_obj
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def filter(self) -> Filter:
|
|
44
|
+
"""The wrapped filter object.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
The filter being introspected.
|
|
48
|
+
"""
|
|
49
|
+
return self._filter
|
|
50
|
+
|
|
51
|
+
def magnitude_response(
|
|
52
|
+
self,
|
|
53
|
+
freqs: NDArray[np.float64] | None = None,
|
|
54
|
+
db: bool = True,
|
|
55
|
+
) -> tuple[NDArray[np.float64], NDArray[np.float64]]:
|
|
56
|
+
"""Get magnitude response.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
freqs: Frequencies in Hz. If None, auto-generate.
|
|
60
|
+
db: If True, return magnitude in dB.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
Tuple of (frequencies, magnitude).
|
|
64
|
+
|
|
65
|
+
Raises:
|
|
66
|
+
ValueError: If freqs is None and filter has no sample_rate.
|
|
67
|
+
"""
|
|
68
|
+
if freqs is None:
|
|
69
|
+
if self._filter.sample_rate is None:
|
|
70
|
+
raise ValueError(
|
|
71
|
+
"Either freqs must be provided or filter must have sample_rate set"
|
|
72
|
+
)
|
|
73
|
+
freqs = np.linspace(0, self._filter.sample_rate / 2, 512)
|
|
74
|
+
|
|
75
|
+
h = self._filter.get_transfer_function(freqs)
|
|
76
|
+
mag = np.abs(h)
|
|
77
|
+
|
|
78
|
+
if db:
|
|
79
|
+
mag = 20 * np.log10(np.maximum(mag, 1e-12))
|
|
80
|
+
|
|
81
|
+
return freqs, mag
|
|
82
|
+
|
|
83
|
+
def phase_response(
|
|
84
|
+
self,
|
|
85
|
+
freqs: NDArray[np.float64] | None = None,
|
|
86
|
+
unwrap: bool = True,
|
|
87
|
+
degrees: bool = True,
|
|
88
|
+
) -> tuple[NDArray[np.float64], NDArray[np.float64]]:
|
|
89
|
+
"""Get phase response.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
freqs: Frequencies in Hz. If None, auto-generate.
|
|
93
|
+
unwrap: If True, unwrap phase to remove discontinuities.
|
|
94
|
+
degrees: If True, return phase in degrees.
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
Tuple of (frequencies, phase).
|
|
98
|
+
|
|
99
|
+
Raises:
|
|
100
|
+
ValueError: If freqs is None and filter has no sample_rate.
|
|
101
|
+
"""
|
|
102
|
+
if freqs is None:
|
|
103
|
+
if self._filter.sample_rate is None:
|
|
104
|
+
raise ValueError(
|
|
105
|
+
"Either freqs must be provided or filter must have sample_rate set"
|
|
106
|
+
)
|
|
107
|
+
freqs = np.linspace(0, self._filter.sample_rate / 2, 512)
|
|
108
|
+
|
|
109
|
+
h = self._filter.get_transfer_function(freqs)
|
|
110
|
+
phase = np.angle(h)
|
|
111
|
+
|
|
112
|
+
if unwrap:
|
|
113
|
+
phase = np.unwrap(phase)
|
|
114
|
+
|
|
115
|
+
if degrees:
|
|
116
|
+
phase = np.degrees(phase)
|
|
117
|
+
|
|
118
|
+
return freqs, phase
|
|
119
|
+
|
|
120
|
+
def group_delay_hz(
|
|
121
|
+
self,
|
|
122
|
+
freqs: NDArray[np.float64] | None = None,
|
|
123
|
+
) -> tuple[NDArray[np.float64], NDArray[np.float64]]:
|
|
124
|
+
"""Get group delay in seconds.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
freqs: Frequencies in Hz. If None, auto-generate.
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
Tuple of (frequencies in Hz, group delay in seconds).
|
|
131
|
+
"""
|
|
132
|
+
w, gd_samples = self._filter.get_group_delay()
|
|
133
|
+
|
|
134
|
+
if self._filter.sample_rate is not None:
|
|
135
|
+
freqs_out = w * self._filter.sample_rate / (2 * np.pi)
|
|
136
|
+
gd_seconds = gd_samples / self._filter.sample_rate
|
|
137
|
+
else:
|
|
138
|
+
freqs_out = w
|
|
139
|
+
gd_seconds = gd_samples
|
|
140
|
+
|
|
141
|
+
return freqs_out, gd_seconds
|
|
142
|
+
|
|
143
|
+
def passband_ripple(
|
|
144
|
+
self,
|
|
145
|
+
passband_edge: float,
|
|
146
|
+
) -> float:
|
|
147
|
+
"""Calculate passband ripple in dB.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
passband_edge: Passband edge frequency in Hz.
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
Peak-to-peak ripple in dB within passband.
|
|
154
|
+
|
|
155
|
+
Raises:
|
|
156
|
+
ValueError: If filter sample_rate is not set.
|
|
157
|
+
"""
|
|
158
|
+
if self._filter.sample_rate is None:
|
|
159
|
+
raise ValueError("Sample rate must be set")
|
|
160
|
+
|
|
161
|
+
freqs = np.linspace(0, passband_edge, 256)
|
|
162
|
+
_, mag_db = self.magnitude_response(freqs, db=True)
|
|
163
|
+
|
|
164
|
+
return float(np.max(mag_db) - np.min(mag_db))
|
|
165
|
+
|
|
166
|
+
def stopband_attenuation(
|
|
167
|
+
self,
|
|
168
|
+
stopband_edge: float,
|
|
169
|
+
) -> float:
|
|
170
|
+
"""Calculate minimum stopband attenuation in dB.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
stopband_edge: Stopband edge frequency in Hz.
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
Minimum attenuation in stopband in dB (positive value).
|
|
177
|
+
|
|
178
|
+
Raises:
|
|
179
|
+
ValueError: If filter sample_rate is not set.
|
|
180
|
+
"""
|
|
181
|
+
if self._filter.sample_rate is None:
|
|
182
|
+
raise ValueError("Sample rate must be set")
|
|
183
|
+
|
|
184
|
+
freqs = np.linspace(stopband_edge, self._filter.sample_rate / 2, 256)
|
|
185
|
+
_, mag_db = self.magnitude_response(freqs, db=True)
|
|
186
|
+
|
|
187
|
+
return float(-np.max(mag_db))
|
|
188
|
+
|
|
189
|
+
def cutoff_frequency(
|
|
190
|
+
self,
|
|
191
|
+
threshold_db: float = -3.0,
|
|
192
|
+
) -> float:
|
|
193
|
+
"""Find -3dB cutoff frequency.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
threshold_db: Threshold in dB (default -3dB).
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
Cutoff frequency in Hz.
|
|
200
|
+
|
|
201
|
+
Raises:
|
|
202
|
+
ValueError: If filter sample_rate is not set.
|
|
203
|
+
"""
|
|
204
|
+
if self._filter.sample_rate is None:
|
|
205
|
+
raise ValueError("Sample rate must be set")
|
|
206
|
+
|
|
207
|
+
freqs = np.linspace(0, self._filter.sample_rate / 2, 1000)
|
|
208
|
+
_, mag_db = self.magnitude_response(freqs, db=True)
|
|
209
|
+
|
|
210
|
+
# Normalize to 0dB at DC
|
|
211
|
+
mag_db = mag_db - mag_db[0]
|
|
212
|
+
|
|
213
|
+
# Find first crossing of threshold
|
|
214
|
+
crossings = np.where(mag_db < threshold_db)[0]
|
|
215
|
+
if len(crossings) == 0:
|
|
216
|
+
return float(freqs[-1])
|
|
217
|
+
|
|
218
|
+
return float(freqs[crossings[0]])
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def plot_bode(
|
|
222
|
+
filt: Filter,
|
|
223
|
+
*,
|
|
224
|
+
figsize: tuple[float, float] = (10, 8),
|
|
225
|
+
freq_range: tuple[float, float] | None = None,
|
|
226
|
+
n_points: int = 512,
|
|
227
|
+
title: str | None = None,
|
|
228
|
+
) -> Figure:
|
|
229
|
+
"""Plot Bode diagram (magnitude and phase response).
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
filt: Filter to plot.
|
|
233
|
+
figsize: Figure size in inches.
|
|
234
|
+
freq_range: Frequency range (min, max) in Hz. None for auto.
|
|
235
|
+
n_points: Number of frequency points.
|
|
236
|
+
title: Plot title.
|
|
237
|
+
|
|
238
|
+
Returns:
|
|
239
|
+
Matplotlib Figure object.
|
|
240
|
+
|
|
241
|
+
Raises:
|
|
242
|
+
ValueError: If filter sample_rate is not set.
|
|
243
|
+
|
|
244
|
+
Example:
|
|
245
|
+
>>> fig = plot_bode(filt)
|
|
246
|
+
>>> plt.show()
|
|
247
|
+
"""
|
|
248
|
+
import matplotlib.pyplot as plt
|
|
249
|
+
|
|
250
|
+
if filt.sample_rate is None:
|
|
251
|
+
raise ValueError("Filter sample rate must be set for plotting")
|
|
252
|
+
|
|
253
|
+
if freq_range is None:
|
|
254
|
+
freq_range = (1, filt.sample_rate / 2)
|
|
255
|
+
|
|
256
|
+
freqs = np.geomspace(freq_range[0], freq_range[1], n_points)
|
|
257
|
+
|
|
258
|
+
introspect = FilterIntrospection(filt)
|
|
259
|
+
_, mag_db = introspect.magnitude_response(freqs, db=True)
|
|
260
|
+
_, phase_deg = introspect.phase_response(freqs, degrees=True)
|
|
261
|
+
|
|
262
|
+
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=figsize, sharex=True)
|
|
263
|
+
|
|
264
|
+
# Magnitude plot
|
|
265
|
+
ax1.semilogx(freqs, mag_db)
|
|
266
|
+
ax1.set_ylabel("Magnitude (dB)")
|
|
267
|
+
ax1.grid(True, which="both", alpha=0.3)
|
|
268
|
+
ax1.axhline(-3, color="r", linestyle="--", alpha=0.5, label="-3 dB")
|
|
269
|
+
ax1.legend()
|
|
270
|
+
|
|
271
|
+
# Phase plot
|
|
272
|
+
ax2.semilogx(freqs, phase_deg)
|
|
273
|
+
ax2.set_xlabel("Frequency (Hz)")
|
|
274
|
+
ax2.set_ylabel("Phase (degrees)")
|
|
275
|
+
ax2.grid(True, which="both", alpha=0.3)
|
|
276
|
+
|
|
277
|
+
if title:
|
|
278
|
+
fig.suptitle(title)
|
|
279
|
+
else:
|
|
280
|
+
fig.suptitle(f"Bode Plot - Order {filt.order} Filter")
|
|
281
|
+
|
|
282
|
+
plt.tight_layout()
|
|
283
|
+
return fig
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def plot_impulse(
|
|
287
|
+
filt: Filter,
|
|
288
|
+
*,
|
|
289
|
+
n_samples: int = 256,
|
|
290
|
+
figsize: tuple[float, float] = (10, 4),
|
|
291
|
+
title: str | None = None,
|
|
292
|
+
) -> Figure:
|
|
293
|
+
"""Plot impulse response.
|
|
294
|
+
|
|
295
|
+
Args:
|
|
296
|
+
filt: Filter to plot.
|
|
297
|
+
n_samples: Number of samples in response.
|
|
298
|
+
figsize: Figure size in inches.
|
|
299
|
+
title: Plot title.
|
|
300
|
+
|
|
301
|
+
Returns:
|
|
302
|
+
Matplotlib Figure object.
|
|
303
|
+
"""
|
|
304
|
+
import matplotlib.pyplot as plt
|
|
305
|
+
|
|
306
|
+
impulse = filt.get_impulse_response(n_samples)
|
|
307
|
+
|
|
308
|
+
fig, ax = plt.subplots(figsize=figsize)
|
|
309
|
+
|
|
310
|
+
if filt.sample_rate is not None:
|
|
311
|
+
t = np.arange(n_samples) / filt.sample_rate * 1e6 # microseconds
|
|
312
|
+
ax.plot(t, impulse)
|
|
313
|
+
ax.set_xlabel("Time (us)")
|
|
314
|
+
else:
|
|
315
|
+
ax.plot(impulse)
|
|
316
|
+
ax.set_xlabel("Samples")
|
|
317
|
+
|
|
318
|
+
ax.set_ylabel("Amplitude")
|
|
319
|
+
ax.grid(True, alpha=0.3)
|
|
320
|
+
ax.axhline(0, color="k", linewidth=0.5)
|
|
321
|
+
|
|
322
|
+
if title:
|
|
323
|
+
ax.set_title(title)
|
|
324
|
+
else:
|
|
325
|
+
ax.set_title("Impulse Response")
|
|
326
|
+
|
|
327
|
+
plt.tight_layout()
|
|
328
|
+
return fig
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def plot_step(
|
|
332
|
+
filt: Filter,
|
|
333
|
+
*,
|
|
334
|
+
n_samples: int = 256,
|
|
335
|
+
figsize: tuple[float, float] = (10, 4),
|
|
336
|
+
title: str | None = None,
|
|
337
|
+
) -> Figure:
|
|
338
|
+
"""Plot step response.
|
|
339
|
+
|
|
340
|
+
Args:
|
|
341
|
+
filt: Filter to plot.
|
|
342
|
+
n_samples: Number of samples in response.
|
|
343
|
+
figsize: Figure size in inches.
|
|
344
|
+
title: Plot title.
|
|
345
|
+
|
|
346
|
+
Returns:
|
|
347
|
+
Matplotlib Figure object.
|
|
348
|
+
"""
|
|
349
|
+
import matplotlib.pyplot as plt
|
|
350
|
+
|
|
351
|
+
step = filt.get_step_response(n_samples)
|
|
352
|
+
|
|
353
|
+
fig, ax = plt.subplots(figsize=figsize)
|
|
354
|
+
|
|
355
|
+
if filt.sample_rate is not None:
|
|
356
|
+
t = np.arange(n_samples) / filt.sample_rate * 1e6 # microseconds
|
|
357
|
+
ax.plot(t, step)
|
|
358
|
+
ax.set_xlabel("Time (us)")
|
|
359
|
+
else:
|
|
360
|
+
ax.plot(step)
|
|
361
|
+
ax.set_xlabel("Samples")
|
|
362
|
+
|
|
363
|
+
ax.set_ylabel("Amplitude")
|
|
364
|
+
ax.grid(True, alpha=0.3)
|
|
365
|
+
ax.axhline(1, color="r", linestyle="--", alpha=0.5, label="Final value")
|
|
366
|
+
ax.legend()
|
|
367
|
+
|
|
368
|
+
if title:
|
|
369
|
+
ax.set_title(title)
|
|
370
|
+
else:
|
|
371
|
+
ax.set_title("Step Response")
|
|
372
|
+
|
|
373
|
+
plt.tight_layout()
|
|
374
|
+
return fig
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def plot_poles_zeros(
|
|
378
|
+
filt: Filter,
|
|
379
|
+
*,
|
|
380
|
+
figsize: tuple[float, float] = (8, 8),
|
|
381
|
+
title: str | None = None,
|
|
382
|
+
) -> Figure:
|
|
383
|
+
"""Plot pole-zero diagram for IIR filter.
|
|
384
|
+
|
|
385
|
+
Args:
|
|
386
|
+
filt: IIR filter to plot.
|
|
387
|
+
figsize: Figure size in inches.
|
|
388
|
+
title: Plot title.
|
|
389
|
+
|
|
390
|
+
Returns:
|
|
391
|
+
Matplotlib Figure object.
|
|
392
|
+
|
|
393
|
+
Raises:
|
|
394
|
+
ValueError: If filter is not an IIRFilter.
|
|
395
|
+
"""
|
|
396
|
+
import matplotlib.pyplot as plt
|
|
397
|
+
|
|
398
|
+
if not isinstance(filt, IIRFilter):
|
|
399
|
+
raise ValueError("Pole-zero plot only available for IIR filters")
|
|
400
|
+
|
|
401
|
+
poles = filt.poles
|
|
402
|
+
zeros = filt.zeros
|
|
403
|
+
|
|
404
|
+
fig, ax = plt.subplots(figsize=figsize)
|
|
405
|
+
|
|
406
|
+
# Draw unit circle
|
|
407
|
+
theta = np.linspace(0, 2 * np.pi, 100)
|
|
408
|
+
ax.plot(np.cos(theta), np.sin(theta), "k--", alpha=0.3, label="Unit circle")
|
|
409
|
+
|
|
410
|
+
# Plot poles and zeros
|
|
411
|
+
ax.scatter(
|
|
412
|
+
np.real(zeros),
|
|
413
|
+
np.imag(zeros),
|
|
414
|
+
marker="o",
|
|
415
|
+
s=100,
|
|
416
|
+
facecolors="none",
|
|
417
|
+
edgecolors="b",
|
|
418
|
+
linewidths=2,
|
|
419
|
+
label="Zeros",
|
|
420
|
+
)
|
|
421
|
+
ax.scatter(
|
|
422
|
+
np.real(poles),
|
|
423
|
+
np.imag(poles),
|
|
424
|
+
marker="x",
|
|
425
|
+
s=100,
|
|
426
|
+
c="r",
|
|
427
|
+
linewidths=2,
|
|
428
|
+
label="Poles",
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
ax.set_xlabel("Real")
|
|
432
|
+
ax.set_ylabel("Imaginary")
|
|
433
|
+
ax.set_aspect("equal")
|
|
434
|
+
ax.grid(True, alpha=0.3)
|
|
435
|
+
ax.legend()
|
|
436
|
+
|
|
437
|
+
# Stability indicator
|
|
438
|
+
is_stable = np.all(np.abs(poles) < 1.0)
|
|
439
|
+
stability_text = "STABLE" if is_stable else "UNSTABLE"
|
|
440
|
+
stability_color = "green" if is_stable else "red"
|
|
441
|
+
ax.text(
|
|
442
|
+
0.95,
|
|
443
|
+
0.95,
|
|
444
|
+
stability_text,
|
|
445
|
+
transform=ax.transAxes,
|
|
446
|
+
fontsize=12,
|
|
447
|
+
fontweight="bold",
|
|
448
|
+
color=stability_color,
|
|
449
|
+
ha="right",
|
|
450
|
+
va="top",
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
if title:
|
|
454
|
+
ax.set_title(title)
|
|
455
|
+
else:
|
|
456
|
+
ax.set_title(f"Pole-Zero Plot (Order {filt.order})")
|
|
457
|
+
|
|
458
|
+
plt.tight_layout()
|
|
459
|
+
return fig
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def plot_group_delay(
|
|
463
|
+
filt: Filter,
|
|
464
|
+
*,
|
|
465
|
+
figsize: tuple[float, float] = (10, 4),
|
|
466
|
+
freq_range: tuple[float, float] | None = None,
|
|
467
|
+
n_points: int = 512,
|
|
468
|
+
title: str | None = None,
|
|
469
|
+
) -> Figure:
|
|
470
|
+
"""Plot group delay.
|
|
471
|
+
|
|
472
|
+
Args:
|
|
473
|
+
filt: Filter to plot.
|
|
474
|
+
figsize: Figure size in inches.
|
|
475
|
+
freq_range: Frequency range (min, max) in Hz.
|
|
476
|
+
n_points: Number of frequency points.
|
|
477
|
+
title: Plot title.
|
|
478
|
+
|
|
479
|
+
Returns:
|
|
480
|
+
Matplotlib Figure object.
|
|
481
|
+
|
|
482
|
+
Raises:
|
|
483
|
+
ValueError: If filter sample_rate is not set.
|
|
484
|
+
"""
|
|
485
|
+
import matplotlib.pyplot as plt
|
|
486
|
+
|
|
487
|
+
if filt.sample_rate is None:
|
|
488
|
+
raise ValueError("Filter sample rate must be set for plotting")
|
|
489
|
+
|
|
490
|
+
introspect = FilterIntrospection(filt)
|
|
491
|
+
freqs, gd = introspect.group_delay_hz()
|
|
492
|
+
|
|
493
|
+
fig, ax = plt.subplots(figsize=figsize)
|
|
494
|
+
|
|
495
|
+
ax.semilogx(freqs, gd * 1e6) # Convert to microseconds
|
|
496
|
+
ax.set_xlabel("Frequency (Hz)")
|
|
497
|
+
ax.set_ylabel("Group Delay (us)")
|
|
498
|
+
ax.grid(True, which="both", alpha=0.3)
|
|
499
|
+
|
|
500
|
+
if title:
|
|
501
|
+
ax.set_title(title)
|
|
502
|
+
else:
|
|
503
|
+
ax.set_title("Group Delay")
|
|
504
|
+
|
|
505
|
+
plt.tight_layout()
|
|
506
|
+
return fig
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
def compare_filters(
|
|
510
|
+
filters: list[Filter],
|
|
511
|
+
labels: list[str] | None = None,
|
|
512
|
+
*,
|
|
513
|
+
figsize: tuple[float, float] = (12, 10),
|
|
514
|
+
freq_range: tuple[float, float] | None = None,
|
|
515
|
+
n_points: int = 512,
|
|
516
|
+
) -> Figure:
|
|
517
|
+
"""Compare multiple filters on the same plots.
|
|
518
|
+
|
|
519
|
+
Args:
|
|
520
|
+
filters: List of filters to compare.
|
|
521
|
+
labels: Labels for each filter. If None, uses "Filter 1", etc.
|
|
522
|
+
figsize: Figure size in inches.
|
|
523
|
+
freq_range: Frequency range (min, max) in Hz.
|
|
524
|
+
n_points: Number of frequency points.
|
|
525
|
+
|
|
526
|
+
Returns:
|
|
527
|
+
Matplotlib Figure object with comparison plots.
|
|
528
|
+
|
|
529
|
+
Raises:
|
|
530
|
+
ValueError: If number of labels doesn't match number of filters or if filter sample_rate is not set.
|
|
531
|
+
"""
|
|
532
|
+
import matplotlib.pyplot as plt
|
|
533
|
+
|
|
534
|
+
if labels is None:
|
|
535
|
+
labels = [f"Filter {i + 1}" for i in range(len(filters))]
|
|
536
|
+
|
|
537
|
+
if len(labels) != len(filters):
|
|
538
|
+
raise ValueError("Number of labels must match number of filters")
|
|
539
|
+
|
|
540
|
+
# Use first filter's sample rate for frequency axis
|
|
541
|
+
sample_rate = filters[0].sample_rate
|
|
542
|
+
if sample_rate is None:
|
|
543
|
+
raise ValueError("Filter sample rate must be set for plotting")
|
|
544
|
+
|
|
545
|
+
if freq_range is None:
|
|
546
|
+
freq_range = (1, sample_rate / 2)
|
|
547
|
+
|
|
548
|
+
freqs = np.geomspace(freq_range[0], freq_range[1], n_points)
|
|
549
|
+
|
|
550
|
+
fig, axes = plt.subplots(2, 2, figsize=figsize)
|
|
551
|
+
|
|
552
|
+
for filt, label in zip(filters, labels, strict=False):
|
|
553
|
+
introspect = FilterIntrospection(filt)
|
|
554
|
+
_, mag_db = introspect.magnitude_response(freqs, db=True)
|
|
555
|
+
_, phase_deg = introspect.phase_response(freqs, degrees=True)
|
|
556
|
+
impulse = filt.get_impulse_response(256)
|
|
557
|
+
step = filt.get_step_response(256)
|
|
558
|
+
|
|
559
|
+
# Magnitude
|
|
560
|
+
axes[0, 0].semilogx(freqs, mag_db, label=label)
|
|
561
|
+
# Phase
|
|
562
|
+
axes[0, 1].semilogx(freqs, phase_deg, label=label)
|
|
563
|
+
# Impulse
|
|
564
|
+
axes[1, 0].plot(impulse, label=label)
|
|
565
|
+
# Step
|
|
566
|
+
axes[1, 1].plot(step, label=label)
|
|
567
|
+
|
|
568
|
+
axes[0, 0].set_ylabel("Magnitude (dB)")
|
|
569
|
+
axes[0, 0].set_title("Magnitude Response")
|
|
570
|
+
axes[0, 0].grid(True, which="both", alpha=0.3)
|
|
571
|
+
axes[0, 0].axhline(-3, color="k", linestyle="--", alpha=0.3)
|
|
572
|
+
axes[0, 0].legend()
|
|
573
|
+
|
|
574
|
+
axes[0, 1].set_ylabel("Phase (degrees)")
|
|
575
|
+
axes[0, 1].set_title("Phase Response")
|
|
576
|
+
axes[0, 1].grid(True, which="both", alpha=0.3)
|
|
577
|
+
axes[0, 1].legend()
|
|
578
|
+
|
|
579
|
+
axes[1, 0].set_xlabel("Samples")
|
|
580
|
+
axes[1, 0].set_ylabel("Amplitude")
|
|
581
|
+
axes[1, 0].set_title("Impulse Response")
|
|
582
|
+
axes[1, 0].grid(True, alpha=0.3)
|
|
583
|
+
axes[1, 0].legend()
|
|
584
|
+
|
|
585
|
+
axes[1, 1].set_xlabel("Samples")
|
|
586
|
+
axes[1, 1].set_ylabel("Amplitude")
|
|
587
|
+
axes[1, 1].set_title("Step Response")
|
|
588
|
+
axes[1, 1].grid(True, alpha=0.3)
|
|
589
|
+
axes[1, 1].axhline(1, color="k", linestyle="--", alpha=0.3)
|
|
590
|
+
axes[1, 1].legend()
|
|
591
|
+
|
|
592
|
+
fig.suptitle("Filter Comparison")
|
|
593
|
+
plt.tight_layout()
|
|
594
|
+
return fig
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
__all__ = [
|
|
598
|
+
"FilterIntrospection",
|
|
599
|
+
"compare_filters",
|
|
600
|
+
"plot_bode",
|
|
601
|
+
"plot_group_delay",
|
|
602
|
+
"plot_impulse",
|
|
603
|
+
"plot_poles_zeros",
|
|
604
|
+
"plot_step",
|
|
605
|
+
]
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""TraceKit guidance module.
|
|
2
|
+
|
|
3
|
+
Provides guided analysis workflows and recommendations.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from oscura.guidance.recommender import (
|
|
7
|
+
AnalysisHistory,
|
|
8
|
+
Recommendation,
|
|
9
|
+
suggest_next_steps,
|
|
10
|
+
)
|
|
11
|
+
from oscura.guidance.wizard import (
|
|
12
|
+
AnalysisWizard,
|
|
13
|
+
WizardResult,
|
|
14
|
+
WizardStep,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"AnalysisHistory",
|
|
19
|
+
"AnalysisWizard",
|
|
20
|
+
"Recommendation",
|
|
21
|
+
"WizardResult",
|
|
22
|
+
"WizardStep",
|
|
23
|
+
"suggest_next_steps",
|
|
24
|
+
]
|