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,731 @@
|
|
|
1
|
+
"""Plot generation for comprehensive analysis reports.
|
|
2
|
+
|
|
3
|
+
This module provides intelligent plot generation for different analysis domains,
|
|
4
|
+
using the existing visualization library and returning figures for the OutputManager
|
|
5
|
+
to save.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
from collections.abc import Callable
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import TYPE_CHECKING, Any
|
|
14
|
+
|
|
15
|
+
import numpy as np
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
import matplotlib
|
|
21
|
+
import matplotlib.pyplot as plt
|
|
22
|
+
|
|
23
|
+
# Use non-interactive backend for automated plot generation
|
|
24
|
+
matplotlib.use("Agg")
|
|
25
|
+
HAS_MATPLOTLIB = True
|
|
26
|
+
except ImportError:
|
|
27
|
+
HAS_MATPLOTLIB = False
|
|
28
|
+
|
|
29
|
+
if TYPE_CHECKING:
|
|
30
|
+
from matplotlib.figure import Figure
|
|
31
|
+
|
|
32
|
+
from oscura.reporting.config import AnalysisConfig, AnalysisDomain
|
|
33
|
+
from oscura.reporting.output import OutputManager
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class PlotGenerator:
|
|
37
|
+
"""Generates visualization plots from analysis results.
|
|
38
|
+
|
|
39
|
+
Intelligently creates appropriate plots for each analysis domain based on
|
|
40
|
+
available data. Uses the existing oscura.visualization module and returns
|
|
41
|
+
matplotlib Figure objects for the OutputManager to save.
|
|
42
|
+
|
|
43
|
+
Attributes:
|
|
44
|
+
config: Analysis configuration (optional, for plot settings).
|
|
45
|
+
|
|
46
|
+
Requirements:
|
|
47
|
+
|
|
48
|
+
Example:
|
|
49
|
+
>>> config = AnalysisConfig(plot_format="png", plot_dpi=150)
|
|
50
|
+
>>> generator = PlotGenerator(config)
|
|
51
|
+
>>> paths = generator.generate_plots(
|
|
52
|
+
... AnalysisDomain.SPECTRAL,
|
|
53
|
+
... {"fft": {"frequencies": freq, "magnitude_db": mag}},
|
|
54
|
+
... output_manager
|
|
55
|
+
... )
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
def __init__(self, config: AnalysisConfig | None = None) -> None:
|
|
59
|
+
"""Initialize plot generator.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
config: Analysis configuration for plot settings (format, DPI, etc.).
|
|
63
|
+
If None, uses defaults.
|
|
64
|
+
|
|
65
|
+
Raises:
|
|
66
|
+
ImportError: If matplotlib is not installed.
|
|
67
|
+
"""
|
|
68
|
+
if not HAS_MATPLOTLIB:
|
|
69
|
+
raise ImportError("matplotlib is required for plot generation")
|
|
70
|
+
|
|
71
|
+
self.config = config
|
|
72
|
+
|
|
73
|
+
def generate_plots(
|
|
74
|
+
self,
|
|
75
|
+
domain: AnalysisDomain,
|
|
76
|
+
results: dict[str, Any],
|
|
77
|
+
output_manager: OutputManager,
|
|
78
|
+
) -> list[Path]:
|
|
79
|
+
"""Generate all appropriate plots for an analysis domain.
|
|
80
|
+
|
|
81
|
+
Inspects the results dictionary and generates appropriate visualization
|
|
82
|
+
plots based on the domain and available data. Returns list of saved
|
|
83
|
+
plot paths.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
domain: Analysis domain (e.g., SPECTRAL, WAVEFORM, DIGITAL).
|
|
87
|
+
results: Dictionary of analysis results for this domain.
|
|
88
|
+
output_manager: OutputManager instance for saving plots.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
List of paths to saved plot files.
|
|
92
|
+
|
|
93
|
+
Example:
|
|
94
|
+
>>> results = {
|
|
95
|
+
... "fft": {"frequencies": freq_array, "magnitude_db": mag_array},
|
|
96
|
+
... "psd": {"frequencies": freq_array, "psd": psd_array}
|
|
97
|
+
... }
|
|
98
|
+
>>> paths = generator.generate_plots(
|
|
99
|
+
... AnalysisDomain.SPECTRAL,
|
|
100
|
+
... results,
|
|
101
|
+
... output_manager
|
|
102
|
+
... )
|
|
103
|
+
"""
|
|
104
|
+
from oscura.reporting.config import AnalysisDomain
|
|
105
|
+
|
|
106
|
+
# Get plot format and DPI from config
|
|
107
|
+
plot_format = self.config.plot_format if self.config else "png"
|
|
108
|
+
plot_dpi = self.config.plot_dpi if self.config else 150
|
|
109
|
+
|
|
110
|
+
saved_paths: list[Path] = []
|
|
111
|
+
|
|
112
|
+
for analysis_name, result_data in results.items():
|
|
113
|
+
# Skip non-dict results
|
|
114
|
+
if not isinstance(result_data, dict):
|
|
115
|
+
continue
|
|
116
|
+
|
|
117
|
+
# Check if we have a registered plot function
|
|
118
|
+
key = (domain, analysis_name)
|
|
119
|
+
if key in PLOT_REGISTRY:
|
|
120
|
+
plot_func = PLOT_REGISTRY[key]
|
|
121
|
+
try:
|
|
122
|
+
fig = plot_func(result_data)
|
|
123
|
+
if fig is not None:
|
|
124
|
+
path = output_manager.save_plot(
|
|
125
|
+
domain,
|
|
126
|
+
analysis_name,
|
|
127
|
+
fig,
|
|
128
|
+
format=plot_format,
|
|
129
|
+
dpi=plot_dpi,
|
|
130
|
+
)
|
|
131
|
+
saved_paths.append(path)
|
|
132
|
+
plt.close(fig) # Prevent memory leaks
|
|
133
|
+
except Exception as e:
|
|
134
|
+
# Log error but continue with other plots
|
|
135
|
+
logger.warning("Failed to generate %s plot: %s", analysis_name, e)
|
|
136
|
+
continue
|
|
137
|
+
|
|
138
|
+
# Also try generic domain-level plots
|
|
139
|
+
try:
|
|
140
|
+
if domain == AnalysisDomain.SPECTRAL:
|
|
141
|
+
saved_paths.extend(
|
|
142
|
+
self._generate_spectral_plots(
|
|
143
|
+
results, domain, output_manager, plot_format, plot_dpi
|
|
144
|
+
)
|
|
145
|
+
)
|
|
146
|
+
elif domain == AnalysisDomain.WAVEFORM:
|
|
147
|
+
saved_paths.extend(
|
|
148
|
+
self._generate_waveform_plots(
|
|
149
|
+
results, domain, output_manager, plot_format, plot_dpi
|
|
150
|
+
)
|
|
151
|
+
)
|
|
152
|
+
elif domain == AnalysisDomain.DIGITAL:
|
|
153
|
+
saved_paths.extend(
|
|
154
|
+
self._generate_digital_plots(
|
|
155
|
+
results, domain, output_manager, plot_format, plot_dpi
|
|
156
|
+
)
|
|
157
|
+
)
|
|
158
|
+
elif domain == AnalysisDomain.STATISTICS:
|
|
159
|
+
saved_paths.extend(
|
|
160
|
+
self._generate_statistics_plots(
|
|
161
|
+
results, domain, output_manager, plot_format, plot_dpi
|
|
162
|
+
)
|
|
163
|
+
)
|
|
164
|
+
elif domain == AnalysisDomain.JITTER:
|
|
165
|
+
saved_paths.extend(
|
|
166
|
+
self._generate_jitter_plots(
|
|
167
|
+
results, domain, output_manager, plot_format, plot_dpi
|
|
168
|
+
)
|
|
169
|
+
)
|
|
170
|
+
elif domain == AnalysisDomain.EYE:
|
|
171
|
+
saved_paths.extend(
|
|
172
|
+
self._generate_eye_plots(results, domain, output_manager, plot_format, plot_dpi)
|
|
173
|
+
)
|
|
174
|
+
elif domain == AnalysisDomain.PATTERNS:
|
|
175
|
+
saved_paths.extend(
|
|
176
|
+
self._generate_pattern_plots(
|
|
177
|
+
results, domain, output_manager, plot_format, plot_dpi
|
|
178
|
+
)
|
|
179
|
+
)
|
|
180
|
+
elif domain == AnalysisDomain.POWER:
|
|
181
|
+
saved_paths.extend(
|
|
182
|
+
self._generate_power_plots(
|
|
183
|
+
results, domain, output_manager, plot_format, plot_dpi
|
|
184
|
+
)
|
|
185
|
+
)
|
|
186
|
+
except Exception as e:
|
|
187
|
+
logger.warning("Error in domain-level plot generation for %s: %s", domain.value, e)
|
|
188
|
+
|
|
189
|
+
return saved_paths
|
|
190
|
+
|
|
191
|
+
def _generate_spectral_plots(
|
|
192
|
+
self,
|
|
193
|
+
results: dict[str, Any],
|
|
194
|
+
domain: AnalysisDomain,
|
|
195
|
+
output_manager: OutputManager,
|
|
196
|
+
plot_format: str,
|
|
197
|
+
plot_dpi: int,
|
|
198
|
+
) -> list[Path]:
|
|
199
|
+
"""Generate spectral analysis plots (FFT, PSD, spectrogram)."""
|
|
200
|
+
paths: list[Path] = []
|
|
201
|
+
|
|
202
|
+
# FFT plot
|
|
203
|
+
if "fft" in results and isinstance(results["fft"], dict):
|
|
204
|
+
fft_data = results["fft"]
|
|
205
|
+
if "frequencies" in fft_data and "magnitude_db" in fft_data:
|
|
206
|
+
try:
|
|
207
|
+
fig = self._plot_spectrum(fft_data, title="FFT Magnitude Spectrum")
|
|
208
|
+
path = output_manager.save_plot(
|
|
209
|
+
domain, "fft_spectrum", fig, format=plot_format, dpi=plot_dpi
|
|
210
|
+
)
|
|
211
|
+
paths.append(path)
|
|
212
|
+
plt.close(fig)
|
|
213
|
+
except Exception:
|
|
214
|
+
pass
|
|
215
|
+
|
|
216
|
+
# PSD plot
|
|
217
|
+
if "psd" in results and isinstance(results["psd"], dict):
|
|
218
|
+
psd_data = results["psd"]
|
|
219
|
+
if "frequencies" in psd_data and "psd" in psd_data:
|
|
220
|
+
try:
|
|
221
|
+
fig = self._plot_spectrum(
|
|
222
|
+
psd_data, title="Power Spectral Density", ylabel="PSD (dB/Hz)"
|
|
223
|
+
)
|
|
224
|
+
path = output_manager.save_plot(
|
|
225
|
+
domain, "psd_spectrum", fig, format=plot_format, dpi=plot_dpi
|
|
226
|
+
)
|
|
227
|
+
paths.append(path)
|
|
228
|
+
plt.close(fig)
|
|
229
|
+
except Exception:
|
|
230
|
+
pass
|
|
231
|
+
|
|
232
|
+
# Spectrogram
|
|
233
|
+
if "spectrogram" in results and isinstance(results["spectrogram"], dict):
|
|
234
|
+
spec_data = results["spectrogram"]
|
|
235
|
+
if "times" in spec_data and "frequencies" in spec_data and "Sxx_db" in spec_data:
|
|
236
|
+
try:
|
|
237
|
+
fig = self._plot_spectrogram(spec_data)
|
|
238
|
+
path = output_manager.save_plot(
|
|
239
|
+
domain, "spectrogram", fig, format=plot_format, dpi=plot_dpi
|
|
240
|
+
)
|
|
241
|
+
paths.append(path)
|
|
242
|
+
plt.close(fig)
|
|
243
|
+
except Exception:
|
|
244
|
+
pass
|
|
245
|
+
|
|
246
|
+
return paths
|
|
247
|
+
|
|
248
|
+
def _generate_waveform_plots(
|
|
249
|
+
self,
|
|
250
|
+
results: dict[str, Any],
|
|
251
|
+
domain: AnalysisDomain,
|
|
252
|
+
output_manager: OutputManager,
|
|
253
|
+
plot_format: str,
|
|
254
|
+
plot_dpi: int,
|
|
255
|
+
) -> list[Path]:
|
|
256
|
+
"""Generate waveform analysis plots (time series, histograms)."""
|
|
257
|
+
paths: list[Path] = []
|
|
258
|
+
|
|
259
|
+
# Look for time-series data
|
|
260
|
+
for key in ["amplitude", "voltage", "signal", "data"]:
|
|
261
|
+
if key in results and isinstance(results[key], np.ndarray | list):
|
|
262
|
+
try:
|
|
263
|
+
fig = self._plot_time_series(
|
|
264
|
+
{"data": results[key]}, title=f"{key.title()} vs Time"
|
|
265
|
+
)
|
|
266
|
+
path = output_manager.save_plot(
|
|
267
|
+
domain, f"{key}_timeseries", fig, format=plot_format, dpi=plot_dpi
|
|
268
|
+
)
|
|
269
|
+
paths.append(path)
|
|
270
|
+
plt.close(fig)
|
|
271
|
+
break
|
|
272
|
+
except Exception:
|
|
273
|
+
pass
|
|
274
|
+
|
|
275
|
+
# Histogram of amplitudes
|
|
276
|
+
for key in ["amplitude", "voltage", "data"]:
|
|
277
|
+
if key in results and isinstance(results[key], np.ndarray | list):
|
|
278
|
+
try:
|
|
279
|
+
fig = self._plot_histogram(
|
|
280
|
+
{"data": results[key]}, title=f"{key.title()} Distribution"
|
|
281
|
+
)
|
|
282
|
+
path = output_manager.save_plot(
|
|
283
|
+
domain, f"{key}_histogram", fig, format=plot_format, dpi=plot_dpi
|
|
284
|
+
)
|
|
285
|
+
paths.append(path)
|
|
286
|
+
plt.close(fig)
|
|
287
|
+
break
|
|
288
|
+
except Exception:
|
|
289
|
+
pass
|
|
290
|
+
|
|
291
|
+
return paths
|
|
292
|
+
|
|
293
|
+
def _generate_digital_plots(
|
|
294
|
+
self,
|
|
295
|
+
results: dict[str, Any],
|
|
296
|
+
domain: AnalysisDomain,
|
|
297
|
+
output_manager: OutputManager,
|
|
298
|
+
plot_format: str,
|
|
299
|
+
plot_dpi: int,
|
|
300
|
+
) -> list[Path]:
|
|
301
|
+
"""Generate digital signal analysis plots (edges, timing)."""
|
|
302
|
+
paths: list[Path] = []
|
|
303
|
+
|
|
304
|
+
# Edge histogram (rise/fall time distribution)
|
|
305
|
+
if "edges" in results and isinstance(results["edges"], dict):
|
|
306
|
+
edges_data = results["edges"]
|
|
307
|
+
if "rise_times" in edges_data:
|
|
308
|
+
try:
|
|
309
|
+
fig = self._plot_histogram(
|
|
310
|
+
{"data": edges_data["rise_times"]}, title="Rise Time Distribution"
|
|
311
|
+
)
|
|
312
|
+
path = output_manager.save_plot(
|
|
313
|
+
domain, "rise_time_hist", fig, format=plot_format, dpi=plot_dpi
|
|
314
|
+
)
|
|
315
|
+
paths.append(path)
|
|
316
|
+
plt.close(fig)
|
|
317
|
+
except Exception:
|
|
318
|
+
pass
|
|
319
|
+
|
|
320
|
+
return paths
|
|
321
|
+
|
|
322
|
+
def _generate_statistics_plots(
|
|
323
|
+
self,
|
|
324
|
+
results: dict[str, Any],
|
|
325
|
+
domain: AnalysisDomain,
|
|
326
|
+
output_manager: OutputManager,
|
|
327
|
+
plot_format: str,
|
|
328
|
+
plot_dpi: int,
|
|
329
|
+
) -> list[Path]:
|
|
330
|
+
"""Generate statistical analysis plots (distributions, box plots)."""
|
|
331
|
+
paths: list[Path] = []
|
|
332
|
+
|
|
333
|
+
# Histogram of distribution
|
|
334
|
+
if "distribution" in results and isinstance(results["distribution"], dict):
|
|
335
|
+
dist_data = results["distribution"]
|
|
336
|
+
if "data" in dist_data:
|
|
337
|
+
try:
|
|
338
|
+
fig = self._plot_histogram(dist_data, title="Statistical Distribution")
|
|
339
|
+
path = output_manager.save_plot(
|
|
340
|
+
domain, "distribution", fig, format=plot_format, dpi=plot_dpi
|
|
341
|
+
)
|
|
342
|
+
paths.append(path)
|
|
343
|
+
plt.close(fig)
|
|
344
|
+
except Exception:
|
|
345
|
+
pass
|
|
346
|
+
|
|
347
|
+
return paths
|
|
348
|
+
|
|
349
|
+
def _generate_jitter_plots(
|
|
350
|
+
self,
|
|
351
|
+
results: dict[str, Any],
|
|
352
|
+
domain: AnalysisDomain,
|
|
353
|
+
output_manager: OutputManager,
|
|
354
|
+
plot_format: str,
|
|
355
|
+
plot_dpi: int,
|
|
356
|
+
) -> list[Path]:
|
|
357
|
+
"""Generate jitter analysis plots (TIE histogram, bathtub curve)."""
|
|
358
|
+
paths: list[Path] = []
|
|
359
|
+
|
|
360
|
+
# TIE (Time Interval Error) histogram
|
|
361
|
+
if "tie" in results and isinstance(results["tie"], np.ndarray | list):
|
|
362
|
+
try:
|
|
363
|
+
fig = self._plot_histogram(
|
|
364
|
+
{"data": results["tie"]}, title="Time Interval Error (TIE)"
|
|
365
|
+
)
|
|
366
|
+
path = output_manager.save_plot(
|
|
367
|
+
domain, "tie_histogram", fig, format=plot_format, dpi=plot_dpi
|
|
368
|
+
)
|
|
369
|
+
paths.append(path)
|
|
370
|
+
plt.close(fig)
|
|
371
|
+
except Exception:
|
|
372
|
+
pass
|
|
373
|
+
|
|
374
|
+
return paths
|
|
375
|
+
|
|
376
|
+
def _generate_eye_plots(
|
|
377
|
+
self,
|
|
378
|
+
results: dict[str, Any],
|
|
379
|
+
domain: AnalysisDomain,
|
|
380
|
+
output_manager: OutputManager,
|
|
381
|
+
plot_format: str,
|
|
382
|
+
plot_dpi: int,
|
|
383
|
+
) -> list[Path]:
|
|
384
|
+
"""Generate eye diagram plots."""
|
|
385
|
+
# Eye diagrams are typically generated by the analyzer itself
|
|
386
|
+
# This is a placeholder for future enhancements
|
|
387
|
+
return []
|
|
388
|
+
|
|
389
|
+
def _generate_pattern_plots(
|
|
390
|
+
self,
|
|
391
|
+
results: dict[str, Any],
|
|
392
|
+
domain: AnalysisDomain,
|
|
393
|
+
output_manager: OutputManager,
|
|
394
|
+
plot_format: str,
|
|
395
|
+
plot_dpi: int,
|
|
396
|
+
) -> list[Path]:
|
|
397
|
+
"""Generate pattern analysis plots (motifs, sequences)."""
|
|
398
|
+
paths: list[Path] = []
|
|
399
|
+
|
|
400
|
+
# Pattern occurrence histogram
|
|
401
|
+
if "patterns" in results and isinstance(results["patterns"], dict):
|
|
402
|
+
pattern_data = results["patterns"]
|
|
403
|
+
if "occurrences" in pattern_data:
|
|
404
|
+
try:
|
|
405
|
+
fig = self._plot_histogram(
|
|
406
|
+
{"data": pattern_data["occurrences"]}, title="Pattern Occurrences"
|
|
407
|
+
)
|
|
408
|
+
path = output_manager.save_plot(
|
|
409
|
+
domain, "pattern_occurrences", fig, format=plot_format, dpi=plot_dpi
|
|
410
|
+
)
|
|
411
|
+
paths.append(path)
|
|
412
|
+
plt.close(fig)
|
|
413
|
+
except Exception:
|
|
414
|
+
pass
|
|
415
|
+
|
|
416
|
+
return paths
|
|
417
|
+
|
|
418
|
+
def _generate_power_plots(
|
|
419
|
+
self,
|
|
420
|
+
results: dict[str, Any],
|
|
421
|
+
domain: AnalysisDomain,
|
|
422
|
+
output_manager: OutputManager,
|
|
423
|
+
plot_format: str,
|
|
424
|
+
plot_dpi: int,
|
|
425
|
+
) -> list[Path]:
|
|
426
|
+
"""Generate power analysis plots (power vs time, efficiency)."""
|
|
427
|
+
paths: list[Path] = []
|
|
428
|
+
|
|
429
|
+
# Power time series
|
|
430
|
+
if "power" in results and isinstance(results["power"], np.ndarray | list):
|
|
431
|
+
try:
|
|
432
|
+
fig = self._plot_time_series(
|
|
433
|
+
{"data": results["power"]}, title="Power vs Time", ylabel="Power (W)"
|
|
434
|
+
)
|
|
435
|
+
path = output_manager.save_plot(
|
|
436
|
+
domain, "power_timeseries", fig, format=plot_format, dpi=plot_dpi
|
|
437
|
+
)
|
|
438
|
+
paths.append(path)
|
|
439
|
+
plt.close(fig)
|
|
440
|
+
except Exception:
|
|
441
|
+
pass
|
|
442
|
+
|
|
443
|
+
return paths
|
|
444
|
+
|
|
445
|
+
# ============================================================================
|
|
446
|
+
# Individual plot methods
|
|
447
|
+
# ============================================================================
|
|
448
|
+
|
|
449
|
+
def _plot_spectrum(
|
|
450
|
+
self,
|
|
451
|
+
data: dict[str, Any],
|
|
452
|
+
title: str = "Spectrum",
|
|
453
|
+
ylabel: str = "Magnitude (dB)",
|
|
454
|
+
) -> Figure:
|
|
455
|
+
"""Plot frequency spectrum (FFT, PSD, etc.).
|
|
456
|
+
|
|
457
|
+
Args:
|
|
458
|
+
data: Dictionary with 'frequencies' and magnitude data.
|
|
459
|
+
title: Plot title.
|
|
460
|
+
ylabel: Y-axis label.
|
|
461
|
+
|
|
462
|
+
Returns:
|
|
463
|
+
Matplotlib Figure object.
|
|
464
|
+
|
|
465
|
+
Raises:
|
|
466
|
+
ValueError: If frequency/magnitude data is missing or empty.
|
|
467
|
+
"""
|
|
468
|
+
fig, ax = plt.subplots(figsize=(10, 6))
|
|
469
|
+
|
|
470
|
+
frequencies = np.asarray(data.get("frequencies", []))
|
|
471
|
+
# Try multiple possible keys for magnitude data
|
|
472
|
+
magnitude = None
|
|
473
|
+
for key in ["magnitude_db", "psd", "magnitude", "power_db"]:
|
|
474
|
+
if key in data:
|
|
475
|
+
magnitude = np.asarray(data[key])
|
|
476
|
+
break
|
|
477
|
+
|
|
478
|
+
if magnitude is None or len(frequencies) == 0 or len(magnitude) == 0:
|
|
479
|
+
plt.close(fig)
|
|
480
|
+
raise ValueError("Missing or empty frequency/magnitude data")
|
|
481
|
+
|
|
482
|
+
# Auto-select frequency unit
|
|
483
|
+
max_freq = frequencies[-1] if len(frequencies) > 0 else 1.0
|
|
484
|
+
if max_freq >= 1e9:
|
|
485
|
+
freq_unit = "GHz"
|
|
486
|
+
freq_scale = 1e9
|
|
487
|
+
elif max_freq >= 1e6:
|
|
488
|
+
freq_unit = "MHz"
|
|
489
|
+
freq_scale = 1e6
|
|
490
|
+
elif max_freq >= 1e3:
|
|
491
|
+
freq_unit = "kHz"
|
|
492
|
+
freq_scale = 1e3
|
|
493
|
+
else:
|
|
494
|
+
freq_unit = "Hz"
|
|
495
|
+
freq_scale = 1.0
|
|
496
|
+
|
|
497
|
+
ax.plot(frequencies / freq_scale, magnitude, linewidth=0.8)
|
|
498
|
+
ax.set_xlabel(f"Frequency ({freq_unit})")
|
|
499
|
+
ax.set_ylabel(ylabel)
|
|
500
|
+
ax.set_title(title)
|
|
501
|
+
ax.grid(True, alpha=0.3, which="both")
|
|
502
|
+
ax.set_xscale("log")
|
|
503
|
+
|
|
504
|
+
fig.tight_layout()
|
|
505
|
+
return fig
|
|
506
|
+
|
|
507
|
+
def _plot_histogram(
|
|
508
|
+
self,
|
|
509
|
+
data: dict[str, Any],
|
|
510
|
+
title: str = "Histogram",
|
|
511
|
+
xlabel: str = "Value",
|
|
512
|
+
) -> Figure:
|
|
513
|
+
"""Plot histogram of data distribution.
|
|
514
|
+
|
|
515
|
+
Args:
|
|
516
|
+
data: Dictionary with 'data' array.
|
|
517
|
+
title: Plot title.
|
|
518
|
+
xlabel: X-axis label.
|
|
519
|
+
|
|
520
|
+
Returns:
|
|
521
|
+
Matplotlib Figure object.
|
|
522
|
+
|
|
523
|
+
Raises:
|
|
524
|
+
ValueError: If data array is empty or contains no finite values.
|
|
525
|
+
"""
|
|
526
|
+
fig, ax = plt.subplots(figsize=(8, 6))
|
|
527
|
+
|
|
528
|
+
values = np.asarray(data.get("data", []))
|
|
529
|
+
if len(values) == 0:
|
|
530
|
+
plt.close(fig)
|
|
531
|
+
raise ValueError("Empty data array for histogram")
|
|
532
|
+
|
|
533
|
+
# Remove NaN/Inf values
|
|
534
|
+
values = values[np.isfinite(values)]
|
|
535
|
+
if len(values) == 0:
|
|
536
|
+
plt.close(fig)
|
|
537
|
+
raise ValueError("No finite values for histogram")
|
|
538
|
+
|
|
539
|
+
# Auto-select number of bins (Sturges' rule with limits)
|
|
540
|
+
n_bins = min(50, max(10, int(np.ceil(np.log2(len(values)) + 1))))
|
|
541
|
+
|
|
542
|
+
ax.hist(values, bins=n_bins, alpha=0.7, edgecolor="black")
|
|
543
|
+
ax.set_xlabel(xlabel)
|
|
544
|
+
ax.set_ylabel("Count")
|
|
545
|
+
ax.set_title(title)
|
|
546
|
+
ax.grid(True, alpha=0.3, axis="y")
|
|
547
|
+
|
|
548
|
+
fig.tight_layout()
|
|
549
|
+
return fig
|
|
550
|
+
|
|
551
|
+
def _plot_time_series(
|
|
552
|
+
self,
|
|
553
|
+
data: dict[str, Any],
|
|
554
|
+
title: str = "Time Series",
|
|
555
|
+
ylabel: str = "Amplitude",
|
|
556
|
+
) -> Figure:
|
|
557
|
+
"""Plot time-domain data.
|
|
558
|
+
|
|
559
|
+
Args:
|
|
560
|
+
data: Dictionary with 'data' and optionally 'time' arrays.
|
|
561
|
+
title: Plot title.
|
|
562
|
+
ylabel: Y-axis label.
|
|
563
|
+
|
|
564
|
+
Returns:
|
|
565
|
+
Matplotlib Figure object.
|
|
566
|
+
|
|
567
|
+
Raises:
|
|
568
|
+
ValueError: If data array is empty.
|
|
569
|
+
"""
|
|
570
|
+
fig, ax = plt.subplots(figsize=(10, 6))
|
|
571
|
+
|
|
572
|
+
values = np.asarray(data.get("data", []))
|
|
573
|
+
if len(values) == 0:
|
|
574
|
+
plt.close(fig)
|
|
575
|
+
raise ValueError("Empty data array for time series")
|
|
576
|
+
|
|
577
|
+
time = data.get("time", np.arange(len(values)))
|
|
578
|
+
time = np.asarray(time)
|
|
579
|
+
|
|
580
|
+
# Auto-select time unit
|
|
581
|
+
max_time = time[-1] if len(time) > 0 else 1.0
|
|
582
|
+
if max_time < 1e-6:
|
|
583
|
+
time_unit = "ns"
|
|
584
|
+
time_scale = 1e9
|
|
585
|
+
elif max_time < 1e-3:
|
|
586
|
+
time_unit = "us"
|
|
587
|
+
time_scale = 1e6
|
|
588
|
+
elif max_time < 1:
|
|
589
|
+
time_unit = "ms"
|
|
590
|
+
time_scale = 1e3
|
|
591
|
+
else:
|
|
592
|
+
time_unit = "s"
|
|
593
|
+
time_scale = 1.0
|
|
594
|
+
|
|
595
|
+
ax.plot(time * time_scale, values, linewidth=0.8)
|
|
596
|
+
ax.set_xlabel(f"Time ({time_unit})")
|
|
597
|
+
ax.set_ylabel(ylabel)
|
|
598
|
+
ax.set_title(title)
|
|
599
|
+
ax.grid(True, alpha=0.3)
|
|
600
|
+
|
|
601
|
+
fig.tight_layout()
|
|
602
|
+
return fig
|
|
603
|
+
|
|
604
|
+
def _plot_spectrogram(self, data: dict[str, Any]) -> Figure:
|
|
605
|
+
"""Plot spectrogram (time-frequency heatmap).
|
|
606
|
+
|
|
607
|
+
Args:
|
|
608
|
+
data: Dictionary with 'times', 'frequencies', and 'Sxx_db' arrays.
|
|
609
|
+
|
|
610
|
+
Returns:
|
|
611
|
+
Matplotlib Figure object.
|
|
612
|
+
|
|
613
|
+
Raises:
|
|
614
|
+
ValueError: If spectrogram data is missing or empty.
|
|
615
|
+
"""
|
|
616
|
+
fig, ax = plt.subplots(figsize=(10, 6))
|
|
617
|
+
|
|
618
|
+
times = np.asarray(data.get("times", []))
|
|
619
|
+
frequencies = np.asarray(data.get("frequencies", []))
|
|
620
|
+
Sxx_db = np.asarray(data.get("Sxx_db", []))
|
|
621
|
+
|
|
622
|
+
if len(times) == 0 or len(frequencies) == 0 or Sxx_db.size == 0:
|
|
623
|
+
plt.close(fig)
|
|
624
|
+
raise ValueError("Missing spectrogram data")
|
|
625
|
+
|
|
626
|
+
# Auto-select units
|
|
627
|
+
max_time = times[-1] if len(times) > 0 else 1.0
|
|
628
|
+
if max_time < 1e-6:
|
|
629
|
+
time_unit = "ns"
|
|
630
|
+
time_scale = 1e9
|
|
631
|
+
elif max_time < 1e-3:
|
|
632
|
+
time_unit = "us"
|
|
633
|
+
time_scale = 1e6
|
|
634
|
+
elif max_time < 1:
|
|
635
|
+
time_unit = "ms"
|
|
636
|
+
time_scale = 1e3
|
|
637
|
+
else:
|
|
638
|
+
time_unit = "s"
|
|
639
|
+
time_scale = 1.0
|
|
640
|
+
|
|
641
|
+
max_freq = frequencies[-1] if len(frequencies) > 0 else 1.0
|
|
642
|
+
if max_freq >= 1e9:
|
|
643
|
+
freq_unit = "GHz"
|
|
644
|
+
freq_scale = 1e9
|
|
645
|
+
elif max_freq >= 1e6:
|
|
646
|
+
freq_unit = "MHz"
|
|
647
|
+
freq_scale = 1e6
|
|
648
|
+
elif max_freq >= 1e3:
|
|
649
|
+
freq_unit = "kHz"
|
|
650
|
+
freq_scale = 1e3
|
|
651
|
+
else:
|
|
652
|
+
freq_unit = "Hz"
|
|
653
|
+
freq_scale = 1.0
|
|
654
|
+
|
|
655
|
+
# Auto color limits
|
|
656
|
+
valid_db = Sxx_db[np.isfinite(Sxx_db)]
|
|
657
|
+
if len(valid_db) > 0:
|
|
658
|
+
vmax = np.max(valid_db)
|
|
659
|
+
vmin = max(np.min(valid_db), vmax - 80)
|
|
660
|
+
else:
|
|
661
|
+
vmin, vmax = None, None
|
|
662
|
+
|
|
663
|
+
pcm = ax.pcolormesh(
|
|
664
|
+
times * time_scale,
|
|
665
|
+
frequencies / freq_scale,
|
|
666
|
+
Sxx_db,
|
|
667
|
+
shading="auto",
|
|
668
|
+
cmap="viridis",
|
|
669
|
+
vmin=vmin,
|
|
670
|
+
vmax=vmax,
|
|
671
|
+
)
|
|
672
|
+
|
|
673
|
+
ax.set_xlabel(f"Time ({time_unit})")
|
|
674
|
+
ax.set_ylabel(f"Frequency ({freq_unit})")
|
|
675
|
+
ax.set_title("Spectrogram")
|
|
676
|
+
|
|
677
|
+
cbar = fig.colorbar(pcm, ax=ax)
|
|
678
|
+
cbar.set_label("Magnitude (dB)")
|
|
679
|
+
|
|
680
|
+
fig.tight_layout()
|
|
681
|
+
return fig
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
# ============================================================================
|
|
685
|
+
# Plot Registry
|
|
686
|
+
# ============================================================================
|
|
687
|
+
|
|
688
|
+
# Maps (domain, analysis_name) tuples to plot generation functions
|
|
689
|
+
# This allows custom plot functions to be registered for specific analyses
|
|
690
|
+
PLOT_REGISTRY: dict[
|
|
691
|
+
tuple[AnalysisDomain, str] | AnalysisDomain, Callable[[dict[str, Any]], Figure]
|
|
692
|
+
] = {}
|
|
693
|
+
|
|
694
|
+
|
|
695
|
+
def register_plot(
|
|
696
|
+
domain: AnalysisDomain,
|
|
697
|
+
analysis_name: str | None = None,
|
|
698
|
+
) -> Callable[[Callable[[dict[str, Any]], Figure]], Callable[[dict[str, Any]], Figure]]:
|
|
699
|
+
"""Decorator to register a custom plot function.
|
|
700
|
+
|
|
701
|
+
Args:
|
|
702
|
+
domain: Analysis domain.
|
|
703
|
+
analysis_name: Specific analysis name (optional). If None, registers
|
|
704
|
+
for entire domain.
|
|
705
|
+
|
|
706
|
+
Returns:
|
|
707
|
+
Decorator function.
|
|
708
|
+
|
|
709
|
+
Example:
|
|
710
|
+
>>> @register_plot(AnalysisDomain.SPECTRAL, "custom_fft")
|
|
711
|
+
... def plot_custom_fft(data: dict[str, Any]) -> Figure:
|
|
712
|
+
... fig, ax = plt.subplots()
|
|
713
|
+
... # Custom plotting code
|
|
714
|
+
... return fig
|
|
715
|
+
"""
|
|
716
|
+
|
|
717
|
+
def decorator(func: Callable[[dict[str, Any]], Figure]) -> Callable[[dict[str, Any]], Figure]:
|
|
718
|
+
if analysis_name:
|
|
719
|
+
PLOT_REGISTRY[(domain, analysis_name)] = func
|
|
720
|
+
else:
|
|
721
|
+
PLOT_REGISTRY[domain] = func
|
|
722
|
+
return func
|
|
723
|
+
|
|
724
|
+
return decorator
|
|
725
|
+
|
|
726
|
+
|
|
727
|
+
__all__ = [
|
|
728
|
+
"PLOT_REGISTRY",
|
|
729
|
+
"PlotGenerator",
|
|
730
|
+
"register_plot",
|
|
731
|
+
]
|