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,702 @@
|
|
|
1
|
+
"""Jitter Analysis Visualization Functions.
|
|
2
|
+
|
|
3
|
+
This module provides visualization functions for jitter analysis including
|
|
4
|
+
TIE histograms, bathtub curves, DDJ/DCD plots, and jitter trend analysis.
|
|
5
|
+
|
|
6
|
+
Example:
|
|
7
|
+
>>> from oscura.visualization.jitter import plot_tie_histogram, plot_bathtub_full
|
|
8
|
+
>>> fig = plot_tie_histogram(tie_data)
|
|
9
|
+
>>> fig = plot_bathtub_full(bathtub_result)
|
|
10
|
+
|
|
11
|
+
References:
|
|
12
|
+
- IEEE 802.3: Jitter measurement specifications
|
|
13
|
+
- JEDEC JESD65B: High-Speed Interface Measurements
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import TYPE_CHECKING, Any, cast
|
|
20
|
+
|
|
21
|
+
import numpy as np
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
import matplotlib.pyplot as plt
|
|
25
|
+
from scipy.stats import norm
|
|
26
|
+
|
|
27
|
+
HAS_MATPLOTLIB = True
|
|
28
|
+
HAS_SCIPY = True
|
|
29
|
+
except ImportError:
|
|
30
|
+
HAS_MATPLOTLIB = False
|
|
31
|
+
HAS_SCIPY = False
|
|
32
|
+
|
|
33
|
+
if TYPE_CHECKING:
|
|
34
|
+
from matplotlib.axes import Axes
|
|
35
|
+
from matplotlib.figure import Figure
|
|
36
|
+
from numpy.typing import NDArray
|
|
37
|
+
|
|
38
|
+
__all__ = [
|
|
39
|
+
"plot_bathtub_full",
|
|
40
|
+
"plot_dcd",
|
|
41
|
+
"plot_ddj",
|
|
42
|
+
"plot_jitter_trend",
|
|
43
|
+
"plot_tie_histogram",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def plot_tie_histogram(
|
|
48
|
+
tie_data: NDArray[np.floating[Any]],
|
|
49
|
+
*,
|
|
50
|
+
ax: Axes | None = None,
|
|
51
|
+
figsize: tuple[float, float] = (10, 6),
|
|
52
|
+
title: str | None = None,
|
|
53
|
+
time_unit: str = "auto",
|
|
54
|
+
bins: int | str = "auto",
|
|
55
|
+
show_gaussian_fit: bool = True,
|
|
56
|
+
show_statistics: bool = True,
|
|
57
|
+
show_rj_dj: bool = True,
|
|
58
|
+
show: bool = True,
|
|
59
|
+
save_path: str | Path | None = None,
|
|
60
|
+
) -> Figure:
|
|
61
|
+
"""Plot Time Interval Error (TIE) histogram with statistical analysis.
|
|
62
|
+
|
|
63
|
+
Creates a histogram of TIE values with optional Gaussian fit overlay
|
|
64
|
+
and RJ/DJ decomposition indicators.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
tie_data: Array of TIE values in seconds.
|
|
68
|
+
ax: Matplotlib axes. If None, creates new figure.
|
|
69
|
+
figsize: Figure size in inches.
|
|
70
|
+
title: Plot title.
|
|
71
|
+
time_unit: Time unit ("s", "ms", "us", "ns", "ps", "auto").
|
|
72
|
+
bins: Number of bins or "auto" for automatic selection.
|
|
73
|
+
show_gaussian_fit: Overlay Gaussian fit for RJ estimation.
|
|
74
|
+
show_statistics: Show statistics box.
|
|
75
|
+
show_rj_dj: Show RJ/DJ separation indicators.
|
|
76
|
+
show: Display plot interactively.
|
|
77
|
+
save_path: Save plot to file.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
Matplotlib Figure object.
|
|
81
|
+
|
|
82
|
+
Example:
|
|
83
|
+
>>> tie = np.random.randn(10000) * 2e-12 # 2 ps RMS jitter
|
|
84
|
+
>>> fig = plot_tie_histogram(tie, time_unit="ps")
|
|
85
|
+
"""
|
|
86
|
+
if not HAS_MATPLOTLIB:
|
|
87
|
+
raise ImportError("matplotlib is required for visualization")
|
|
88
|
+
|
|
89
|
+
# Create figure if needed
|
|
90
|
+
if ax is None:
|
|
91
|
+
fig, ax = plt.subplots(figsize=figsize)
|
|
92
|
+
else:
|
|
93
|
+
fig_temp = ax.get_figure()
|
|
94
|
+
if fig_temp is None:
|
|
95
|
+
raise ValueError("Axes must have an associated figure")
|
|
96
|
+
fig = cast("Figure", fig_temp)
|
|
97
|
+
|
|
98
|
+
# Select time unit
|
|
99
|
+
if time_unit == "auto":
|
|
100
|
+
max_tie = np.max(np.abs(tie_data))
|
|
101
|
+
if max_tie < 1e-12:
|
|
102
|
+
time_unit = "fs"
|
|
103
|
+
time_mult = 1e15
|
|
104
|
+
elif max_tie < 1e-9:
|
|
105
|
+
time_unit = "ps"
|
|
106
|
+
time_mult = 1e12
|
|
107
|
+
elif max_tie < 1e-6:
|
|
108
|
+
time_unit = "ns"
|
|
109
|
+
time_mult = 1e9
|
|
110
|
+
else:
|
|
111
|
+
time_unit = "us"
|
|
112
|
+
time_mult = 1e6
|
|
113
|
+
else:
|
|
114
|
+
time_mult = {
|
|
115
|
+
"s": 1,
|
|
116
|
+
"ms": 1e3,
|
|
117
|
+
"us": 1e6,
|
|
118
|
+
"ns": 1e9,
|
|
119
|
+
"ps": 1e12,
|
|
120
|
+
"fs": 1e15,
|
|
121
|
+
}.get(time_unit, 1e12)
|
|
122
|
+
|
|
123
|
+
tie_scaled = tie_data * time_mult
|
|
124
|
+
|
|
125
|
+
# Calculate statistics
|
|
126
|
+
mean_val = np.mean(tie_scaled)
|
|
127
|
+
std_val = np.std(tie_scaled)
|
|
128
|
+
pp_val = np.ptp(tie_scaled)
|
|
129
|
+
rms_val = np.sqrt(np.mean(tie_scaled**2))
|
|
130
|
+
|
|
131
|
+
# Plot histogram
|
|
132
|
+
counts, bin_edges, patches = ax.hist(
|
|
133
|
+
tie_scaled,
|
|
134
|
+
bins=bins,
|
|
135
|
+
density=True,
|
|
136
|
+
color="#3498DB",
|
|
137
|
+
alpha=0.7,
|
|
138
|
+
edgecolor="black",
|
|
139
|
+
linewidth=0.5,
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
# Gaussian fit overlay
|
|
143
|
+
if show_gaussian_fit and HAS_SCIPY:
|
|
144
|
+
x_fit = np.linspace(bin_edges[0], bin_edges[-1], 200)
|
|
145
|
+
y_fit = norm.pdf(x_fit, mean_val, std_val)
|
|
146
|
+
ax.plot(
|
|
147
|
+
x_fit, y_fit, "r-", linewidth=2, label=f"Gaussian Fit (sigma={std_val:.2f} {time_unit})"
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
# RJ/DJ indicators
|
|
151
|
+
if show_rj_dj:
|
|
152
|
+
# Mark ±3sigma region (RJ contribution)
|
|
153
|
+
ax.axvline(
|
|
154
|
+
mean_val - 3 * std_val, color="#E74C3C", linestyle="--", linewidth=1.5, alpha=0.7
|
|
155
|
+
)
|
|
156
|
+
ax.axvline(
|
|
157
|
+
mean_val + 3 * std_val, color="#E74C3C", linestyle="--", linewidth=1.5, alpha=0.7
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
# Shade RJ region
|
|
161
|
+
ax.axvspan(
|
|
162
|
+
mean_val - 3 * std_val,
|
|
163
|
+
mean_val + 3 * std_val,
|
|
164
|
+
alpha=0.1,
|
|
165
|
+
color="#E74C3C",
|
|
166
|
+
label="±3sigma (99.7% RJ)",
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
# Statistics box
|
|
170
|
+
if show_statistics:
|
|
171
|
+
stats_text = (
|
|
172
|
+
f"Mean: {mean_val:.2f} {time_unit}\n"
|
|
173
|
+
f"RMS: {rms_val:.2f} {time_unit}\n"
|
|
174
|
+
f"Std Dev: {std_val:.2f} {time_unit}\n"
|
|
175
|
+
f"Peak-Peak: {pp_val:.2f} {time_unit}"
|
|
176
|
+
)
|
|
177
|
+
ax.text(
|
|
178
|
+
0.98,
|
|
179
|
+
0.98,
|
|
180
|
+
stats_text,
|
|
181
|
+
transform=ax.transAxes,
|
|
182
|
+
fontsize=9,
|
|
183
|
+
verticalalignment="top",
|
|
184
|
+
horizontalalignment="right",
|
|
185
|
+
bbox={"boxstyle": "round", "facecolor": "wheat", "alpha": 0.9},
|
|
186
|
+
fontfamily="monospace",
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
# Labels
|
|
190
|
+
ax.set_xlabel(f"TIE ({time_unit})", fontsize=11)
|
|
191
|
+
ax.set_ylabel("Probability Density", fontsize=11)
|
|
192
|
+
ax.grid(True, alpha=0.3)
|
|
193
|
+
ax.legend(loc="upper left")
|
|
194
|
+
|
|
195
|
+
if title:
|
|
196
|
+
ax.set_title(title, fontsize=12, fontweight="bold")
|
|
197
|
+
else:
|
|
198
|
+
ax.set_title("Time Interval Error Distribution", fontsize=12, fontweight="bold")
|
|
199
|
+
|
|
200
|
+
fig.tight_layout()
|
|
201
|
+
|
|
202
|
+
if save_path is not None:
|
|
203
|
+
fig.savefig(save_path, dpi=300, bbox_inches="tight")
|
|
204
|
+
|
|
205
|
+
if show:
|
|
206
|
+
plt.show()
|
|
207
|
+
|
|
208
|
+
return fig
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def plot_bathtub_full(
|
|
212
|
+
positions: NDArray[np.floating[Any]],
|
|
213
|
+
ber_left: NDArray[np.floating[Any]],
|
|
214
|
+
ber_right: NDArray[np.floating[Any]],
|
|
215
|
+
*,
|
|
216
|
+
ber_total: NDArray[np.floating[Any]] | None = None,
|
|
217
|
+
target_ber: float = 1e-12,
|
|
218
|
+
eye_opening: float | None = None,
|
|
219
|
+
ax: Axes | None = None,
|
|
220
|
+
figsize: tuple[float, float] = (10, 6),
|
|
221
|
+
title: str | None = None,
|
|
222
|
+
show_target: bool = True,
|
|
223
|
+
show_eye_opening: bool = True,
|
|
224
|
+
show: bool = True,
|
|
225
|
+
save_path: str | Path | None = None,
|
|
226
|
+
) -> Figure:
|
|
227
|
+
"""Plot full bathtub curve with left/right BER and eye opening.
|
|
228
|
+
|
|
229
|
+
Creates a bathtub curve showing bit error rate vs sampling position
|
|
230
|
+
within the unit interval, with target BER marker and eye opening
|
|
231
|
+
annotation.
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
positions: Sample positions in UI (0 to 1).
|
|
235
|
+
ber_left: Left-side BER values.
|
|
236
|
+
ber_right: Right-side BER values.
|
|
237
|
+
ber_total: Total BER values (optional, computed if not provided).
|
|
238
|
+
target_ber: Target BER for eye opening calculation.
|
|
239
|
+
eye_opening: Pre-calculated eye opening in UI (optional).
|
|
240
|
+
ax: Matplotlib axes.
|
|
241
|
+
figsize: Figure size.
|
|
242
|
+
title: Plot title.
|
|
243
|
+
show_target: Show target BER line.
|
|
244
|
+
show_eye_opening: Annotate eye opening.
|
|
245
|
+
show: Display plot.
|
|
246
|
+
save_path: Save path.
|
|
247
|
+
|
|
248
|
+
Returns:
|
|
249
|
+
Matplotlib Figure object.
|
|
250
|
+
|
|
251
|
+
Example:
|
|
252
|
+
>>> pos = np.linspace(0, 1, 100)
|
|
253
|
+
>>> ber_l = 0.5 * erfc((pos - 0) / 0.1 / np.sqrt(2))
|
|
254
|
+
>>> ber_r = 0.5 * erfc((1 - pos) / 0.1 / np.sqrt(2))
|
|
255
|
+
>>> fig = plot_bathtub_full(pos, ber_l, ber_r, target_ber=1e-12)
|
|
256
|
+
"""
|
|
257
|
+
if not HAS_MATPLOTLIB:
|
|
258
|
+
raise ImportError("matplotlib is required for visualization")
|
|
259
|
+
|
|
260
|
+
if ax is None:
|
|
261
|
+
fig, ax = plt.subplots(figsize=figsize)
|
|
262
|
+
else:
|
|
263
|
+
fig_temp = ax.get_figure()
|
|
264
|
+
if fig_temp is None:
|
|
265
|
+
raise ValueError("Axes must have an associated figure")
|
|
266
|
+
fig = cast("Figure", fig_temp)
|
|
267
|
+
|
|
268
|
+
# Compute total BER if not provided
|
|
269
|
+
if ber_total is None:
|
|
270
|
+
ber_total = ber_left + ber_right
|
|
271
|
+
|
|
272
|
+
# Clip very small values for log plot
|
|
273
|
+
ber_left_plot = np.clip(ber_left, 1e-18, 1)
|
|
274
|
+
ber_right_plot = np.clip(ber_right, 1e-18, 1)
|
|
275
|
+
ber_total_plot = np.clip(ber_total, 1e-18, 1)
|
|
276
|
+
|
|
277
|
+
# Plot BER curves
|
|
278
|
+
ax.semilogy(positions, ber_left_plot, "b-", linewidth=2, label="BER Left", alpha=0.8)
|
|
279
|
+
ax.semilogy(positions, ber_right_plot, "r-", linewidth=2, label="BER Right", alpha=0.8)
|
|
280
|
+
ax.semilogy(positions, ber_total_plot, "k-", linewidth=2.5, label="BER Total")
|
|
281
|
+
|
|
282
|
+
# Target BER line
|
|
283
|
+
if show_target:
|
|
284
|
+
ax.axhline(
|
|
285
|
+
target_ber,
|
|
286
|
+
color="#27AE60",
|
|
287
|
+
linestyle="--",
|
|
288
|
+
linewidth=2,
|
|
289
|
+
label=f"Target BER = {target_ber:.0e}",
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
# Eye opening annotation
|
|
293
|
+
if show_eye_opening:
|
|
294
|
+
# Find eye opening at target BER
|
|
295
|
+
if eye_opening is None:
|
|
296
|
+
# Find crossover points
|
|
297
|
+
left_cross = np.where(ber_total_plot < target_ber)[0]
|
|
298
|
+
if len(left_cross) > 0:
|
|
299
|
+
left_edge = positions[left_cross[0]]
|
|
300
|
+
right_edge = positions[left_cross[-1]]
|
|
301
|
+
eye_opening = right_edge - left_edge
|
|
302
|
+
else:
|
|
303
|
+
eye_opening = 0
|
|
304
|
+
|
|
305
|
+
if eye_opening > 0:
|
|
306
|
+
# Draw eye opening bracket
|
|
307
|
+
center = 0.5
|
|
308
|
+
left_edge = center - eye_opening / 2
|
|
309
|
+
right_edge = center + eye_opening / 2
|
|
310
|
+
|
|
311
|
+
ax.annotate(
|
|
312
|
+
"",
|
|
313
|
+
xy=(right_edge, target_ber),
|
|
314
|
+
xytext=(left_edge, target_ber),
|
|
315
|
+
arrowprops={"arrowstyle": "<->", "color": "#27AE60", "lw": 2},
|
|
316
|
+
)
|
|
317
|
+
ax.text(
|
|
318
|
+
center,
|
|
319
|
+
target_ber * 0.1,
|
|
320
|
+
f"Eye Opening: {eye_opening:.3f} UI",
|
|
321
|
+
ha="center",
|
|
322
|
+
va="top",
|
|
323
|
+
fontsize=10,
|
|
324
|
+
fontweight="bold",
|
|
325
|
+
color="#27AE60",
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
# Shading for bathtub
|
|
329
|
+
ax.fill_between(positions, 1e-18, ber_total_plot, alpha=0.1, color="gray")
|
|
330
|
+
|
|
331
|
+
# Labels
|
|
332
|
+
ax.set_xlabel("Sample Position (UI)", fontsize=11)
|
|
333
|
+
ax.set_ylabel("Bit Error Rate", fontsize=11)
|
|
334
|
+
ax.set_xlim(0, 1)
|
|
335
|
+
ax.set_ylim(1e-15, 1)
|
|
336
|
+
ax.grid(True, which="both", alpha=0.3)
|
|
337
|
+
ax.legend(loc="upper right")
|
|
338
|
+
|
|
339
|
+
if title:
|
|
340
|
+
ax.set_title(title, fontsize=12, fontweight="bold")
|
|
341
|
+
else:
|
|
342
|
+
ax.set_title("Bathtub Curve", fontsize=12, fontweight="bold")
|
|
343
|
+
|
|
344
|
+
fig.tight_layout()
|
|
345
|
+
|
|
346
|
+
if save_path is not None:
|
|
347
|
+
fig.savefig(save_path, dpi=300, bbox_inches="tight")
|
|
348
|
+
|
|
349
|
+
if show:
|
|
350
|
+
plt.show()
|
|
351
|
+
|
|
352
|
+
return fig
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def plot_ddj(
|
|
356
|
+
patterns: list[str],
|
|
357
|
+
jitter_values: NDArray[np.floating[Any]],
|
|
358
|
+
*,
|
|
359
|
+
ax: Axes | None = None,
|
|
360
|
+
figsize: tuple[float, float] = (12, 6),
|
|
361
|
+
title: str | None = None,
|
|
362
|
+
time_unit: str = "ps",
|
|
363
|
+
show: bool = True,
|
|
364
|
+
save_path: str | Path | None = None,
|
|
365
|
+
) -> Figure:
|
|
366
|
+
"""Plot Data-Dependent Jitter (DDJ) by bit pattern.
|
|
367
|
+
|
|
368
|
+
Creates a bar chart showing jitter contribution for each bit pattern,
|
|
369
|
+
useful for identifying pattern-dependent timing variations.
|
|
370
|
+
|
|
371
|
+
Args:
|
|
372
|
+
patterns: List of bit pattern strings (e.g., ["010", "011", "100"]).
|
|
373
|
+
jitter_values: Jitter values for each pattern.
|
|
374
|
+
ax: Matplotlib axes.
|
|
375
|
+
figsize: Figure size.
|
|
376
|
+
title: Plot title.
|
|
377
|
+
time_unit: Time unit for display.
|
|
378
|
+
show: Display plot.
|
|
379
|
+
save_path: Save path.
|
|
380
|
+
|
|
381
|
+
Returns:
|
|
382
|
+
Matplotlib Figure object.
|
|
383
|
+
|
|
384
|
+
Example:
|
|
385
|
+
>>> patterns = ["000", "001", "010", "011", "100", "101", "110", "111"]
|
|
386
|
+
>>> ddj = np.array([0, 2.1, -1.5, 0.5, 0.8, -0.3, 1.2, -0.8]) # ps
|
|
387
|
+
>>> fig = plot_ddj(patterns, ddj, time_unit="ps")
|
|
388
|
+
"""
|
|
389
|
+
if not HAS_MATPLOTLIB:
|
|
390
|
+
raise ImportError("matplotlib is required for visualization")
|
|
391
|
+
|
|
392
|
+
if ax is None:
|
|
393
|
+
fig, ax = plt.subplots(figsize=figsize)
|
|
394
|
+
else:
|
|
395
|
+
fig_temp = ax.get_figure()
|
|
396
|
+
if fig_temp is None:
|
|
397
|
+
raise ValueError("Axes must have an associated figure")
|
|
398
|
+
fig = cast("Figure", fig_temp)
|
|
399
|
+
|
|
400
|
+
# Color bars based on sign
|
|
401
|
+
colors = ["#E74C3C" if v < 0 else "#27AE60" for v in jitter_values]
|
|
402
|
+
|
|
403
|
+
# Bar chart
|
|
404
|
+
x_pos = np.arange(len(patterns))
|
|
405
|
+
ax.bar(x_pos, jitter_values, color=colors, edgecolor="black", linewidth=0.5)
|
|
406
|
+
|
|
407
|
+
# Reference line at zero
|
|
408
|
+
ax.axhline(0, color="gray", linestyle="-", linewidth=1)
|
|
409
|
+
|
|
410
|
+
# Labels
|
|
411
|
+
ax.set_xticks(x_pos)
|
|
412
|
+
ax.set_xticklabels(patterns, fontfamily="monospace", fontsize=10)
|
|
413
|
+
ax.set_xlabel("Bit Pattern", fontsize=11)
|
|
414
|
+
ax.set_ylabel(f"DDJ ({time_unit})", fontsize=11)
|
|
415
|
+
ax.grid(True, axis="y", alpha=0.3)
|
|
416
|
+
|
|
417
|
+
# Add DDJ pp annotation
|
|
418
|
+
ddj_pp = np.ptp(jitter_values)
|
|
419
|
+
ax.text(
|
|
420
|
+
0.98,
|
|
421
|
+
0.98,
|
|
422
|
+
f"DDJ pk-pk: {ddj_pp:.2f} {time_unit}",
|
|
423
|
+
transform=ax.transAxes,
|
|
424
|
+
fontsize=10,
|
|
425
|
+
ha="right",
|
|
426
|
+
va="top",
|
|
427
|
+
bbox={"boxstyle": "round", "facecolor": "wheat", "alpha": 0.9},
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
if title:
|
|
431
|
+
ax.set_title(title, fontsize=12, fontweight="bold")
|
|
432
|
+
else:
|
|
433
|
+
ax.set_title("Data-Dependent Jitter by Pattern", fontsize=12, fontweight="bold")
|
|
434
|
+
|
|
435
|
+
fig.tight_layout()
|
|
436
|
+
|
|
437
|
+
if save_path is not None:
|
|
438
|
+
fig.savefig(save_path, dpi=300, bbox_inches="tight")
|
|
439
|
+
|
|
440
|
+
if show:
|
|
441
|
+
plt.show()
|
|
442
|
+
|
|
443
|
+
return fig
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def plot_dcd(
|
|
447
|
+
high_times: NDArray[np.floating[Any]],
|
|
448
|
+
low_times: NDArray[np.floating[Any]],
|
|
449
|
+
*,
|
|
450
|
+
ax: Axes | None = None,
|
|
451
|
+
figsize: tuple[float, float] = (10, 6),
|
|
452
|
+
title: str | None = None,
|
|
453
|
+
time_unit: str = "auto",
|
|
454
|
+
show: bool = True,
|
|
455
|
+
save_path: str | Path | None = None,
|
|
456
|
+
) -> Figure:
|
|
457
|
+
"""Plot Duty Cycle Distortion (DCD) analysis.
|
|
458
|
+
|
|
459
|
+
Creates overlaid histograms of high and low pulse times to visualize
|
|
460
|
+
duty cycle distortion.
|
|
461
|
+
|
|
462
|
+
Args:
|
|
463
|
+
high_times: Array of high-state durations.
|
|
464
|
+
low_times: Array of low-state durations.
|
|
465
|
+
ax: Matplotlib axes.
|
|
466
|
+
figsize: Figure size.
|
|
467
|
+
title: Plot title.
|
|
468
|
+
time_unit: Time unit.
|
|
469
|
+
show: Display plot.
|
|
470
|
+
save_path: Save path.
|
|
471
|
+
|
|
472
|
+
Returns:
|
|
473
|
+
Matplotlib Figure object.
|
|
474
|
+
"""
|
|
475
|
+
if not HAS_MATPLOTLIB:
|
|
476
|
+
raise ImportError("matplotlib is required for visualization")
|
|
477
|
+
|
|
478
|
+
if ax is None:
|
|
479
|
+
fig, ax = plt.subplots(figsize=figsize)
|
|
480
|
+
else:
|
|
481
|
+
fig_temp = ax.get_figure()
|
|
482
|
+
if fig_temp is None:
|
|
483
|
+
raise ValueError("Axes must have an associated figure")
|
|
484
|
+
fig = cast("Figure", fig_temp)
|
|
485
|
+
|
|
486
|
+
# Select time unit
|
|
487
|
+
if time_unit == "auto":
|
|
488
|
+
max_time = max(np.max(high_times), np.max(low_times))
|
|
489
|
+
if max_time < 1e-9:
|
|
490
|
+
time_unit = "ps"
|
|
491
|
+
time_mult = 1e12
|
|
492
|
+
elif max_time < 1e-6:
|
|
493
|
+
time_unit = "ns"
|
|
494
|
+
time_mult = 1e9
|
|
495
|
+
else:
|
|
496
|
+
time_unit = "us"
|
|
497
|
+
time_mult = 1e6
|
|
498
|
+
else:
|
|
499
|
+
time_mult = {"s": 1, "ms": 1e3, "us": 1e6, "ns": 1e9, "ps": 1e12}.get(time_unit, 1e9)
|
|
500
|
+
|
|
501
|
+
high_scaled = high_times * time_mult
|
|
502
|
+
low_scaled = low_times * time_mult
|
|
503
|
+
|
|
504
|
+
# Calculate statistics
|
|
505
|
+
mean_high = np.mean(high_scaled)
|
|
506
|
+
mean_low = np.mean(low_scaled)
|
|
507
|
+
period = mean_high + mean_low
|
|
508
|
+
duty_cycle = mean_high / period * 100
|
|
509
|
+
dcd = (mean_high - mean_low) / 2
|
|
510
|
+
|
|
511
|
+
# Determine common bins
|
|
512
|
+
all_times = np.concatenate([high_scaled, low_scaled])
|
|
513
|
+
bins = np.linspace(np.min(all_times) * 0.95, np.max(all_times) * 1.05, 50)
|
|
514
|
+
|
|
515
|
+
# Plot histograms
|
|
516
|
+
ax.hist(
|
|
517
|
+
high_scaled,
|
|
518
|
+
bins=bins,
|
|
519
|
+
alpha=0.6,
|
|
520
|
+
color="#E74C3C",
|
|
521
|
+
label="High Time",
|
|
522
|
+
edgecolor="black",
|
|
523
|
+
linewidth=0.5,
|
|
524
|
+
)
|
|
525
|
+
ax.hist(
|
|
526
|
+
low_scaled,
|
|
527
|
+
bins=bins,
|
|
528
|
+
alpha=0.6,
|
|
529
|
+
color="#3498DB",
|
|
530
|
+
label="Low Time",
|
|
531
|
+
edgecolor="black",
|
|
532
|
+
linewidth=0.5,
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
# Mean lines
|
|
536
|
+
ax.axvline(mean_high, color="#E74C3C", linestyle="--", linewidth=2, alpha=0.8)
|
|
537
|
+
ax.axvline(mean_low, color="#3498DB", linestyle="--", linewidth=2, alpha=0.8)
|
|
538
|
+
|
|
539
|
+
# Statistics box
|
|
540
|
+
stats_text = (
|
|
541
|
+
f"Mean High: {mean_high:.2f} {time_unit}\n"
|
|
542
|
+
f"Mean Low: {mean_low:.2f} {time_unit}\n"
|
|
543
|
+
f"Duty Cycle: {duty_cycle:.1f}%\n"
|
|
544
|
+
f"DCD: {dcd:.2f} {time_unit}"
|
|
545
|
+
)
|
|
546
|
+
ax.text(
|
|
547
|
+
0.98,
|
|
548
|
+
0.98,
|
|
549
|
+
stats_text,
|
|
550
|
+
transform=ax.transAxes,
|
|
551
|
+
fontsize=9,
|
|
552
|
+
va="top",
|
|
553
|
+
ha="right",
|
|
554
|
+
bbox={"boxstyle": "round", "facecolor": "wheat", "alpha": 0.9},
|
|
555
|
+
fontfamily="monospace",
|
|
556
|
+
)
|
|
557
|
+
|
|
558
|
+
ax.set_xlabel(f"Pulse Width ({time_unit})", fontsize=11)
|
|
559
|
+
ax.set_ylabel("Count", fontsize=11)
|
|
560
|
+
ax.grid(True, alpha=0.3)
|
|
561
|
+
ax.legend(loc="upper left")
|
|
562
|
+
|
|
563
|
+
if title:
|
|
564
|
+
ax.set_title(title, fontsize=12, fontweight="bold")
|
|
565
|
+
else:
|
|
566
|
+
ax.set_title("Duty Cycle Distortion Analysis", fontsize=12, fontweight="bold")
|
|
567
|
+
|
|
568
|
+
fig.tight_layout()
|
|
569
|
+
|
|
570
|
+
if save_path is not None:
|
|
571
|
+
fig.savefig(save_path, dpi=300, bbox_inches="tight")
|
|
572
|
+
|
|
573
|
+
if show:
|
|
574
|
+
plt.show()
|
|
575
|
+
|
|
576
|
+
return fig
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
def plot_jitter_trend(
|
|
580
|
+
time_axis: NDArray[np.floating[Any]],
|
|
581
|
+
jitter_values: NDArray[np.floating[Any]],
|
|
582
|
+
*,
|
|
583
|
+
ax: Axes | None = None,
|
|
584
|
+
figsize: tuple[float, float] = (12, 5),
|
|
585
|
+
title: str | None = None,
|
|
586
|
+
time_unit: str = "auto",
|
|
587
|
+
jitter_unit: str = "auto",
|
|
588
|
+
show_trend: bool = True,
|
|
589
|
+
show_bounds: bool = True,
|
|
590
|
+
show: bool = True,
|
|
591
|
+
save_path: str | Path | None = None,
|
|
592
|
+
) -> Figure:
|
|
593
|
+
"""Plot jitter trend over time.
|
|
594
|
+
|
|
595
|
+
Creates a time series plot of jitter values with optional trend line
|
|
596
|
+
and statistical bounds.
|
|
597
|
+
|
|
598
|
+
Args:
|
|
599
|
+
time_axis: Time values (e.g., cycle number or time in seconds).
|
|
600
|
+
jitter_values: Jitter values at each time point.
|
|
601
|
+
ax: Matplotlib axes.
|
|
602
|
+
figsize: Figure size.
|
|
603
|
+
title: Plot title.
|
|
604
|
+
time_unit: Time axis unit.
|
|
605
|
+
jitter_unit: Jitter axis unit.
|
|
606
|
+
show_trend: Show linear trend line.
|
|
607
|
+
show_bounds: Show ±3σ bounds.
|
|
608
|
+
show: Display plot.
|
|
609
|
+
save_path: Save path.
|
|
610
|
+
|
|
611
|
+
Returns:
|
|
612
|
+
Matplotlib Figure object.
|
|
613
|
+
"""
|
|
614
|
+
if not HAS_MATPLOTLIB:
|
|
615
|
+
raise ImportError("matplotlib is required for visualization")
|
|
616
|
+
|
|
617
|
+
if ax is None:
|
|
618
|
+
fig, ax = plt.subplots(figsize=figsize)
|
|
619
|
+
else:
|
|
620
|
+
fig_temp = ax.get_figure()
|
|
621
|
+
if fig_temp is None:
|
|
622
|
+
raise ValueError("Axes must have an associated figure")
|
|
623
|
+
fig = cast("Figure", fig_temp)
|
|
624
|
+
|
|
625
|
+
# Auto-select jitter unit
|
|
626
|
+
if jitter_unit == "auto":
|
|
627
|
+
max_jitter = np.max(np.abs(jitter_values))
|
|
628
|
+
if max_jitter < 1e-9:
|
|
629
|
+
jitter_unit = "ps"
|
|
630
|
+
jitter_mult = 1e12
|
|
631
|
+
elif max_jitter < 1e-6:
|
|
632
|
+
jitter_unit = "ns"
|
|
633
|
+
jitter_mult = 1e9
|
|
634
|
+
else:
|
|
635
|
+
jitter_unit = "us"
|
|
636
|
+
jitter_mult = 1e6
|
|
637
|
+
else:
|
|
638
|
+
jitter_mult = {"s": 1, "ms": 1e3, "us": 1e6, "ns": 1e9, "ps": 1e12}.get(jitter_unit, 1e12)
|
|
639
|
+
|
|
640
|
+
jitter_scaled = jitter_values * jitter_mult
|
|
641
|
+
|
|
642
|
+
# Plot jitter values
|
|
643
|
+
ax.plot(time_axis, jitter_scaled, "b-", linewidth=0.8, alpha=0.7, label="Jitter")
|
|
644
|
+
|
|
645
|
+
mean_val = np.mean(jitter_scaled)
|
|
646
|
+
std_val = np.std(jitter_scaled)
|
|
647
|
+
|
|
648
|
+
# Mean line
|
|
649
|
+
ax.axhline(
|
|
650
|
+
mean_val,
|
|
651
|
+
color="gray",
|
|
652
|
+
linestyle="-",
|
|
653
|
+
linewidth=1,
|
|
654
|
+
label=f"Mean: {mean_val:.2f} {jitter_unit}",
|
|
655
|
+
)
|
|
656
|
+
|
|
657
|
+
# Statistical bounds
|
|
658
|
+
if show_bounds:
|
|
659
|
+
ax.axhline(mean_val + 3 * std_val, color="#E74C3C", linestyle="--", linewidth=1, alpha=0.7)
|
|
660
|
+
ax.axhline(
|
|
661
|
+
mean_val - 3 * std_val,
|
|
662
|
+
color="#E74C3C",
|
|
663
|
+
linestyle="--",
|
|
664
|
+
linewidth=1,
|
|
665
|
+
alpha=0.7,
|
|
666
|
+
label=f"±3sigma: {3 * std_val:.2f} {jitter_unit}",
|
|
667
|
+
)
|
|
668
|
+
ax.fill_between(
|
|
669
|
+
time_axis, mean_val - 3 * std_val, mean_val + 3 * std_val, alpha=0.1, color="#E74C3C"
|
|
670
|
+
)
|
|
671
|
+
|
|
672
|
+
# Trend line
|
|
673
|
+
if show_trend:
|
|
674
|
+
z = np.polyfit(time_axis, jitter_scaled, 1)
|
|
675
|
+
p = np.poly1d(z)
|
|
676
|
+
ax.plot(
|
|
677
|
+
time_axis,
|
|
678
|
+
p(time_axis),
|
|
679
|
+
"g-",
|
|
680
|
+
linewidth=2,
|
|
681
|
+
label=f"Trend: {z[0]:.2e} {jitter_unit}/unit",
|
|
682
|
+
)
|
|
683
|
+
|
|
684
|
+
ax.set_xlabel(f"Time ({time_unit})" if time_unit != "auto" else "Sample Index", fontsize=11)
|
|
685
|
+
ax.set_ylabel(f"Jitter ({jitter_unit})", fontsize=11)
|
|
686
|
+
ax.grid(True, alpha=0.3)
|
|
687
|
+
ax.legend(loc="upper right")
|
|
688
|
+
|
|
689
|
+
if title:
|
|
690
|
+
ax.set_title(title, fontsize=12, fontweight="bold")
|
|
691
|
+
else:
|
|
692
|
+
ax.set_title("Jitter Trend Analysis", fontsize=12, fontweight="bold")
|
|
693
|
+
|
|
694
|
+
fig.tight_layout()
|
|
695
|
+
|
|
696
|
+
if save_path is not None:
|
|
697
|
+
fig.savefig(save_path, dpi=300, bbox_inches="tight")
|
|
698
|
+
|
|
699
|
+
if show:
|
|
700
|
+
plt.show()
|
|
701
|
+
|
|
702
|
+
return fig
|