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,808 @@
|
|
|
1
|
+
"""Signal Integrity Visualization Functions.
|
|
2
|
+
|
|
3
|
+
This module provides visualization functions for signal integrity analysis
|
|
4
|
+
including TDR impedance plots, S-parameter displays, setup/hold timing
|
|
5
|
+
diagrams, and eye diagram enhancements.
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
>>> from oscura.visualization.signal_integrity import plot_tdr, plot_sparams
|
|
9
|
+
>>> fig = plot_tdr(impedance_profile, distance_axis)
|
|
10
|
+
>>> fig = plot_sparams(frequencies, s11, s21)
|
|
11
|
+
|
|
12
|
+
References:
|
|
13
|
+
- IEEE 370-2020: Electrical Characterization of Printed Circuit Board
|
|
14
|
+
- TDR impedance measurement best practices
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import TYPE_CHECKING, Any, cast
|
|
21
|
+
|
|
22
|
+
import numpy as np
|
|
23
|
+
|
|
24
|
+
try:
|
|
25
|
+
import matplotlib.pyplot as plt
|
|
26
|
+
|
|
27
|
+
HAS_MATPLOTLIB = True
|
|
28
|
+
except ImportError:
|
|
29
|
+
HAS_MATPLOTLIB = False
|
|
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
|
+
__all__ = [
|
|
37
|
+
"plot_setup_hold_timing",
|
|
38
|
+
"plot_sparams_magnitude",
|
|
39
|
+
"plot_sparams_phase",
|
|
40
|
+
"plot_tdr",
|
|
41
|
+
"plot_timing_margin",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def plot_tdr(
|
|
46
|
+
impedance: NDArray[np.floating[Any]],
|
|
47
|
+
distance: NDArray[np.floating[Any]],
|
|
48
|
+
*,
|
|
49
|
+
z0: float = 50.0,
|
|
50
|
+
ax: Axes | None = None,
|
|
51
|
+
figsize: tuple[float, float] = (12, 6),
|
|
52
|
+
title: str | None = None,
|
|
53
|
+
distance_unit: str = "auto",
|
|
54
|
+
show_reference: bool = True,
|
|
55
|
+
show_discontinuities: bool = True,
|
|
56
|
+
discontinuity_threshold: float = 5.0,
|
|
57
|
+
show: bool = True,
|
|
58
|
+
save_path: str | Path | None = None,
|
|
59
|
+
) -> Figure:
|
|
60
|
+
"""Plot TDR impedance profile vs distance.
|
|
61
|
+
|
|
62
|
+
Creates a Time Domain Reflectometry impedance plot showing impedance
|
|
63
|
+
as a function of distance along a transmission line, with annotations
|
|
64
|
+
for discontinuities and reference impedance.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
impedance: Impedance values in Ohms.
|
|
68
|
+
distance: Distance values (in meters).
|
|
69
|
+
z0: Reference impedance (Ohms) for the reference line.
|
|
70
|
+
ax: Matplotlib axes. If None, creates new figure.
|
|
71
|
+
figsize: Figure size in inches (only if ax is None).
|
|
72
|
+
title: Plot title.
|
|
73
|
+
distance_unit: Distance unit ("m", "cm", "mm", "auto").
|
|
74
|
+
show_reference: Show reference impedance line at z0.
|
|
75
|
+
show_discontinuities: Annotate significant discontinuities.
|
|
76
|
+
discontinuity_threshold: Impedance change threshold (Ohms) for marking.
|
|
77
|
+
show: Display plot interactively.
|
|
78
|
+
save_path: Save plot to file.
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
Matplotlib Figure object.
|
|
82
|
+
|
|
83
|
+
Raises:
|
|
84
|
+
ImportError: If matplotlib is not available.
|
|
85
|
+
ValueError: If input arrays have different lengths.
|
|
86
|
+
|
|
87
|
+
Example:
|
|
88
|
+
>>> z_profile = np.array([50, 50, 75, 75, 50, 50])
|
|
89
|
+
>>> dist = np.linspace(0, 0.5, 6) # 0 to 50 cm
|
|
90
|
+
>>> fig = plot_tdr(z_profile, dist, z0=50, show=False)
|
|
91
|
+
>>> fig.savefig("tdr_impedance.png")
|
|
92
|
+
"""
|
|
93
|
+
if not HAS_MATPLOTLIB:
|
|
94
|
+
raise ImportError("matplotlib is required for visualization")
|
|
95
|
+
|
|
96
|
+
if len(impedance) != len(distance):
|
|
97
|
+
raise ValueError(
|
|
98
|
+
f"impedance and distance must have same length "
|
|
99
|
+
f"(got {len(impedance)} and {len(distance)})"
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
# Create figure if needed
|
|
103
|
+
if ax is None:
|
|
104
|
+
fig, ax = plt.subplots(figsize=figsize)
|
|
105
|
+
else:
|
|
106
|
+
fig_temp = ax.get_figure()
|
|
107
|
+
if fig_temp is None:
|
|
108
|
+
raise ValueError("Axes must have an associated figure")
|
|
109
|
+
fig = cast("Figure", fig_temp)
|
|
110
|
+
|
|
111
|
+
# Convert distance units
|
|
112
|
+
if distance_unit == "auto":
|
|
113
|
+
max_dist = np.max(distance)
|
|
114
|
+
if max_dist < 0.01:
|
|
115
|
+
distance_unit = "mm"
|
|
116
|
+
distance_mult = 1000.0
|
|
117
|
+
elif max_dist < 1.0:
|
|
118
|
+
distance_unit = "cm"
|
|
119
|
+
distance_mult = 100.0
|
|
120
|
+
else:
|
|
121
|
+
distance_unit = "m"
|
|
122
|
+
distance_mult = 1.0
|
|
123
|
+
else:
|
|
124
|
+
distance_mult = {"m": 1.0, "cm": 100.0, "mm": 1000.0}.get(distance_unit, 1.0)
|
|
125
|
+
|
|
126
|
+
dist_scaled = distance * distance_mult
|
|
127
|
+
|
|
128
|
+
# Clip impedance for display (handle inf values)
|
|
129
|
+
impedance_display = np.clip(impedance, 0, 500)
|
|
130
|
+
|
|
131
|
+
# Plot impedance profile
|
|
132
|
+
ax.plot(dist_scaled, impedance_display, "b-", linewidth=2, label="Impedance")
|
|
133
|
+
|
|
134
|
+
# Fill regions based on impedance deviation from z0
|
|
135
|
+
for i in range(len(dist_scaled) - 1):
|
|
136
|
+
z = impedance_display[i]
|
|
137
|
+
if z > z0 + discontinuity_threshold:
|
|
138
|
+
color = "#FFA500" # Orange for high-Z
|
|
139
|
+
alpha = 0.3
|
|
140
|
+
elif z < z0 - discontinuity_threshold:
|
|
141
|
+
color = "#1E90FF" # Blue for low-Z
|
|
142
|
+
alpha = 0.3
|
|
143
|
+
else:
|
|
144
|
+
color = "#90EE90" # Light green for matched
|
|
145
|
+
alpha = 0.2
|
|
146
|
+
|
|
147
|
+
ax.fill_between(
|
|
148
|
+
[dist_scaled[i], dist_scaled[i + 1]],
|
|
149
|
+
[z0, z0],
|
|
150
|
+
[z, impedance_display[i + 1]],
|
|
151
|
+
color=color,
|
|
152
|
+
alpha=alpha,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
# Reference line
|
|
156
|
+
if show_reference:
|
|
157
|
+
ax.axhline(z0, color="gray", linestyle="--", linewidth=1.5, label=f"Z0 = {z0} Ω")
|
|
158
|
+
|
|
159
|
+
# Find and annotate discontinuities
|
|
160
|
+
if show_discontinuities:
|
|
161
|
+
# Find significant changes
|
|
162
|
+
z_diff = np.abs(np.diff(impedance_display))
|
|
163
|
+
discontinuities = np.where(z_diff > discontinuity_threshold)[0]
|
|
164
|
+
|
|
165
|
+
for idx in discontinuities:
|
|
166
|
+
z_before = impedance_display[idx]
|
|
167
|
+
z_after = impedance_display[idx + 1]
|
|
168
|
+
d = dist_scaled[idx]
|
|
169
|
+
|
|
170
|
+
# Determine discontinuity type
|
|
171
|
+
if z_after > z_before + discontinuity_threshold:
|
|
172
|
+
disc_type = "High-Z"
|
|
173
|
+
color = "orange"
|
|
174
|
+
elif z_after < z_before - discontinuity_threshold:
|
|
175
|
+
disc_type = "Low-Z"
|
|
176
|
+
color = "blue"
|
|
177
|
+
else:
|
|
178
|
+
continue
|
|
179
|
+
|
|
180
|
+
# Add marker
|
|
181
|
+
ax.plot(d, z_after, "o", color=color, markersize=8)
|
|
182
|
+
|
|
183
|
+
# Add annotation
|
|
184
|
+
z_str = f"{z_after:.0f}" if z_after < 500 else "Open"
|
|
185
|
+
ax.annotate(
|
|
186
|
+
f"{disc_type}\n{z_str} Ω",
|
|
187
|
+
xy=(d, z_after),
|
|
188
|
+
xytext=(10, 10),
|
|
189
|
+
textcoords="offset points",
|
|
190
|
+
fontsize=8,
|
|
191
|
+
ha="left",
|
|
192
|
+
bbox={"boxstyle": "round,pad=0.3", "facecolor": "white", "alpha": 0.8},
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
# Labels and formatting
|
|
196
|
+
ax.set_xlabel(f"Distance ({distance_unit})", fontsize=11)
|
|
197
|
+
ax.set_ylabel("Impedance (Ω)", fontsize=11)
|
|
198
|
+
ax.set_xlim(0, dist_scaled[-1])
|
|
199
|
+
|
|
200
|
+
# Set y-axis limits with padding
|
|
201
|
+
y_min = max(0, np.min(impedance_display) - 10)
|
|
202
|
+
y_max = min(200, np.max(impedance_display) + 10)
|
|
203
|
+
ax.set_ylim(y_min, y_max)
|
|
204
|
+
|
|
205
|
+
ax.grid(True, alpha=0.3)
|
|
206
|
+
ax.legend(loc="upper right")
|
|
207
|
+
|
|
208
|
+
if title:
|
|
209
|
+
ax.set_title(title, fontsize=12, fontweight="bold")
|
|
210
|
+
else:
|
|
211
|
+
ax.set_title("TDR Impedance Profile", fontsize=12, fontweight="bold")
|
|
212
|
+
|
|
213
|
+
fig.tight_layout()
|
|
214
|
+
|
|
215
|
+
# Save if requested
|
|
216
|
+
if save_path is not None:
|
|
217
|
+
fig.savefig(save_path, dpi=300, bbox_inches="tight")
|
|
218
|
+
|
|
219
|
+
# Show if requested
|
|
220
|
+
if show:
|
|
221
|
+
plt.show()
|
|
222
|
+
|
|
223
|
+
return fig
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def plot_sparams_magnitude(
|
|
227
|
+
frequencies: NDArray[np.floating[Any]],
|
|
228
|
+
s11: NDArray[np.complexfloating[Any, Any]] | NDArray[np.floating[Any]] | None = None,
|
|
229
|
+
s21: NDArray[np.complexfloating[Any, Any]] | NDArray[np.floating[Any]] | None = None,
|
|
230
|
+
s12: NDArray[np.complexfloating[Any, Any]] | NDArray[np.floating[Any]] | None = None,
|
|
231
|
+
s22: NDArray[np.complexfloating[Any, Any]] | NDArray[np.floating[Any]] | None = None,
|
|
232
|
+
*,
|
|
233
|
+
ax: Axes | None = None,
|
|
234
|
+
figsize: tuple[float, float] = (12, 6),
|
|
235
|
+
title: str | None = None,
|
|
236
|
+
freq_unit: str = "auto",
|
|
237
|
+
show_markers: bool = True,
|
|
238
|
+
db_3_marker: bool = True,
|
|
239
|
+
show: bool = True,
|
|
240
|
+
save_path: str | Path | None = None,
|
|
241
|
+
) -> Figure:
|
|
242
|
+
"""Plot S-parameter magnitude vs frequency.
|
|
243
|
+
|
|
244
|
+
Creates a frequency response plot showing S-parameter magnitudes
|
|
245
|
+
in dB with optional -3dB marker for bandwidth measurement.
|
|
246
|
+
|
|
247
|
+
Args:
|
|
248
|
+
frequencies: Frequency array in Hz.
|
|
249
|
+
s11: S11 (input reflection) - complex or dB values.
|
|
250
|
+
s21: S21 (forward transmission) - complex or dB values.
|
|
251
|
+
s12: S12 (reverse transmission) - complex or dB values.
|
|
252
|
+
s22: S22 (output reflection) - complex or dB values.
|
|
253
|
+
ax: Matplotlib axes. If None, creates new figure.
|
|
254
|
+
figsize: Figure size in inches.
|
|
255
|
+
title: Plot title.
|
|
256
|
+
freq_unit: Frequency unit ("Hz", "kHz", "MHz", "GHz", "auto").
|
|
257
|
+
show_markers: Show markers at key frequencies.
|
|
258
|
+
db_3_marker: Show -3dB bandwidth marker for S21.
|
|
259
|
+
show: Display plot interactively.
|
|
260
|
+
save_path: Save plot to file.
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
Matplotlib Figure object.
|
|
264
|
+
|
|
265
|
+
Example:
|
|
266
|
+
>>> freq = np.linspace(1e6, 1e9, 1000)
|
|
267
|
+
>>> s21 = 1 / (1 + 1j * freq / 100e6) # Low-pass response
|
|
268
|
+
>>> fig = plot_sparams_magnitude(freq, s21=s21)
|
|
269
|
+
"""
|
|
270
|
+
if not HAS_MATPLOTLIB:
|
|
271
|
+
raise ImportError("matplotlib is required for visualization")
|
|
272
|
+
|
|
273
|
+
# Create figure if needed
|
|
274
|
+
if ax is None:
|
|
275
|
+
fig, ax = plt.subplots(figsize=figsize)
|
|
276
|
+
else:
|
|
277
|
+
fig_temp = ax.get_figure()
|
|
278
|
+
if fig_temp is None:
|
|
279
|
+
raise ValueError("Axes must have an associated figure")
|
|
280
|
+
fig = cast("Figure", fig_temp)
|
|
281
|
+
|
|
282
|
+
# Select frequency unit
|
|
283
|
+
if freq_unit == "auto":
|
|
284
|
+
max_freq = np.max(frequencies)
|
|
285
|
+
if max_freq >= 1e9:
|
|
286
|
+
freq_unit = "GHz"
|
|
287
|
+
freq_div = 1e9
|
|
288
|
+
elif max_freq >= 1e6:
|
|
289
|
+
freq_unit = "MHz"
|
|
290
|
+
freq_div = 1e6
|
|
291
|
+
elif max_freq >= 1e3:
|
|
292
|
+
freq_unit = "kHz"
|
|
293
|
+
freq_div = 1e3
|
|
294
|
+
else:
|
|
295
|
+
freq_unit = "Hz"
|
|
296
|
+
freq_div = 1.0
|
|
297
|
+
else:
|
|
298
|
+
freq_div = {"Hz": 1.0, "kHz": 1e3, "MHz": 1e6, "GHz": 1e9}.get(freq_unit, 1.0)
|
|
299
|
+
|
|
300
|
+
freq_scaled = frequencies / freq_div
|
|
301
|
+
|
|
302
|
+
def to_db(s: NDArray[Any]) -> NDArray[np.floating[Any]]:
|
|
303
|
+
"""Convert S-parameter to dB."""
|
|
304
|
+
if np.iscomplexobj(s):
|
|
305
|
+
result: NDArray[np.floating[Any]] = 20 * np.log10(np.abs(s) + 1e-12)
|
|
306
|
+
return result
|
|
307
|
+
return np.asarray(s, dtype=np.float64)
|
|
308
|
+
|
|
309
|
+
# Color scheme
|
|
310
|
+
colors = {"S11": "#E74C3C", "S21": "#3498DB", "S12": "#2ECC71", "S22": "#9B59B6"}
|
|
311
|
+
linestyles = {"S11": "-", "S21": "-", "S12": "--", "S22": "--"}
|
|
312
|
+
|
|
313
|
+
params = [("S11", s11), ("S21", s21), ("S12", s12), ("S22", s22)]
|
|
314
|
+
|
|
315
|
+
for name, s_param in params:
|
|
316
|
+
if s_param is not None:
|
|
317
|
+
s_db = to_db(s_param)
|
|
318
|
+
ax.semilogx(
|
|
319
|
+
freq_scaled,
|
|
320
|
+
s_db,
|
|
321
|
+
color=colors[name],
|
|
322
|
+
linestyle=linestyles[name],
|
|
323
|
+
linewidth=2,
|
|
324
|
+
label=name,
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
# -3dB marker for S21
|
|
328
|
+
if name == "S21" and db_3_marker:
|
|
329
|
+
max_db = np.max(s_db)
|
|
330
|
+
db_3_level = max_db - 3
|
|
331
|
+
|
|
332
|
+
# Find -3dB crossover
|
|
333
|
+
crossings = np.where(np.diff(np.sign(s_db - db_3_level)))[0]
|
|
334
|
+
if len(crossings) > 0:
|
|
335
|
+
f_3db = float(freq_scaled[crossings[0]])
|
|
336
|
+
db_3_level_float = float(db_3_level)
|
|
337
|
+
ax.axhline(
|
|
338
|
+
db_3_level_float, color="gray", linestyle=":", alpha=0.7, linewidth=1
|
|
339
|
+
)
|
|
340
|
+
ax.axvline(f_3db, color="gray", linestyle=":", alpha=0.7, linewidth=1)
|
|
341
|
+
ax.plot(f_3db, db_3_level_float, "ko", markersize=6)
|
|
342
|
+
ax.annotate(
|
|
343
|
+
f"-3dB: {f_3db:.2f} {freq_unit}",
|
|
344
|
+
xy=(f_3db, db_3_level_float),
|
|
345
|
+
xytext=(10, -15),
|
|
346
|
+
textcoords="offset points",
|
|
347
|
+
fontsize=9,
|
|
348
|
+
ha="left",
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
# Labels and formatting
|
|
352
|
+
ax.set_xlabel(f"Frequency ({freq_unit})", fontsize=11)
|
|
353
|
+
ax.set_ylabel("Magnitude (dB)", fontsize=11)
|
|
354
|
+
ax.grid(True, which="both", alpha=0.3)
|
|
355
|
+
ax.legend(loc="best")
|
|
356
|
+
|
|
357
|
+
if title:
|
|
358
|
+
ax.set_title(title, fontsize=12, fontweight="bold")
|
|
359
|
+
else:
|
|
360
|
+
ax.set_title("S-Parameter Magnitude Response", fontsize=12, fontweight="bold")
|
|
361
|
+
|
|
362
|
+
fig.tight_layout()
|
|
363
|
+
|
|
364
|
+
if save_path is not None:
|
|
365
|
+
fig.savefig(save_path, dpi=300, bbox_inches="tight")
|
|
366
|
+
|
|
367
|
+
if show:
|
|
368
|
+
plt.show()
|
|
369
|
+
|
|
370
|
+
return fig
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def plot_sparams_phase(
|
|
374
|
+
frequencies: NDArray[np.floating[Any]],
|
|
375
|
+
s11: NDArray[np.complexfloating[Any, Any]] | None = None,
|
|
376
|
+
s21: NDArray[np.complexfloating[Any, Any]] | None = None,
|
|
377
|
+
*,
|
|
378
|
+
ax: Axes | None = None,
|
|
379
|
+
figsize: tuple[float, float] = (12, 6),
|
|
380
|
+
title: str | None = None,
|
|
381
|
+
freq_unit: str = "auto",
|
|
382
|
+
unwrap: bool = True,
|
|
383
|
+
show: bool = True,
|
|
384
|
+
save_path: str | Path | None = None,
|
|
385
|
+
) -> Figure:
|
|
386
|
+
"""Plot S-parameter phase vs frequency.
|
|
387
|
+
|
|
388
|
+
Args:
|
|
389
|
+
frequencies: Frequency array in Hz.
|
|
390
|
+
s11: S11 complex values.
|
|
391
|
+
s21: S21 complex values.
|
|
392
|
+
ax: Matplotlib axes.
|
|
393
|
+
figsize: Figure size.
|
|
394
|
+
title: Plot title.
|
|
395
|
+
freq_unit: Frequency unit.
|
|
396
|
+
unwrap: Unwrap phase discontinuities.
|
|
397
|
+
show: Display plot.
|
|
398
|
+
save_path: Save path.
|
|
399
|
+
|
|
400
|
+
Returns:
|
|
401
|
+
Matplotlib Figure object.
|
|
402
|
+
"""
|
|
403
|
+
if not HAS_MATPLOTLIB:
|
|
404
|
+
raise ImportError("matplotlib is required for visualization")
|
|
405
|
+
|
|
406
|
+
if ax is None:
|
|
407
|
+
fig, ax = plt.subplots(figsize=figsize)
|
|
408
|
+
else:
|
|
409
|
+
fig_temp = ax.get_figure()
|
|
410
|
+
if fig_temp is None:
|
|
411
|
+
raise ValueError("Axes must have an associated figure")
|
|
412
|
+
fig = cast("Figure", fig_temp)
|
|
413
|
+
|
|
414
|
+
# Select frequency unit
|
|
415
|
+
if freq_unit == "auto":
|
|
416
|
+
max_freq = np.max(frequencies)
|
|
417
|
+
if max_freq >= 1e9:
|
|
418
|
+
freq_unit = "GHz"
|
|
419
|
+
freq_div = 1e9
|
|
420
|
+
elif max_freq >= 1e6:
|
|
421
|
+
freq_unit = "MHz"
|
|
422
|
+
freq_div = 1e6
|
|
423
|
+
else:
|
|
424
|
+
freq_unit = "kHz"
|
|
425
|
+
freq_div = 1e3
|
|
426
|
+
else:
|
|
427
|
+
freq_div = {"Hz": 1.0, "kHz": 1e3, "MHz": 1e6, "GHz": 1e9}.get(freq_unit, 1.0)
|
|
428
|
+
|
|
429
|
+
freq_scaled = frequencies / freq_div
|
|
430
|
+
|
|
431
|
+
colors = {"S11": "#E74C3C", "S21": "#3498DB"}
|
|
432
|
+
|
|
433
|
+
for name, s_param in [("S11", s11), ("S21", s21)]:
|
|
434
|
+
if s_param is not None:
|
|
435
|
+
phase = np.angle(s_param, deg=True)
|
|
436
|
+
if unwrap:
|
|
437
|
+
phase = np.rad2deg(np.unwrap(np.deg2rad(phase)))
|
|
438
|
+
|
|
439
|
+
ax.semilogx(freq_scaled, phase, color=colors[name], linewidth=2, label=name)
|
|
440
|
+
|
|
441
|
+
ax.set_xlabel(f"Frequency ({freq_unit})", fontsize=11)
|
|
442
|
+
ax.set_ylabel("Phase (degrees)", fontsize=11)
|
|
443
|
+
ax.grid(True, which="both", alpha=0.3)
|
|
444
|
+
ax.legend(loc="best")
|
|
445
|
+
|
|
446
|
+
if title:
|
|
447
|
+
ax.set_title(title, fontsize=12, fontweight="bold")
|
|
448
|
+
else:
|
|
449
|
+
ax.set_title("S-Parameter Phase Response", fontsize=12, fontweight="bold")
|
|
450
|
+
|
|
451
|
+
fig.tight_layout()
|
|
452
|
+
|
|
453
|
+
if save_path is not None:
|
|
454
|
+
fig.savefig(save_path, dpi=300, bbox_inches="tight")
|
|
455
|
+
|
|
456
|
+
if show:
|
|
457
|
+
plt.show()
|
|
458
|
+
|
|
459
|
+
return fig
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def plot_setup_hold_timing(
|
|
463
|
+
clock_edges: NDArray[np.floating[Any]],
|
|
464
|
+
data_edges: NDArray[np.floating[Any]],
|
|
465
|
+
setup_time: float,
|
|
466
|
+
hold_time: float,
|
|
467
|
+
*,
|
|
468
|
+
clock_data: NDArray[np.floating[Any]] | None = None,
|
|
469
|
+
data_data: NDArray[np.floating[Any]] | None = None,
|
|
470
|
+
time_axis: NDArray[np.floating[Any]] | None = None,
|
|
471
|
+
ax: Axes | None = None,
|
|
472
|
+
figsize: tuple[float, float] = (14, 8),
|
|
473
|
+
title: str | None = None,
|
|
474
|
+
time_unit: str = "auto",
|
|
475
|
+
show_margins: bool = True,
|
|
476
|
+
setup_spec: float | None = None,
|
|
477
|
+
hold_spec: float | None = None,
|
|
478
|
+
show: bool = True,
|
|
479
|
+
save_path: str | Path | None = None,
|
|
480
|
+
) -> Figure:
|
|
481
|
+
"""Plot setup/hold timing diagram with annotations.
|
|
482
|
+
|
|
483
|
+
Creates a timing diagram showing clock and data relationships
|
|
484
|
+
with setup and hold time annotations and optional pass/fail
|
|
485
|
+
indication against specifications.
|
|
486
|
+
|
|
487
|
+
Args:
|
|
488
|
+
clock_edges: Array of clock edge times (rising edges).
|
|
489
|
+
data_edges: Array of data transition times.
|
|
490
|
+
setup_time: Measured setup time (seconds).
|
|
491
|
+
hold_time: Measured hold time (seconds).
|
|
492
|
+
clock_data: Optional clock waveform for display.
|
|
493
|
+
data_data: Optional data waveform for display.
|
|
494
|
+
time_axis: Time axis for waveforms.
|
|
495
|
+
ax: Matplotlib axes.
|
|
496
|
+
figsize: Figure size.
|
|
497
|
+
title: Plot title.
|
|
498
|
+
time_unit: Time unit ("s", "ms", "us", "ns", "ps", "auto").
|
|
499
|
+
show_margins: Show setup/hold timing arrows.
|
|
500
|
+
setup_spec: Setup time specification for pass/fail.
|
|
501
|
+
hold_spec: Hold time specification for pass/fail.
|
|
502
|
+
show: Display plot.
|
|
503
|
+
save_path: Save path.
|
|
504
|
+
|
|
505
|
+
Returns:
|
|
506
|
+
Matplotlib Figure object.
|
|
507
|
+
|
|
508
|
+
Example:
|
|
509
|
+
>>> clk_edges = np.array([0, 10e-9, 20e-9])
|
|
510
|
+
>>> data_edges = np.array([8e-9, 18e-9])
|
|
511
|
+
>>> fig = plot_setup_hold_timing(
|
|
512
|
+
... clk_edges, data_edges,
|
|
513
|
+
... setup_time=2e-9, hold_time=1e-9,
|
|
514
|
+
... setup_spec=1e-9, hold_spec=0.5e-9
|
|
515
|
+
... )
|
|
516
|
+
"""
|
|
517
|
+
if not HAS_MATPLOTLIB:
|
|
518
|
+
raise ImportError("matplotlib is required for visualization")
|
|
519
|
+
|
|
520
|
+
# Create figure with multiple rows
|
|
521
|
+
if ax is not None:
|
|
522
|
+
fig_temp = ax.get_figure()
|
|
523
|
+
if fig_temp is None:
|
|
524
|
+
raise ValueError("Axes must have an associated figure")
|
|
525
|
+
fig = cast("Figure", fig_temp)
|
|
526
|
+
axes = [ax]
|
|
527
|
+
n_rows = 1
|
|
528
|
+
else:
|
|
529
|
+
n_rows = 3 if clock_data is not None else 1
|
|
530
|
+
fig, axes = plt.subplots(
|
|
531
|
+
n_rows, 1, figsize=figsize, sharex=True, gridspec_kw={"height_ratios": [1] * n_rows}
|
|
532
|
+
)
|
|
533
|
+
if n_rows == 1:
|
|
534
|
+
axes = [axes]
|
|
535
|
+
|
|
536
|
+
# Select time unit
|
|
537
|
+
if time_unit == "auto":
|
|
538
|
+
max_time = max(np.max(clock_edges), np.max(data_edges))
|
|
539
|
+
if max_time < 1e-9:
|
|
540
|
+
time_unit = "ps"
|
|
541
|
+
time_mult = 1e12
|
|
542
|
+
elif max_time < 1e-6:
|
|
543
|
+
time_unit = "ns"
|
|
544
|
+
time_mult = 1e9
|
|
545
|
+
elif max_time < 1e-3:
|
|
546
|
+
time_unit = "us"
|
|
547
|
+
time_mult = 1e6
|
|
548
|
+
else:
|
|
549
|
+
time_unit = "ms"
|
|
550
|
+
time_mult = 1e3
|
|
551
|
+
else:
|
|
552
|
+
time_mult = {"s": 1, "ms": 1e3, "us": 1e6, "ns": 1e9, "ps": 1e12}.get(time_unit, 1e9)
|
|
553
|
+
|
|
554
|
+
setup_scaled = setup_time * time_mult
|
|
555
|
+
hold_scaled = hold_time * time_mult
|
|
556
|
+
|
|
557
|
+
# If waveforms provided, plot them
|
|
558
|
+
if clock_data is not None and data_data is not None and time_axis is not None:
|
|
559
|
+
time_scaled = time_axis * time_mult
|
|
560
|
+
|
|
561
|
+
# Clock waveform
|
|
562
|
+
ax_clk = axes[0]
|
|
563
|
+
ax_clk.step(time_scaled, clock_data, where="post", color="#3498DB", linewidth=2)
|
|
564
|
+
ax_clk.set_ylabel(
|
|
565
|
+
"CLK", rotation=0, ha="right", va="center", fontsize=11, fontweight="bold"
|
|
566
|
+
)
|
|
567
|
+
ax_clk.set_ylim(-0.2, 1.3)
|
|
568
|
+
ax_clk.set_yticks([0, 1])
|
|
569
|
+
ax_clk.grid(True, axis="x", alpha=0.3)
|
|
570
|
+
|
|
571
|
+
# Data waveform
|
|
572
|
+
ax_data = axes[1]
|
|
573
|
+
ax_data.step(time_scaled, data_data, where="post", color="#E74C3C", linewidth=2)
|
|
574
|
+
ax_data.set_ylabel(
|
|
575
|
+
"DATA", rotation=0, ha="right", va="center", fontsize=11, fontweight="bold"
|
|
576
|
+
)
|
|
577
|
+
ax_data.set_ylim(-0.2, 1.3)
|
|
578
|
+
ax_data.set_yticks([0, 1])
|
|
579
|
+
ax_data.grid(True, axis="x", alpha=0.3)
|
|
580
|
+
|
|
581
|
+
ax_timing = axes[2] if len(axes) > 2 else axes[-1]
|
|
582
|
+
else:
|
|
583
|
+
ax_timing = axes[0]
|
|
584
|
+
|
|
585
|
+
# Timing annotation panel
|
|
586
|
+
ax_timing.set_ylim(0, 1)
|
|
587
|
+
ax_timing.set_xlim(0, max(clock_edges[-1], data_edges[-1]) * time_mult * 1.1)
|
|
588
|
+
ax_timing.axis("off")
|
|
589
|
+
|
|
590
|
+
# Draw timing arrows for first clock edge
|
|
591
|
+
if len(clock_edges) > 0 and len(data_edges) > 0:
|
|
592
|
+
clk_edge = clock_edges[0] * time_mult
|
|
593
|
+
|
|
594
|
+
# Find nearest data edge before clock
|
|
595
|
+
data_before = data_edges[data_edges < clock_edges[0]]
|
|
596
|
+
if len(data_before) > 0:
|
|
597
|
+
data_edge = data_before[-1] * time_mult
|
|
598
|
+
|
|
599
|
+
# Setup time arrow (data_edge to clk_edge)
|
|
600
|
+
if show_margins:
|
|
601
|
+
y_setup = 0.7
|
|
602
|
+
ax_timing.annotate(
|
|
603
|
+
"",
|
|
604
|
+
xy=(clk_edge, y_setup),
|
|
605
|
+
xytext=(data_edge, y_setup),
|
|
606
|
+
arrowprops={
|
|
607
|
+
"arrowstyle": "<->",
|
|
608
|
+
"color": "#27AE60",
|
|
609
|
+
"lw": 2,
|
|
610
|
+
},
|
|
611
|
+
)
|
|
612
|
+
ax_timing.text(
|
|
613
|
+
(data_edge + clk_edge) / 2,
|
|
614
|
+
y_setup + 0.1,
|
|
615
|
+
f"Setup: {setup_scaled:.2f} {time_unit}",
|
|
616
|
+
ha="center",
|
|
617
|
+
va="bottom",
|
|
618
|
+
fontsize=10,
|
|
619
|
+
fontweight="bold",
|
|
620
|
+
color="#27AE60",
|
|
621
|
+
)
|
|
622
|
+
|
|
623
|
+
# Find nearest data edge after clock
|
|
624
|
+
data_after = data_edges[data_edges > clock_edges[0]]
|
|
625
|
+
if len(data_after) > 0:
|
|
626
|
+
data_edge_after = data_after[0] * time_mult
|
|
627
|
+
|
|
628
|
+
# Hold time arrow (clk_edge to data_edge_after)
|
|
629
|
+
if show_margins:
|
|
630
|
+
y_hold = 0.3
|
|
631
|
+
ax_timing.annotate(
|
|
632
|
+
"",
|
|
633
|
+
xy=(data_edge_after, y_hold),
|
|
634
|
+
xytext=(clk_edge, y_hold),
|
|
635
|
+
arrowprops={
|
|
636
|
+
"arrowstyle": "<->",
|
|
637
|
+
"color": "#E67E22",
|
|
638
|
+
"lw": 2,
|
|
639
|
+
},
|
|
640
|
+
)
|
|
641
|
+
ax_timing.text(
|
|
642
|
+
(clk_edge + data_edge_after) / 2,
|
|
643
|
+
y_hold + 0.1,
|
|
644
|
+
f"Hold: {hold_scaled:.2f} {time_unit}",
|
|
645
|
+
ha="center",
|
|
646
|
+
va="bottom",
|
|
647
|
+
fontsize=10,
|
|
648
|
+
fontweight="bold",
|
|
649
|
+
color="#E67E22",
|
|
650
|
+
)
|
|
651
|
+
|
|
652
|
+
# Add pass/fail status
|
|
653
|
+
status_y = 0.9
|
|
654
|
+
if setup_spec is not None:
|
|
655
|
+
setup_pass = setup_time >= setup_spec
|
|
656
|
+
status = "PASS" if setup_pass else "FAIL"
|
|
657
|
+
color = "#27AE60" if setup_pass else "#E74C3C"
|
|
658
|
+
ax_timing.text(
|
|
659
|
+
0.02,
|
|
660
|
+
status_y,
|
|
661
|
+
f"Setup: {status} (spec: {setup_spec * time_mult:.2f} {time_unit})",
|
|
662
|
+
transform=ax_timing.transAxes,
|
|
663
|
+
fontsize=10,
|
|
664
|
+
color=color,
|
|
665
|
+
fontweight="bold",
|
|
666
|
+
)
|
|
667
|
+
status_y -= 0.15
|
|
668
|
+
|
|
669
|
+
if hold_spec is not None:
|
|
670
|
+
hold_pass = hold_time >= hold_spec
|
|
671
|
+
status = "PASS" if hold_pass else "FAIL"
|
|
672
|
+
color = "#27AE60" if hold_pass else "#E74C3C"
|
|
673
|
+
ax_timing.text(
|
|
674
|
+
0.02,
|
|
675
|
+
status_y,
|
|
676
|
+
f"Hold: {status} (spec: {hold_spec * time_mult:.2f} {time_unit})",
|
|
677
|
+
transform=ax_timing.transAxes,
|
|
678
|
+
fontsize=10,
|
|
679
|
+
color=color,
|
|
680
|
+
fontweight="bold",
|
|
681
|
+
)
|
|
682
|
+
|
|
683
|
+
# Set x-label on bottom axes
|
|
684
|
+
axes[-1].set_xlabel(f"Time ({time_unit})", fontsize=11)
|
|
685
|
+
|
|
686
|
+
if title:
|
|
687
|
+
fig.suptitle(title, fontsize=14, fontweight="bold")
|
|
688
|
+
else:
|
|
689
|
+
fig.suptitle("Setup/Hold Timing Analysis", fontsize=14, fontweight="bold")
|
|
690
|
+
|
|
691
|
+
fig.tight_layout()
|
|
692
|
+
|
|
693
|
+
if save_path is not None:
|
|
694
|
+
fig.savefig(save_path, dpi=300, bbox_inches="tight")
|
|
695
|
+
|
|
696
|
+
if show:
|
|
697
|
+
plt.show()
|
|
698
|
+
|
|
699
|
+
return fig
|
|
700
|
+
|
|
701
|
+
|
|
702
|
+
def plot_timing_margin(
|
|
703
|
+
setup_times: NDArray[np.floating[Any]],
|
|
704
|
+
hold_times: NDArray[np.floating[Any]],
|
|
705
|
+
*,
|
|
706
|
+
setup_spec: float | None = None,
|
|
707
|
+
hold_spec: float | None = None,
|
|
708
|
+
ax: Axes | None = None,
|
|
709
|
+
figsize: tuple[float, float] = (10, 8),
|
|
710
|
+
title: str | None = None,
|
|
711
|
+
time_unit: str = "ns",
|
|
712
|
+
show: bool = True,
|
|
713
|
+
save_path: str | Path | None = None,
|
|
714
|
+
) -> Figure:
|
|
715
|
+
"""Plot setup vs hold timing margin scatter plot.
|
|
716
|
+
|
|
717
|
+
Creates a scatter plot showing the distribution of setup and hold
|
|
718
|
+
times with specification regions marked.
|
|
719
|
+
|
|
720
|
+
Args:
|
|
721
|
+
setup_times: Array of setup time measurements.
|
|
722
|
+
hold_times: Array of hold time measurements.
|
|
723
|
+
setup_spec: Setup time specification.
|
|
724
|
+
hold_spec: Hold time specification.
|
|
725
|
+
ax: Matplotlib axes.
|
|
726
|
+
figsize: Figure size.
|
|
727
|
+
title: Plot title.
|
|
728
|
+
time_unit: Time unit for display.
|
|
729
|
+
show: Display plot.
|
|
730
|
+
save_path: Save path.
|
|
731
|
+
|
|
732
|
+
Returns:
|
|
733
|
+
Matplotlib Figure object.
|
|
734
|
+
"""
|
|
735
|
+
if not HAS_MATPLOTLIB:
|
|
736
|
+
raise ImportError("matplotlib is required for visualization")
|
|
737
|
+
|
|
738
|
+
if ax is None:
|
|
739
|
+
fig, ax = plt.subplots(figsize=figsize)
|
|
740
|
+
else:
|
|
741
|
+
fig_temp = ax.get_figure()
|
|
742
|
+
if fig_temp is None:
|
|
743
|
+
raise ValueError("Axes must have an associated figure")
|
|
744
|
+
fig = cast("Figure", fig_temp)
|
|
745
|
+
|
|
746
|
+
time_mult = {"s": 1, "ms": 1e3, "us": 1e6, "ns": 1e9, "ps": 1e12}.get(time_unit, 1e9)
|
|
747
|
+
|
|
748
|
+
setup_scaled = setup_times * time_mult
|
|
749
|
+
hold_scaled = hold_times * time_mult
|
|
750
|
+
|
|
751
|
+
# Scatter plot
|
|
752
|
+
ax.scatter(setup_scaled, hold_scaled, c="#3498DB", alpha=0.6, s=50)
|
|
753
|
+
|
|
754
|
+
# Add specification lines if provided
|
|
755
|
+
if setup_spec is not None:
|
|
756
|
+
spec_scaled = setup_spec * time_mult
|
|
757
|
+
ax.axvline(
|
|
758
|
+
spec_scaled,
|
|
759
|
+
color="#E74C3C",
|
|
760
|
+
linestyle="--",
|
|
761
|
+
linewidth=2,
|
|
762
|
+
label=f"Setup Spec ({spec_scaled:.2f} {time_unit})",
|
|
763
|
+
)
|
|
764
|
+
|
|
765
|
+
if hold_spec is not None:
|
|
766
|
+
spec_scaled = hold_spec * time_mult
|
|
767
|
+
ax.axhline(
|
|
768
|
+
spec_scaled,
|
|
769
|
+
color="#E67E22",
|
|
770
|
+
linestyle="--",
|
|
771
|
+
linewidth=2,
|
|
772
|
+
label=f"Hold Spec ({spec_scaled:.2f} {time_unit})",
|
|
773
|
+
)
|
|
774
|
+
|
|
775
|
+
# Mark pass/fail regions
|
|
776
|
+
if setup_spec is not None and hold_spec is not None:
|
|
777
|
+
x_lim = ax.get_xlim()
|
|
778
|
+
y_lim = ax.get_ylim()
|
|
779
|
+
|
|
780
|
+
# Pass region (upper right)
|
|
781
|
+
ax.fill_between(
|
|
782
|
+
[setup_spec * time_mult, x_lim[1]],
|
|
783
|
+
[hold_spec * time_mult, hold_spec * time_mult],
|
|
784
|
+
[y_lim[1], y_lim[1]],
|
|
785
|
+
color="#27AE60",
|
|
786
|
+
alpha=0.1,
|
|
787
|
+
label="Pass Region",
|
|
788
|
+
)
|
|
789
|
+
|
|
790
|
+
ax.set_xlabel(f"Setup Time ({time_unit})", fontsize=11)
|
|
791
|
+
ax.set_ylabel(f"Hold Time ({time_unit})", fontsize=11)
|
|
792
|
+
ax.grid(True, alpha=0.3)
|
|
793
|
+
ax.legend(loc="best")
|
|
794
|
+
|
|
795
|
+
if title:
|
|
796
|
+
ax.set_title(title, fontsize=12, fontweight="bold")
|
|
797
|
+
else:
|
|
798
|
+
ax.set_title("Setup/Hold Timing Margin", fontsize=12, fontweight="bold")
|
|
799
|
+
|
|
800
|
+
fig.tight_layout()
|
|
801
|
+
|
|
802
|
+
if save_path is not None:
|
|
803
|
+
fig.savefig(save_path, dpi=300, bbox_inches="tight")
|
|
804
|
+
|
|
805
|
+
if show:
|
|
806
|
+
plt.show()
|
|
807
|
+
|
|
808
|
+
return fig
|