oscura 0.0.1__py3-none-any.whl → 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- oscura/__init__.py +813 -8
- oscura/__main__.py +392 -0
- oscura/analyzers/__init__.py +37 -0
- oscura/analyzers/digital/__init__.py +177 -0
- oscura/analyzers/digital/bus.py +691 -0
- oscura/analyzers/digital/clock.py +805 -0
- oscura/analyzers/digital/correlation.py +720 -0
- oscura/analyzers/digital/edges.py +632 -0
- oscura/analyzers/digital/extraction.py +413 -0
- oscura/analyzers/digital/quality.py +878 -0
- oscura/analyzers/digital/signal_quality.py +877 -0
- oscura/analyzers/digital/thresholds.py +708 -0
- oscura/analyzers/digital/timing.py +1104 -0
- oscura/analyzers/eye/__init__.py +46 -0
- oscura/analyzers/eye/diagram.py +434 -0
- oscura/analyzers/eye/metrics.py +555 -0
- oscura/analyzers/jitter/__init__.py +83 -0
- oscura/analyzers/jitter/ber.py +333 -0
- oscura/analyzers/jitter/decomposition.py +759 -0
- oscura/analyzers/jitter/measurements.py +413 -0
- oscura/analyzers/jitter/spectrum.py +220 -0
- oscura/analyzers/measurements.py +40 -0
- oscura/analyzers/packet/__init__.py +171 -0
- oscura/analyzers/packet/daq.py +1077 -0
- oscura/analyzers/packet/metrics.py +437 -0
- oscura/analyzers/packet/parser.py +327 -0
- oscura/analyzers/packet/payload.py +2156 -0
- oscura/analyzers/packet/payload_analysis.py +1312 -0
- oscura/analyzers/packet/payload_extraction.py +236 -0
- oscura/analyzers/packet/payload_patterns.py +670 -0
- oscura/analyzers/packet/stream.py +359 -0
- oscura/analyzers/patterns/__init__.py +266 -0
- oscura/analyzers/patterns/clustering.py +1036 -0
- oscura/analyzers/patterns/discovery.py +539 -0
- oscura/analyzers/patterns/learning.py +797 -0
- oscura/analyzers/patterns/matching.py +1091 -0
- oscura/analyzers/patterns/periodic.py +650 -0
- oscura/analyzers/patterns/sequences.py +767 -0
- oscura/analyzers/power/__init__.py +116 -0
- oscura/analyzers/power/ac_power.py +391 -0
- oscura/analyzers/power/basic.py +383 -0
- oscura/analyzers/power/conduction.py +314 -0
- oscura/analyzers/power/efficiency.py +297 -0
- oscura/analyzers/power/ripple.py +356 -0
- oscura/analyzers/power/soa.py +372 -0
- oscura/analyzers/power/switching.py +479 -0
- oscura/analyzers/protocol/__init__.py +150 -0
- oscura/analyzers/protocols/__init__.py +150 -0
- oscura/analyzers/protocols/base.py +500 -0
- oscura/analyzers/protocols/can.py +620 -0
- oscura/analyzers/protocols/can_fd.py +448 -0
- oscura/analyzers/protocols/flexray.py +405 -0
- oscura/analyzers/protocols/hdlc.py +399 -0
- oscura/analyzers/protocols/i2c.py +368 -0
- oscura/analyzers/protocols/i2s.py +296 -0
- oscura/analyzers/protocols/jtag.py +393 -0
- oscura/analyzers/protocols/lin.py +445 -0
- oscura/analyzers/protocols/manchester.py +333 -0
- oscura/analyzers/protocols/onewire.py +501 -0
- oscura/analyzers/protocols/spi.py +334 -0
- oscura/analyzers/protocols/swd.py +325 -0
- oscura/analyzers/protocols/uart.py +393 -0
- oscura/analyzers/protocols/usb.py +495 -0
- oscura/analyzers/signal_integrity/__init__.py +63 -0
- oscura/analyzers/signal_integrity/embedding.py +294 -0
- oscura/analyzers/signal_integrity/equalization.py +370 -0
- oscura/analyzers/signal_integrity/sparams.py +484 -0
- oscura/analyzers/spectral/__init__.py +53 -0
- oscura/analyzers/spectral/chunked.py +273 -0
- oscura/analyzers/spectral/chunked_fft.py +571 -0
- oscura/analyzers/spectral/chunked_wavelet.py +391 -0
- oscura/analyzers/spectral/fft.py +92 -0
- oscura/analyzers/statistical/__init__.py +250 -0
- oscura/analyzers/statistical/checksum.py +923 -0
- oscura/analyzers/statistical/chunked_corr.py +228 -0
- oscura/analyzers/statistical/classification.py +778 -0
- oscura/analyzers/statistical/entropy.py +1113 -0
- oscura/analyzers/statistical/ngrams.py +614 -0
- oscura/analyzers/statistics/__init__.py +119 -0
- oscura/analyzers/statistics/advanced.py +885 -0
- oscura/analyzers/statistics/basic.py +263 -0
- oscura/analyzers/statistics/correlation.py +630 -0
- oscura/analyzers/statistics/distribution.py +298 -0
- oscura/analyzers/statistics/outliers.py +463 -0
- oscura/analyzers/statistics/streaming.py +93 -0
- oscura/analyzers/statistics/trend.py +520 -0
- oscura/analyzers/validation.py +598 -0
- oscura/analyzers/waveform/__init__.py +36 -0
- oscura/analyzers/waveform/measurements.py +943 -0
- oscura/analyzers/waveform/measurements_with_uncertainty.py +371 -0
- oscura/analyzers/waveform/spectral.py +1689 -0
- oscura/analyzers/waveform/wavelets.py +298 -0
- oscura/api/__init__.py +62 -0
- oscura/api/dsl.py +538 -0
- oscura/api/fluent.py +571 -0
- oscura/api/operators.py +498 -0
- oscura/api/optimization.py +392 -0
- oscura/api/profiling.py +396 -0
- oscura/automotive/__init__.py +73 -0
- oscura/automotive/can/__init__.py +52 -0
- oscura/automotive/can/analysis.py +356 -0
- oscura/automotive/can/checksum.py +250 -0
- oscura/automotive/can/correlation.py +212 -0
- oscura/automotive/can/discovery.py +355 -0
- oscura/automotive/can/message_wrapper.py +375 -0
- oscura/automotive/can/models.py +385 -0
- oscura/automotive/can/patterns.py +381 -0
- oscura/automotive/can/session.py +452 -0
- oscura/automotive/can/state_machine.py +300 -0
- oscura/automotive/can/stimulus_response.py +461 -0
- oscura/automotive/dbc/__init__.py +15 -0
- oscura/automotive/dbc/generator.py +156 -0
- oscura/automotive/dbc/parser.py +146 -0
- oscura/automotive/dtc/__init__.py +30 -0
- oscura/automotive/dtc/database.py +3036 -0
- oscura/automotive/j1939/__init__.py +14 -0
- oscura/automotive/j1939/decoder.py +745 -0
- oscura/automotive/loaders/__init__.py +35 -0
- oscura/automotive/loaders/asc.py +98 -0
- oscura/automotive/loaders/blf.py +77 -0
- oscura/automotive/loaders/csv_can.py +136 -0
- oscura/automotive/loaders/dispatcher.py +136 -0
- oscura/automotive/loaders/mdf.py +331 -0
- oscura/automotive/loaders/pcap.py +132 -0
- oscura/automotive/obd/__init__.py +14 -0
- oscura/automotive/obd/decoder.py +707 -0
- oscura/automotive/uds/__init__.py +48 -0
- oscura/automotive/uds/decoder.py +265 -0
- oscura/automotive/uds/models.py +64 -0
- oscura/automotive/visualization.py +369 -0
- oscura/batch/__init__.py +55 -0
- oscura/batch/advanced.py +627 -0
- oscura/batch/aggregate.py +300 -0
- oscura/batch/analyze.py +139 -0
- oscura/batch/logging.py +487 -0
- oscura/batch/metrics.py +556 -0
- oscura/builders/__init__.py +41 -0
- oscura/builders/signal_builder.py +1131 -0
- oscura/cli/__init__.py +14 -0
- oscura/cli/batch.py +339 -0
- oscura/cli/characterize.py +273 -0
- oscura/cli/compare.py +775 -0
- oscura/cli/decode.py +551 -0
- oscura/cli/main.py +247 -0
- oscura/cli/shell.py +350 -0
- oscura/comparison/__init__.py +66 -0
- oscura/comparison/compare.py +397 -0
- oscura/comparison/golden.py +487 -0
- oscura/comparison/limits.py +391 -0
- oscura/comparison/mask.py +434 -0
- oscura/comparison/trace_diff.py +30 -0
- oscura/comparison/visualization.py +481 -0
- oscura/compliance/__init__.py +70 -0
- oscura/compliance/advanced.py +756 -0
- oscura/compliance/masks.py +363 -0
- oscura/compliance/reporting.py +483 -0
- oscura/compliance/testing.py +298 -0
- oscura/component/__init__.py +38 -0
- oscura/component/impedance.py +365 -0
- oscura/component/reactive.py +598 -0
- oscura/component/transmission_line.py +312 -0
- oscura/config/__init__.py +191 -0
- oscura/config/defaults.py +254 -0
- oscura/config/loader.py +348 -0
- oscura/config/memory.py +271 -0
- oscura/config/migration.py +458 -0
- oscura/config/pipeline.py +1077 -0
- oscura/config/preferences.py +530 -0
- oscura/config/protocol.py +875 -0
- oscura/config/schema.py +713 -0
- oscura/config/settings.py +420 -0
- oscura/config/thresholds.py +599 -0
- oscura/convenience.py +457 -0
- oscura/core/__init__.py +299 -0
- oscura/core/audit.py +457 -0
- oscura/core/backend_selector.py +405 -0
- oscura/core/cache.py +590 -0
- oscura/core/cancellation.py +439 -0
- oscura/core/confidence.py +225 -0
- oscura/core/config.py +506 -0
- oscura/core/correlation.py +216 -0
- oscura/core/cross_domain.py +422 -0
- oscura/core/debug.py +301 -0
- oscura/core/edge_cases.py +541 -0
- oscura/core/exceptions.py +535 -0
- oscura/core/gpu_backend.py +523 -0
- oscura/core/lazy.py +832 -0
- oscura/core/log_query.py +540 -0
- oscura/core/logging.py +931 -0
- oscura/core/logging_advanced.py +952 -0
- oscura/core/memoize.py +171 -0
- oscura/core/memory_check.py +274 -0
- oscura/core/memory_guard.py +290 -0
- oscura/core/memory_limits.py +336 -0
- oscura/core/memory_monitor.py +453 -0
- oscura/core/memory_progress.py +465 -0
- oscura/core/memory_warnings.py +315 -0
- oscura/core/numba_backend.py +362 -0
- oscura/core/performance.py +352 -0
- oscura/core/progress.py +524 -0
- oscura/core/provenance.py +358 -0
- oscura/core/results.py +331 -0
- oscura/core/types.py +504 -0
- oscura/core/uncertainty.py +383 -0
- oscura/discovery/__init__.py +52 -0
- oscura/discovery/anomaly_detector.py +672 -0
- oscura/discovery/auto_decoder.py +415 -0
- oscura/discovery/comparison.py +497 -0
- oscura/discovery/quality_validator.py +528 -0
- oscura/discovery/signal_detector.py +769 -0
- oscura/dsl/__init__.py +73 -0
- oscura/dsl/commands.py +246 -0
- oscura/dsl/interpreter.py +455 -0
- oscura/dsl/parser.py +689 -0
- oscura/dsl/repl.py +172 -0
- oscura/exceptions.py +59 -0
- oscura/exploratory/__init__.py +111 -0
- oscura/exploratory/error_recovery.py +642 -0
- oscura/exploratory/fuzzy.py +513 -0
- oscura/exploratory/fuzzy_advanced.py +786 -0
- oscura/exploratory/legacy.py +831 -0
- oscura/exploratory/parse.py +358 -0
- oscura/exploratory/recovery.py +275 -0
- oscura/exploratory/sync.py +382 -0
- oscura/exploratory/unknown.py +707 -0
- oscura/export/__init__.py +25 -0
- oscura/export/wireshark/README.md +265 -0
- oscura/export/wireshark/__init__.py +47 -0
- oscura/export/wireshark/generator.py +312 -0
- oscura/export/wireshark/lua_builder.py +159 -0
- oscura/export/wireshark/templates/dissector.lua.j2 +92 -0
- oscura/export/wireshark/type_mapping.py +165 -0
- oscura/export/wireshark/validator.py +105 -0
- oscura/exporters/__init__.py +94 -0
- oscura/exporters/csv.py +303 -0
- oscura/exporters/exporters.py +44 -0
- oscura/exporters/hdf5.py +219 -0
- oscura/exporters/html_export.py +701 -0
- oscura/exporters/json_export.py +291 -0
- oscura/exporters/markdown_export.py +367 -0
- oscura/exporters/matlab_export.py +354 -0
- oscura/exporters/npz_export.py +219 -0
- oscura/exporters/spice_export.py +210 -0
- oscura/extensibility/__init__.py +131 -0
- oscura/extensibility/docs.py +752 -0
- oscura/extensibility/extensions.py +1125 -0
- oscura/extensibility/logging.py +259 -0
- oscura/extensibility/measurements.py +485 -0
- oscura/extensibility/plugins.py +414 -0
- oscura/extensibility/registry.py +346 -0
- oscura/extensibility/templates.py +913 -0
- oscura/extensibility/validation.py +651 -0
- oscura/filtering/__init__.py +89 -0
- oscura/filtering/base.py +563 -0
- oscura/filtering/convenience.py +564 -0
- oscura/filtering/design.py +725 -0
- oscura/filtering/filters.py +32 -0
- oscura/filtering/introspection.py +605 -0
- oscura/guidance/__init__.py +24 -0
- oscura/guidance/recommender.py +429 -0
- oscura/guidance/wizard.py +518 -0
- oscura/inference/__init__.py +251 -0
- oscura/inference/active_learning/README.md +153 -0
- oscura/inference/active_learning/__init__.py +38 -0
- oscura/inference/active_learning/lstar.py +257 -0
- oscura/inference/active_learning/observation_table.py +230 -0
- oscura/inference/active_learning/oracle.py +78 -0
- oscura/inference/active_learning/teachers/__init__.py +15 -0
- oscura/inference/active_learning/teachers/simulator.py +192 -0
- oscura/inference/adaptive_tuning.py +453 -0
- oscura/inference/alignment.py +653 -0
- oscura/inference/bayesian.py +943 -0
- oscura/inference/binary.py +1016 -0
- oscura/inference/crc_reverse.py +711 -0
- oscura/inference/logic.py +288 -0
- oscura/inference/message_format.py +1305 -0
- oscura/inference/protocol.py +417 -0
- oscura/inference/protocol_dsl.py +1084 -0
- oscura/inference/protocol_library.py +1230 -0
- oscura/inference/sequences.py +809 -0
- oscura/inference/signal_intelligence.py +1509 -0
- oscura/inference/spectral.py +215 -0
- oscura/inference/state_machine.py +634 -0
- oscura/inference/stream.py +918 -0
- oscura/integrations/__init__.py +59 -0
- oscura/integrations/llm.py +1827 -0
- oscura/jupyter/__init__.py +32 -0
- oscura/jupyter/display.py +268 -0
- oscura/jupyter/magic.py +334 -0
- oscura/loaders/__init__.py +526 -0
- oscura/loaders/binary.py +69 -0
- oscura/loaders/configurable.py +1255 -0
- oscura/loaders/csv.py +26 -0
- oscura/loaders/csv_loader.py +473 -0
- oscura/loaders/hdf5.py +9 -0
- oscura/loaders/hdf5_loader.py +510 -0
- oscura/loaders/lazy.py +370 -0
- oscura/loaders/mmap_loader.py +583 -0
- oscura/loaders/numpy_loader.py +436 -0
- oscura/loaders/pcap.py +432 -0
- oscura/loaders/preprocessing.py +368 -0
- oscura/loaders/rigol.py +287 -0
- oscura/loaders/sigrok.py +321 -0
- oscura/loaders/tdms.py +367 -0
- oscura/loaders/tektronix.py +711 -0
- oscura/loaders/validation.py +584 -0
- oscura/loaders/vcd.py +464 -0
- oscura/loaders/wav.py +233 -0
- oscura/math/__init__.py +45 -0
- oscura/math/arithmetic.py +824 -0
- oscura/math/interpolation.py +413 -0
- oscura/onboarding/__init__.py +39 -0
- oscura/onboarding/help.py +498 -0
- oscura/onboarding/tutorials.py +405 -0
- oscura/onboarding/wizard.py +466 -0
- oscura/optimization/__init__.py +19 -0
- oscura/optimization/parallel.py +440 -0
- oscura/optimization/search.py +532 -0
- oscura/pipeline/__init__.py +43 -0
- oscura/pipeline/base.py +338 -0
- oscura/pipeline/composition.py +242 -0
- oscura/pipeline/parallel.py +448 -0
- oscura/pipeline/pipeline.py +375 -0
- oscura/pipeline/reverse_engineering.py +1119 -0
- oscura/plugins/__init__.py +122 -0
- oscura/plugins/base.py +272 -0
- oscura/plugins/cli.py +497 -0
- oscura/plugins/discovery.py +411 -0
- oscura/plugins/isolation.py +418 -0
- oscura/plugins/lifecycle.py +959 -0
- oscura/plugins/manager.py +493 -0
- oscura/plugins/registry.py +421 -0
- oscura/plugins/versioning.py +372 -0
- oscura/py.typed +0 -0
- oscura/quality/__init__.py +65 -0
- oscura/quality/ensemble.py +740 -0
- oscura/quality/explainer.py +338 -0
- oscura/quality/scoring.py +616 -0
- oscura/quality/warnings.py +456 -0
- oscura/reporting/__init__.py +248 -0
- oscura/reporting/advanced.py +1234 -0
- oscura/reporting/analyze.py +448 -0
- oscura/reporting/argument_preparer.py +596 -0
- oscura/reporting/auto_report.py +507 -0
- oscura/reporting/batch.py +615 -0
- oscura/reporting/chart_selection.py +223 -0
- oscura/reporting/comparison.py +330 -0
- oscura/reporting/config.py +615 -0
- oscura/reporting/content/__init__.py +39 -0
- oscura/reporting/content/executive.py +127 -0
- oscura/reporting/content/filtering.py +191 -0
- oscura/reporting/content/minimal.py +257 -0
- oscura/reporting/content/verbosity.py +162 -0
- oscura/reporting/core.py +508 -0
- oscura/reporting/core_formats/__init__.py +17 -0
- oscura/reporting/core_formats/multi_format.py +210 -0
- oscura/reporting/engine.py +836 -0
- oscura/reporting/export.py +366 -0
- oscura/reporting/formatting/__init__.py +129 -0
- oscura/reporting/formatting/emphasis.py +81 -0
- oscura/reporting/formatting/numbers.py +403 -0
- oscura/reporting/formatting/standards.py +55 -0
- oscura/reporting/formatting.py +466 -0
- oscura/reporting/html.py +578 -0
- oscura/reporting/index.py +590 -0
- oscura/reporting/multichannel.py +296 -0
- oscura/reporting/output.py +379 -0
- oscura/reporting/pdf.py +373 -0
- oscura/reporting/plots.py +731 -0
- oscura/reporting/pptx_export.py +360 -0
- oscura/reporting/renderers/__init__.py +11 -0
- oscura/reporting/renderers/pdf.py +94 -0
- oscura/reporting/sections.py +471 -0
- oscura/reporting/standards.py +680 -0
- oscura/reporting/summary_generator.py +368 -0
- oscura/reporting/tables.py +397 -0
- oscura/reporting/template_system.py +724 -0
- oscura/reporting/templates/__init__.py +15 -0
- oscura/reporting/templates/definition.py +205 -0
- oscura/reporting/templates/index.html +649 -0
- oscura/reporting/templates/index.md +173 -0
- oscura/schemas/__init__.py +158 -0
- oscura/schemas/bus_configuration.json +322 -0
- oscura/schemas/device_mapping.json +182 -0
- oscura/schemas/packet_format.json +418 -0
- oscura/schemas/protocol_definition.json +363 -0
- oscura/search/__init__.py +16 -0
- oscura/search/anomaly.py +292 -0
- oscura/search/context.py +149 -0
- oscura/search/pattern.py +160 -0
- oscura/session/__init__.py +34 -0
- oscura/session/annotations.py +289 -0
- oscura/session/history.py +313 -0
- oscura/session/session.py +445 -0
- oscura/streaming/__init__.py +43 -0
- oscura/streaming/chunked.py +611 -0
- oscura/streaming/progressive.py +393 -0
- oscura/streaming/realtime.py +622 -0
- oscura/testing/__init__.py +54 -0
- oscura/testing/synthetic.py +808 -0
- oscura/triggering/__init__.py +68 -0
- oscura/triggering/base.py +229 -0
- oscura/triggering/edge.py +353 -0
- oscura/triggering/pattern.py +344 -0
- oscura/triggering/pulse.py +581 -0
- oscura/triggering/window.py +453 -0
- oscura/ui/__init__.py +48 -0
- oscura/ui/formatters.py +526 -0
- oscura/ui/progressive_display.py +340 -0
- oscura/utils/__init__.py +99 -0
- oscura/utils/autodetect.py +338 -0
- oscura/utils/buffer.py +389 -0
- oscura/utils/lazy.py +407 -0
- oscura/utils/lazy_imports.py +147 -0
- oscura/utils/memory.py +836 -0
- oscura/utils/memory_advanced.py +1326 -0
- oscura/utils/memory_extensions.py +465 -0
- oscura/utils/progressive.py +352 -0
- oscura/utils/windowing.py +362 -0
- oscura/visualization/__init__.py +321 -0
- oscura/visualization/accessibility.py +526 -0
- oscura/visualization/annotations.py +374 -0
- oscura/visualization/axis_scaling.py +305 -0
- oscura/visualization/colors.py +453 -0
- oscura/visualization/digital.py +337 -0
- oscura/visualization/eye.py +420 -0
- oscura/visualization/histogram.py +281 -0
- oscura/visualization/interactive.py +858 -0
- oscura/visualization/jitter.py +702 -0
- oscura/visualization/keyboard.py +394 -0
- oscura/visualization/layout.py +365 -0
- oscura/visualization/optimization.py +1028 -0
- oscura/visualization/palettes.py +446 -0
- oscura/visualization/plot.py +92 -0
- oscura/visualization/power.py +290 -0
- oscura/visualization/power_extended.py +626 -0
- oscura/visualization/presets.py +467 -0
- oscura/visualization/protocols.py +932 -0
- oscura/visualization/render.py +207 -0
- oscura/visualization/rendering.py +444 -0
- oscura/visualization/reverse_engineering.py +791 -0
- oscura/visualization/signal_integrity.py +808 -0
- oscura/visualization/specialized.py +553 -0
- oscura/visualization/spectral.py +811 -0
- oscura/visualization/styles.py +381 -0
- oscura/visualization/thumbnails.py +311 -0
- oscura/visualization/time_axis.py +351 -0
- oscura/visualization/waveform.py +367 -0
- oscura/workflow/__init__.py +13 -0
- oscura/workflow/dag.py +377 -0
- oscura/workflows/__init__.py +58 -0
- oscura/workflows/compliance.py +280 -0
- oscura/workflows/digital.py +272 -0
- oscura/workflows/multi_trace.py +502 -0
- oscura/workflows/power.py +178 -0
- oscura/workflows/protocol.py +492 -0
- oscura/workflows/reverse_engineering.py +639 -0
- oscura/workflows/signal_integrity.py +227 -0
- oscura-0.1.0.dist-info/METADATA +300 -0
- oscura-0.1.0.dist-info/RECORD +463 -0
- oscura-0.1.0.dist-info/entry_points.txt +2 -0
- {oscura-0.0.1.dist-info → oscura-0.1.0.dist-info}/licenses/LICENSE +1 -1
- oscura-0.0.1.dist-info/METADATA +0 -63
- oscura-0.0.1.dist-info/RECORD +0 -5
- {oscura-0.0.1.dist-info → oscura-0.1.0.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
"""Time-aware X-axis formatting and optimization.
|
|
2
|
+
|
|
3
|
+
This module provides intelligent time axis formatting with automatic unit
|
|
4
|
+
selection, relative time offsets, and cursor readout with full precision.
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
>>> from oscura.visualization.time_axis import format_time_axis
|
|
9
|
+
>>> labels = format_time_axis(time_values, unit="auto")
|
|
10
|
+
|
|
11
|
+
References:
|
|
12
|
+
- SI prefixes for time units
|
|
13
|
+
- IEEE publication time axis standards
|
|
14
|
+
- Matplotlib formatter customization
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from typing import TYPE_CHECKING, Literal
|
|
20
|
+
|
|
21
|
+
import numpy as np
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from numpy.typing import NDArray
|
|
25
|
+
|
|
26
|
+
TimeUnit = Literal["s", "ms", "us", "ns", "ps", "auto"]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def select_time_unit(
|
|
30
|
+
time_range: float,
|
|
31
|
+
*,
|
|
32
|
+
prefer_larger: bool = False,
|
|
33
|
+
) -> TimeUnit:
|
|
34
|
+
"""Automatically select appropriate time unit based on range.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
time_range: Time range in seconds.
|
|
38
|
+
prefer_larger: Prefer larger units when ambiguous.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
Selected time unit ("s", "ms", "us", "ns", "ps").
|
|
42
|
+
|
|
43
|
+
Example:
|
|
44
|
+
>>> select_time_unit(0.001) # 1 ms
|
|
45
|
+
'ms'
|
|
46
|
+
>>> select_time_unit(1e-6) # 1 us
|
|
47
|
+
'us'
|
|
48
|
+
|
|
49
|
+
References:
|
|
50
|
+
VIS-014: Adaptive X-Axis Time Window
|
|
51
|
+
"""
|
|
52
|
+
if time_range >= 1.0:
|
|
53
|
+
return "s"
|
|
54
|
+
elif time_range >= 1e-3:
|
|
55
|
+
return "ms" if not prefer_larger else "s"
|
|
56
|
+
elif time_range >= 1e-6:
|
|
57
|
+
return "us" if not prefer_larger else "ms"
|
|
58
|
+
elif time_range >= 1e-9:
|
|
59
|
+
return "ns" if not prefer_larger else "us"
|
|
60
|
+
else:
|
|
61
|
+
return "ps" if not prefer_larger else "ns"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def convert_time_values(
|
|
65
|
+
time: NDArray[np.float64],
|
|
66
|
+
unit: TimeUnit,
|
|
67
|
+
) -> NDArray[np.float64]:
|
|
68
|
+
"""Convert time values to specified unit.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
time: Time array in seconds.
|
|
72
|
+
unit: Target time unit.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
Time array in target unit.
|
|
76
|
+
|
|
77
|
+
Raises:
|
|
78
|
+
ValueError: If unit is invalid.
|
|
79
|
+
|
|
80
|
+
Example:
|
|
81
|
+
>>> time_s = np.array([0.001, 0.002, 0.003])
|
|
82
|
+
>>> time_ms = convert_time_values(time_s, "ms")
|
|
83
|
+
>>> # Returns [1.0, 2.0, 3.0]
|
|
84
|
+
|
|
85
|
+
References:
|
|
86
|
+
VIS-014: Adaptive X-Axis Time Window
|
|
87
|
+
"""
|
|
88
|
+
multipliers = {
|
|
89
|
+
"s": 1.0,
|
|
90
|
+
"ms": 1e3,
|
|
91
|
+
"us": 1e6,
|
|
92
|
+
"ns": 1e9,
|
|
93
|
+
"ps": 1e12,
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if unit == "auto":
|
|
97
|
+
time_range = float(np.ptp(time))
|
|
98
|
+
unit = select_time_unit(time_range)
|
|
99
|
+
|
|
100
|
+
if unit not in multipliers:
|
|
101
|
+
raise ValueError(f"Invalid time unit: {unit}")
|
|
102
|
+
|
|
103
|
+
return time * multipliers[unit]
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def format_time_labels(
|
|
107
|
+
time: NDArray[np.float64],
|
|
108
|
+
unit: TimeUnit = "auto",
|
|
109
|
+
*,
|
|
110
|
+
precision: int | None = None,
|
|
111
|
+
scientific_threshold: float = 1e6,
|
|
112
|
+
) -> list[str]:
|
|
113
|
+
"""Format time values as labels with appropriate precision.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
time: Time array in seconds.
|
|
117
|
+
unit: Time unit ("s", "ms", "us", "ns", "ps", "auto").
|
|
118
|
+
precision: Number of decimal places (auto if None).
|
|
119
|
+
scientific_threshold: Use scientific notation above this value.
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
List of formatted time labels.
|
|
123
|
+
|
|
124
|
+
Example:
|
|
125
|
+
>>> time = np.array([0.0, 0.001, 0.002])
|
|
126
|
+
>>> labels = format_time_labels(time, unit="ms")
|
|
127
|
+
>>> # Returns ['0', '1', '2']
|
|
128
|
+
|
|
129
|
+
References:
|
|
130
|
+
VIS-014: Adaptive X-Axis Time Window
|
|
131
|
+
"""
|
|
132
|
+
# Convert to target unit
|
|
133
|
+
time_converted = convert_time_values(time, unit)
|
|
134
|
+
|
|
135
|
+
# Auto-select precision based on value range
|
|
136
|
+
if precision is None:
|
|
137
|
+
value_range = np.ptp(time_converted)
|
|
138
|
+
if value_range == 0:
|
|
139
|
+
precision = 1
|
|
140
|
+
else:
|
|
141
|
+
# Use enough precision to show differences
|
|
142
|
+
magnitude = np.log10(value_range)
|
|
143
|
+
precision = max(0, int(np.ceil(2 - magnitude)))
|
|
144
|
+
|
|
145
|
+
# Format labels
|
|
146
|
+
labels = []
|
|
147
|
+
for val in time_converted:
|
|
148
|
+
if abs(val) >= scientific_threshold:
|
|
149
|
+
# Scientific notation
|
|
150
|
+
labels.append(f"{val:.{precision}e}")
|
|
151
|
+
else:
|
|
152
|
+
# Fixed point
|
|
153
|
+
labels.append(f"{val:.{precision}f}".rstrip("0").rstrip("."))
|
|
154
|
+
|
|
155
|
+
return labels
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def create_relative_time(
|
|
159
|
+
time: NDArray[np.float64],
|
|
160
|
+
*,
|
|
161
|
+
start_at_zero: bool = True,
|
|
162
|
+
reference_time: float | None = None,
|
|
163
|
+
) -> NDArray[np.float64]:
|
|
164
|
+
"""Create relative time axis starting at zero or reference.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
time: Absolute time array in seconds.
|
|
168
|
+
start_at_zero: Start time axis at t=0.
|
|
169
|
+
reference_time: Reference time (uses first sample if None).
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
Relative time array.
|
|
173
|
+
|
|
174
|
+
Example:
|
|
175
|
+
>>> time_abs = np.array([1000.5, 1000.6, 1000.7])
|
|
176
|
+
>>> time_rel = create_relative_time(time_abs)
|
|
177
|
+
>>> # Returns [0.0, 0.1, 0.2]
|
|
178
|
+
|
|
179
|
+
References:
|
|
180
|
+
VIS-014: Adaptive X-Axis Time Window
|
|
181
|
+
"""
|
|
182
|
+
if len(time) == 0:
|
|
183
|
+
return time
|
|
184
|
+
|
|
185
|
+
if reference_time is None:
|
|
186
|
+
reference_time = time[0] if start_at_zero else 0.0
|
|
187
|
+
|
|
188
|
+
return time - reference_time
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def calculate_major_ticks(
|
|
192
|
+
time_min: float,
|
|
193
|
+
time_max: float,
|
|
194
|
+
*,
|
|
195
|
+
target_count: int = 7,
|
|
196
|
+
unit: TimeUnit = "auto",
|
|
197
|
+
) -> NDArray[np.float64]:
|
|
198
|
+
"""Calculate major tick positions for time axis.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
time_min: Minimum time value in seconds.
|
|
202
|
+
time_max: Maximum time value in seconds.
|
|
203
|
+
target_count: Target number of major ticks.
|
|
204
|
+
unit: Time unit for tick alignment.
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
Array of major tick positions in seconds.
|
|
208
|
+
|
|
209
|
+
Example:
|
|
210
|
+
>>> ticks = calculate_major_ticks(0, 0.01, target_count=5, unit="ms")
|
|
211
|
+
|
|
212
|
+
References:
|
|
213
|
+
VIS-014: Adaptive X-Axis Time Window
|
|
214
|
+
VIS-019: Grid Auto-Spacing
|
|
215
|
+
"""
|
|
216
|
+
time_range = time_max - time_min
|
|
217
|
+
|
|
218
|
+
if time_range <= 0:
|
|
219
|
+
return np.array([time_min])
|
|
220
|
+
|
|
221
|
+
# Select unit if auto
|
|
222
|
+
if unit == "auto":
|
|
223
|
+
unit = select_time_unit(time_range)
|
|
224
|
+
|
|
225
|
+
# Convert to selected unit
|
|
226
|
+
multipliers = {
|
|
227
|
+
"s": 1.0,
|
|
228
|
+
"ms": 1e3,
|
|
229
|
+
"us": 1e6,
|
|
230
|
+
"ns": 1e9,
|
|
231
|
+
"ps": 1e12,
|
|
232
|
+
}
|
|
233
|
+
multiplier = multipliers[unit]
|
|
234
|
+
|
|
235
|
+
time_min_unit = time_min * multiplier
|
|
236
|
+
time_max_unit = time_max * multiplier
|
|
237
|
+
range_unit = time_max_unit - time_min_unit
|
|
238
|
+
|
|
239
|
+
# Calculate rough spacing
|
|
240
|
+
rough_spacing = range_unit / target_count
|
|
241
|
+
|
|
242
|
+
# Round to nice number
|
|
243
|
+
nice_spacing = _round_to_nice_time(rough_spacing)
|
|
244
|
+
|
|
245
|
+
# Generate ticks
|
|
246
|
+
first_tick = np.ceil(time_min_unit / nice_spacing) * nice_spacing
|
|
247
|
+
n_ticks = int((time_max_unit - first_tick) / nice_spacing) + 1
|
|
248
|
+
|
|
249
|
+
ticks_unit = first_tick + np.arange(n_ticks) * nice_spacing
|
|
250
|
+
|
|
251
|
+
# Convert back to seconds
|
|
252
|
+
ticks = ticks_unit / multiplier
|
|
253
|
+
|
|
254
|
+
# Filter to range
|
|
255
|
+
filtered_ticks: NDArray[np.float64] = ticks[(ticks >= time_min) & (ticks <= time_max)]
|
|
256
|
+
|
|
257
|
+
return filtered_ticks
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _round_to_nice_time(value: float) -> float:
|
|
261
|
+
"""Round to nice time value (1, 2, 5, 10, 20, 50 × 10^n). # noqa: RUF002
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
value: Value to round.
|
|
265
|
+
|
|
266
|
+
Returns:
|
|
267
|
+
Nice rounded value.
|
|
268
|
+
"""
|
|
269
|
+
if value <= 0:
|
|
270
|
+
return 1.0
|
|
271
|
+
|
|
272
|
+
exponent = np.floor(np.log10(value))
|
|
273
|
+
mantissa = value / (10**exponent)
|
|
274
|
+
|
|
275
|
+
# Nice fractions for time
|
|
276
|
+
nice_fractions = [1.0, 2.0, 5.0, 10.0]
|
|
277
|
+
|
|
278
|
+
# Find closest
|
|
279
|
+
distances = [abs(f - mantissa) for f in nice_fractions]
|
|
280
|
+
min_idx = np.argmin(distances)
|
|
281
|
+
nice_mantissa = nice_fractions[min_idx]
|
|
282
|
+
|
|
283
|
+
# Handle overflow
|
|
284
|
+
if nice_mantissa >= 10.0:
|
|
285
|
+
nice_mantissa = 1.0
|
|
286
|
+
exponent += 1
|
|
287
|
+
|
|
288
|
+
return nice_mantissa * (10**exponent) # type: ignore[no-any-return]
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def format_cursor_readout(
|
|
292
|
+
time_value: float,
|
|
293
|
+
*,
|
|
294
|
+
unit: TimeUnit = "auto",
|
|
295
|
+
full_precision: bool = True,
|
|
296
|
+
) -> str:
|
|
297
|
+
"""Format time value for cursor readout with full precision.
|
|
298
|
+
|
|
299
|
+
Args:
|
|
300
|
+
time_value: Time value in seconds.
|
|
301
|
+
unit: Display unit.
|
|
302
|
+
full_precision: Show full floating-point precision.
|
|
303
|
+
|
|
304
|
+
Returns:
|
|
305
|
+
Formatted time string.
|
|
306
|
+
|
|
307
|
+
Example:
|
|
308
|
+
>>> readout = format_cursor_readout(1.23456789e-6, unit="us")
|
|
309
|
+
>>> # Returns "1.23456789 μs"
|
|
310
|
+
|
|
311
|
+
References:
|
|
312
|
+
VIS-014: Adaptive X-Axis Time Window (cursor readout)
|
|
313
|
+
"""
|
|
314
|
+
# Select unit if auto
|
|
315
|
+
if unit == "auto":
|
|
316
|
+
unit = select_time_unit(abs(time_value))
|
|
317
|
+
|
|
318
|
+
# Convert to unit
|
|
319
|
+
time_converted = convert_time_values(np.array([time_value]), unit)[0]
|
|
320
|
+
|
|
321
|
+
# Unit symbols
|
|
322
|
+
unit_symbols = {
|
|
323
|
+
"s": "s",
|
|
324
|
+
"ms": "ms",
|
|
325
|
+
"us": "μs",
|
|
326
|
+
"ns": "ns",
|
|
327
|
+
"ps": "ps",
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
symbol = unit_symbols.get(unit, unit)
|
|
331
|
+
|
|
332
|
+
# Format with appropriate precision
|
|
333
|
+
if full_precision:
|
|
334
|
+
# Maximum useful precision (avoid floating point noise)
|
|
335
|
+
formatted = f"{time_converted:.12g}"
|
|
336
|
+
else:
|
|
337
|
+
# Standard precision
|
|
338
|
+
formatted = f"{time_converted:.6g}"
|
|
339
|
+
|
|
340
|
+
return f"{formatted} {symbol}"
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
__all__ = [
|
|
344
|
+
"TimeUnit",
|
|
345
|
+
"calculate_major_ticks",
|
|
346
|
+
"convert_time_values",
|
|
347
|
+
"create_relative_time",
|
|
348
|
+
"format_cursor_readout",
|
|
349
|
+
"format_time_labels",
|
|
350
|
+
"select_time_unit",
|
|
351
|
+
]
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
"""Waveform visualization functions.
|
|
2
|
+
|
|
3
|
+
This module provides time-domain waveform and multi-channel plots
|
|
4
|
+
with measurement annotations.
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
>>> from oscura.visualization.waveform import plot_waveform, plot_multi_channel
|
|
9
|
+
>>> plot_waveform(trace)
|
|
10
|
+
>>> plot_multi_channel([ch1, ch2, ch3])
|
|
11
|
+
|
|
12
|
+
References:
|
|
13
|
+
matplotlib best practices for scientific visualization
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from typing import TYPE_CHECKING, Any, cast
|
|
19
|
+
|
|
20
|
+
import numpy as np
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
import matplotlib.pyplot as plt
|
|
24
|
+
|
|
25
|
+
HAS_MATPLOTLIB = True
|
|
26
|
+
except ImportError:
|
|
27
|
+
HAS_MATPLOTLIB = False
|
|
28
|
+
|
|
29
|
+
from oscura.core.types import DigitalTrace, WaveformTrace
|
|
30
|
+
|
|
31
|
+
if TYPE_CHECKING:
|
|
32
|
+
from matplotlib.axes import Axes
|
|
33
|
+
from matplotlib.figure import Figure
|
|
34
|
+
from numpy.typing import NDArray
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def plot_waveform(
|
|
38
|
+
trace: WaveformTrace,
|
|
39
|
+
*,
|
|
40
|
+
ax: Axes | None = None,
|
|
41
|
+
time_unit: str = "auto",
|
|
42
|
+
time_range: tuple[float, float] | None = None,
|
|
43
|
+
show_grid: bool = True,
|
|
44
|
+
color: str = "C0",
|
|
45
|
+
label: str | None = None,
|
|
46
|
+
show_measurements: dict[str, Any] | None = None,
|
|
47
|
+
title: str | None = None,
|
|
48
|
+
xlabel: str = "Time",
|
|
49
|
+
ylabel: str = "Amplitude",
|
|
50
|
+
show: bool = True,
|
|
51
|
+
save_path: str | None = None,
|
|
52
|
+
figsize: tuple[float, float] = (10, 6),
|
|
53
|
+
) -> Figure:
|
|
54
|
+
"""Plot time-domain waveform.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
trace: Waveform trace to plot.
|
|
58
|
+
ax: Matplotlib axes. If None, creates new figure.
|
|
59
|
+
time_unit: Time unit ("s", "ms", "us", "ns", "auto").
|
|
60
|
+
time_range: Optional (start, end) time range in seconds to display.
|
|
61
|
+
show_grid: Show grid lines.
|
|
62
|
+
color: Line color.
|
|
63
|
+
label: Legend label.
|
|
64
|
+
show_measurements: Dictionary of measurements to annotate.
|
|
65
|
+
title: Plot title.
|
|
66
|
+
xlabel: X-axis label (appended with time unit).
|
|
67
|
+
ylabel: Y-axis label.
|
|
68
|
+
show: If True, call plt.show() to display the plot.
|
|
69
|
+
save_path: Path to save the figure. If None, figure is not saved.
|
|
70
|
+
figsize: Figure size (width, height) in inches. Only used if ax is None.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
Matplotlib Figure object.
|
|
74
|
+
|
|
75
|
+
Raises:
|
|
76
|
+
ImportError: If matplotlib is not installed.
|
|
77
|
+
ValueError: If axes has no associated figure.
|
|
78
|
+
|
|
79
|
+
Example:
|
|
80
|
+
>>> import oscura as tk
|
|
81
|
+
>>> trace = tk.load("signal.wfm")
|
|
82
|
+
>>> fig = tk.plot_waveform(trace, time_unit="us", show=False)
|
|
83
|
+
>>> fig.savefig("waveform.png")
|
|
84
|
+
|
|
85
|
+
>>> # With custom styling
|
|
86
|
+
>>> fig = tk.plot_waveform(trace,
|
|
87
|
+
... title="Captured Signal",
|
|
88
|
+
... xlabel="Time",
|
|
89
|
+
... ylabel="Voltage",
|
|
90
|
+
... color="blue")
|
|
91
|
+
"""
|
|
92
|
+
if not HAS_MATPLOTLIB:
|
|
93
|
+
raise ImportError("matplotlib is required for visualization")
|
|
94
|
+
|
|
95
|
+
if ax is None:
|
|
96
|
+
fig, ax = plt.subplots(figsize=figsize)
|
|
97
|
+
else:
|
|
98
|
+
fig_temp = ax.get_figure()
|
|
99
|
+
if fig_temp is None:
|
|
100
|
+
raise ValueError("Axes must have an associated figure")
|
|
101
|
+
fig = cast("Figure", fig_temp)
|
|
102
|
+
|
|
103
|
+
# Calculate time axis
|
|
104
|
+
time = trace.time_vector
|
|
105
|
+
|
|
106
|
+
# Auto-select time unit
|
|
107
|
+
if time_unit == "auto":
|
|
108
|
+
duration = time[-1] if len(time) > 0 else 0
|
|
109
|
+
if duration < 1e-6:
|
|
110
|
+
time_unit = "ns"
|
|
111
|
+
elif duration < 1e-3:
|
|
112
|
+
time_unit = "us"
|
|
113
|
+
elif duration < 1:
|
|
114
|
+
time_unit = "ms"
|
|
115
|
+
else:
|
|
116
|
+
time_unit = "s"
|
|
117
|
+
|
|
118
|
+
time_multipliers = {"s": 1.0, "ms": 1e3, "us": 1e6, "ns": 1e9}
|
|
119
|
+
multiplier = time_multipliers.get(time_unit, 1.0)
|
|
120
|
+
time_scaled = time * multiplier
|
|
121
|
+
|
|
122
|
+
# Plot waveform
|
|
123
|
+
ax.plot(time_scaled, trace.data, color=color, label=label, linewidth=0.8)
|
|
124
|
+
|
|
125
|
+
# Apply time range if specified
|
|
126
|
+
if time_range is not None:
|
|
127
|
+
ax.set_xlim(time_range[0] * multiplier, time_range[1] * multiplier)
|
|
128
|
+
|
|
129
|
+
# Labels
|
|
130
|
+
ax.set_xlabel(f"{xlabel} ({time_unit})")
|
|
131
|
+
ax.set_ylabel(ylabel)
|
|
132
|
+
|
|
133
|
+
if title:
|
|
134
|
+
ax.set_title(title)
|
|
135
|
+
elif trace.metadata.channel_name:
|
|
136
|
+
ax.set_title(f"Waveform - {trace.metadata.channel_name}")
|
|
137
|
+
|
|
138
|
+
if show_grid:
|
|
139
|
+
ax.grid(True, alpha=0.3)
|
|
140
|
+
|
|
141
|
+
if label:
|
|
142
|
+
ax.legend()
|
|
143
|
+
|
|
144
|
+
# Add measurement annotations
|
|
145
|
+
if show_measurements:
|
|
146
|
+
_add_measurement_annotations(ax, trace, show_measurements, time_unit, multiplier)
|
|
147
|
+
|
|
148
|
+
fig.tight_layout()
|
|
149
|
+
|
|
150
|
+
# Save if path provided
|
|
151
|
+
if save_path is not None:
|
|
152
|
+
fig.savefig(save_path, dpi=300, bbox_inches="tight")
|
|
153
|
+
|
|
154
|
+
# Show if requested
|
|
155
|
+
if show:
|
|
156
|
+
plt.show()
|
|
157
|
+
|
|
158
|
+
return fig
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def plot_multi_channel(
|
|
162
|
+
traces: list[WaveformTrace | DigitalTrace],
|
|
163
|
+
*,
|
|
164
|
+
names: list[str] | None = None,
|
|
165
|
+
shared_x: bool = True,
|
|
166
|
+
share_x: bool | None = None,
|
|
167
|
+
colors: list[str] | None = None,
|
|
168
|
+
time_unit: str = "auto",
|
|
169
|
+
show_grid: bool = True,
|
|
170
|
+
figsize: tuple[float, float] | None = None,
|
|
171
|
+
title: str | None = None,
|
|
172
|
+
) -> Figure:
|
|
173
|
+
"""Plot multiple channels in stacked subplots.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
traces: List of traces to plot.
|
|
177
|
+
names: Channel names for labels.
|
|
178
|
+
shared_x: Share x-axis across subplots.
|
|
179
|
+
share_x: Alias for shared_x (for compatibility).
|
|
180
|
+
colors: List of colors for each trace. If None, uses default cycle.
|
|
181
|
+
time_unit: Time unit ("s", "ms", "us", "ns", "auto").
|
|
182
|
+
show_grid: Show grid lines.
|
|
183
|
+
figsize: Figure size (width, height) in inches.
|
|
184
|
+
title: Overall figure title.
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
Matplotlib Figure object.
|
|
188
|
+
|
|
189
|
+
Raises:
|
|
190
|
+
ImportError: If matplotlib is not available.
|
|
191
|
+
|
|
192
|
+
Example:
|
|
193
|
+
>>> fig = plot_multi_channel([ch1, ch2, ch3], names=["CLK", "DATA", "CS"])
|
|
194
|
+
>>> plt.show()
|
|
195
|
+
"""
|
|
196
|
+
# Handle share_x alias
|
|
197
|
+
if share_x is not None:
|
|
198
|
+
shared_x = share_x
|
|
199
|
+
if not HAS_MATPLOTLIB:
|
|
200
|
+
raise ImportError("matplotlib is required for visualization")
|
|
201
|
+
|
|
202
|
+
n_channels = len(traces)
|
|
203
|
+
|
|
204
|
+
if names is None:
|
|
205
|
+
names = [f"CH{i + 1}" for i in range(n_channels)]
|
|
206
|
+
|
|
207
|
+
if figsize is None:
|
|
208
|
+
figsize = (10, 2 * n_channels)
|
|
209
|
+
|
|
210
|
+
fig, axes = plt.subplots(
|
|
211
|
+
n_channels,
|
|
212
|
+
1,
|
|
213
|
+
figsize=figsize,
|
|
214
|
+
sharex=shared_x,
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
if n_channels == 1:
|
|
218
|
+
axes = [axes]
|
|
219
|
+
|
|
220
|
+
# Auto-select time unit from first trace
|
|
221
|
+
if time_unit == "auto" and len(traces) > 0:
|
|
222
|
+
ref_trace = traces[0]
|
|
223
|
+
duration = len(ref_trace.data) * ref_trace.metadata.time_base
|
|
224
|
+
if duration < 1e-6:
|
|
225
|
+
time_unit = "ns"
|
|
226
|
+
elif duration < 1e-3:
|
|
227
|
+
time_unit = "us"
|
|
228
|
+
elif duration < 1:
|
|
229
|
+
time_unit = "ms"
|
|
230
|
+
else:
|
|
231
|
+
time_unit = "s"
|
|
232
|
+
|
|
233
|
+
time_multipliers = {"s": 1.0, "ms": 1e3, "us": 1e6, "ns": 1e9}
|
|
234
|
+
multiplier = time_multipliers.get(time_unit, 1.0)
|
|
235
|
+
|
|
236
|
+
for i, (trace, name, ax) in enumerate(zip(traces, names, axes, strict=False)):
|
|
237
|
+
time = trace.time_vector * multiplier
|
|
238
|
+
color = colors[i] if colors is not None and i < len(colors) else f"C{i}"
|
|
239
|
+
|
|
240
|
+
if isinstance(trace, WaveformTrace):
|
|
241
|
+
ax.plot(time, trace.data, color=color, linewidth=0.8)
|
|
242
|
+
ax.set_ylabel("V")
|
|
243
|
+
else:
|
|
244
|
+
# Digital trace - step plot
|
|
245
|
+
ax.step(time, trace.data.astype(int), color=color, where="post", linewidth=1.0)
|
|
246
|
+
ax.set_ylim(-0.1, 1.1)
|
|
247
|
+
ax.set_yticks([0, 1])
|
|
248
|
+
ax.set_yticklabels(["L", "H"])
|
|
249
|
+
|
|
250
|
+
ax.set_ylabel(name, rotation=0, ha="right", va="center")
|
|
251
|
+
|
|
252
|
+
if show_grid:
|
|
253
|
+
ax.grid(True, alpha=0.3)
|
|
254
|
+
|
|
255
|
+
# Only show x-label on bottom plot
|
|
256
|
+
if i == n_channels - 1:
|
|
257
|
+
ax.set_xlabel(f"Time ({time_unit})")
|
|
258
|
+
|
|
259
|
+
if title:
|
|
260
|
+
fig.suptitle(title)
|
|
261
|
+
|
|
262
|
+
fig.tight_layout()
|
|
263
|
+
return fig
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def plot_xy(
|
|
267
|
+
x_trace: WaveformTrace | NDArray[np.float64],
|
|
268
|
+
y_trace: WaveformTrace | NDArray[np.float64],
|
|
269
|
+
*,
|
|
270
|
+
ax: Axes | None = None,
|
|
271
|
+
color: str = "C0",
|
|
272
|
+
marker: str = "",
|
|
273
|
+
alpha: float = 0.7,
|
|
274
|
+
title: str | None = None,
|
|
275
|
+
) -> Figure:
|
|
276
|
+
"""Plot X-Y (Lissajous) diagram.
|
|
277
|
+
|
|
278
|
+
Args:
|
|
279
|
+
x_trace: X-axis waveform.
|
|
280
|
+
y_trace: Y-axis waveform.
|
|
281
|
+
ax: Matplotlib axes.
|
|
282
|
+
color: Line/marker color.
|
|
283
|
+
marker: Marker style.
|
|
284
|
+
alpha: Transparency.
|
|
285
|
+
title: Plot title.
|
|
286
|
+
|
|
287
|
+
Returns:
|
|
288
|
+
Matplotlib Figure object.
|
|
289
|
+
|
|
290
|
+
Raises:
|
|
291
|
+
ImportError: If matplotlib is not available.
|
|
292
|
+
ValueError: If axes has no associated figure.
|
|
293
|
+
|
|
294
|
+
Example:
|
|
295
|
+
>>> fig = plot_xy(ch1, ch2) # Phase relationship
|
|
296
|
+
"""
|
|
297
|
+
if not HAS_MATPLOTLIB:
|
|
298
|
+
raise ImportError("matplotlib is required for visualization")
|
|
299
|
+
|
|
300
|
+
if ax is None:
|
|
301
|
+
fig, ax = plt.subplots(figsize=(6, 6))
|
|
302
|
+
else:
|
|
303
|
+
fig_temp = ax.get_figure()
|
|
304
|
+
if fig_temp is None:
|
|
305
|
+
raise ValueError("Axes must have an associated figure")
|
|
306
|
+
fig = cast("Figure", fig_temp)
|
|
307
|
+
|
|
308
|
+
x_data = x_trace.data if isinstance(x_trace, WaveformTrace) else x_trace
|
|
309
|
+
y_data = y_trace.data if isinstance(y_trace, WaveformTrace) else y_trace
|
|
310
|
+
|
|
311
|
+
# Ensure same length
|
|
312
|
+
min_len = min(len(x_data), len(y_data))
|
|
313
|
+
x_data = x_data[:min_len]
|
|
314
|
+
y_data = y_data[:min_len]
|
|
315
|
+
|
|
316
|
+
ax.plot(x_data, y_data, color=color, marker=marker, alpha=alpha, linewidth=0.5)
|
|
317
|
+
|
|
318
|
+
ax.set_xlabel("X (V)")
|
|
319
|
+
ax.set_ylabel("Y (V)")
|
|
320
|
+
ax.set_aspect("equal")
|
|
321
|
+
ax.grid(True, alpha=0.3)
|
|
322
|
+
|
|
323
|
+
if title:
|
|
324
|
+
ax.set_title(title)
|
|
325
|
+
|
|
326
|
+
fig.tight_layout()
|
|
327
|
+
return fig
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def _add_measurement_annotations(
|
|
331
|
+
ax: Axes,
|
|
332
|
+
trace: WaveformTrace,
|
|
333
|
+
measurements: dict[str, Any],
|
|
334
|
+
time_unit: str,
|
|
335
|
+
multiplier: float,
|
|
336
|
+
) -> None:
|
|
337
|
+
"""Add measurement annotations to plot."""
|
|
338
|
+
# Create annotation text
|
|
339
|
+
text_lines = []
|
|
340
|
+
|
|
341
|
+
for name, value in measurements.items():
|
|
342
|
+
if isinstance(value, dict):
|
|
343
|
+
val = value.get("value", value)
|
|
344
|
+
unit = value.get("unit", "")
|
|
345
|
+
if isinstance(val, float) and not np.isnan(val):
|
|
346
|
+
text_lines.append(f"{name}: {val:.4g} {unit}")
|
|
347
|
+
elif isinstance(value, float) and not np.isnan(value):
|
|
348
|
+
text_lines.append(f"{name}: {value:.4g}")
|
|
349
|
+
|
|
350
|
+
if text_lines:
|
|
351
|
+
text = "\n".join(text_lines)
|
|
352
|
+
ax.annotate(
|
|
353
|
+
text,
|
|
354
|
+
xy=(0.02, 0.98),
|
|
355
|
+
xycoords="axes fraction",
|
|
356
|
+
verticalalignment="top",
|
|
357
|
+
fontfamily="monospace",
|
|
358
|
+
fontsize=8,
|
|
359
|
+
bbox={"boxstyle": "round", "facecolor": "wheat", "alpha": 0.8},
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
__all__ = [
|
|
364
|
+
"plot_multi_channel",
|
|
365
|
+
"plot_waveform",
|
|
366
|
+
"plot_xy",
|
|
367
|
+
]
|