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,553 @@
|
|
|
1
|
+
"""Specialized plot types for protocol analysis and state visualization.
|
|
2
|
+
|
|
3
|
+
This module provides specialized visualizations including protocol timing
|
|
4
|
+
diagrams, state machine views, and bus transaction timelines.
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
>>> from oscura.visualization.specialized import plot_protocol_timing
|
|
9
|
+
>>> fig = plot_protocol_timing(decoded_packets, sample_rate=1e6)
|
|
10
|
+
|
|
11
|
+
References:
|
|
12
|
+
- Wavedrom-style digital waveform rendering
|
|
13
|
+
- State machine diagram standards
|
|
14
|
+
- Bus protocol visualization best practices
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from dataclasses import dataclass
|
|
20
|
+
from typing import TYPE_CHECKING, Literal
|
|
21
|
+
|
|
22
|
+
import numpy as np
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from matplotlib.axes import Axes
|
|
26
|
+
from matplotlib.figure import Figure
|
|
27
|
+
from numpy.typing import NDArray
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
import matplotlib.pyplot as plt
|
|
31
|
+
from matplotlib import patches
|
|
32
|
+
|
|
33
|
+
HAS_MATPLOTLIB = True
|
|
34
|
+
except ImportError:
|
|
35
|
+
HAS_MATPLOTLIB = False
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class ProtocolSignal:
|
|
40
|
+
"""Protocol signal for timing diagram.
|
|
41
|
+
|
|
42
|
+
Attributes:
|
|
43
|
+
name: Signal name
|
|
44
|
+
data: Signal data (0/1 for digital, values for analog)
|
|
45
|
+
type: Signal type ("digital", "clock", "bus", "analog")
|
|
46
|
+
transitions: List of transition times
|
|
47
|
+
annotations: Dict of time -> annotation text
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
name: str
|
|
51
|
+
data: NDArray[np.float64]
|
|
52
|
+
type: Literal["digital", "clock", "bus", "analog"] = "digital"
|
|
53
|
+
transitions: list[float] | None = None
|
|
54
|
+
annotations: dict[float, str] | None = None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass
|
|
58
|
+
class StateTransition:
|
|
59
|
+
"""State machine transition.
|
|
60
|
+
|
|
61
|
+
Attributes:
|
|
62
|
+
from_state: Source state name
|
|
63
|
+
to_state: Target state name
|
|
64
|
+
condition: Transition condition/label
|
|
65
|
+
style: Line style ("solid", "dashed", "dotted")
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
from_state: str
|
|
69
|
+
to_state: str
|
|
70
|
+
condition: str = ""
|
|
71
|
+
style: Literal["solid", "dashed", "dotted"] = "solid"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def plot_protocol_timing(
|
|
75
|
+
signals: list[ProtocolSignal],
|
|
76
|
+
sample_rate: float,
|
|
77
|
+
*,
|
|
78
|
+
time_range: tuple[float, float] | None = None,
|
|
79
|
+
time_unit: str = "auto",
|
|
80
|
+
style: Literal["wavedrom", "classic"] = "wavedrom",
|
|
81
|
+
figsize: tuple[float, float] | None = None,
|
|
82
|
+
title: str | None = None,
|
|
83
|
+
) -> Figure:
|
|
84
|
+
"""Plot protocol timing diagram in wavedrom style.
|
|
85
|
+
|
|
86
|
+
Creates a timing diagram showing digital signals, clock edges, and
|
|
87
|
+
bus transactions with annotations for protocol events.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
signals: List of ProtocolSignal objects to plot.
|
|
91
|
+
sample_rate: Sample rate in Hz.
|
|
92
|
+
time_range: Time range to plot (t_min, t_max) in seconds. None = full range.
|
|
93
|
+
time_unit: Time unit for x-axis ("s", "ms", "us", "ns", "auto").
|
|
94
|
+
style: Diagram style ("wavedrom" = clean digital, "classic" = traditional).
|
|
95
|
+
figsize: Figure size (width, height). Auto-calculated if None.
|
|
96
|
+
title: Plot title.
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
Matplotlib Figure object.
|
|
100
|
+
|
|
101
|
+
Raises:
|
|
102
|
+
ImportError: If matplotlib is not available.
|
|
103
|
+
ValueError: If signals list is empty.
|
|
104
|
+
|
|
105
|
+
Example:
|
|
106
|
+
>>> sda = ProtocolSignal("SDA", sda_data, type="digital")
|
|
107
|
+
>>> scl = ProtocolSignal("SCL", scl_data, type="clock")
|
|
108
|
+
>>> fig = plot_protocol_timing(
|
|
109
|
+
... [scl, sda],
|
|
110
|
+
... sample_rate=1e6,
|
|
111
|
+
... style="wavedrom",
|
|
112
|
+
... title="I2C Transaction"
|
|
113
|
+
... )
|
|
114
|
+
|
|
115
|
+
References:
|
|
116
|
+
VIS-021: Specialized - Protocol Timing Diagram
|
|
117
|
+
Wavedrom digital waveform rendering
|
|
118
|
+
"""
|
|
119
|
+
if not HAS_MATPLOTLIB:
|
|
120
|
+
raise ImportError("matplotlib is required for visualization")
|
|
121
|
+
|
|
122
|
+
if len(signals) == 0:
|
|
123
|
+
raise ValueError("signals list cannot be empty")
|
|
124
|
+
|
|
125
|
+
n_signals = len(signals)
|
|
126
|
+
|
|
127
|
+
# Auto-calculate figure size
|
|
128
|
+
if figsize is None:
|
|
129
|
+
width = 12
|
|
130
|
+
height = max(4, n_signals * 0.8 + 1)
|
|
131
|
+
figsize = (width, height)
|
|
132
|
+
|
|
133
|
+
fig, axes = plt.subplots(
|
|
134
|
+
n_signals,
|
|
135
|
+
1,
|
|
136
|
+
figsize=figsize,
|
|
137
|
+
sharex=True,
|
|
138
|
+
gridspec_kw={"hspace": 0.1},
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
if n_signals == 1:
|
|
142
|
+
axes = [axes]
|
|
143
|
+
|
|
144
|
+
# Determine time range
|
|
145
|
+
if time_range is None:
|
|
146
|
+
max_len = max(len(sig.data) for sig in signals)
|
|
147
|
+
t_min = 0.0
|
|
148
|
+
t_max = max_len / sample_rate
|
|
149
|
+
else:
|
|
150
|
+
t_min, t_max = time_range
|
|
151
|
+
|
|
152
|
+
# Select time unit
|
|
153
|
+
if time_unit == "auto":
|
|
154
|
+
time_range_val = t_max - t_min
|
|
155
|
+
if time_range_val < 1e-6:
|
|
156
|
+
time_unit = "ns"
|
|
157
|
+
time_mult = 1e9
|
|
158
|
+
elif time_range_val < 1e-3:
|
|
159
|
+
time_unit = "us"
|
|
160
|
+
time_mult = 1e6
|
|
161
|
+
elif time_range_val < 1:
|
|
162
|
+
time_unit = "ms"
|
|
163
|
+
time_mult = 1e3
|
|
164
|
+
else:
|
|
165
|
+
time_unit = "s"
|
|
166
|
+
time_mult = 1.0
|
|
167
|
+
else:
|
|
168
|
+
time_mult = {"s": 1.0, "ms": 1e3, "us": 1e6, "ns": 1e9}.get(time_unit, 1.0)
|
|
169
|
+
|
|
170
|
+
# Plot each signal
|
|
171
|
+
for _idx, (signal, ax) in enumerate(zip(signals, axes, strict=False)):
|
|
172
|
+
# Create time vector
|
|
173
|
+
time = np.arange(len(signal.data)) / sample_rate * time_mult
|
|
174
|
+
|
|
175
|
+
# Filter to time range
|
|
176
|
+
mask = (time >= t_min * time_mult) & (time <= t_max * time_mult)
|
|
177
|
+
time = time[mask]
|
|
178
|
+
data = signal.data[mask]
|
|
179
|
+
|
|
180
|
+
if style == "wavedrom":
|
|
181
|
+
_plot_wavedrom_signal(ax, time, data, signal)
|
|
182
|
+
else:
|
|
183
|
+
_plot_classic_signal(ax, time, data, signal)
|
|
184
|
+
|
|
185
|
+
# Add signal name label
|
|
186
|
+
ax.set_ylabel(signal.name, rotation=0, ha="right", va="center", fontsize=10)
|
|
187
|
+
ax.set_ylim(-0.2, 1.3)
|
|
188
|
+
|
|
189
|
+
# Remove y-axis ticks
|
|
190
|
+
ax.set_yticks([])
|
|
191
|
+
|
|
192
|
+
# Grid for timing
|
|
193
|
+
ax.grid(True, axis="x", alpha=0.3, linestyle=":")
|
|
194
|
+
|
|
195
|
+
# Add annotations
|
|
196
|
+
if signal.annotations:
|
|
197
|
+
for t, text in signal.annotations.items():
|
|
198
|
+
if t_min <= t <= t_max:
|
|
199
|
+
ax.annotate(
|
|
200
|
+
text,
|
|
201
|
+
xy=(t * time_mult, 1.2),
|
|
202
|
+
fontsize=8,
|
|
203
|
+
ha="center",
|
|
204
|
+
bbox={"boxstyle": "round,pad=0.3", "facecolor": "yellow", "alpha": 0.7},
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
# X-axis label only on bottom plot
|
|
208
|
+
axes[-1].set_xlabel(f"Time ({time_unit})", fontsize=11)
|
|
209
|
+
|
|
210
|
+
if title:
|
|
211
|
+
fig.suptitle(title, fontsize=14, y=0.98)
|
|
212
|
+
|
|
213
|
+
fig.tight_layout()
|
|
214
|
+
return fig
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _plot_wavedrom_signal(
|
|
218
|
+
ax: Axes,
|
|
219
|
+
time: NDArray[np.float64],
|
|
220
|
+
data: NDArray[np.float64],
|
|
221
|
+
signal: ProtocolSignal,
|
|
222
|
+
) -> None:
|
|
223
|
+
"""Plot signal in wavedrom style (clean digital waveform)."""
|
|
224
|
+
if signal.type == "clock":
|
|
225
|
+
# Clock signal: square wave
|
|
226
|
+
for i in range(len(time) - 1):
|
|
227
|
+
level = 1 if data[i] > 0.5 else 0
|
|
228
|
+
ax.plot(
|
|
229
|
+
[time[i], time[i + 1]],
|
|
230
|
+
[level, level],
|
|
231
|
+
"b-",
|
|
232
|
+
linewidth=1.5,
|
|
233
|
+
)
|
|
234
|
+
# Vertical transition
|
|
235
|
+
if i < len(time) - 1:
|
|
236
|
+
next_level = 1 if data[i + 1] > 0.5 else 0
|
|
237
|
+
if level != next_level:
|
|
238
|
+
ax.plot(
|
|
239
|
+
[time[i + 1], time[i + 1]],
|
|
240
|
+
[level, next_level],
|
|
241
|
+
"b-",
|
|
242
|
+
linewidth=1.5,
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
elif signal.type == "digital":
|
|
246
|
+
# Digital signal: step function with transitions
|
|
247
|
+
for i in range(len(time) - 1):
|
|
248
|
+
level = 1 if data[i] > 0.5 else 0
|
|
249
|
+
ax.plot(
|
|
250
|
+
[time[i], time[i + 1]],
|
|
251
|
+
[level, level],
|
|
252
|
+
"k-",
|
|
253
|
+
linewidth=1.5,
|
|
254
|
+
)
|
|
255
|
+
# Vertical transition with slight slant for visual clarity
|
|
256
|
+
if i < len(time) - 1:
|
|
257
|
+
next_level = 1 if data[i + 1] > 0.5 else 0
|
|
258
|
+
if level != next_level:
|
|
259
|
+
transition_width = (time[i + 1] - time[i]) * 0.1
|
|
260
|
+
ax.plot(
|
|
261
|
+
[time[i + 1] - transition_width, time[i + 1]],
|
|
262
|
+
[level, next_level],
|
|
263
|
+
"k-",
|
|
264
|
+
linewidth=1.5,
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
elif signal.type == "bus":
|
|
268
|
+
# Bus signal: show as high-impedance or data values
|
|
269
|
+
ax.fill_between(time, 0.3, 0.7, alpha=0.3, color="gray")
|
|
270
|
+
ax.plot(time, np.full_like(time, 0.5), "k-", linewidth=0.5)
|
|
271
|
+
|
|
272
|
+
else:
|
|
273
|
+
# Analog signal
|
|
274
|
+
ax.plot(time, data, "r-", linewidth=1.2)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _plot_classic_signal(
|
|
278
|
+
ax: Axes,
|
|
279
|
+
time: NDArray[np.float64],
|
|
280
|
+
data: NDArray[np.float64],
|
|
281
|
+
signal: ProtocolSignal,
|
|
282
|
+
) -> None:
|
|
283
|
+
"""Plot signal in classic style (traditional oscilloscope-like)."""
|
|
284
|
+
ax.plot(time, data, "b-", linewidth=1.2)
|
|
285
|
+
ax.axhline(0.5, color="gray", linestyle="--", linewidth=0.5, alpha=0.5)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def plot_state_machine(
|
|
289
|
+
states: list[str],
|
|
290
|
+
transitions: list[StateTransition],
|
|
291
|
+
*,
|
|
292
|
+
initial_state: str | None = None,
|
|
293
|
+
final_states: list[str] | None = None,
|
|
294
|
+
layout: Literal["circular", "hierarchical", "force"] = "circular",
|
|
295
|
+
figsize: tuple[float, float] = (10, 8),
|
|
296
|
+
title: str | None = None,
|
|
297
|
+
) -> Figure:
|
|
298
|
+
"""Plot state machine diagram.
|
|
299
|
+
|
|
300
|
+
Creates a state diagram showing states as nodes and transitions as
|
|
301
|
+
directed edges with condition labels.
|
|
302
|
+
|
|
303
|
+
Args:
|
|
304
|
+
states: List of state names.
|
|
305
|
+
transitions: List of StateTransition objects.
|
|
306
|
+
initial_state: Initial state (marked with double circle).
|
|
307
|
+
final_states: List of final states (marked with double circle).
|
|
308
|
+
layout: Layout algorithm for state positioning.
|
|
309
|
+
figsize: Figure size (width, height).
|
|
310
|
+
title: Plot title.
|
|
311
|
+
|
|
312
|
+
Returns:
|
|
313
|
+
Matplotlib Figure object.
|
|
314
|
+
|
|
315
|
+
Raises:
|
|
316
|
+
ImportError: If matplotlib is not available.
|
|
317
|
+
|
|
318
|
+
Example:
|
|
319
|
+
>>> states = ["IDLE", "ACTIVE", "WAIT", "DONE"]
|
|
320
|
+
>>> transitions = [
|
|
321
|
+
... StateTransition("IDLE", "ACTIVE", "START"),
|
|
322
|
+
... StateTransition("ACTIVE", "WAIT", "BUSY"),
|
|
323
|
+
... StateTransition("WAIT", "ACTIVE", "RETRY"),
|
|
324
|
+
... StateTransition("ACTIVE", "DONE", "COMPLETE"),
|
|
325
|
+
... ]
|
|
326
|
+
>>> fig = plot_state_machine(
|
|
327
|
+
... states, transitions, initial_state="IDLE", final_states=["DONE"]
|
|
328
|
+
... )
|
|
329
|
+
|
|
330
|
+
References:
|
|
331
|
+
VIS-022: Specialized - State Machine View
|
|
332
|
+
"""
|
|
333
|
+
if not HAS_MATPLOTLIB:
|
|
334
|
+
raise ImportError("matplotlib is required for visualization")
|
|
335
|
+
|
|
336
|
+
fig, ax = plt.subplots(figsize=figsize)
|
|
337
|
+
|
|
338
|
+
# Calculate state positions using selected layout
|
|
339
|
+
positions = _calculate_state_positions(states, layout)
|
|
340
|
+
|
|
341
|
+
# Draw states as circles
|
|
342
|
+
state_radius = 0.15
|
|
343
|
+
|
|
344
|
+
for state, (x, y) in positions.items():
|
|
345
|
+
# Draw state circle
|
|
346
|
+
circle = patches.Circle(
|
|
347
|
+
(x, y),
|
|
348
|
+
state_radius,
|
|
349
|
+
fill=True,
|
|
350
|
+
facecolor="lightblue",
|
|
351
|
+
edgecolor="black",
|
|
352
|
+
linewidth=2.0,
|
|
353
|
+
)
|
|
354
|
+
ax.add_patch(circle)
|
|
355
|
+
|
|
356
|
+
# Mark initial state with double circle
|
|
357
|
+
if state == initial_state:
|
|
358
|
+
outer_circle = patches.Circle(
|
|
359
|
+
(x, y),
|
|
360
|
+
state_radius * 1.2,
|
|
361
|
+
fill=False,
|
|
362
|
+
edgecolor="black",
|
|
363
|
+
linewidth=2.0,
|
|
364
|
+
)
|
|
365
|
+
ax.add_patch(outer_circle)
|
|
366
|
+
|
|
367
|
+
# Mark final states with double circle
|
|
368
|
+
if final_states and state in final_states:
|
|
369
|
+
inner_circle = patches.Circle(
|
|
370
|
+
(x, y),
|
|
371
|
+
state_radius * 0.8,
|
|
372
|
+
fill=False,
|
|
373
|
+
edgecolor="black",
|
|
374
|
+
linewidth=2.0,
|
|
375
|
+
)
|
|
376
|
+
ax.add_patch(inner_circle)
|
|
377
|
+
|
|
378
|
+
# Add state label
|
|
379
|
+
ax.text(
|
|
380
|
+
x,
|
|
381
|
+
y,
|
|
382
|
+
state,
|
|
383
|
+
ha="center",
|
|
384
|
+
va="center",
|
|
385
|
+
fontsize=10,
|
|
386
|
+
fontweight="bold",
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
# Draw transitions as arrows
|
|
390
|
+
for trans in transitions:
|
|
391
|
+
if trans.from_state not in positions or trans.to_state not in positions:
|
|
392
|
+
continue
|
|
393
|
+
|
|
394
|
+
x1, y1 = positions[trans.from_state]
|
|
395
|
+
x2, y2 = positions[trans.to_state]
|
|
396
|
+
|
|
397
|
+
# Calculate arrow start/end on circle perimeter
|
|
398
|
+
dx = x2 - x1
|
|
399
|
+
dy = y2 - y1
|
|
400
|
+
dist = np.sqrt(dx**2 + dy**2)
|
|
401
|
+
|
|
402
|
+
if dist < 1e-6:
|
|
403
|
+
# Self-loop
|
|
404
|
+
_draw_self_loop(ax, x1, y1, state_radius, trans.condition)
|
|
405
|
+
continue
|
|
406
|
+
|
|
407
|
+
# Normalize
|
|
408
|
+
dx_norm = dx / dist
|
|
409
|
+
dy_norm = dy / dist
|
|
410
|
+
|
|
411
|
+
# Arrow start/end on circle edges
|
|
412
|
+
arrow_start_x = x1 + dx_norm * state_radius
|
|
413
|
+
arrow_start_y = y1 + dy_norm * state_radius
|
|
414
|
+
arrow_end_x = x2 - dx_norm * state_radius
|
|
415
|
+
arrow_end_y = y2 - dy_norm * state_radius
|
|
416
|
+
|
|
417
|
+
# Line style
|
|
418
|
+
linestyle = {
|
|
419
|
+
"solid": "-",
|
|
420
|
+
"dashed": "--",
|
|
421
|
+
"dotted": ":",
|
|
422
|
+
}.get(trans.style, "-")
|
|
423
|
+
|
|
424
|
+
# Draw arrow
|
|
425
|
+
ax.annotate(
|
|
426
|
+
"",
|
|
427
|
+
xy=(arrow_end_x, arrow_end_y),
|
|
428
|
+
xytext=(arrow_start_x, arrow_start_y),
|
|
429
|
+
arrowprops={
|
|
430
|
+
"arrowstyle": "->",
|
|
431
|
+
"lw": 1.5,
|
|
432
|
+
"linestyle": linestyle,
|
|
433
|
+
"color": "black",
|
|
434
|
+
},
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
# Add transition label
|
|
438
|
+
if trans.condition:
|
|
439
|
+
mid_x = (x1 + x2) / 2
|
|
440
|
+
mid_y = (y1 + y2) / 2
|
|
441
|
+
ax.text(
|
|
442
|
+
mid_x,
|
|
443
|
+
mid_y,
|
|
444
|
+
trans.condition,
|
|
445
|
+
fontsize=8,
|
|
446
|
+
ha="center",
|
|
447
|
+
bbox={
|
|
448
|
+
"boxstyle": "round,pad=0.3",
|
|
449
|
+
"facecolor": "white",
|
|
450
|
+
"edgecolor": "gray",
|
|
451
|
+
"alpha": 0.9,
|
|
452
|
+
},
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
# Set axis properties
|
|
456
|
+
ax.set_aspect("equal")
|
|
457
|
+
ax.axis("off")
|
|
458
|
+
ax.set_xlim(-0.2, 1.2)
|
|
459
|
+
ax.set_ylim(-0.2, 1.2)
|
|
460
|
+
|
|
461
|
+
if title:
|
|
462
|
+
ax.set_title(title, fontsize=14, pad=20)
|
|
463
|
+
|
|
464
|
+
fig.tight_layout()
|
|
465
|
+
return fig
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
def _calculate_state_positions(
|
|
469
|
+
states: list[str],
|
|
470
|
+
layout: str,
|
|
471
|
+
) -> dict[str, tuple[float, float]]:
|
|
472
|
+
"""Calculate state positions using layout algorithm."""
|
|
473
|
+
n_states = len(states)
|
|
474
|
+
positions = {}
|
|
475
|
+
|
|
476
|
+
if layout == "circular":
|
|
477
|
+
# Arrange states in a circle
|
|
478
|
+
angle_step = 2 * np.pi / n_states
|
|
479
|
+
for i, state in enumerate(states):
|
|
480
|
+
angle = i * angle_step
|
|
481
|
+
x = 0.5 + 0.4 * np.cos(angle)
|
|
482
|
+
y = 0.5 + 0.4 * np.sin(angle)
|
|
483
|
+
positions[state] = (x, y)
|
|
484
|
+
|
|
485
|
+
elif layout == "hierarchical":
|
|
486
|
+
# Arrange in rows (simplified hierarchical)
|
|
487
|
+
states_per_row = int(np.ceil(np.sqrt(n_states)))
|
|
488
|
+
for i, state in enumerate(states):
|
|
489
|
+
row = i // states_per_row
|
|
490
|
+
col = i % states_per_row
|
|
491
|
+
x = (col + 0.5) / states_per_row
|
|
492
|
+
y = 1.0 - (row + 0.5) / np.ceil(n_states / states_per_row)
|
|
493
|
+
positions[state] = (x, y)
|
|
494
|
+
|
|
495
|
+
else: # force-directed (simplified)
|
|
496
|
+
# Use random positions as a placeholder for true force-directed layout
|
|
497
|
+
np.random.seed(42)
|
|
498
|
+
for i, state in enumerate(states): # noqa: B007
|
|
499
|
+
x = 0.2 + 0.6 * np.random.rand()
|
|
500
|
+
y = 0.2 + 0.6 * np.random.rand()
|
|
501
|
+
positions[state] = (x, y)
|
|
502
|
+
|
|
503
|
+
return positions
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
def _draw_self_loop(
|
|
507
|
+
ax: Axes,
|
|
508
|
+
x: float,
|
|
509
|
+
y: float,
|
|
510
|
+
radius: float,
|
|
511
|
+
label: str,
|
|
512
|
+
) -> None:
|
|
513
|
+
"""Draw self-loop transition on state."""
|
|
514
|
+
# Draw arc above state
|
|
515
|
+
arc = patches.Arc(
|
|
516
|
+
(x, y + radius),
|
|
517
|
+
width=radius * 1.5,
|
|
518
|
+
height=radius * 1.5,
|
|
519
|
+
angle=0,
|
|
520
|
+
theta1=0,
|
|
521
|
+
theta2=180,
|
|
522
|
+
linewidth=1.5,
|
|
523
|
+
edgecolor="black",
|
|
524
|
+
fill=False,
|
|
525
|
+
)
|
|
526
|
+
ax.add_patch(arc)
|
|
527
|
+
|
|
528
|
+
# Add arrow head
|
|
529
|
+
ax.annotate(
|
|
530
|
+
"",
|
|
531
|
+
xy=(x - radius * 0.7, y + radius * 0.3),
|
|
532
|
+
xytext=(x - radius * 0.5, y + radius * 0.5),
|
|
533
|
+
arrowprops={"arrowstyle": "->", "lw": 1.5, "color": "black"},
|
|
534
|
+
)
|
|
535
|
+
|
|
536
|
+
# Add label
|
|
537
|
+
if label:
|
|
538
|
+
ax.text(
|
|
539
|
+
x,
|
|
540
|
+
y + radius * 2.2,
|
|
541
|
+
label,
|
|
542
|
+
fontsize=8,
|
|
543
|
+
ha="center",
|
|
544
|
+
bbox={"boxstyle": "round,pad=0.2", "facecolor": "white", "alpha": 0.9},
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
__all__ = [
|
|
549
|
+
"ProtocolSignal",
|
|
550
|
+
"StateTransition",
|
|
551
|
+
"plot_protocol_timing",
|
|
552
|
+
"plot_state_machine",
|
|
553
|
+
]
|