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,811 @@
|
|
|
1
|
+
"""Spectral visualization functions.
|
|
2
|
+
|
|
3
|
+
This module provides spectrum and spectrogram plots for
|
|
4
|
+
frequency-domain analysis visualization.
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
>>> from oscura.visualization.spectral import plot_spectrum, plot_spectrogram
|
|
9
|
+
>>> plot_spectrum(trace)
|
|
10
|
+
>>> plot_spectrogram(trace)
|
|
11
|
+
|
|
12
|
+
References:
|
|
13
|
+
matplotlib best practices for scientific visualization
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import TYPE_CHECKING, Any, Literal, cast
|
|
20
|
+
|
|
21
|
+
import numpy as np
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
import matplotlib.pyplot as plt
|
|
25
|
+
from matplotlib.colors import Normalize # noqa: F401
|
|
26
|
+
|
|
27
|
+
HAS_MATPLOTLIB = True
|
|
28
|
+
except ImportError:
|
|
29
|
+
HAS_MATPLOTLIB = False
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
if TYPE_CHECKING:
|
|
33
|
+
from matplotlib.axes import Axes
|
|
34
|
+
from matplotlib.figure import Figure
|
|
35
|
+
from numpy.typing import NDArray
|
|
36
|
+
|
|
37
|
+
from oscura.core.types import WaveformTrace
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def plot_spectrum(
|
|
41
|
+
trace: WaveformTrace,
|
|
42
|
+
*,
|
|
43
|
+
ax: Axes | None = None,
|
|
44
|
+
freq_unit: str = "auto",
|
|
45
|
+
db_ref: float | None = None,
|
|
46
|
+
freq_range: tuple[float, float] | None = None,
|
|
47
|
+
show_grid: bool = True,
|
|
48
|
+
color: str = "C0",
|
|
49
|
+
title: str | None = None,
|
|
50
|
+
window: str = "hann",
|
|
51
|
+
xscale: Literal["linear", "log"] = "log",
|
|
52
|
+
show: bool = True,
|
|
53
|
+
save_path: str | None = None,
|
|
54
|
+
figsize: tuple[float, float] = (10, 6),
|
|
55
|
+
xlim: tuple[float, float] | None = None,
|
|
56
|
+
ylim: tuple[float, float] | None = None,
|
|
57
|
+
fft_result: tuple[Any, Any] | None = None,
|
|
58
|
+
log_scale: bool = True,
|
|
59
|
+
db_scale: bool | None = None,
|
|
60
|
+
) -> Figure:
|
|
61
|
+
"""Plot magnitude spectrum.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
trace: Waveform trace to analyze.
|
|
65
|
+
ax: Matplotlib axes. If None, creates new figure.
|
|
66
|
+
freq_unit: Frequency unit ("Hz", "kHz", "MHz", "auto").
|
|
67
|
+
db_ref: Reference for dB scaling. If None, uses max value.
|
|
68
|
+
freq_range: Frequency range (min, max) in Hz to display.
|
|
69
|
+
show_grid: Show grid lines.
|
|
70
|
+
color: Line color.
|
|
71
|
+
title: Plot title.
|
|
72
|
+
window: Window function for FFT.
|
|
73
|
+
xscale: X-axis scale ("linear" or "log"). Deprecated, use log_scale instead.
|
|
74
|
+
show: If True, call plt.show() to display the plot.
|
|
75
|
+
save_path: Path to save the figure. If None, figure is not saved.
|
|
76
|
+
figsize: Figure size (width, height) in inches. Only used if ax is None.
|
|
77
|
+
xlim: X-axis limits (min, max) in selected frequency units.
|
|
78
|
+
ylim: Y-axis limits (min, max) in dB.
|
|
79
|
+
fft_result: Pre-computed FFT result (frequencies, magnitudes). If None, computes FFT.
|
|
80
|
+
log_scale: Use logarithmic scale for frequency axis (default True).
|
|
81
|
+
db_scale: Deprecated alias for log_scale. If provided, overrides log_scale.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
Matplotlib Figure object.
|
|
85
|
+
|
|
86
|
+
Raises:
|
|
87
|
+
ImportError: If matplotlib is not installed.
|
|
88
|
+
ValueError: If axes must have an associated figure.
|
|
89
|
+
|
|
90
|
+
Example:
|
|
91
|
+
>>> import oscura as osc
|
|
92
|
+
>>> trace = osc.load("signal.wfm")
|
|
93
|
+
>>> fig = osc.plot_spectrum(trace, freq_unit="MHz", log_scale=True)
|
|
94
|
+
|
|
95
|
+
>>> # With pre-computed FFT
|
|
96
|
+
>>> freq, mag = osc.fft(trace)
|
|
97
|
+
>>> fig = osc.plot_spectrum(trace, fft_result=(freq, mag), show=False)
|
|
98
|
+
>>> fig.savefig("spectrum.png")
|
|
99
|
+
"""
|
|
100
|
+
if not HAS_MATPLOTLIB:
|
|
101
|
+
raise ImportError("matplotlib is required for visualization")
|
|
102
|
+
|
|
103
|
+
# Handle deprecated db_scale parameter
|
|
104
|
+
if db_scale is not None:
|
|
105
|
+
log_scale = db_scale
|
|
106
|
+
|
|
107
|
+
from oscura.analyzers.waveform.spectral import fft
|
|
108
|
+
|
|
109
|
+
if ax is None:
|
|
110
|
+
fig, ax = plt.subplots(figsize=figsize)
|
|
111
|
+
else:
|
|
112
|
+
fig_temp = ax.get_figure()
|
|
113
|
+
if fig_temp is None:
|
|
114
|
+
raise ValueError("Axes must have an associated figure")
|
|
115
|
+
fig = cast("Figure", fig_temp)
|
|
116
|
+
|
|
117
|
+
# Compute FFT if not provided
|
|
118
|
+
if fft_result is not None:
|
|
119
|
+
freq, mag_db = fft_result
|
|
120
|
+
else:
|
|
121
|
+
freq, mag_db = fft(trace, window=window) # type: ignore[misc]
|
|
122
|
+
|
|
123
|
+
# Auto-select frequency unit
|
|
124
|
+
if freq_unit == "auto":
|
|
125
|
+
max_freq = freq[-1]
|
|
126
|
+
if max_freq >= 1e9:
|
|
127
|
+
freq_unit = "GHz"
|
|
128
|
+
elif max_freq >= 1e6:
|
|
129
|
+
freq_unit = "MHz"
|
|
130
|
+
elif max_freq >= 1e3:
|
|
131
|
+
freq_unit = "kHz"
|
|
132
|
+
else:
|
|
133
|
+
freq_unit = "Hz"
|
|
134
|
+
|
|
135
|
+
freq_divisors = {"Hz": 1.0, "kHz": 1e3, "MHz": 1e6, "GHz": 1e9}
|
|
136
|
+
divisor = freq_divisors.get(freq_unit, 1.0)
|
|
137
|
+
freq_scaled = freq / divisor
|
|
138
|
+
|
|
139
|
+
# Adjust dB reference if specified
|
|
140
|
+
if db_ref is not None:
|
|
141
|
+
mag_db = mag_db - db_ref
|
|
142
|
+
|
|
143
|
+
# Plot
|
|
144
|
+
ax.plot(freq_scaled, mag_db, color=color, linewidth=0.8)
|
|
145
|
+
|
|
146
|
+
ax.set_xlabel(f"Frequency ({freq_unit})")
|
|
147
|
+
ax.set_ylabel("Magnitude (dB)")
|
|
148
|
+
|
|
149
|
+
# Use log_scale parameter, fall back to xscale for backward compatibility
|
|
150
|
+
# Note: xscale is Literal["linear", "log"] so can never be "log" at this point
|
|
151
|
+
ax.set_xscale("log" if log_scale else "linear")
|
|
152
|
+
|
|
153
|
+
if title:
|
|
154
|
+
ax.set_title(title)
|
|
155
|
+
else:
|
|
156
|
+
ax.set_title("Magnitude Spectrum")
|
|
157
|
+
|
|
158
|
+
if show_grid:
|
|
159
|
+
ax.grid(True, alpha=0.3, which="both")
|
|
160
|
+
|
|
161
|
+
# Set reasonable y-limits
|
|
162
|
+
valid_db = mag_db[np.isfinite(mag_db)]
|
|
163
|
+
if len(valid_db) > 0:
|
|
164
|
+
y_max = np.max(valid_db)
|
|
165
|
+
y_min = max(np.min(valid_db), y_max - 120) # Limit dynamic range
|
|
166
|
+
ax.set_ylim(y_min, y_max + 5)
|
|
167
|
+
|
|
168
|
+
# Apply custom limits if specified
|
|
169
|
+
if freq_range is not None:
|
|
170
|
+
ax.set_xlim(freq_range[0] / divisor, freq_range[1] / divisor)
|
|
171
|
+
elif xlim is not None:
|
|
172
|
+
ax.set_xlim(xlim)
|
|
173
|
+
|
|
174
|
+
if ylim is not None:
|
|
175
|
+
ax.set_ylim(ylim)
|
|
176
|
+
|
|
177
|
+
fig.tight_layout()
|
|
178
|
+
|
|
179
|
+
# Save if path provided
|
|
180
|
+
if save_path is not None:
|
|
181
|
+
fig.savefig(save_path, dpi=300, bbox_inches="tight")
|
|
182
|
+
|
|
183
|
+
# Show if requested
|
|
184
|
+
if show:
|
|
185
|
+
plt.show()
|
|
186
|
+
|
|
187
|
+
return fig
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def plot_spectrogram(
|
|
191
|
+
trace: WaveformTrace,
|
|
192
|
+
*,
|
|
193
|
+
ax: Axes | None = None,
|
|
194
|
+
time_unit: str = "auto",
|
|
195
|
+
freq_unit: str = "auto",
|
|
196
|
+
cmap: str = "viridis",
|
|
197
|
+
vmin: float | None = None,
|
|
198
|
+
vmax: float | None = None,
|
|
199
|
+
title: str | None = None,
|
|
200
|
+
window: str = "hann",
|
|
201
|
+
nperseg: int | None = None,
|
|
202
|
+
nfft: int | None = None,
|
|
203
|
+
overlap: float | None = None,
|
|
204
|
+
) -> Figure:
|
|
205
|
+
"""Plot spectrogram (time-frequency representation).
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
trace: Waveform trace to analyze.
|
|
209
|
+
ax: Matplotlib axes. If None, creates new figure.
|
|
210
|
+
time_unit: Time unit ("s", "ms", "us", "auto").
|
|
211
|
+
freq_unit: Frequency unit ("Hz", "kHz", "MHz", "auto").
|
|
212
|
+
cmap: Colormap name.
|
|
213
|
+
vmin: Minimum dB value for color scaling.
|
|
214
|
+
vmax: Maximum dB value for color scaling.
|
|
215
|
+
title: Plot title.
|
|
216
|
+
window: Window function.
|
|
217
|
+
nperseg: Segment length for STFT.
|
|
218
|
+
nfft: FFT length. If specified, overrides nperseg.
|
|
219
|
+
overlap: Overlap fraction (0.0 to 1.0). Default is 0.5 (50%).
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
Matplotlib Figure object.
|
|
223
|
+
|
|
224
|
+
Raises:
|
|
225
|
+
ImportError: If matplotlib is not installed.
|
|
226
|
+
ValueError: If axes must have an associated figure.
|
|
227
|
+
|
|
228
|
+
Example:
|
|
229
|
+
>>> fig = plot_spectrogram(trace)
|
|
230
|
+
>>> plt.show()
|
|
231
|
+
"""
|
|
232
|
+
if not HAS_MATPLOTLIB:
|
|
233
|
+
raise ImportError("matplotlib is required for visualization")
|
|
234
|
+
|
|
235
|
+
from oscura.analyzers.waveform.spectral import spectrogram
|
|
236
|
+
|
|
237
|
+
if ax is None:
|
|
238
|
+
fig, ax = plt.subplots(figsize=(10, 4))
|
|
239
|
+
else:
|
|
240
|
+
fig_temp = ax.get_figure()
|
|
241
|
+
if fig_temp is None:
|
|
242
|
+
raise ValueError("Axes must have an associated figure")
|
|
243
|
+
fig = cast("Figure", fig_temp)
|
|
244
|
+
|
|
245
|
+
# Handle nfft as alias for nperseg
|
|
246
|
+
if nfft is not None:
|
|
247
|
+
nperseg = nfft
|
|
248
|
+
|
|
249
|
+
# Compute spectrogram with optional overlap
|
|
250
|
+
noverlap = None
|
|
251
|
+
if overlap is not None and nperseg is not None:
|
|
252
|
+
noverlap = int(nperseg * overlap)
|
|
253
|
+
times, freq, Sxx_db = spectrogram(trace, window=window, nperseg=nperseg, noverlap=noverlap)
|
|
254
|
+
|
|
255
|
+
# Auto-select units
|
|
256
|
+
if time_unit == "auto":
|
|
257
|
+
max_time = times[-1] if len(times) > 0 else 0
|
|
258
|
+
if max_time < 1e-6:
|
|
259
|
+
time_unit = "ns"
|
|
260
|
+
elif max_time < 1e-3:
|
|
261
|
+
time_unit = "us"
|
|
262
|
+
elif max_time < 1:
|
|
263
|
+
time_unit = "ms"
|
|
264
|
+
else:
|
|
265
|
+
time_unit = "s"
|
|
266
|
+
|
|
267
|
+
if freq_unit == "auto":
|
|
268
|
+
max_freq = freq[-1] if len(freq) > 0 else 0
|
|
269
|
+
if max_freq >= 1e9:
|
|
270
|
+
freq_unit = "GHz"
|
|
271
|
+
elif max_freq >= 1e6:
|
|
272
|
+
freq_unit = "MHz"
|
|
273
|
+
elif max_freq >= 1e3:
|
|
274
|
+
freq_unit = "kHz"
|
|
275
|
+
else:
|
|
276
|
+
freq_unit = "Hz"
|
|
277
|
+
|
|
278
|
+
time_multipliers = {"s": 1.0, "ms": 1e3, "us": 1e6, "ns": 1e9}
|
|
279
|
+
freq_divisors = {"Hz": 1.0, "kHz": 1e3, "MHz": 1e6, "GHz": 1e9}
|
|
280
|
+
|
|
281
|
+
time_mult = time_multipliers.get(time_unit, 1.0)
|
|
282
|
+
freq_div = freq_divisors.get(freq_unit, 1.0)
|
|
283
|
+
|
|
284
|
+
times_scaled = times * time_mult
|
|
285
|
+
freq_scaled = freq / freq_div
|
|
286
|
+
|
|
287
|
+
# Auto color limits
|
|
288
|
+
if vmin is None or vmax is None:
|
|
289
|
+
valid_db = Sxx_db[np.isfinite(Sxx_db)]
|
|
290
|
+
if len(valid_db) > 0:
|
|
291
|
+
if vmax is None:
|
|
292
|
+
vmax = np.max(valid_db)
|
|
293
|
+
if vmin is None:
|
|
294
|
+
vmin = max(np.min(valid_db), vmax - 80)
|
|
295
|
+
|
|
296
|
+
# Plot
|
|
297
|
+
pcm = ax.pcolormesh(
|
|
298
|
+
times_scaled,
|
|
299
|
+
freq_scaled,
|
|
300
|
+
Sxx_db,
|
|
301
|
+
shading="auto",
|
|
302
|
+
cmap=cmap,
|
|
303
|
+
vmin=vmin,
|
|
304
|
+
vmax=vmax,
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
ax.set_xlabel(f"Time ({time_unit})")
|
|
308
|
+
ax.set_ylabel(f"Frequency ({freq_unit})")
|
|
309
|
+
|
|
310
|
+
if title:
|
|
311
|
+
ax.set_title(title)
|
|
312
|
+
else:
|
|
313
|
+
ax.set_title("Spectrogram")
|
|
314
|
+
|
|
315
|
+
# Colorbar
|
|
316
|
+
cbar = fig.colorbar(pcm, ax=ax)
|
|
317
|
+
cbar.set_label("Magnitude (dB)")
|
|
318
|
+
|
|
319
|
+
fig.tight_layout()
|
|
320
|
+
return fig
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def plot_psd(
|
|
324
|
+
trace: WaveformTrace,
|
|
325
|
+
*,
|
|
326
|
+
ax: Axes | None = None,
|
|
327
|
+
freq_unit: str = "auto",
|
|
328
|
+
show_grid: bool = True,
|
|
329
|
+
color: str = "C0",
|
|
330
|
+
title: str | None = None,
|
|
331
|
+
window: str = "hann",
|
|
332
|
+
xscale: Literal["linear", "log"] = "log",
|
|
333
|
+
) -> Figure:
|
|
334
|
+
"""Plot Power Spectral Density.
|
|
335
|
+
|
|
336
|
+
Args:
|
|
337
|
+
trace: Waveform trace to analyze.
|
|
338
|
+
ax: Matplotlib axes.
|
|
339
|
+
freq_unit: Frequency unit.
|
|
340
|
+
show_grid: Show grid lines.
|
|
341
|
+
color: Line color.
|
|
342
|
+
title: Plot title.
|
|
343
|
+
window: Window function.
|
|
344
|
+
xscale: X-axis scale.
|
|
345
|
+
|
|
346
|
+
Returns:
|
|
347
|
+
Matplotlib Figure object.
|
|
348
|
+
|
|
349
|
+
Raises:
|
|
350
|
+
ImportError: If matplotlib is not installed.
|
|
351
|
+
ValueError: If axes must have an associated figure.
|
|
352
|
+
|
|
353
|
+
Example:
|
|
354
|
+
>>> fig = plot_psd(trace)
|
|
355
|
+
>>> plt.show()
|
|
356
|
+
"""
|
|
357
|
+
if not HAS_MATPLOTLIB:
|
|
358
|
+
raise ImportError("matplotlib is required for visualization")
|
|
359
|
+
|
|
360
|
+
from oscura.analyzers.waveform.spectral import psd
|
|
361
|
+
|
|
362
|
+
if ax is None:
|
|
363
|
+
fig, ax = plt.subplots(figsize=(10, 4))
|
|
364
|
+
else:
|
|
365
|
+
fig_temp = ax.get_figure()
|
|
366
|
+
if fig_temp is None:
|
|
367
|
+
raise ValueError("Axes must have an associated figure")
|
|
368
|
+
fig = cast("Figure", fig_temp)
|
|
369
|
+
|
|
370
|
+
# Compute PSD
|
|
371
|
+
freq, psd_db = psd(trace, window=window)
|
|
372
|
+
|
|
373
|
+
# Auto-select frequency unit
|
|
374
|
+
if freq_unit == "auto":
|
|
375
|
+
max_freq = freq[-1]
|
|
376
|
+
if max_freq >= 1e9:
|
|
377
|
+
freq_unit = "GHz"
|
|
378
|
+
elif max_freq >= 1e6:
|
|
379
|
+
freq_unit = "MHz"
|
|
380
|
+
elif max_freq >= 1e3:
|
|
381
|
+
freq_unit = "kHz"
|
|
382
|
+
else:
|
|
383
|
+
freq_unit = "Hz"
|
|
384
|
+
|
|
385
|
+
freq_divisors = {"Hz": 1.0, "kHz": 1e3, "MHz": 1e6, "GHz": 1e9}
|
|
386
|
+
divisor = freq_divisors.get(freq_unit, 1.0)
|
|
387
|
+
freq_scaled = freq / divisor
|
|
388
|
+
|
|
389
|
+
# Plot
|
|
390
|
+
ax.plot(freq_scaled, psd_db, color=color, linewidth=0.8)
|
|
391
|
+
|
|
392
|
+
ax.set_xlabel(f"Frequency ({freq_unit})")
|
|
393
|
+
ax.set_ylabel("PSD (dB/Hz)")
|
|
394
|
+
ax.set_xscale(xscale)
|
|
395
|
+
|
|
396
|
+
if title:
|
|
397
|
+
ax.set_title(title)
|
|
398
|
+
else:
|
|
399
|
+
ax.set_title("Power Spectral Density")
|
|
400
|
+
|
|
401
|
+
if show_grid:
|
|
402
|
+
ax.grid(True, alpha=0.3, which="both")
|
|
403
|
+
|
|
404
|
+
fig.tight_layout()
|
|
405
|
+
return fig
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def plot_fft(
|
|
409
|
+
trace: WaveformTrace,
|
|
410
|
+
*,
|
|
411
|
+
ax: Axes | None = None,
|
|
412
|
+
show: bool = True,
|
|
413
|
+
save_path: str | None = None,
|
|
414
|
+
title: str | None = None,
|
|
415
|
+
xlabel: str = "Frequency",
|
|
416
|
+
ylabel: str = "Magnitude (dB)",
|
|
417
|
+
figsize: tuple[float, float] = (10, 6),
|
|
418
|
+
freq_unit: str = "auto",
|
|
419
|
+
log_scale: bool = True,
|
|
420
|
+
show_grid: bool = True,
|
|
421
|
+
color: str = "C0",
|
|
422
|
+
window: str = "hann",
|
|
423
|
+
xlim: tuple[float, float] | None = None,
|
|
424
|
+
ylim: tuple[float, float] | None = None,
|
|
425
|
+
) -> Figure:
|
|
426
|
+
"""Plot FFT magnitude spectrum.
|
|
427
|
+
|
|
428
|
+
Computes and plots the FFT magnitude spectrum of a waveform trace.
|
|
429
|
+
This is a convenience function that combines FFT computation and visualization.
|
|
430
|
+
|
|
431
|
+
Args:
|
|
432
|
+
trace: Waveform trace to analyze and plot.
|
|
433
|
+
ax: Matplotlib axes. If None, creates new figure.
|
|
434
|
+
show: If True, call plt.show() to display the plot.
|
|
435
|
+
save_path: Path to save the figure. If None, figure is not saved.
|
|
436
|
+
title: Plot title. If None, uses default "FFT Magnitude Spectrum".
|
|
437
|
+
xlabel: X-axis label (appended with frequency unit).
|
|
438
|
+
ylabel: Y-axis label.
|
|
439
|
+
figsize: Figure size (width, height) in inches. Only used if ax is None.
|
|
440
|
+
freq_unit: Frequency unit ("Hz", "kHz", "MHz", "GHz", "auto").
|
|
441
|
+
log_scale: Use logarithmic scale for frequency axis.
|
|
442
|
+
show_grid: Show grid lines.
|
|
443
|
+
color: Line color.
|
|
444
|
+
window: Window function for FFT computation.
|
|
445
|
+
xlim: X-axis limits (min, max) in selected frequency units.
|
|
446
|
+
ylim: Y-axis limits (min, max) in dB.
|
|
447
|
+
|
|
448
|
+
Returns:
|
|
449
|
+
Matplotlib Figure object.
|
|
450
|
+
|
|
451
|
+
Raises:
|
|
452
|
+
ImportError: If matplotlib is not installed.
|
|
453
|
+
ValueError: If axes must have an associated figure.
|
|
454
|
+
|
|
455
|
+
Example:
|
|
456
|
+
>>> import oscura as osc
|
|
457
|
+
>>> trace = osc.load("signal.wfm")
|
|
458
|
+
>>> fig = osc.plot_fft(trace, freq_unit="MHz", show=False)
|
|
459
|
+
>>> fig.savefig("spectrum.png")
|
|
460
|
+
|
|
461
|
+
>>> # With custom styling
|
|
462
|
+
>>> fig = osc.plot_fft(trace,
|
|
463
|
+
... title="Signal FFT",
|
|
464
|
+
... log_scale=True,
|
|
465
|
+
... xlim=(1e3, 1e6),
|
|
466
|
+
... ylim=(-100, 0))
|
|
467
|
+
|
|
468
|
+
References:
|
|
469
|
+
IEEE 1241-2010: Standard for Terminology and Test Methods for
|
|
470
|
+
Analog-to-Digital Converters
|
|
471
|
+
"""
|
|
472
|
+
if not HAS_MATPLOTLIB:
|
|
473
|
+
raise ImportError("matplotlib is required for visualization")
|
|
474
|
+
|
|
475
|
+
# Create figure if needed
|
|
476
|
+
if ax is None:
|
|
477
|
+
fig, ax = plt.subplots(figsize=figsize)
|
|
478
|
+
else:
|
|
479
|
+
fig_temp = ax.get_figure()
|
|
480
|
+
if fig_temp is None:
|
|
481
|
+
raise ValueError("Axes must have an associated figure")
|
|
482
|
+
fig = cast("Figure", fig_temp)
|
|
483
|
+
|
|
484
|
+
# Use plot_spectrum to do the actual plotting
|
|
485
|
+
xscale_value: Literal["linear", "log"] = "log" if log_scale else "linear"
|
|
486
|
+
plot_spectrum(
|
|
487
|
+
trace,
|
|
488
|
+
ax=ax,
|
|
489
|
+
freq_unit=freq_unit,
|
|
490
|
+
show_grid=show_grid,
|
|
491
|
+
color=color,
|
|
492
|
+
title=title if title else "FFT Magnitude Spectrum",
|
|
493
|
+
window=window,
|
|
494
|
+
xscale=xscale_value,
|
|
495
|
+
)
|
|
496
|
+
|
|
497
|
+
# Apply custom labels if different from defaults
|
|
498
|
+
if xlabel != "Frequency":
|
|
499
|
+
# Get current label to preserve unit
|
|
500
|
+
current_label = ax.get_xlabel()
|
|
501
|
+
if "(" in current_label and ")" in current_label:
|
|
502
|
+
unit = current_label[current_label.find("(") : current_label.find(")") + 1]
|
|
503
|
+
ax.set_xlabel(f"{xlabel} {unit}")
|
|
504
|
+
else:
|
|
505
|
+
ax.set_xlabel(xlabel)
|
|
506
|
+
|
|
507
|
+
if ylabel != "Magnitude (dB)":
|
|
508
|
+
ax.set_ylabel(ylabel)
|
|
509
|
+
|
|
510
|
+
# Apply custom limits if specified
|
|
511
|
+
if xlim is not None:
|
|
512
|
+
ax.set_xlim(xlim)
|
|
513
|
+
|
|
514
|
+
if ylim is not None:
|
|
515
|
+
ax.set_ylim(ylim)
|
|
516
|
+
|
|
517
|
+
# Save if path provided
|
|
518
|
+
if save_path is not None:
|
|
519
|
+
fig.savefig(save_path, dpi=300, bbox_inches="tight")
|
|
520
|
+
|
|
521
|
+
# Show if requested
|
|
522
|
+
if show:
|
|
523
|
+
plt.show()
|
|
524
|
+
|
|
525
|
+
return fig
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
def plot_thd_bars(
|
|
529
|
+
harmonic_magnitudes: NDArray[np.floating[Any]],
|
|
530
|
+
*,
|
|
531
|
+
fundamental_freq: float | None = None,
|
|
532
|
+
ax: Axes | None = None,
|
|
533
|
+
figsize: tuple[float, float] = (10, 6),
|
|
534
|
+
title: str | None = None,
|
|
535
|
+
thd_value: float | None = None,
|
|
536
|
+
show_thd: bool = True,
|
|
537
|
+
reference_db: float = 0.0,
|
|
538
|
+
show: bool = True,
|
|
539
|
+
save_path: str | Path | None = None,
|
|
540
|
+
) -> Figure:
|
|
541
|
+
"""Plot THD harmonic bar chart.
|
|
542
|
+
|
|
543
|
+
Creates a bar chart showing harmonic content relative to the fundamental,
|
|
544
|
+
useful for visualizing Total Harmonic Distortion analysis results.
|
|
545
|
+
|
|
546
|
+
Args:
|
|
547
|
+
harmonic_magnitudes: Array of harmonic magnitudes in dB (relative to fundamental).
|
|
548
|
+
Index 0 = fundamental (0 dB), Index 1 = 2nd harmonic, etc.
|
|
549
|
+
fundamental_freq: Fundamental frequency in Hz (for x-axis labels).
|
|
550
|
+
ax: Matplotlib axes. If None, creates new figure.
|
|
551
|
+
figsize: Figure size in inches.
|
|
552
|
+
title: Plot title.
|
|
553
|
+
thd_value: Pre-calculated THD value in dB or % to display.
|
|
554
|
+
show_thd: Show THD annotation on plot.
|
|
555
|
+
reference_db: Reference level for the fundamental (default 0 dB).
|
|
556
|
+
show: Display plot interactively.
|
|
557
|
+
save_path: Save plot to file.
|
|
558
|
+
|
|
559
|
+
Returns:
|
|
560
|
+
Matplotlib Figure object.
|
|
561
|
+
|
|
562
|
+
Example:
|
|
563
|
+
>>> # Harmonic magnitudes relative to fundamental (in dB)
|
|
564
|
+
>>> harmonics = np.array([0, -40, -60, -55, -70, -65]) # Fund, H2, H3, H4, H5, H6
|
|
565
|
+
>>> fig = plot_thd_bars(harmonics, fundamental_freq=1000, thd_value=-38.5)
|
|
566
|
+
|
|
567
|
+
References:
|
|
568
|
+
IEEE 1241-2010: ADC Testing Standards
|
|
569
|
+
IEC 61000-4-7: Harmonics measurement
|
|
570
|
+
"""
|
|
571
|
+
if not HAS_MATPLOTLIB:
|
|
572
|
+
raise ImportError("matplotlib is required for visualization")
|
|
573
|
+
|
|
574
|
+
if ax is None:
|
|
575
|
+
fig, ax = plt.subplots(figsize=figsize)
|
|
576
|
+
else:
|
|
577
|
+
fig_temp = ax.get_figure()
|
|
578
|
+
if fig_temp is None:
|
|
579
|
+
raise ValueError("Axes must have an associated figure")
|
|
580
|
+
fig = cast("Figure", fig_temp)
|
|
581
|
+
|
|
582
|
+
n_harmonics = len(harmonic_magnitudes)
|
|
583
|
+
|
|
584
|
+
# Create x-positions for harmonics
|
|
585
|
+
x_pos = np.arange(n_harmonics)
|
|
586
|
+
|
|
587
|
+
# Create labels
|
|
588
|
+
if fundamental_freq is not None:
|
|
589
|
+
labels = [
|
|
590
|
+
f"H{i + 1}\n({(i + 1) * fundamental_freq / 1e3:.1f} kHz)"
|
|
591
|
+
if fundamental_freq >= 1000
|
|
592
|
+
else f"H{i + 1}\n({(i + 1) * fundamental_freq:.0f} Hz)"
|
|
593
|
+
for i in range(n_harmonics)
|
|
594
|
+
]
|
|
595
|
+
labels[0] = (
|
|
596
|
+
f"Fund\n({fundamental_freq / 1e3:.1f} kHz)"
|
|
597
|
+
if fundamental_freq >= 1000
|
|
598
|
+
else f"Fund\n({fundamental_freq:.0f} Hz)"
|
|
599
|
+
)
|
|
600
|
+
else:
|
|
601
|
+
labels = [f"H{i + 1}" for i in range(n_harmonics)]
|
|
602
|
+
labels[0] = "Fund"
|
|
603
|
+
|
|
604
|
+
# Color code: fundamental in blue, harmonics in orange/red based on magnitude
|
|
605
|
+
colors = []
|
|
606
|
+
for i, mag in enumerate(harmonic_magnitudes):
|
|
607
|
+
if i == 0:
|
|
608
|
+
colors.append("#3498DB") # Blue for fundamental
|
|
609
|
+
elif mag > -30:
|
|
610
|
+
colors.append("#E74C3C") # Red for significant harmonics
|
|
611
|
+
elif mag > -50:
|
|
612
|
+
colors.append("#F39C12") # Orange for moderate
|
|
613
|
+
else:
|
|
614
|
+
colors.append("#95A5A6") # Gray for low
|
|
615
|
+
|
|
616
|
+
# Plot bars
|
|
617
|
+
ax.bar(
|
|
618
|
+
x_pos, harmonic_magnitudes - reference_db, color=colors, edgecolor="black", linewidth=0.5
|
|
619
|
+
)
|
|
620
|
+
|
|
621
|
+
# Reference line at fundamental level
|
|
622
|
+
ax.axhline(0, color="gray", linestyle="--", linewidth=1, alpha=0.7)
|
|
623
|
+
|
|
624
|
+
# THD annotation
|
|
625
|
+
if show_thd and thd_value is not None:
|
|
626
|
+
# Position in upper right
|
|
627
|
+
if thd_value > 0:
|
|
628
|
+
thd_text = f"THD: {thd_value:.2f}%"
|
|
629
|
+
else:
|
|
630
|
+
thd_text = f"THD: {thd_value:.1f} dB"
|
|
631
|
+
|
|
632
|
+
ax.text(
|
|
633
|
+
0.98,
|
|
634
|
+
0.98,
|
|
635
|
+
thd_text,
|
|
636
|
+
transform=ax.transAxes,
|
|
637
|
+
fontsize=12,
|
|
638
|
+
fontweight="bold",
|
|
639
|
+
ha="right",
|
|
640
|
+
va="top",
|
|
641
|
+
bbox={"boxstyle": "round,pad=0.5", "facecolor": "wheat", "alpha": 0.9},
|
|
642
|
+
)
|
|
643
|
+
|
|
644
|
+
# Labels
|
|
645
|
+
ax.set_xticks(x_pos)
|
|
646
|
+
ax.set_xticklabels(labels, fontsize=9)
|
|
647
|
+
ax.set_xlabel("Harmonic", fontsize=11)
|
|
648
|
+
ax.set_ylabel("Magnitude (dB rel. to fundamental)", fontsize=11)
|
|
649
|
+
ax.grid(True, axis="y", alpha=0.3)
|
|
650
|
+
|
|
651
|
+
# Y-axis limits
|
|
652
|
+
min_mag = min(harmonic_magnitudes) - reference_db
|
|
653
|
+
ax.set_ylim(min(min_mag - 10, -80), 10)
|
|
654
|
+
|
|
655
|
+
if title:
|
|
656
|
+
ax.set_title(title, fontsize=12, fontweight="bold")
|
|
657
|
+
else:
|
|
658
|
+
ax.set_title("Harmonic Distortion Analysis", fontsize=12, fontweight="bold")
|
|
659
|
+
|
|
660
|
+
fig.tight_layout()
|
|
661
|
+
|
|
662
|
+
if save_path is not None:
|
|
663
|
+
fig.savefig(save_path, dpi=300, bbox_inches="tight")
|
|
664
|
+
|
|
665
|
+
if show:
|
|
666
|
+
plt.show()
|
|
667
|
+
|
|
668
|
+
return fig
|
|
669
|
+
|
|
670
|
+
|
|
671
|
+
def plot_quality_summary(
|
|
672
|
+
metrics: dict[str, float],
|
|
673
|
+
*,
|
|
674
|
+
ax: Axes | None = None,
|
|
675
|
+
figsize: tuple[float, float] = (10, 6),
|
|
676
|
+
title: str | None = None,
|
|
677
|
+
show_specs: dict[str, float] | None = None,
|
|
678
|
+
show: bool = True,
|
|
679
|
+
save_path: str | Path | None = None,
|
|
680
|
+
) -> Figure:
|
|
681
|
+
"""Plot ADC/signal quality summary with metrics.
|
|
682
|
+
|
|
683
|
+
Creates a summary panel showing SNR, SINAD, THD, ENOB, and SFDR
|
|
684
|
+
with optional pass/fail indication against specifications.
|
|
685
|
+
|
|
686
|
+
Args:
|
|
687
|
+
metrics: Dictionary with keys like "snr", "sinad", "thd", "enob", "sfdr".
|
|
688
|
+
ax: Matplotlib axes.
|
|
689
|
+
figsize: Figure size.
|
|
690
|
+
title: Plot title.
|
|
691
|
+
show_specs: Dictionary of specification values for pass/fail.
|
|
692
|
+
show: Display plot.
|
|
693
|
+
save_path: Save path.
|
|
694
|
+
|
|
695
|
+
Returns:
|
|
696
|
+
Matplotlib Figure object.
|
|
697
|
+
|
|
698
|
+
Example:
|
|
699
|
+
>>> metrics = {"snr": 72.5, "sinad": 70.2, "thd": -65.3, "enob": 11.2, "sfdr": 75.8}
|
|
700
|
+
>>> specs = {"snr": 70.0, "enob": 10.0}
|
|
701
|
+
>>> fig = plot_quality_summary(metrics, show_specs=specs)
|
|
702
|
+
|
|
703
|
+
References:
|
|
704
|
+
IEEE 1241-2010: ADC Testing Standards
|
|
705
|
+
"""
|
|
706
|
+
if not HAS_MATPLOTLIB:
|
|
707
|
+
raise ImportError("matplotlib is required for visualization")
|
|
708
|
+
|
|
709
|
+
if ax is None:
|
|
710
|
+
fig, ax = plt.subplots(figsize=figsize)
|
|
711
|
+
else:
|
|
712
|
+
fig_temp = ax.get_figure()
|
|
713
|
+
if fig_temp is None:
|
|
714
|
+
raise ValueError("Axes must have an associated figure")
|
|
715
|
+
fig = cast("Figure", fig_temp)
|
|
716
|
+
|
|
717
|
+
# Define metric display info
|
|
718
|
+
metric_info = {
|
|
719
|
+
"snr": {"name": "SNR", "unit": "dB", "higher_better": True},
|
|
720
|
+
"sinad": {"name": "SINAD", "unit": "dB", "higher_better": True},
|
|
721
|
+
"thd": {
|
|
722
|
+
"name": "THD",
|
|
723
|
+
"unit": "dB",
|
|
724
|
+
"higher_better": False,
|
|
725
|
+
}, # Lower (more negative) is better
|
|
726
|
+
"enob": {"name": "ENOB", "unit": "bits", "higher_better": True},
|
|
727
|
+
"sfdr": {"name": "SFDR", "unit": "dBc", "higher_better": True},
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
# Filter to available metrics
|
|
731
|
+
available_metrics = [(k, v) for k, v in metrics.items() if k in metric_info]
|
|
732
|
+
n_metrics = len(available_metrics)
|
|
733
|
+
|
|
734
|
+
if n_metrics == 0:
|
|
735
|
+
ax.text(0.5, 0.5, "No metrics available", ha="center", va="center", fontsize=14)
|
|
736
|
+
ax.axis("off")
|
|
737
|
+
return fig
|
|
738
|
+
|
|
739
|
+
# Create horizontal bar chart
|
|
740
|
+
y_pos = np.arange(n_metrics)
|
|
741
|
+
values = [v for _, v in available_metrics]
|
|
742
|
+
names = [metric_info[k]["name"] for k, _ in available_metrics]
|
|
743
|
+
|
|
744
|
+
# Determine colors based on pass/fail
|
|
745
|
+
colors = []
|
|
746
|
+
for key, value in available_metrics:
|
|
747
|
+
if show_specs and key in show_specs:
|
|
748
|
+
spec = show_specs[key]
|
|
749
|
+
info = metric_info[key]
|
|
750
|
+
if info["higher_better"]:
|
|
751
|
+
passed = value >= spec
|
|
752
|
+
else:
|
|
753
|
+
# For THD, more negative is better
|
|
754
|
+
passed = value <= spec
|
|
755
|
+
colors.append("#27AE60" if passed else "#E74C3C")
|
|
756
|
+
else:
|
|
757
|
+
colors.append("#3498DB")
|
|
758
|
+
|
|
759
|
+
# Plot horizontal bars
|
|
760
|
+
ax.barh(y_pos, values, color=colors, edgecolor="black", linewidth=0.5)
|
|
761
|
+
|
|
762
|
+
# Add value labels
|
|
763
|
+
for i, (key, value) in enumerate(available_metrics):
|
|
764
|
+
unit = metric_info[key]["unit"]
|
|
765
|
+
label_text = f"{value:.1f} {unit}"
|
|
766
|
+
ax.text(
|
|
767
|
+
value + 2 if value >= 0 else value - 2,
|
|
768
|
+
i,
|
|
769
|
+
label_text,
|
|
770
|
+
va="center",
|
|
771
|
+
ha="left" if value >= 0 else "right",
|
|
772
|
+
fontsize=10,
|
|
773
|
+
fontweight="bold",
|
|
774
|
+
)
|
|
775
|
+
|
|
776
|
+
# Add spec markers
|
|
777
|
+
if show_specs:
|
|
778
|
+
for i, (key, _) in enumerate(available_metrics):
|
|
779
|
+
if key in show_specs:
|
|
780
|
+
spec = show_specs[key]
|
|
781
|
+
ax.plot(spec, i, "k|", markersize=20, markeredgewidth=2)
|
|
782
|
+
ax.text(spec, i + 0.3, f"Spec: {spec}", fontsize=8, ha="center")
|
|
783
|
+
|
|
784
|
+
ax.set_yticks(y_pos)
|
|
785
|
+
ax.set_yticklabels([str(name) for name in names], fontsize=11)
|
|
786
|
+
ax.set_xlabel("Value", fontsize=11)
|
|
787
|
+
ax.grid(True, axis="x", alpha=0.3)
|
|
788
|
+
ax.invert_yaxis()
|
|
789
|
+
|
|
790
|
+
if title:
|
|
791
|
+
ax.set_title(title, fontsize=12, fontweight="bold")
|
|
792
|
+
else:
|
|
793
|
+
ax.set_title("Signal Quality Summary (IEEE 1241-2010)", fontsize=12, fontweight="bold")
|
|
794
|
+
|
|
795
|
+
fig.tight_layout()
|
|
796
|
+
|
|
797
|
+
if save_path is not None:
|
|
798
|
+
fig.savefig(save_path, dpi=300, bbox_inches="tight")
|
|
799
|
+
|
|
800
|
+
if show:
|
|
801
|
+
plt.show()
|
|
802
|
+
|
|
803
|
+
return fig
|
|
804
|
+
|
|
805
|
+
|
|
806
|
+
__all__ = [
|
|
807
|
+
"plot_fft",
|
|
808
|
+
"plot_psd",
|
|
809
|
+
"plot_spectrogram",
|
|
810
|
+
"plot_spectrum",
|
|
811
|
+
]
|