oscura 0.0.1__py3-none-any.whl → 0.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- oscura/__init__.py +813 -8
- oscura/__main__.py +392 -0
- oscura/analyzers/__init__.py +37 -0
- oscura/analyzers/digital/__init__.py +177 -0
- oscura/analyzers/digital/bus.py +691 -0
- oscura/analyzers/digital/clock.py +805 -0
- oscura/analyzers/digital/correlation.py +720 -0
- oscura/analyzers/digital/edges.py +632 -0
- oscura/analyzers/digital/extraction.py +413 -0
- oscura/analyzers/digital/quality.py +878 -0
- oscura/analyzers/digital/signal_quality.py +877 -0
- oscura/analyzers/digital/thresholds.py +708 -0
- oscura/analyzers/digital/timing.py +1104 -0
- oscura/analyzers/eye/__init__.py +46 -0
- oscura/analyzers/eye/diagram.py +434 -0
- oscura/analyzers/eye/metrics.py +555 -0
- oscura/analyzers/jitter/__init__.py +83 -0
- oscura/analyzers/jitter/ber.py +333 -0
- oscura/analyzers/jitter/decomposition.py +759 -0
- oscura/analyzers/jitter/measurements.py +413 -0
- oscura/analyzers/jitter/spectrum.py +220 -0
- oscura/analyzers/measurements.py +40 -0
- oscura/analyzers/packet/__init__.py +171 -0
- oscura/analyzers/packet/daq.py +1077 -0
- oscura/analyzers/packet/metrics.py +437 -0
- oscura/analyzers/packet/parser.py +327 -0
- oscura/analyzers/packet/payload.py +2156 -0
- oscura/analyzers/packet/payload_analysis.py +1312 -0
- oscura/analyzers/packet/payload_extraction.py +236 -0
- oscura/analyzers/packet/payload_patterns.py +670 -0
- oscura/analyzers/packet/stream.py +359 -0
- oscura/analyzers/patterns/__init__.py +266 -0
- oscura/analyzers/patterns/clustering.py +1036 -0
- oscura/analyzers/patterns/discovery.py +539 -0
- oscura/analyzers/patterns/learning.py +797 -0
- oscura/analyzers/patterns/matching.py +1091 -0
- oscura/analyzers/patterns/periodic.py +650 -0
- oscura/analyzers/patterns/sequences.py +767 -0
- oscura/analyzers/power/__init__.py +116 -0
- oscura/analyzers/power/ac_power.py +391 -0
- oscura/analyzers/power/basic.py +383 -0
- oscura/analyzers/power/conduction.py +314 -0
- oscura/analyzers/power/efficiency.py +297 -0
- oscura/analyzers/power/ripple.py +356 -0
- oscura/analyzers/power/soa.py +372 -0
- oscura/analyzers/power/switching.py +479 -0
- oscura/analyzers/protocol/__init__.py +150 -0
- oscura/analyzers/protocols/__init__.py +150 -0
- oscura/analyzers/protocols/base.py +500 -0
- oscura/analyzers/protocols/can.py +620 -0
- oscura/analyzers/protocols/can_fd.py +448 -0
- oscura/analyzers/protocols/flexray.py +405 -0
- oscura/analyzers/protocols/hdlc.py +399 -0
- oscura/analyzers/protocols/i2c.py +368 -0
- oscura/analyzers/protocols/i2s.py +296 -0
- oscura/analyzers/protocols/jtag.py +393 -0
- oscura/analyzers/protocols/lin.py +445 -0
- oscura/analyzers/protocols/manchester.py +333 -0
- oscura/analyzers/protocols/onewire.py +501 -0
- oscura/analyzers/protocols/spi.py +334 -0
- oscura/analyzers/protocols/swd.py +325 -0
- oscura/analyzers/protocols/uart.py +393 -0
- oscura/analyzers/protocols/usb.py +495 -0
- oscura/analyzers/signal_integrity/__init__.py +63 -0
- oscura/analyzers/signal_integrity/embedding.py +294 -0
- oscura/analyzers/signal_integrity/equalization.py +370 -0
- oscura/analyzers/signal_integrity/sparams.py +484 -0
- oscura/analyzers/spectral/__init__.py +53 -0
- oscura/analyzers/spectral/chunked.py +273 -0
- oscura/analyzers/spectral/chunked_fft.py +571 -0
- oscura/analyzers/spectral/chunked_wavelet.py +391 -0
- oscura/analyzers/spectral/fft.py +92 -0
- oscura/analyzers/statistical/__init__.py +250 -0
- oscura/analyzers/statistical/checksum.py +923 -0
- oscura/analyzers/statistical/chunked_corr.py +228 -0
- oscura/analyzers/statistical/classification.py +778 -0
- oscura/analyzers/statistical/entropy.py +1113 -0
- oscura/analyzers/statistical/ngrams.py +614 -0
- oscura/analyzers/statistics/__init__.py +119 -0
- oscura/analyzers/statistics/advanced.py +885 -0
- oscura/analyzers/statistics/basic.py +263 -0
- oscura/analyzers/statistics/correlation.py +630 -0
- oscura/analyzers/statistics/distribution.py +298 -0
- oscura/analyzers/statistics/outliers.py +463 -0
- oscura/analyzers/statistics/streaming.py +93 -0
- oscura/analyzers/statistics/trend.py +520 -0
- oscura/analyzers/validation.py +598 -0
- oscura/analyzers/waveform/__init__.py +36 -0
- oscura/analyzers/waveform/measurements.py +943 -0
- oscura/analyzers/waveform/measurements_with_uncertainty.py +371 -0
- oscura/analyzers/waveform/spectral.py +1689 -0
- oscura/analyzers/waveform/wavelets.py +298 -0
- oscura/api/__init__.py +62 -0
- oscura/api/dsl.py +538 -0
- oscura/api/fluent.py +571 -0
- oscura/api/operators.py +498 -0
- oscura/api/optimization.py +392 -0
- oscura/api/profiling.py +396 -0
- oscura/automotive/__init__.py +73 -0
- oscura/automotive/can/__init__.py +52 -0
- oscura/automotive/can/analysis.py +356 -0
- oscura/automotive/can/checksum.py +250 -0
- oscura/automotive/can/correlation.py +212 -0
- oscura/automotive/can/discovery.py +355 -0
- oscura/automotive/can/message_wrapper.py +375 -0
- oscura/automotive/can/models.py +385 -0
- oscura/automotive/can/patterns.py +381 -0
- oscura/automotive/can/session.py +452 -0
- oscura/automotive/can/state_machine.py +300 -0
- oscura/automotive/can/stimulus_response.py +461 -0
- oscura/automotive/dbc/__init__.py +15 -0
- oscura/automotive/dbc/generator.py +156 -0
- oscura/automotive/dbc/parser.py +146 -0
- oscura/automotive/dtc/__init__.py +30 -0
- oscura/automotive/dtc/database.py +3036 -0
- oscura/automotive/j1939/__init__.py +14 -0
- oscura/automotive/j1939/decoder.py +745 -0
- oscura/automotive/loaders/__init__.py +35 -0
- oscura/automotive/loaders/asc.py +98 -0
- oscura/automotive/loaders/blf.py +77 -0
- oscura/automotive/loaders/csv_can.py +136 -0
- oscura/automotive/loaders/dispatcher.py +136 -0
- oscura/automotive/loaders/mdf.py +331 -0
- oscura/automotive/loaders/pcap.py +132 -0
- oscura/automotive/obd/__init__.py +14 -0
- oscura/automotive/obd/decoder.py +707 -0
- oscura/automotive/uds/__init__.py +48 -0
- oscura/automotive/uds/decoder.py +265 -0
- oscura/automotive/uds/models.py +64 -0
- oscura/automotive/visualization.py +369 -0
- oscura/batch/__init__.py +55 -0
- oscura/batch/advanced.py +627 -0
- oscura/batch/aggregate.py +300 -0
- oscura/batch/analyze.py +139 -0
- oscura/batch/logging.py +487 -0
- oscura/batch/metrics.py +556 -0
- oscura/builders/__init__.py +41 -0
- oscura/builders/signal_builder.py +1131 -0
- oscura/cli/__init__.py +14 -0
- oscura/cli/batch.py +339 -0
- oscura/cli/characterize.py +273 -0
- oscura/cli/compare.py +775 -0
- oscura/cli/decode.py +551 -0
- oscura/cli/main.py +247 -0
- oscura/cli/shell.py +350 -0
- oscura/comparison/__init__.py +66 -0
- oscura/comparison/compare.py +397 -0
- oscura/comparison/golden.py +487 -0
- oscura/comparison/limits.py +391 -0
- oscura/comparison/mask.py +434 -0
- oscura/comparison/trace_diff.py +30 -0
- oscura/comparison/visualization.py +481 -0
- oscura/compliance/__init__.py +70 -0
- oscura/compliance/advanced.py +756 -0
- oscura/compliance/masks.py +363 -0
- oscura/compliance/reporting.py +483 -0
- oscura/compliance/testing.py +298 -0
- oscura/component/__init__.py +38 -0
- oscura/component/impedance.py +365 -0
- oscura/component/reactive.py +598 -0
- oscura/component/transmission_line.py +312 -0
- oscura/config/__init__.py +191 -0
- oscura/config/defaults.py +254 -0
- oscura/config/loader.py +348 -0
- oscura/config/memory.py +271 -0
- oscura/config/migration.py +458 -0
- oscura/config/pipeline.py +1077 -0
- oscura/config/preferences.py +530 -0
- oscura/config/protocol.py +875 -0
- oscura/config/schema.py +713 -0
- oscura/config/settings.py +420 -0
- oscura/config/thresholds.py +599 -0
- oscura/convenience.py +457 -0
- oscura/core/__init__.py +299 -0
- oscura/core/audit.py +457 -0
- oscura/core/backend_selector.py +405 -0
- oscura/core/cache.py +590 -0
- oscura/core/cancellation.py +439 -0
- oscura/core/confidence.py +225 -0
- oscura/core/config.py +506 -0
- oscura/core/correlation.py +216 -0
- oscura/core/cross_domain.py +422 -0
- oscura/core/debug.py +301 -0
- oscura/core/edge_cases.py +541 -0
- oscura/core/exceptions.py +535 -0
- oscura/core/gpu_backend.py +523 -0
- oscura/core/lazy.py +832 -0
- oscura/core/log_query.py +540 -0
- oscura/core/logging.py +931 -0
- oscura/core/logging_advanced.py +952 -0
- oscura/core/memoize.py +171 -0
- oscura/core/memory_check.py +274 -0
- oscura/core/memory_guard.py +290 -0
- oscura/core/memory_limits.py +336 -0
- oscura/core/memory_monitor.py +453 -0
- oscura/core/memory_progress.py +465 -0
- oscura/core/memory_warnings.py +315 -0
- oscura/core/numba_backend.py +362 -0
- oscura/core/performance.py +352 -0
- oscura/core/progress.py +524 -0
- oscura/core/provenance.py +358 -0
- oscura/core/results.py +331 -0
- oscura/core/types.py +504 -0
- oscura/core/uncertainty.py +383 -0
- oscura/discovery/__init__.py +52 -0
- oscura/discovery/anomaly_detector.py +672 -0
- oscura/discovery/auto_decoder.py +415 -0
- oscura/discovery/comparison.py +497 -0
- oscura/discovery/quality_validator.py +528 -0
- oscura/discovery/signal_detector.py +769 -0
- oscura/dsl/__init__.py +73 -0
- oscura/dsl/commands.py +246 -0
- oscura/dsl/interpreter.py +455 -0
- oscura/dsl/parser.py +689 -0
- oscura/dsl/repl.py +172 -0
- oscura/exceptions.py +59 -0
- oscura/exploratory/__init__.py +111 -0
- oscura/exploratory/error_recovery.py +642 -0
- oscura/exploratory/fuzzy.py +513 -0
- oscura/exploratory/fuzzy_advanced.py +786 -0
- oscura/exploratory/legacy.py +831 -0
- oscura/exploratory/parse.py +358 -0
- oscura/exploratory/recovery.py +275 -0
- oscura/exploratory/sync.py +382 -0
- oscura/exploratory/unknown.py +707 -0
- oscura/export/__init__.py +25 -0
- oscura/export/wireshark/README.md +265 -0
- oscura/export/wireshark/__init__.py +47 -0
- oscura/export/wireshark/generator.py +312 -0
- oscura/export/wireshark/lua_builder.py +159 -0
- oscura/export/wireshark/templates/dissector.lua.j2 +92 -0
- oscura/export/wireshark/type_mapping.py +165 -0
- oscura/export/wireshark/validator.py +105 -0
- oscura/exporters/__init__.py +94 -0
- oscura/exporters/csv.py +303 -0
- oscura/exporters/exporters.py +44 -0
- oscura/exporters/hdf5.py +219 -0
- oscura/exporters/html_export.py +701 -0
- oscura/exporters/json_export.py +291 -0
- oscura/exporters/markdown_export.py +367 -0
- oscura/exporters/matlab_export.py +354 -0
- oscura/exporters/npz_export.py +219 -0
- oscura/exporters/spice_export.py +210 -0
- oscura/extensibility/__init__.py +131 -0
- oscura/extensibility/docs.py +752 -0
- oscura/extensibility/extensions.py +1125 -0
- oscura/extensibility/logging.py +259 -0
- oscura/extensibility/measurements.py +485 -0
- oscura/extensibility/plugins.py +414 -0
- oscura/extensibility/registry.py +346 -0
- oscura/extensibility/templates.py +913 -0
- oscura/extensibility/validation.py +651 -0
- oscura/filtering/__init__.py +89 -0
- oscura/filtering/base.py +563 -0
- oscura/filtering/convenience.py +564 -0
- oscura/filtering/design.py +725 -0
- oscura/filtering/filters.py +32 -0
- oscura/filtering/introspection.py +605 -0
- oscura/guidance/__init__.py +24 -0
- oscura/guidance/recommender.py +429 -0
- oscura/guidance/wizard.py +518 -0
- oscura/inference/__init__.py +251 -0
- oscura/inference/active_learning/README.md +153 -0
- oscura/inference/active_learning/__init__.py +38 -0
- oscura/inference/active_learning/lstar.py +257 -0
- oscura/inference/active_learning/observation_table.py +230 -0
- oscura/inference/active_learning/oracle.py +78 -0
- oscura/inference/active_learning/teachers/__init__.py +15 -0
- oscura/inference/active_learning/teachers/simulator.py +192 -0
- oscura/inference/adaptive_tuning.py +453 -0
- oscura/inference/alignment.py +653 -0
- oscura/inference/bayesian.py +943 -0
- oscura/inference/binary.py +1016 -0
- oscura/inference/crc_reverse.py +711 -0
- oscura/inference/logic.py +288 -0
- oscura/inference/message_format.py +1305 -0
- oscura/inference/protocol.py +417 -0
- oscura/inference/protocol_dsl.py +1084 -0
- oscura/inference/protocol_library.py +1230 -0
- oscura/inference/sequences.py +809 -0
- oscura/inference/signal_intelligence.py +1509 -0
- oscura/inference/spectral.py +215 -0
- oscura/inference/state_machine.py +634 -0
- oscura/inference/stream.py +918 -0
- oscura/integrations/__init__.py +59 -0
- oscura/integrations/llm.py +1827 -0
- oscura/jupyter/__init__.py +32 -0
- oscura/jupyter/display.py +268 -0
- oscura/jupyter/magic.py +334 -0
- oscura/loaders/__init__.py +526 -0
- oscura/loaders/binary.py +69 -0
- oscura/loaders/configurable.py +1255 -0
- oscura/loaders/csv.py +26 -0
- oscura/loaders/csv_loader.py +473 -0
- oscura/loaders/hdf5.py +9 -0
- oscura/loaders/hdf5_loader.py +510 -0
- oscura/loaders/lazy.py +370 -0
- oscura/loaders/mmap_loader.py +583 -0
- oscura/loaders/numpy_loader.py +436 -0
- oscura/loaders/pcap.py +432 -0
- oscura/loaders/preprocessing.py +368 -0
- oscura/loaders/rigol.py +287 -0
- oscura/loaders/sigrok.py +321 -0
- oscura/loaders/tdms.py +367 -0
- oscura/loaders/tektronix.py +711 -0
- oscura/loaders/validation.py +584 -0
- oscura/loaders/vcd.py +464 -0
- oscura/loaders/wav.py +233 -0
- oscura/math/__init__.py +45 -0
- oscura/math/arithmetic.py +824 -0
- oscura/math/interpolation.py +413 -0
- oscura/onboarding/__init__.py +39 -0
- oscura/onboarding/help.py +498 -0
- oscura/onboarding/tutorials.py +405 -0
- oscura/onboarding/wizard.py +466 -0
- oscura/optimization/__init__.py +19 -0
- oscura/optimization/parallel.py +440 -0
- oscura/optimization/search.py +532 -0
- oscura/pipeline/__init__.py +43 -0
- oscura/pipeline/base.py +338 -0
- oscura/pipeline/composition.py +242 -0
- oscura/pipeline/parallel.py +448 -0
- oscura/pipeline/pipeline.py +375 -0
- oscura/pipeline/reverse_engineering.py +1119 -0
- oscura/plugins/__init__.py +122 -0
- oscura/plugins/base.py +272 -0
- oscura/plugins/cli.py +497 -0
- oscura/plugins/discovery.py +411 -0
- oscura/plugins/isolation.py +418 -0
- oscura/plugins/lifecycle.py +959 -0
- oscura/plugins/manager.py +493 -0
- oscura/plugins/registry.py +421 -0
- oscura/plugins/versioning.py +372 -0
- oscura/py.typed +0 -0
- oscura/quality/__init__.py +65 -0
- oscura/quality/ensemble.py +740 -0
- oscura/quality/explainer.py +338 -0
- oscura/quality/scoring.py +616 -0
- oscura/quality/warnings.py +456 -0
- oscura/reporting/__init__.py +248 -0
- oscura/reporting/advanced.py +1234 -0
- oscura/reporting/analyze.py +448 -0
- oscura/reporting/argument_preparer.py +596 -0
- oscura/reporting/auto_report.py +507 -0
- oscura/reporting/batch.py +615 -0
- oscura/reporting/chart_selection.py +223 -0
- oscura/reporting/comparison.py +330 -0
- oscura/reporting/config.py +615 -0
- oscura/reporting/content/__init__.py +39 -0
- oscura/reporting/content/executive.py +127 -0
- oscura/reporting/content/filtering.py +191 -0
- oscura/reporting/content/minimal.py +257 -0
- oscura/reporting/content/verbosity.py +162 -0
- oscura/reporting/core.py +508 -0
- oscura/reporting/core_formats/__init__.py +17 -0
- oscura/reporting/core_formats/multi_format.py +210 -0
- oscura/reporting/engine.py +836 -0
- oscura/reporting/export.py +366 -0
- oscura/reporting/formatting/__init__.py +129 -0
- oscura/reporting/formatting/emphasis.py +81 -0
- oscura/reporting/formatting/numbers.py +403 -0
- oscura/reporting/formatting/standards.py +55 -0
- oscura/reporting/formatting.py +466 -0
- oscura/reporting/html.py +578 -0
- oscura/reporting/index.py +590 -0
- oscura/reporting/multichannel.py +296 -0
- oscura/reporting/output.py +379 -0
- oscura/reporting/pdf.py +373 -0
- oscura/reporting/plots.py +731 -0
- oscura/reporting/pptx_export.py +360 -0
- oscura/reporting/renderers/__init__.py +11 -0
- oscura/reporting/renderers/pdf.py +94 -0
- oscura/reporting/sections.py +471 -0
- oscura/reporting/standards.py +680 -0
- oscura/reporting/summary_generator.py +368 -0
- oscura/reporting/tables.py +397 -0
- oscura/reporting/template_system.py +724 -0
- oscura/reporting/templates/__init__.py +15 -0
- oscura/reporting/templates/definition.py +205 -0
- oscura/reporting/templates/index.html +649 -0
- oscura/reporting/templates/index.md +173 -0
- oscura/schemas/__init__.py +158 -0
- oscura/schemas/bus_configuration.json +322 -0
- oscura/schemas/device_mapping.json +182 -0
- oscura/schemas/packet_format.json +418 -0
- oscura/schemas/protocol_definition.json +363 -0
- oscura/search/__init__.py +16 -0
- oscura/search/anomaly.py +292 -0
- oscura/search/context.py +149 -0
- oscura/search/pattern.py +160 -0
- oscura/session/__init__.py +34 -0
- oscura/session/annotations.py +289 -0
- oscura/session/history.py +313 -0
- oscura/session/session.py +445 -0
- oscura/streaming/__init__.py +43 -0
- oscura/streaming/chunked.py +611 -0
- oscura/streaming/progressive.py +393 -0
- oscura/streaming/realtime.py +622 -0
- oscura/testing/__init__.py +54 -0
- oscura/testing/synthetic.py +808 -0
- oscura/triggering/__init__.py +68 -0
- oscura/triggering/base.py +229 -0
- oscura/triggering/edge.py +353 -0
- oscura/triggering/pattern.py +344 -0
- oscura/triggering/pulse.py +581 -0
- oscura/triggering/window.py +453 -0
- oscura/ui/__init__.py +48 -0
- oscura/ui/formatters.py +526 -0
- oscura/ui/progressive_display.py +340 -0
- oscura/utils/__init__.py +99 -0
- oscura/utils/autodetect.py +338 -0
- oscura/utils/buffer.py +389 -0
- oscura/utils/lazy.py +407 -0
- oscura/utils/lazy_imports.py +147 -0
- oscura/utils/memory.py +836 -0
- oscura/utils/memory_advanced.py +1326 -0
- oscura/utils/memory_extensions.py +465 -0
- oscura/utils/progressive.py +352 -0
- oscura/utils/windowing.py +362 -0
- oscura/visualization/__init__.py +321 -0
- oscura/visualization/accessibility.py +526 -0
- oscura/visualization/annotations.py +374 -0
- oscura/visualization/axis_scaling.py +305 -0
- oscura/visualization/colors.py +453 -0
- oscura/visualization/digital.py +337 -0
- oscura/visualization/eye.py +420 -0
- oscura/visualization/histogram.py +281 -0
- oscura/visualization/interactive.py +858 -0
- oscura/visualization/jitter.py +702 -0
- oscura/visualization/keyboard.py +394 -0
- oscura/visualization/layout.py +365 -0
- oscura/visualization/optimization.py +1028 -0
- oscura/visualization/palettes.py +446 -0
- oscura/visualization/plot.py +92 -0
- oscura/visualization/power.py +290 -0
- oscura/visualization/power_extended.py +626 -0
- oscura/visualization/presets.py +467 -0
- oscura/visualization/protocols.py +932 -0
- oscura/visualization/render.py +207 -0
- oscura/visualization/rendering.py +444 -0
- oscura/visualization/reverse_engineering.py +791 -0
- oscura/visualization/signal_integrity.py +808 -0
- oscura/visualization/specialized.py +553 -0
- oscura/visualization/spectral.py +811 -0
- oscura/visualization/styles.py +381 -0
- oscura/visualization/thumbnails.py +311 -0
- oscura/visualization/time_axis.py +351 -0
- oscura/visualization/waveform.py +367 -0
- oscura/workflow/__init__.py +13 -0
- oscura/workflow/dag.py +377 -0
- oscura/workflows/__init__.py +58 -0
- oscura/workflows/compliance.py +280 -0
- oscura/workflows/digital.py +272 -0
- oscura/workflows/multi_trace.py +502 -0
- oscura/workflows/power.py +178 -0
- oscura/workflows/protocol.py +492 -0
- oscura/workflows/reverse_engineering.py +639 -0
- oscura/workflows/signal_integrity.py +227 -0
- oscura-0.1.1.dist-info/METADATA +300 -0
- oscura-0.1.1.dist-info/RECORD +463 -0
- oscura-0.1.1.dist-info/entry_points.txt +2 -0
- {oscura-0.0.1.dist-info → oscura-0.1.1.dist-info}/licenses/LICENSE +1 -1
- oscura-0.0.1.dist-info/METADATA +0 -63
- oscura-0.0.1.dist-info/RECORD +0 -5
- {oscura-0.0.1.dist-info → oscura-0.1.1.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,1104 @@
|
|
|
1
|
+
"""Advanced timing measurements for digital signals.
|
|
2
|
+
|
|
3
|
+
This module provides IEEE 181-2011 and JEDEC compliant timing measurements
|
|
4
|
+
including propagation delay, setup/hold time, slew rate, phase, and skew.
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
>>> from oscura.analyzers.digital.timing import propagation_delay, setup_time
|
|
9
|
+
>>> delay = propagation_delay(trace1, trace2)
|
|
10
|
+
>>> t_setup = setup_time(data_trace, clock_trace, clock_edge="rising")
|
|
11
|
+
|
|
12
|
+
References:
|
|
13
|
+
IEEE 181-2011: Standard for Transitional Waveform Definitions
|
|
14
|
+
IEEE 2414-2020: Standard for Jitter and Phase Noise
|
|
15
|
+
JEDEC Standard No. 65B: High-Speed Interface Timing
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from dataclasses import dataclass
|
|
21
|
+
from typing import TYPE_CHECKING, Literal
|
|
22
|
+
|
|
23
|
+
import numpy as np
|
|
24
|
+
|
|
25
|
+
from oscura.core.exceptions import InsufficientDataError
|
|
26
|
+
from oscura.core.types import DigitalTrace, WaveformTrace
|
|
27
|
+
|
|
28
|
+
if TYPE_CHECKING:
|
|
29
|
+
from numpy.typing import NDArray
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class ClockRecoveryResult:
|
|
34
|
+
"""Result of clock recovery analysis.
|
|
35
|
+
|
|
36
|
+
Attributes:
|
|
37
|
+
frequency: Recovered clock frequency in Hz.
|
|
38
|
+
period: Recovered clock period in seconds.
|
|
39
|
+
method: Method used for recovery ("fft" or "edge").
|
|
40
|
+
confidence: Confidence score (0.0 to 1.0).
|
|
41
|
+
jitter_rms: RMS jitter in seconds (edge method only).
|
|
42
|
+
jitter_pp: Peak-to-peak jitter in seconds (edge method only).
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
frequency: float
|
|
46
|
+
period: float
|
|
47
|
+
method: str
|
|
48
|
+
confidence: float
|
|
49
|
+
jitter_rms: float | None = None
|
|
50
|
+
jitter_pp: float | None = None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass
|
|
54
|
+
class TimingViolation:
|
|
55
|
+
"""Represents a timing violation.
|
|
56
|
+
|
|
57
|
+
Attributes:
|
|
58
|
+
timestamp: Time of violation in seconds.
|
|
59
|
+
violation_type: Type of violation ("setup" or "hold").
|
|
60
|
+
measured: Measured time in seconds.
|
|
61
|
+
required: Required time (specification) in seconds.
|
|
62
|
+
margin: Margin to specification (negative = violation).
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
timestamp: float
|
|
66
|
+
violation_type: str
|
|
67
|
+
measured: float
|
|
68
|
+
required: float
|
|
69
|
+
margin: float
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass
|
|
73
|
+
class RMSJitterResult:
|
|
74
|
+
"""Result of RMS jitter measurement.
|
|
75
|
+
|
|
76
|
+
Attributes:
|
|
77
|
+
rms: RMS jitter in seconds.
|
|
78
|
+
mean: Mean period in seconds.
|
|
79
|
+
samples: Number of edges used.
|
|
80
|
+
uncertainty: Measurement uncertainty (1-sigma) in seconds.
|
|
81
|
+
edge_type: Type of edges used.
|
|
82
|
+
|
|
83
|
+
References:
|
|
84
|
+
IEEE 2414-2020 Section 5.1
|
|
85
|
+
TIM-007
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
rms: float
|
|
89
|
+
mean: float
|
|
90
|
+
samples: int
|
|
91
|
+
uncertainty: float
|
|
92
|
+
edge_type: str
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def propagation_delay(
|
|
96
|
+
input_trace: WaveformTrace | DigitalTrace,
|
|
97
|
+
output_trace: WaveformTrace | DigitalTrace,
|
|
98
|
+
*,
|
|
99
|
+
ref_level: float = 0.5,
|
|
100
|
+
edge_type: Literal["rising", "falling", "both"] = "rising",
|
|
101
|
+
return_all: bool = False,
|
|
102
|
+
) -> float | NDArray[np.float64]:
|
|
103
|
+
"""Measure propagation delay between two signals.
|
|
104
|
+
|
|
105
|
+
Computes the time delay from input edge to corresponding output edge
|
|
106
|
+
at the specified reference level per IEEE 181-2011.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
input_trace: Input signal trace.
|
|
110
|
+
output_trace: Output signal trace.
|
|
111
|
+
ref_level: Reference level as fraction (0.0 to 1.0). Default 0.5 (50%).
|
|
112
|
+
edge_type: Type of edges to measure:
|
|
113
|
+
- "rising": Low-to-high transitions
|
|
114
|
+
- "falling": High-to-low transitions
|
|
115
|
+
- "both": All transitions
|
|
116
|
+
return_all: If True, return array of all delays. If False, return mean.
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
Propagation delay in seconds (mean if return_all=False), or array of delays.
|
|
120
|
+
|
|
121
|
+
Raises:
|
|
122
|
+
InsufficientDataError: If traces have insufficient edges.
|
|
123
|
+
|
|
124
|
+
Example:
|
|
125
|
+
>>> delay = propagation_delay(input_trace, output_trace)
|
|
126
|
+
>>> print(f"Propagation delay: {delay * 1e9:.2f} ns")
|
|
127
|
+
|
|
128
|
+
References:
|
|
129
|
+
IEEE 181-2011 Section 5.6
|
|
130
|
+
"""
|
|
131
|
+
# Get edge timestamps for both signals
|
|
132
|
+
input_edges = _get_edge_timestamps(input_trace, edge_type, ref_level)
|
|
133
|
+
output_edges = _get_edge_timestamps(output_trace, edge_type, ref_level)
|
|
134
|
+
|
|
135
|
+
if len(input_edges) == 0:
|
|
136
|
+
raise InsufficientDataError(
|
|
137
|
+
"No edges found in input trace",
|
|
138
|
+
required=1,
|
|
139
|
+
available=0,
|
|
140
|
+
analysis_type="propagation_delay",
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
if len(output_edges) == 0:
|
|
144
|
+
raise InsufficientDataError(
|
|
145
|
+
"No edges found in output trace",
|
|
146
|
+
required=1,
|
|
147
|
+
available=0,
|
|
148
|
+
analysis_type="propagation_delay",
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
# Match input edges to nearest subsequent output edges
|
|
152
|
+
delays: list[float] = []
|
|
153
|
+
|
|
154
|
+
for in_edge in input_edges:
|
|
155
|
+
# Find output edges after this input edge
|
|
156
|
+
subsequent_outputs = output_edges[output_edges > in_edge]
|
|
157
|
+
if len(subsequent_outputs) > 0:
|
|
158
|
+
# Use nearest subsequent output edge
|
|
159
|
+
delay = subsequent_outputs[0] - in_edge
|
|
160
|
+
if delay > 0:
|
|
161
|
+
delays.append(delay)
|
|
162
|
+
|
|
163
|
+
if len(delays) == 0:
|
|
164
|
+
if return_all:
|
|
165
|
+
return np.array([], dtype=np.float64)
|
|
166
|
+
return np.nan
|
|
167
|
+
|
|
168
|
+
delays_arr = np.array(delays, dtype=np.float64)
|
|
169
|
+
|
|
170
|
+
if return_all:
|
|
171
|
+
return delays_arr
|
|
172
|
+
return float(np.mean(delays_arr))
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def setup_time(
|
|
176
|
+
data_trace: WaveformTrace | DigitalTrace,
|
|
177
|
+
clock_trace: WaveformTrace | DigitalTrace,
|
|
178
|
+
*,
|
|
179
|
+
clock_edge: Literal["rising", "falling"] = "rising",
|
|
180
|
+
data_stable_level: float = 0.5,
|
|
181
|
+
return_all: bool = False,
|
|
182
|
+
) -> float | NDArray[np.float64]:
|
|
183
|
+
"""Measure setup time between data and clock signals.
|
|
184
|
+
|
|
185
|
+
Computes the time from when data becomes stable to the clock edge
|
|
186
|
+
per JEDEC timing standards.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
data_trace: Data signal trace.
|
|
190
|
+
clock_trace: Clock signal trace.
|
|
191
|
+
clock_edge: Type of clock edge to reference ("rising" or "falling").
|
|
192
|
+
data_stable_level: Reference level for data stability (0.0 to 1.0).
|
|
193
|
+
return_all: If True, return array of all setup times. If False, return mean.
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
Setup time in seconds (positive = data stable before clock).
|
|
197
|
+
|
|
198
|
+
Example:
|
|
199
|
+
>>> t_setup = setup_time(data_trace, clock_trace, clock_edge="rising")
|
|
200
|
+
>>> print(f"Setup time: {t_setup * 1e9:.2f} ns")
|
|
201
|
+
|
|
202
|
+
References:
|
|
203
|
+
JEDEC Standard No. 65B
|
|
204
|
+
"""
|
|
205
|
+
# Get clock edges
|
|
206
|
+
clock_edges = _get_edge_timestamps(clock_trace, clock_edge, 0.5)
|
|
207
|
+
|
|
208
|
+
if len(clock_edges) == 0:
|
|
209
|
+
if return_all:
|
|
210
|
+
return np.array([], dtype=np.float64)
|
|
211
|
+
return np.nan
|
|
212
|
+
|
|
213
|
+
# Get all data edges (both rising and falling)
|
|
214
|
+
data_edges = _get_edge_timestamps(data_trace, "both", data_stable_level)
|
|
215
|
+
|
|
216
|
+
if len(data_edges) == 0:
|
|
217
|
+
if return_all:
|
|
218
|
+
return np.array([], dtype=np.float64)
|
|
219
|
+
return np.nan
|
|
220
|
+
|
|
221
|
+
# For each clock edge, find the most recent data edge
|
|
222
|
+
setup_times: list[float] = []
|
|
223
|
+
|
|
224
|
+
for clk_edge in clock_edges:
|
|
225
|
+
# Find data edges before this clock edge
|
|
226
|
+
prior_data_edges = data_edges[data_edges < clk_edge]
|
|
227
|
+
if len(prior_data_edges) > 0:
|
|
228
|
+
# Setup time = clock edge - last data edge
|
|
229
|
+
setup = clk_edge - prior_data_edges[-1]
|
|
230
|
+
setup_times.append(setup)
|
|
231
|
+
|
|
232
|
+
if len(setup_times) == 0:
|
|
233
|
+
if return_all:
|
|
234
|
+
return np.array([], dtype=np.float64)
|
|
235
|
+
return np.nan
|
|
236
|
+
|
|
237
|
+
result = np.array(setup_times, dtype=np.float64)
|
|
238
|
+
|
|
239
|
+
if return_all:
|
|
240
|
+
return result
|
|
241
|
+
return float(np.mean(result))
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def hold_time(
|
|
245
|
+
data_trace: WaveformTrace | DigitalTrace,
|
|
246
|
+
clock_trace: WaveformTrace | DigitalTrace,
|
|
247
|
+
*,
|
|
248
|
+
clock_edge: Literal["rising", "falling"] = "rising",
|
|
249
|
+
data_stable_level: float = 0.5,
|
|
250
|
+
return_all: bool = False,
|
|
251
|
+
) -> float | NDArray[np.float64]:
|
|
252
|
+
"""Measure hold time between clock and data signals.
|
|
253
|
+
|
|
254
|
+
Computes the time from clock edge to when data changes
|
|
255
|
+
per JEDEC timing standards.
|
|
256
|
+
|
|
257
|
+
Args:
|
|
258
|
+
data_trace: Data signal trace.
|
|
259
|
+
clock_trace: Clock signal trace.
|
|
260
|
+
clock_edge: Type of clock edge to reference ("rising" or "falling").
|
|
261
|
+
data_stable_level: Reference level for data transition (0.0 to 1.0).
|
|
262
|
+
return_all: If True, return array of all hold times. If False, return mean.
|
|
263
|
+
|
|
264
|
+
Returns:
|
|
265
|
+
Hold time in seconds (positive = data stable after clock).
|
|
266
|
+
|
|
267
|
+
Example:
|
|
268
|
+
>>> t_hold = hold_time(data_trace, clock_trace, clock_edge="rising")
|
|
269
|
+
>>> print(f"Hold time: {t_hold * 1e9:.2f} ns")
|
|
270
|
+
|
|
271
|
+
References:
|
|
272
|
+
JEDEC Standard No. 65B
|
|
273
|
+
"""
|
|
274
|
+
# Get clock edges
|
|
275
|
+
clock_edges = _get_edge_timestamps(clock_trace, clock_edge, 0.5)
|
|
276
|
+
|
|
277
|
+
if len(clock_edges) == 0:
|
|
278
|
+
if return_all:
|
|
279
|
+
return np.array([], dtype=np.float64)
|
|
280
|
+
return np.nan
|
|
281
|
+
|
|
282
|
+
# Get all data edges
|
|
283
|
+
data_edges = _get_edge_timestamps(data_trace, "both", data_stable_level)
|
|
284
|
+
|
|
285
|
+
if len(data_edges) == 0:
|
|
286
|
+
if return_all:
|
|
287
|
+
return np.array([], dtype=np.float64)
|
|
288
|
+
return np.nan
|
|
289
|
+
|
|
290
|
+
# For each clock edge, find the next data edge
|
|
291
|
+
hold_times: list[float] = []
|
|
292
|
+
|
|
293
|
+
for clk_edge in clock_edges:
|
|
294
|
+
# Find data edges after this clock edge
|
|
295
|
+
subsequent_data_edges = data_edges[data_edges > clk_edge]
|
|
296
|
+
if len(subsequent_data_edges) > 0:
|
|
297
|
+
# Hold time = next data edge - clock edge
|
|
298
|
+
hold = subsequent_data_edges[0] - clk_edge
|
|
299
|
+
hold_times.append(hold)
|
|
300
|
+
|
|
301
|
+
if len(hold_times) == 0:
|
|
302
|
+
if return_all:
|
|
303
|
+
return np.array([], dtype=np.float64)
|
|
304
|
+
return np.nan
|
|
305
|
+
|
|
306
|
+
result = np.array(hold_times, dtype=np.float64)
|
|
307
|
+
|
|
308
|
+
if return_all:
|
|
309
|
+
return result
|
|
310
|
+
return float(np.mean(result))
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def slew_rate(
|
|
314
|
+
trace: WaveformTrace,
|
|
315
|
+
*,
|
|
316
|
+
ref_levels: tuple[float, float] = (0.2, 0.8),
|
|
317
|
+
edge_type: Literal["rising", "falling", "both"] = "rising",
|
|
318
|
+
return_all: bool = False,
|
|
319
|
+
) -> float | NDArray[np.float64]:
|
|
320
|
+
"""Measure slew rate (dV/dt) during signal transitions.
|
|
321
|
+
|
|
322
|
+
Computes the rate of voltage change during edge transitions
|
|
323
|
+
per IEEE 181-2011.
|
|
324
|
+
|
|
325
|
+
Args:
|
|
326
|
+
trace: Input waveform trace.
|
|
327
|
+
ref_levels: Reference levels as fractions (default 20%-80%).
|
|
328
|
+
edge_type: Type of edges to measure.
|
|
329
|
+
return_all: If True, return array of all slew rates. If False, return mean.
|
|
330
|
+
|
|
331
|
+
Returns:
|
|
332
|
+
Slew rate in V/s (positive for rising, negative for falling).
|
|
333
|
+
|
|
334
|
+
Example:
|
|
335
|
+
>>> sr = slew_rate(trace)
|
|
336
|
+
>>> print(f"Slew rate: {sr / 1e6:.2f} V/us")
|
|
337
|
+
|
|
338
|
+
References:
|
|
339
|
+
IEEE 181-2011 Section 5.2
|
|
340
|
+
"""
|
|
341
|
+
if len(trace.data) < 3:
|
|
342
|
+
if return_all:
|
|
343
|
+
return np.array([], dtype=np.float64)
|
|
344
|
+
return np.nan
|
|
345
|
+
|
|
346
|
+
data = trace.data
|
|
347
|
+
sample_period = trace.metadata.time_base
|
|
348
|
+
|
|
349
|
+
# Find signal levels
|
|
350
|
+
low, high = _find_levels(data)
|
|
351
|
+
amplitude = high - low
|
|
352
|
+
|
|
353
|
+
if amplitude <= 0:
|
|
354
|
+
if return_all:
|
|
355
|
+
return np.array([], dtype=np.float64)
|
|
356
|
+
return np.nan
|
|
357
|
+
|
|
358
|
+
# Calculate reference voltages
|
|
359
|
+
v_low = low + ref_levels[0] * amplitude
|
|
360
|
+
v_high = low + ref_levels[1] * amplitude
|
|
361
|
+
dv = v_high - v_low
|
|
362
|
+
|
|
363
|
+
slew_rates: list[float] = []
|
|
364
|
+
|
|
365
|
+
if edge_type in ("rising", "both"):
|
|
366
|
+
# Find rising transitions
|
|
367
|
+
rising_start = np.where((data[:-1] < v_low) & (data[1:] >= v_low))[0]
|
|
368
|
+
|
|
369
|
+
for start_idx in rising_start:
|
|
370
|
+
# Find where signal reaches v_high
|
|
371
|
+
remaining = data[start_idx:]
|
|
372
|
+
above_high = remaining >= v_high
|
|
373
|
+
|
|
374
|
+
if np.any(above_high):
|
|
375
|
+
end_offset = np.argmax(above_high)
|
|
376
|
+
dt = end_offset * sample_period
|
|
377
|
+
if dt > 0:
|
|
378
|
+
slew_rates.append(float(dv / dt))
|
|
379
|
+
|
|
380
|
+
if edge_type in ("falling", "both"):
|
|
381
|
+
# Find falling transitions
|
|
382
|
+
falling_start = np.where((data[:-1] > v_high) & (data[1:] <= v_high))[0]
|
|
383
|
+
|
|
384
|
+
for start_idx in falling_start:
|
|
385
|
+
# Find where signal reaches v_low
|
|
386
|
+
remaining = data[start_idx:]
|
|
387
|
+
below_low = remaining <= v_low
|
|
388
|
+
|
|
389
|
+
if np.any(below_low):
|
|
390
|
+
end_offset = np.argmax(below_low)
|
|
391
|
+
dt = end_offset * sample_period
|
|
392
|
+
if dt > 0:
|
|
393
|
+
slew_rates.append(float(-dv / dt)) # Negative for falling
|
|
394
|
+
|
|
395
|
+
if len(slew_rates) == 0:
|
|
396
|
+
if return_all:
|
|
397
|
+
return np.array([], dtype=np.float64)
|
|
398
|
+
return np.nan
|
|
399
|
+
|
|
400
|
+
result = np.array(slew_rates, dtype=np.float64)
|
|
401
|
+
|
|
402
|
+
if return_all:
|
|
403
|
+
return result
|
|
404
|
+
return float(np.mean(result))
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def phase(
|
|
408
|
+
trace1: WaveformTrace,
|
|
409
|
+
trace2: WaveformTrace,
|
|
410
|
+
*,
|
|
411
|
+
method: Literal["edge", "fft"] = "edge",
|
|
412
|
+
unit: Literal["degrees", "radians"] = "degrees",
|
|
413
|
+
) -> float:
|
|
414
|
+
"""Measure phase difference between two signals.
|
|
415
|
+
|
|
416
|
+
Computes the phase relationship between two waveforms using
|
|
417
|
+
either edge-based or FFT-based methods.
|
|
418
|
+
|
|
419
|
+
Args:
|
|
420
|
+
trace1: Reference signal trace.
|
|
421
|
+
trace2: Signal to measure phase relative to reference.
|
|
422
|
+
method: Measurement method:
|
|
423
|
+
- "edge": Edge-to-edge timing (default, more accurate for digital)
|
|
424
|
+
- "fft": Cross-spectral phase (better for analog/noisy signals)
|
|
425
|
+
unit: Output unit ("degrees" or "radians").
|
|
426
|
+
|
|
427
|
+
Returns:
|
|
428
|
+
Phase difference in specified units. Positive = trace2 leads trace1.
|
|
429
|
+
|
|
430
|
+
Raises:
|
|
431
|
+
ValueError: If method is not recognized.
|
|
432
|
+
|
|
433
|
+
Example:
|
|
434
|
+
>>> phase_deg = phase(ref_trace, sig_trace)
|
|
435
|
+
>>> print(f"Phase: {phase_deg:.1f} degrees")
|
|
436
|
+
|
|
437
|
+
References:
|
|
438
|
+
IEEE 181-2011 Section 5.8
|
|
439
|
+
"""
|
|
440
|
+
if method == "edge":
|
|
441
|
+
return _phase_edge(trace1, trace2, unit)
|
|
442
|
+
elif method == "fft":
|
|
443
|
+
return _phase_fft(trace1, trace2, unit)
|
|
444
|
+
else:
|
|
445
|
+
raise ValueError(f"Unknown method: {method}")
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def _phase_edge(
|
|
449
|
+
trace1: WaveformTrace,
|
|
450
|
+
trace2: WaveformTrace,
|
|
451
|
+
unit: Literal["degrees", "radians"],
|
|
452
|
+
) -> float:
|
|
453
|
+
"""Compute phase using edge timing."""
|
|
454
|
+
# Get rising edges for both signals
|
|
455
|
+
edges1 = _get_edge_timestamps(trace1, "rising", 0.5)
|
|
456
|
+
edges2 = _get_edge_timestamps(trace2, "rising", 0.5)
|
|
457
|
+
|
|
458
|
+
if len(edges1) < 2 or len(edges2) < 2:
|
|
459
|
+
return np.nan # type: ignore[no-any-return]
|
|
460
|
+
|
|
461
|
+
# Calculate period from first signal
|
|
462
|
+
period1 = np.mean(np.diff(edges1))
|
|
463
|
+
|
|
464
|
+
if period1 <= 0:
|
|
465
|
+
return np.nan # type: ignore[no-any-return]
|
|
466
|
+
|
|
467
|
+
# Calculate phase from edge differences
|
|
468
|
+
phase_times: list[float] = []
|
|
469
|
+
|
|
470
|
+
for e1 in edges1:
|
|
471
|
+
# Find nearest edge in trace2
|
|
472
|
+
diffs = edges2 - e1
|
|
473
|
+
# Find closest edge (could be before or after)
|
|
474
|
+
idx = np.argmin(np.abs(diffs))
|
|
475
|
+
phase_times.append(diffs[idx])
|
|
476
|
+
|
|
477
|
+
if len(phase_times) == 0:
|
|
478
|
+
return np.nan # type: ignore[no-any-return]
|
|
479
|
+
|
|
480
|
+
mean_phase_time = np.mean(phase_times)
|
|
481
|
+
|
|
482
|
+
# Convert to phase angle
|
|
483
|
+
phase_rad = 2 * np.pi * mean_phase_time / period1
|
|
484
|
+
|
|
485
|
+
# Normalize to [-pi, pi]
|
|
486
|
+
phase_rad = (phase_rad + np.pi) % (2 * np.pi) - np.pi
|
|
487
|
+
|
|
488
|
+
if unit == "degrees":
|
|
489
|
+
return float(np.degrees(phase_rad))
|
|
490
|
+
return float(phase_rad)
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def _phase_fft(
|
|
494
|
+
trace1: WaveformTrace,
|
|
495
|
+
trace2: WaveformTrace,
|
|
496
|
+
unit: Literal["degrees", "radians"],
|
|
497
|
+
) -> float:
|
|
498
|
+
"""Compute phase using FFT cross-spectral analysis."""
|
|
499
|
+
data1 = trace1.data - np.mean(trace1.data)
|
|
500
|
+
data2 = trace2.data - np.mean(trace2.data)
|
|
501
|
+
|
|
502
|
+
# Ensure same length
|
|
503
|
+
n = min(len(data1), len(data2))
|
|
504
|
+
data1 = data1[:n]
|
|
505
|
+
data2 = data2[:n]
|
|
506
|
+
|
|
507
|
+
if n < 16:
|
|
508
|
+
return np.nan # type: ignore[no-any-return]
|
|
509
|
+
|
|
510
|
+
# Compute FFTs
|
|
511
|
+
fft1 = np.fft.rfft(data1)
|
|
512
|
+
fft2 = np.fft.rfft(data2)
|
|
513
|
+
|
|
514
|
+
# Cross-spectrum
|
|
515
|
+
cross = fft2 * np.conj(fft1)
|
|
516
|
+
|
|
517
|
+
# Find fundamental frequency (strongest component after DC)
|
|
518
|
+
magnitudes = np.abs(cross)
|
|
519
|
+
fund_idx = np.argmax(magnitudes[1:]) + 1
|
|
520
|
+
|
|
521
|
+
# Phase at fundamental
|
|
522
|
+
phase_rad = np.angle(cross[fund_idx])
|
|
523
|
+
|
|
524
|
+
if unit == "degrees":
|
|
525
|
+
return float(np.degrees(phase_rad))
|
|
526
|
+
return float(phase_rad)
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
def skew(
|
|
530
|
+
traces: list[WaveformTrace | DigitalTrace],
|
|
531
|
+
*,
|
|
532
|
+
reference_idx: int = 0,
|
|
533
|
+
edge_type: Literal["rising", "falling"] = "rising",
|
|
534
|
+
) -> dict[str, float | NDArray[np.float64]]:
|
|
535
|
+
"""Measure timing skew between multiple signals.
|
|
536
|
+
|
|
537
|
+
Computes the timing offset of each signal relative to a reference
|
|
538
|
+
per IEEE 181-2011.
|
|
539
|
+
|
|
540
|
+
Args:
|
|
541
|
+
traces: List of signal traces to compare.
|
|
542
|
+
reference_idx: Index of reference signal (default 0).
|
|
543
|
+
edge_type: Type of edges to use for comparison.
|
|
544
|
+
|
|
545
|
+
Returns:
|
|
546
|
+
Dictionary with skew statistics:
|
|
547
|
+
- skew_values: Array of skew for each non-reference trace
|
|
548
|
+
- min: Minimum skew
|
|
549
|
+
- max: Maximum skew
|
|
550
|
+
- mean: Mean skew
|
|
551
|
+
- range: Max - min (total skew spread)
|
|
552
|
+
|
|
553
|
+
Raises:
|
|
554
|
+
ValueError: If fewer than 2 traces or reference_idx out of range.
|
|
555
|
+
|
|
556
|
+
Example:
|
|
557
|
+
>>> result = skew([clk1, clk2, clk3])
|
|
558
|
+
>>> print(f"Max skew: {result['max'] * 1e12:.0f} ps")
|
|
559
|
+
|
|
560
|
+
References:
|
|
561
|
+
IEEE 181-2011 Section 5.7
|
|
562
|
+
"""
|
|
563
|
+
if len(traces) < 2:
|
|
564
|
+
raise ValueError("Need at least 2 traces for skew measurement")
|
|
565
|
+
|
|
566
|
+
if reference_idx >= len(traces):
|
|
567
|
+
raise ValueError(f"reference_idx {reference_idx} out of range")
|
|
568
|
+
|
|
569
|
+
# Get reference edges
|
|
570
|
+
ref_trace = traces[reference_idx]
|
|
571
|
+
ref_edges = _get_edge_timestamps(ref_trace, edge_type, 0.5)
|
|
572
|
+
|
|
573
|
+
if len(ref_edges) == 0:
|
|
574
|
+
return {
|
|
575
|
+
"skew_values": np.array([], dtype=np.float64),
|
|
576
|
+
"min": float(np.nan),
|
|
577
|
+
"max": float(np.nan),
|
|
578
|
+
"mean": float(np.nan),
|
|
579
|
+
"range": float(np.nan),
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
# Compute skew for all traces (including reference which has 0 skew)
|
|
583
|
+
all_skews: list[float] = []
|
|
584
|
+
skew_values: list[float] = []
|
|
585
|
+
|
|
586
|
+
for i, trace in enumerate(traces):
|
|
587
|
+
if i == reference_idx:
|
|
588
|
+
# Reference has zero skew by definition
|
|
589
|
+
all_skews.append(0.0)
|
|
590
|
+
continue
|
|
591
|
+
|
|
592
|
+
trace_edges = _get_edge_timestamps(trace, edge_type, 0.5)
|
|
593
|
+
|
|
594
|
+
if len(trace_edges) == 0:
|
|
595
|
+
skew_val = np.nan
|
|
596
|
+
else:
|
|
597
|
+
# Match edges and compute skew
|
|
598
|
+
edge_skews = []
|
|
599
|
+
for ref_edge in ref_edges:
|
|
600
|
+
# Find nearest edge in this trace
|
|
601
|
+
diffs = np.abs(trace_edges - ref_edge)
|
|
602
|
+
nearest_idx = np.argmin(diffs)
|
|
603
|
+
skew_val_edge = trace_edges[nearest_idx] - ref_edge
|
|
604
|
+
edge_skews.append(skew_val_edge)
|
|
605
|
+
|
|
606
|
+
skew_val = float(np.mean(edge_skews)) if len(edge_skews) > 0 else np.nan
|
|
607
|
+
|
|
608
|
+
skew_values.append(skew_val)
|
|
609
|
+
all_skews.append(skew_val)
|
|
610
|
+
|
|
611
|
+
skew_arr = np.array(skew_values, dtype=np.float64)
|
|
612
|
+
all_skews_arr = np.array(all_skews, dtype=np.float64)
|
|
613
|
+
valid_all_skews = all_skews_arr[~np.isnan(all_skews_arr)]
|
|
614
|
+
|
|
615
|
+
if len(valid_all_skews) == 0:
|
|
616
|
+
return {
|
|
617
|
+
"skew_values": skew_arr,
|
|
618
|
+
"min": np.nan,
|
|
619
|
+
"max": np.nan,
|
|
620
|
+
"mean": np.nan,
|
|
621
|
+
"range": np.nan,
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
# Compute statistics across ALL traces (including reference)
|
|
625
|
+
return {
|
|
626
|
+
"skew_values": skew_arr,
|
|
627
|
+
"min": float(np.min(valid_all_skews)),
|
|
628
|
+
"max": float(np.max(valid_all_skews)),
|
|
629
|
+
"mean": float(np.mean(valid_all_skews)),
|
|
630
|
+
"range": float(np.max(valid_all_skews) - np.min(valid_all_skews)),
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
def recover_clock_fft(
|
|
635
|
+
trace: WaveformTrace | DigitalTrace,
|
|
636
|
+
*,
|
|
637
|
+
min_freq: float | None = None,
|
|
638
|
+
max_freq: float | None = None,
|
|
639
|
+
) -> ClockRecoveryResult:
|
|
640
|
+
"""Recover clock frequency using FFT peak detection.
|
|
641
|
+
|
|
642
|
+
Detects the dominant frequency component in the signal using
|
|
643
|
+
FFT analysis, suitable for periodic digital signals.
|
|
644
|
+
|
|
645
|
+
Args:
|
|
646
|
+
trace: Input trace (analog or digital).
|
|
647
|
+
min_freq: Minimum frequency to consider (Hz). Default: sample_rate/1000.
|
|
648
|
+
max_freq: Maximum frequency to consider (Hz). Default: sample_rate/2.
|
|
649
|
+
|
|
650
|
+
Returns:
|
|
651
|
+
ClockRecoveryResult with recovered frequency and confidence.
|
|
652
|
+
|
|
653
|
+
Raises:
|
|
654
|
+
InsufficientDataError: If trace has fewer than 16 samples.
|
|
655
|
+
|
|
656
|
+
Example:
|
|
657
|
+
>>> result = recover_clock_fft(trace)
|
|
658
|
+
>>> print(f"Clock: {result.frequency / 1e6:.3f} MHz")
|
|
659
|
+
|
|
660
|
+
References:
|
|
661
|
+
IEEE 1241-2010 Section 4.1
|
|
662
|
+
"""
|
|
663
|
+
data = trace.data.astype(np.float64) if isinstance(trace, DigitalTrace) else trace.data
|
|
664
|
+
|
|
665
|
+
n = len(data)
|
|
666
|
+
sample_rate = trace.metadata.sample_rate
|
|
667
|
+
|
|
668
|
+
if n < 16:
|
|
669
|
+
raise InsufficientDataError(
|
|
670
|
+
"FFT clock recovery requires at least 16 samples",
|
|
671
|
+
required=16,
|
|
672
|
+
available=n,
|
|
673
|
+
analysis_type="clock_recovery_fft",
|
|
674
|
+
)
|
|
675
|
+
|
|
676
|
+
# Set frequency range defaults
|
|
677
|
+
if min_freq is None:
|
|
678
|
+
min_freq = sample_rate / 1000
|
|
679
|
+
if max_freq is None:
|
|
680
|
+
max_freq = sample_rate / 2
|
|
681
|
+
|
|
682
|
+
# Remove DC and compute FFT
|
|
683
|
+
data_centered = data - np.mean(data)
|
|
684
|
+
nfft = int(2 ** np.ceil(np.log2(n)))
|
|
685
|
+
spectrum = np.fft.rfft(data_centered, n=nfft)
|
|
686
|
+
freq = np.fft.rfftfreq(nfft, d=1.0 / sample_rate)
|
|
687
|
+
magnitude = np.abs(spectrum)
|
|
688
|
+
|
|
689
|
+
# Apply frequency range mask
|
|
690
|
+
mask = (freq >= min_freq) & (freq <= max_freq)
|
|
691
|
+
valid_indices = np.where(mask)[0]
|
|
692
|
+
|
|
693
|
+
if len(valid_indices) == 0:
|
|
694
|
+
return ClockRecoveryResult(
|
|
695
|
+
frequency=np.nan,
|
|
696
|
+
period=np.nan,
|
|
697
|
+
method="fft",
|
|
698
|
+
confidence=0.0,
|
|
699
|
+
)
|
|
700
|
+
|
|
701
|
+
# Find peak in valid range
|
|
702
|
+
local_peak_idx = np.argmax(magnitude[valid_indices])
|
|
703
|
+
peak_idx = valid_indices[local_peak_idx]
|
|
704
|
+
peak_freq = freq[peak_idx]
|
|
705
|
+
peak_mag = magnitude[peak_idx]
|
|
706
|
+
|
|
707
|
+
# Calculate confidence (ratio of peak to RMS of spectrum)
|
|
708
|
+
rms_mag = np.sqrt(np.mean(magnitude[valid_indices] ** 2))
|
|
709
|
+
confidence = min(1.0, (peak_mag / rms_mag - 1) / 10) if rms_mag > 0 else 0.0
|
|
710
|
+
|
|
711
|
+
# Parabolic interpolation for more accurate frequency
|
|
712
|
+
if 0 < peak_idx < len(magnitude) - 1:
|
|
713
|
+
alpha = magnitude[peak_idx - 1]
|
|
714
|
+
beta = magnitude[peak_idx]
|
|
715
|
+
gamma = magnitude[peak_idx + 1]
|
|
716
|
+
|
|
717
|
+
if beta > alpha and beta > gamma:
|
|
718
|
+
freq_resolution = sample_rate / nfft
|
|
719
|
+
delta = 0.5 * (alpha - gamma) / (alpha - 2 * beta + gamma + 1e-12)
|
|
720
|
+
peak_freq = peak_freq + delta * freq_resolution
|
|
721
|
+
|
|
722
|
+
period = 1.0 / peak_freq if peak_freq > 0 else np.nan
|
|
723
|
+
|
|
724
|
+
return ClockRecoveryResult(
|
|
725
|
+
frequency=float(peak_freq),
|
|
726
|
+
period=float(period),
|
|
727
|
+
method="fft",
|
|
728
|
+
confidence=float(confidence),
|
|
729
|
+
)
|
|
730
|
+
|
|
731
|
+
|
|
732
|
+
def recover_clock_edge(
|
|
733
|
+
trace: WaveformTrace | DigitalTrace,
|
|
734
|
+
*,
|
|
735
|
+
edge_type: Literal["rising", "falling"] = "rising",
|
|
736
|
+
threshold: float | None = None,
|
|
737
|
+
) -> ClockRecoveryResult:
|
|
738
|
+
"""Recover clock frequency from edge timestamps.
|
|
739
|
+
|
|
740
|
+
Computes clock frequency from edge-to-edge timing, also
|
|
741
|
+
providing jitter statistics.
|
|
742
|
+
|
|
743
|
+
Args:
|
|
744
|
+
trace: Input trace (analog or digital).
|
|
745
|
+
edge_type: Type of edges to use ("rising" or "falling").
|
|
746
|
+
threshold: Threshold for edge detection (analog traces only).
|
|
747
|
+
|
|
748
|
+
Returns:
|
|
749
|
+
ClockRecoveryResult with frequency and jitter statistics.
|
|
750
|
+
|
|
751
|
+
Example:
|
|
752
|
+
>>> result = recover_clock_edge(trace)
|
|
753
|
+
>>> print(f"Clock: {result.frequency / 1e6:.3f} MHz")
|
|
754
|
+
>>> print(f"Jitter RMS: {result.jitter_rms * 1e12:.1f} ps")
|
|
755
|
+
|
|
756
|
+
References:
|
|
757
|
+
IEEE 2414-2020 Section 4
|
|
758
|
+
"""
|
|
759
|
+
# Get edge timestamps
|
|
760
|
+
ref_level = 0.5 if threshold is None else threshold
|
|
761
|
+
edges = _get_edge_timestamps(trace, edge_type, ref_level)
|
|
762
|
+
|
|
763
|
+
if len(edges) < 3:
|
|
764
|
+
return ClockRecoveryResult(
|
|
765
|
+
frequency=np.nan,
|
|
766
|
+
period=np.nan,
|
|
767
|
+
method="edge",
|
|
768
|
+
confidence=0.0,
|
|
769
|
+
)
|
|
770
|
+
|
|
771
|
+
# Compute periods
|
|
772
|
+
periods = np.diff(edges)
|
|
773
|
+
|
|
774
|
+
if len(periods) == 0:
|
|
775
|
+
return ClockRecoveryResult(
|
|
776
|
+
frequency=np.nan,
|
|
777
|
+
period=np.nan,
|
|
778
|
+
method="edge",
|
|
779
|
+
confidence=0.0,
|
|
780
|
+
)
|
|
781
|
+
|
|
782
|
+
# Calculate statistics
|
|
783
|
+
mean_period = float(np.mean(periods))
|
|
784
|
+
std_period = float(np.std(periods))
|
|
785
|
+
frequency = 1.0 / mean_period if mean_period > 0 else np.nan
|
|
786
|
+
|
|
787
|
+
# Jitter statistics
|
|
788
|
+
jitter_rms = std_period
|
|
789
|
+
jitter_pp = float(np.max(periods) - np.min(periods))
|
|
790
|
+
|
|
791
|
+
# Confidence based on period consistency (low jitter = high confidence)
|
|
792
|
+
if mean_period > 0:
|
|
793
|
+
cv = std_period / mean_period # Coefficient of variation
|
|
794
|
+
confidence = max(0.0, min(1.0, 1.0 - cv * 10))
|
|
795
|
+
else:
|
|
796
|
+
confidence = 0.0
|
|
797
|
+
|
|
798
|
+
return ClockRecoveryResult(
|
|
799
|
+
frequency=float(frequency),
|
|
800
|
+
period=mean_period,
|
|
801
|
+
method="edge",
|
|
802
|
+
confidence=float(confidence),
|
|
803
|
+
jitter_rms=jitter_rms,
|
|
804
|
+
jitter_pp=jitter_pp,
|
|
805
|
+
)
|
|
806
|
+
|
|
807
|
+
|
|
808
|
+
# =============================================================================
|
|
809
|
+
# Helper Functions
|
|
810
|
+
# =============================================================================
|
|
811
|
+
|
|
812
|
+
|
|
813
|
+
def _get_edge_timestamps(
|
|
814
|
+
trace: WaveformTrace | DigitalTrace,
|
|
815
|
+
edge_type: Literal["rising", "falling", "both"],
|
|
816
|
+
ref_level: float = 0.5,
|
|
817
|
+
) -> NDArray[np.float64]:
|
|
818
|
+
"""Get edge timestamps from a trace.
|
|
819
|
+
|
|
820
|
+
Args:
|
|
821
|
+
trace: Input trace.
|
|
822
|
+
edge_type: Type of edges to find.
|
|
823
|
+
ref_level: Reference level for analog traces (0.0 to 1.0).
|
|
824
|
+
|
|
825
|
+
Returns:
|
|
826
|
+
Array of edge timestamps in seconds.
|
|
827
|
+
"""
|
|
828
|
+
if isinstance(trace, DigitalTrace):
|
|
829
|
+
data = trace.data.astype(np.float64)
|
|
830
|
+
sample_rate = trace.metadata.sample_rate
|
|
831
|
+
else:
|
|
832
|
+
data = trace.data
|
|
833
|
+
sample_rate = trace.metadata.sample_rate
|
|
834
|
+
|
|
835
|
+
if len(data) < 2:
|
|
836
|
+
return np.array([], dtype=np.float64)
|
|
837
|
+
|
|
838
|
+
sample_period = 1.0 / sample_rate
|
|
839
|
+
|
|
840
|
+
# Find threshold level
|
|
841
|
+
low, high = _find_levels(data)
|
|
842
|
+
threshold = low + ref_level * (high - low)
|
|
843
|
+
|
|
844
|
+
timestamps: list[float] = []
|
|
845
|
+
|
|
846
|
+
if edge_type in ("rising", "both"):
|
|
847
|
+
crossings = np.where((data[:-1] < threshold) & (data[1:] >= threshold))[0]
|
|
848
|
+
for idx in crossings:
|
|
849
|
+
# Linear interpolation
|
|
850
|
+
if idx < len(data) - 1:
|
|
851
|
+
v1, v2 = data[idx], data[idx + 1]
|
|
852
|
+
if abs(v2 - v1) > 1e-12:
|
|
853
|
+
t_offset = (threshold - v1) / (v2 - v1) * sample_period
|
|
854
|
+
t_offset = max(0, min(sample_period, t_offset))
|
|
855
|
+
else:
|
|
856
|
+
t_offset = sample_period / 2
|
|
857
|
+
timestamps.append(idx * sample_period + t_offset)
|
|
858
|
+
|
|
859
|
+
if edge_type in ("falling", "both"):
|
|
860
|
+
crossings = np.where((data[:-1] >= threshold) & (data[1:] < threshold))[0]
|
|
861
|
+
for idx in crossings:
|
|
862
|
+
if idx < len(data) - 1:
|
|
863
|
+
v1, v2 = data[idx], data[idx + 1]
|
|
864
|
+
if abs(v2 - v1) > 1e-12:
|
|
865
|
+
t_offset = (threshold - v1) / (v2 - v1) * sample_period
|
|
866
|
+
t_offset = max(0, min(sample_period, t_offset))
|
|
867
|
+
else:
|
|
868
|
+
t_offset = sample_period / 2
|
|
869
|
+
timestamps.append(idx * sample_period + t_offset)
|
|
870
|
+
|
|
871
|
+
timestamps.sort()
|
|
872
|
+
return np.array(timestamps, dtype=np.float64)
|
|
873
|
+
|
|
874
|
+
|
|
875
|
+
def _find_levels(data: NDArray[np.float64]) -> tuple[float, float]:
|
|
876
|
+
"""Find low and high levels using histogram method.
|
|
877
|
+
|
|
878
|
+
Args:
|
|
879
|
+
data: Waveform data array.
|
|
880
|
+
|
|
881
|
+
Returns:
|
|
882
|
+
Tuple of (low_level, high_level).
|
|
883
|
+
"""
|
|
884
|
+
# Use percentiles for robust level detection
|
|
885
|
+
p10, p90 = np.percentile(data, [10, 90])
|
|
886
|
+
|
|
887
|
+
# Refine using histogram peaks
|
|
888
|
+
try:
|
|
889
|
+
hist, bin_edges = np.histogram(data, bins=50)
|
|
890
|
+
bin_centers = (bin_edges[:-1] + bin_edges[1:]) / 2
|
|
891
|
+
|
|
892
|
+
# Find peaks in lower and upper halves
|
|
893
|
+
mid_idx = len(hist) // 2
|
|
894
|
+
low_idx = np.argmax(hist[:mid_idx])
|
|
895
|
+
high_idx = mid_idx + np.argmax(hist[mid_idx:])
|
|
896
|
+
|
|
897
|
+
low = bin_centers[low_idx]
|
|
898
|
+
high = bin_centers[high_idx]
|
|
899
|
+
|
|
900
|
+
# Sanity check
|
|
901
|
+
if high <= low:
|
|
902
|
+
return float(p10), float(p90)
|
|
903
|
+
|
|
904
|
+
return float(low), float(high)
|
|
905
|
+
except (ValueError, IndexError):
|
|
906
|
+
return float(p10), float(p90)
|
|
907
|
+
|
|
908
|
+
|
|
909
|
+
def rms_jitter(
|
|
910
|
+
trace: WaveformTrace | DigitalTrace,
|
|
911
|
+
*,
|
|
912
|
+
edge_type: Literal["rising", "falling", "both"] = "rising",
|
|
913
|
+
threshold: float = 0.5,
|
|
914
|
+
) -> RMSJitterResult:
|
|
915
|
+
"""Measure RMS jitter from edge timing variations.
|
|
916
|
+
|
|
917
|
+
Computes root-mean-square jitter as the standard deviation of edge
|
|
918
|
+
timing variations per IEEE 2414-2020. RMS jitter characterizes the
|
|
919
|
+
random component of timing uncertainty.
|
|
920
|
+
|
|
921
|
+
Args:
|
|
922
|
+
trace: Input trace (analog or digital).
|
|
923
|
+
edge_type: Type of edges to measure ("rising", "falling", or "both").
|
|
924
|
+
threshold: Threshold for edge detection (0.0 to 1.0).
|
|
925
|
+
|
|
926
|
+
Returns:
|
|
927
|
+
RMSJitterResult containing RMS jitter and statistics.
|
|
928
|
+
|
|
929
|
+
Example:
|
|
930
|
+
>>> result = rms_jitter(clock_trace)
|
|
931
|
+
>>> print(f"RMS jitter: {result.rms * 1e12:.2f} ps")
|
|
932
|
+
>>> print(f"Uncertainty: +/- {result.uncertainty * 1e12:.2f} ps")
|
|
933
|
+
|
|
934
|
+
References:
|
|
935
|
+
IEEE 2414-2020 Section 5.1
|
|
936
|
+
TIM-007
|
|
937
|
+
"""
|
|
938
|
+
# Get edge timestamps
|
|
939
|
+
edges = _get_edge_timestamps(trace, edge_type, threshold)
|
|
940
|
+
|
|
941
|
+
if len(edges) < 3:
|
|
942
|
+
return RMSJitterResult(
|
|
943
|
+
rms=np.nan,
|
|
944
|
+
mean=np.nan,
|
|
945
|
+
samples=0,
|
|
946
|
+
uncertainty=np.nan,
|
|
947
|
+
edge_type=edge_type,
|
|
948
|
+
)
|
|
949
|
+
|
|
950
|
+
# Calculate periods
|
|
951
|
+
periods = np.diff(edges)
|
|
952
|
+
|
|
953
|
+
if len(periods) < 2:
|
|
954
|
+
return RMSJitterResult(
|
|
955
|
+
rms=np.nan,
|
|
956
|
+
mean=np.nan,
|
|
957
|
+
samples=len(edges),
|
|
958
|
+
uncertainty=np.nan,
|
|
959
|
+
edge_type=edge_type,
|
|
960
|
+
)
|
|
961
|
+
|
|
962
|
+
# RMS jitter is the standard deviation of periods
|
|
963
|
+
mean_period = float(np.mean(periods))
|
|
964
|
+
jitter_rms = float(np.std(periods, ddof=1))
|
|
965
|
+
|
|
966
|
+
# Measurement uncertainty (1-sigma)
|
|
967
|
+
# For N samples, uncertainty of std estimate is std / sqrt(2*(N-1))
|
|
968
|
+
n = len(periods)
|
|
969
|
+
uncertainty = jitter_rms / np.sqrt(2 * (n - 1)) if n > 1 else np.nan
|
|
970
|
+
|
|
971
|
+
return RMSJitterResult(
|
|
972
|
+
rms=jitter_rms,
|
|
973
|
+
mean=mean_period,
|
|
974
|
+
samples=n,
|
|
975
|
+
uncertainty=uncertainty,
|
|
976
|
+
edge_type=edge_type,
|
|
977
|
+
)
|
|
978
|
+
|
|
979
|
+
|
|
980
|
+
def peak_to_peak_jitter(
|
|
981
|
+
trace: WaveformTrace | DigitalTrace,
|
|
982
|
+
*,
|
|
983
|
+
edge_type: Literal["rising", "falling", "both"] = "rising",
|
|
984
|
+
threshold: float = 0.5,
|
|
985
|
+
) -> float:
|
|
986
|
+
"""Measure peak-to-peak jitter from edge timing variations.
|
|
987
|
+
|
|
988
|
+
Pk-Pk jitter is the maximum range of edge timing deviations from
|
|
989
|
+
the ideal periodic timing, measured over the observation window.
|
|
990
|
+
|
|
991
|
+
Args:
|
|
992
|
+
trace: Input trace (analog or digital).
|
|
993
|
+
edge_type: Type of edges to measure ("rising", "falling", or "both").
|
|
994
|
+
threshold: Threshold for edge detection (0.0 to 1.0).
|
|
995
|
+
|
|
996
|
+
Returns:
|
|
997
|
+
Peak-to-peak jitter in seconds.
|
|
998
|
+
|
|
999
|
+
Example:
|
|
1000
|
+
>>> jitter_pp = peak_to_peak_jitter(clock_trace)
|
|
1001
|
+
>>> print(f"Pk-Pk jitter: {jitter_pp * 1e12:.2f} ps")
|
|
1002
|
+
|
|
1003
|
+
References:
|
|
1004
|
+
IEEE 2414-2020 Section 5.2
|
|
1005
|
+
TIM-008
|
|
1006
|
+
"""
|
|
1007
|
+
# Get edge timestamps
|
|
1008
|
+
edges = _get_edge_timestamps(trace, edge_type, threshold)
|
|
1009
|
+
|
|
1010
|
+
if len(edges) < 3:
|
|
1011
|
+
return np.nan # type: ignore[no-any-return]
|
|
1012
|
+
|
|
1013
|
+
# Calculate periods
|
|
1014
|
+
periods = np.diff(edges)
|
|
1015
|
+
|
|
1016
|
+
if len(periods) < 2:
|
|
1017
|
+
return np.nan # type: ignore[no-any-return]
|
|
1018
|
+
|
|
1019
|
+
# Pk-Pk jitter is the range of period variations
|
|
1020
|
+
jitter_pp = float(np.max(periods) - np.min(periods))
|
|
1021
|
+
|
|
1022
|
+
return jitter_pp
|
|
1023
|
+
|
|
1024
|
+
|
|
1025
|
+
def time_interval_error(
|
|
1026
|
+
trace: WaveformTrace | DigitalTrace,
|
|
1027
|
+
*,
|
|
1028
|
+
edge_type: Literal["rising", "falling"] = "rising",
|
|
1029
|
+
nominal_period: float | None = None,
|
|
1030
|
+
threshold: float = 0.5,
|
|
1031
|
+
) -> NDArray[np.float64]:
|
|
1032
|
+
"""Measure Time Interval Error (TIE) from clock signal.
|
|
1033
|
+
|
|
1034
|
+
TIE is the deviation of each edge from its ideal position based on
|
|
1035
|
+
the recovered clock period. Provides a time series of jitter values
|
|
1036
|
+
for trend analysis and decomposition.
|
|
1037
|
+
|
|
1038
|
+
Args:
|
|
1039
|
+
trace: Input trace (analog or digital).
|
|
1040
|
+
edge_type: Type of edges to measure ("rising" or "falling").
|
|
1041
|
+
nominal_period: Expected period in seconds. If None, computed from data.
|
|
1042
|
+
threshold: Threshold for edge detection (0.0 to 1.0).
|
|
1043
|
+
|
|
1044
|
+
Returns:
|
|
1045
|
+
Array of TIE values in seconds, one per edge.
|
|
1046
|
+
|
|
1047
|
+
Raises:
|
|
1048
|
+
InsufficientDataError: If trace has fewer than 3 edges.
|
|
1049
|
+
|
|
1050
|
+
Example:
|
|
1051
|
+
>>> tie = time_interval_error(clock_trace)
|
|
1052
|
+
>>> plt.plot(tie * 1e12)
|
|
1053
|
+
>>> plt.ylabel("TIE (ps)")
|
|
1054
|
+
>>> plt.xlabel("Edge number")
|
|
1055
|
+
|
|
1056
|
+
References:
|
|
1057
|
+
IEEE 2414-2020 Section 5.1
|
|
1058
|
+
TIM-009
|
|
1059
|
+
"""
|
|
1060
|
+
# Get edge timestamps
|
|
1061
|
+
edges = _get_edge_timestamps(trace, edge_type, threshold)
|
|
1062
|
+
|
|
1063
|
+
if len(edges) < 3:
|
|
1064
|
+
raise InsufficientDataError(
|
|
1065
|
+
"TIE measurement requires at least 3 edges",
|
|
1066
|
+
required=3,
|
|
1067
|
+
available=len(edges),
|
|
1068
|
+
analysis_type="time_interval_error",
|
|
1069
|
+
)
|
|
1070
|
+
|
|
1071
|
+
# Calculate actual periods
|
|
1072
|
+
periods = np.diff(edges)
|
|
1073
|
+
|
|
1074
|
+
# Use mean period if nominal not provided
|
|
1075
|
+
if nominal_period is None:
|
|
1076
|
+
nominal_period = np.mean(periods)
|
|
1077
|
+
|
|
1078
|
+
# Calculate ideal edge positions
|
|
1079
|
+
n_edges = len(edges)
|
|
1080
|
+
start_time = edges[0]
|
|
1081
|
+
ideal_positions = start_time + np.arange(n_edges) * nominal_period
|
|
1082
|
+
|
|
1083
|
+
# TIE is actual - ideal
|
|
1084
|
+
tie: NDArray[np.float64] = edges - ideal_positions
|
|
1085
|
+
|
|
1086
|
+
return tie
|
|
1087
|
+
|
|
1088
|
+
|
|
1089
|
+
__all__ = [
|
|
1090
|
+
"ClockRecoveryResult",
|
|
1091
|
+
"RMSJitterResult",
|
|
1092
|
+
"TimingViolation",
|
|
1093
|
+
"hold_time",
|
|
1094
|
+
"peak_to_peak_jitter",
|
|
1095
|
+
"phase",
|
|
1096
|
+
"propagation_delay",
|
|
1097
|
+
"recover_clock_edge",
|
|
1098
|
+
"recover_clock_fft",
|
|
1099
|
+
"rms_jitter",
|
|
1100
|
+
"setup_time",
|
|
1101
|
+
"skew",
|
|
1102
|
+
"slew_rate",
|
|
1103
|
+
"time_interval_error",
|
|
1104
|
+
]
|