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,759 @@
|
|
|
1
|
+
"""Jitter decomposition into random and deterministic components.
|
|
2
|
+
|
|
3
|
+
This module implements IEEE 2414-2020 compliant jitter decomposition
|
|
4
|
+
using the dual-Dirac model and spectral analysis techniques.
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
>>> from oscura.analyzers.jitter.decomposition import extract_rj, extract_dj
|
|
9
|
+
>>> rj_result = extract_rj(tie_data)
|
|
10
|
+
>>> print(f"RJ RMS: {rj_result.rj_rms * 1e12:.2f} ps")
|
|
11
|
+
|
|
12
|
+
References:
|
|
13
|
+
IEEE 2414-2020: Standard for Jitter and Phase Noise
|
|
14
|
+
Dual-Dirac Model: JEDEC JESD65C
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from dataclasses import dataclass
|
|
20
|
+
from typing import TYPE_CHECKING, Literal
|
|
21
|
+
|
|
22
|
+
import numpy as np
|
|
23
|
+
from scipy import stats
|
|
24
|
+
|
|
25
|
+
from oscura.core.exceptions import InsufficientDataError
|
|
26
|
+
|
|
27
|
+
if TYPE_CHECKING:
|
|
28
|
+
from numpy.typing import NDArray
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class RandomJitterResult:
|
|
33
|
+
"""Result of random jitter extraction.
|
|
34
|
+
|
|
35
|
+
Attributes:
|
|
36
|
+
rj_rms: Random jitter RMS value in seconds.
|
|
37
|
+
method: Method used for extraction ("tail_fit" or "q_scale").
|
|
38
|
+
confidence: Confidence score (0.0 to 1.0).
|
|
39
|
+
sigma: Gaussian sigma parameter in seconds.
|
|
40
|
+
mu: Gaussian mean offset in seconds.
|
|
41
|
+
n_samples: Number of TIE samples used.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
rj_rms: float
|
|
45
|
+
method: str
|
|
46
|
+
confidence: float
|
|
47
|
+
sigma: float
|
|
48
|
+
mu: float
|
|
49
|
+
n_samples: int
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass
|
|
53
|
+
class DeterministicJitterResult:
|
|
54
|
+
"""Result of deterministic jitter extraction.
|
|
55
|
+
|
|
56
|
+
Attributes:
|
|
57
|
+
dj_pp: Peak-to-peak deterministic jitter in seconds.
|
|
58
|
+
dj_delta: Dual-Dirac delta (half-width) in seconds.
|
|
59
|
+
method: Method used for extraction.
|
|
60
|
+
confidence: Confidence score (0.0 to 1.0).
|
|
61
|
+
histogram: Histogram counts for analysis.
|
|
62
|
+
bin_centers: Bin centers for histogram.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
dj_pp: float
|
|
66
|
+
dj_delta: float
|
|
67
|
+
method: str
|
|
68
|
+
confidence: float
|
|
69
|
+
histogram: NDArray[np.float64] | None = None
|
|
70
|
+
bin_centers: NDArray[np.float64] | None = None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass
|
|
74
|
+
class PeriodicJitterResult:
|
|
75
|
+
"""Result of periodic jitter extraction.
|
|
76
|
+
|
|
77
|
+
Attributes:
|
|
78
|
+
components: List of (frequency_hz, amplitude_seconds) tuples.
|
|
79
|
+
pj_pp: Total periodic jitter peak-to-peak in seconds.
|
|
80
|
+
dominant_frequency: Most significant PJ frequency in Hz.
|
|
81
|
+
dominant_amplitude: Amplitude at dominant frequency in seconds.
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
components: list[tuple[float, float]]
|
|
85
|
+
pj_pp: float
|
|
86
|
+
dominant_frequency: float | None
|
|
87
|
+
dominant_amplitude: float | None
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@dataclass
|
|
91
|
+
class DataDependentJitterResult:
|
|
92
|
+
"""Result of data-dependent jitter extraction.
|
|
93
|
+
|
|
94
|
+
Attributes:
|
|
95
|
+
ddj_pp: Peak-to-peak DDJ in seconds.
|
|
96
|
+
pattern_histogram: Jitter vs bit pattern histogram.
|
|
97
|
+
pattern_length: Length of bit patterns analyzed.
|
|
98
|
+
isi_coefficient: ISI correlation coefficient.
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
ddj_pp: float
|
|
102
|
+
pattern_histogram: dict[str, float]
|
|
103
|
+
pattern_length: int
|
|
104
|
+
isi_coefficient: float
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@dataclass
|
|
108
|
+
class JitterDecomposition:
|
|
109
|
+
"""Complete jitter decomposition result.
|
|
110
|
+
|
|
111
|
+
Attributes:
|
|
112
|
+
rj: Random jitter component.
|
|
113
|
+
dj: Deterministic jitter component.
|
|
114
|
+
pj: Periodic jitter component (optional).
|
|
115
|
+
ddj: Data-dependent jitter component (optional).
|
|
116
|
+
tj_pp: Total jitter peak-to-peak at measured BER.
|
|
117
|
+
ber_measured: BER at which TJ was measured.
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
rj: RandomJitterResult
|
|
121
|
+
dj: DeterministicJitterResult
|
|
122
|
+
pj: PeriodicJitterResult | None = None
|
|
123
|
+
ddj: DataDependentJitterResult | None = None
|
|
124
|
+
tj_pp: float | None = None
|
|
125
|
+
ber_measured: float | None = None
|
|
126
|
+
|
|
127
|
+
@property
|
|
128
|
+
def rj_rms(self) -> float:
|
|
129
|
+
"""Convenience property for random jitter RMS."""
|
|
130
|
+
return self.rj.rj_rms
|
|
131
|
+
|
|
132
|
+
@property
|
|
133
|
+
def dj_pp(self) -> float:
|
|
134
|
+
"""Convenience property for deterministic jitter peak-to-peak."""
|
|
135
|
+
return self.dj.dj_pp
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def extract_rj(
|
|
139
|
+
tie_data: NDArray[np.float64],
|
|
140
|
+
*,
|
|
141
|
+
method: Literal["tail_fit", "q_scale", "auto"] = "auto",
|
|
142
|
+
min_samples: int = 1000,
|
|
143
|
+
) -> RandomJitterResult:
|
|
144
|
+
"""Extract random jitter component from TIE data.
|
|
145
|
+
|
|
146
|
+
Uses the dual-Dirac model to separate random (Gaussian) jitter
|
|
147
|
+
from the total jitter distribution. RJ is the unbounded random
|
|
148
|
+
component typically caused by thermal noise.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
tie_data: Time Interval Error data array in seconds.
|
|
152
|
+
method: Extraction method:
|
|
153
|
+
- "tail_fit": Fit Gaussian to distribution tails
|
|
154
|
+
- "q_scale": Q-scale (probabilistic) analysis
|
|
155
|
+
- "auto": Automatically select best method
|
|
156
|
+
min_samples: Minimum samples required for analysis.
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
RandomJitterResult with RJ_rms and analysis details.
|
|
160
|
+
|
|
161
|
+
Raises:
|
|
162
|
+
InsufficientDataError: If tie_data has fewer than min_samples.
|
|
163
|
+
ValueError: If method is not recognized.
|
|
164
|
+
|
|
165
|
+
Example:
|
|
166
|
+
>>> rj = extract_rj(tie_data)
|
|
167
|
+
>>> print(f"RJ: {rj.rj_rms * 1e12:.2f} ps RMS")
|
|
168
|
+
|
|
169
|
+
References:
|
|
170
|
+
IEEE 2414-2020 Section 6.2
|
|
171
|
+
"""
|
|
172
|
+
if len(tie_data) < min_samples:
|
|
173
|
+
raise InsufficientDataError(
|
|
174
|
+
f"RJ extraction requires at least {min_samples} samples",
|
|
175
|
+
required=min_samples,
|
|
176
|
+
available=len(tie_data),
|
|
177
|
+
analysis_type="random_jitter_extraction",
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
# Remove NaN values
|
|
181
|
+
valid_data = tie_data[~np.isnan(tie_data)]
|
|
182
|
+
|
|
183
|
+
if len(valid_data) < min_samples:
|
|
184
|
+
raise InsufficientDataError(
|
|
185
|
+
f"RJ extraction requires at least {min_samples} valid samples",
|
|
186
|
+
required=min_samples,
|
|
187
|
+
available=len(valid_data),
|
|
188
|
+
analysis_type="random_jitter_extraction",
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
# Select method
|
|
192
|
+
if method == "auto":
|
|
193
|
+
# Use Q-scale for large datasets, tail_fit for smaller
|
|
194
|
+
method = "q_scale" if len(valid_data) > 10000 else "tail_fit"
|
|
195
|
+
|
|
196
|
+
if method == "tail_fit":
|
|
197
|
+
return _extract_rj_tail_fit(valid_data)
|
|
198
|
+
elif method == "q_scale":
|
|
199
|
+
return _extract_rj_q_scale(valid_data)
|
|
200
|
+
else:
|
|
201
|
+
raise ValueError(f"Unknown method: {method}")
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _extract_rj_tail_fit(tie_data: NDArray[np.float64]) -> RandomJitterResult:
|
|
205
|
+
"""Extract RJ using Gaussian tail fitting.
|
|
206
|
+
|
|
207
|
+
Fits a Gaussian distribution to the outer tails of the TIE histogram
|
|
208
|
+
where deterministic jitter has minimal effect.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
tie_data: Time Interval Error data array in seconds.
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
RandomJitterResult with RJ_rms and analysis details.
|
|
215
|
+
"""
|
|
216
|
+
# For pure Gaussian data, the tails should follow a Gaussian perfectly.
|
|
217
|
+
# The key insight is to use Q-Q plot analysis on the extreme tails.
|
|
218
|
+
|
|
219
|
+
sorted_data = np.sort(tie_data)
|
|
220
|
+
n = len(sorted_data)
|
|
221
|
+
|
|
222
|
+
# Use percentiles to estimate Gaussian parameters
|
|
223
|
+
# For a Gaussian: P16 = μ - σ, P50 = μ, P84 = μ + σ # noqa: RUF003
|
|
224
|
+
p16 = np.percentile(sorted_data, 16)
|
|
225
|
+
p50 = np.percentile(sorted_data, 50)
|
|
226
|
+
p84 = np.percentile(sorted_data, 84)
|
|
227
|
+
|
|
228
|
+
# Estimate sigma from the 68% confidence interval
|
|
229
|
+
sigma_estimate = (p84 - p16) / 2
|
|
230
|
+
mu_estimate = p50
|
|
231
|
+
|
|
232
|
+
# Refine estimate using tail data (beyond ±2 sigma)
|
|
233
|
+
# For pure Gaussian, fit Q-Q plot in the tails
|
|
234
|
+
tail_fraction = 0.025 # Use outer 2.5% on each side (beyond ~2 sigma)
|
|
235
|
+
lower_tail_idx = int(n * tail_fraction)
|
|
236
|
+
upper_tail_idx = int(n * (1 - tail_fraction))
|
|
237
|
+
|
|
238
|
+
# Get tail indices
|
|
239
|
+
tail_indices = np.concatenate([np.arange(0, lower_tail_idx), np.arange(upper_tail_idx, n)])
|
|
240
|
+
|
|
241
|
+
if len(tail_indices) >= 10:
|
|
242
|
+
# Q-Q plot analysis on tails
|
|
243
|
+
tail_data = sorted_data[tail_indices]
|
|
244
|
+
tail_probabilities = np.array(
|
|
245
|
+
[i / (n - 1) if i < lower_tail_idx else (i + 1) / (n - 1) for i in tail_indices]
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
# Get theoretical quantiles
|
|
249
|
+
theoretical_quantiles = stats.norm.ppf(tail_probabilities)
|
|
250
|
+
valid_mask = np.isfinite(theoretical_quantiles)
|
|
251
|
+
|
|
252
|
+
if np.sum(valid_mask) >= 10:
|
|
253
|
+
# Linear regression: data = sigma * theoretical + mu
|
|
254
|
+
slope, intercept, r_value, _, _ = stats.linregress(
|
|
255
|
+
theoretical_quantiles[valid_mask], tail_data[valid_mask]
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
sigma = abs(slope)
|
|
259
|
+
mu = intercept
|
|
260
|
+
confidence = max(0.0, min(1.0, r_value**2))
|
|
261
|
+
else:
|
|
262
|
+
sigma = sigma_estimate
|
|
263
|
+
mu = mu_estimate
|
|
264
|
+
confidence = 0.6
|
|
265
|
+
else:
|
|
266
|
+
sigma = sigma_estimate
|
|
267
|
+
mu = mu_estimate
|
|
268
|
+
confidence = 0.6
|
|
269
|
+
|
|
270
|
+
return RandomJitterResult(
|
|
271
|
+
rj_rms=sigma,
|
|
272
|
+
method="tail_fit",
|
|
273
|
+
confidence=confidence,
|
|
274
|
+
sigma=sigma,
|
|
275
|
+
mu=mu,
|
|
276
|
+
n_samples=len(tie_data),
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _extract_rj_q_scale(tie_data: NDArray[np.float64]) -> RandomJitterResult:
|
|
281
|
+
"""Extract RJ using Q-scale (probability plot) analysis.
|
|
282
|
+
|
|
283
|
+
Uses quantile-quantile analysis to separate Gaussian (random)
|
|
284
|
+
from non-Gaussian (deterministic) components.
|
|
285
|
+
|
|
286
|
+
Args:
|
|
287
|
+
tie_data: Time Interval Error data array in seconds.
|
|
288
|
+
|
|
289
|
+
Returns:
|
|
290
|
+
RandomJitterResult with RJ_rms and analysis details.
|
|
291
|
+
"""
|
|
292
|
+
n = len(tie_data)
|
|
293
|
+
sorted_data = np.sort(tie_data)
|
|
294
|
+
|
|
295
|
+
# Calculate theoretical Gaussian quantiles
|
|
296
|
+
probabilities = (np.arange(1, n + 1) - 0.5) / n
|
|
297
|
+
theoretical_quantiles = stats.norm.ppf(probabilities)
|
|
298
|
+
|
|
299
|
+
# Remove infinities from edges
|
|
300
|
+
valid_mask = np.isfinite(theoretical_quantiles)
|
|
301
|
+
theoretical_quantiles = theoretical_quantiles[valid_mask]
|
|
302
|
+
sorted_data = sorted_data[valid_mask]
|
|
303
|
+
|
|
304
|
+
# Focus on the linear (Gaussian) region in the tails
|
|
305
|
+
# Use outer 30% of data for slope estimation
|
|
306
|
+
n_valid = len(sorted_data)
|
|
307
|
+
tail_frac = 0.15
|
|
308
|
+
|
|
309
|
+
lower_idx = int(n_valid * tail_frac)
|
|
310
|
+
upper_idx = int(n_valid * (1 - tail_frac))
|
|
311
|
+
|
|
312
|
+
# Combine tail indices
|
|
313
|
+
tail_indices = np.concatenate([np.arange(0, lower_idx), np.arange(upper_idx, n_valid)])
|
|
314
|
+
|
|
315
|
+
if len(tail_indices) < 10:
|
|
316
|
+
# Fall back to simple estimation
|
|
317
|
+
sigma = np.std(sorted_data)
|
|
318
|
+
mu = np.mean(sorted_data)
|
|
319
|
+
confidence = 0.3
|
|
320
|
+
else:
|
|
321
|
+
# Linear regression on Q-Q tail data
|
|
322
|
+
x_tail = theoretical_quantiles[tail_indices]
|
|
323
|
+
y_tail = sorted_data[tail_indices]
|
|
324
|
+
|
|
325
|
+
slope, intercept, r_value, _p_value, _std_err = stats.linregress(x_tail, y_tail)
|
|
326
|
+
|
|
327
|
+
# Slope of Q-Q plot is sigma, intercept is mu
|
|
328
|
+
sigma = abs(slope)
|
|
329
|
+
mu = intercept
|
|
330
|
+
confidence = min(1.0, max(0.0, r_value**2))
|
|
331
|
+
|
|
332
|
+
return RandomJitterResult(
|
|
333
|
+
rj_rms=sigma,
|
|
334
|
+
method="q_scale",
|
|
335
|
+
confidence=confidence,
|
|
336
|
+
sigma=sigma,
|
|
337
|
+
mu=mu,
|
|
338
|
+
n_samples=n,
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def extract_dj(
|
|
343
|
+
tie_data: NDArray[np.float64],
|
|
344
|
+
rj_result: RandomJitterResult | None = None,
|
|
345
|
+
*,
|
|
346
|
+
min_samples: int = 1000,
|
|
347
|
+
) -> DeterministicJitterResult:
|
|
348
|
+
"""Extract deterministic jitter component from TIE data.
|
|
349
|
+
|
|
350
|
+
DJ is the bounded, repeatable component of jitter. It is calculated
|
|
351
|
+
as TJ - RJ contribution using the dual-Dirac model.
|
|
352
|
+
|
|
353
|
+
Args:
|
|
354
|
+
tie_data: Time Interval Error data array in seconds.
|
|
355
|
+
rj_result: Pre-computed RJ result (computed if None).
|
|
356
|
+
min_samples: Minimum samples required.
|
|
357
|
+
|
|
358
|
+
Returns:
|
|
359
|
+
DeterministicJitterResult with DJ_pp value.
|
|
360
|
+
|
|
361
|
+
Raises:
|
|
362
|
+
InsufficientDataError: If insufficient samples.
|
|
363
|
+
|
|
364
|
+
Example:
|
|
365
|
+
>>> dj = extract_dj(tie_data)
|
|
366
|
+
>>> print(f"DJ: {dj.dj_pp * 1e12:.2f} ps peak-to-peak")
|
|
367
|
+
|
|
368
|
+
References:
|
|
369
|
+
IEEE 2414-2020 Section 6.3
|
|
370
|
+
"""
|
|
371
|
+
if len(tie_data) < min_samples:
|
|
372
|
+
raise InsufficientDataError(
|
|
373
|
+
f"DJ extraction requires at least {min_samples} samples",
|
|
374
|
+
required=min_samples,
|
|
375
|
+
available=len(tie_data),
|
|
376
|
+
analysis_type="deterministic_jitter_extraction",
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
valid_data = tie_data[~np.isnan(tie_data)]
|
|
380
|
+
|
|
381
|
+
# Get RJ if not provided - use tail fitting for better RJ isolation
|
|
382
|
+
if rj_result is None:
|
|
383
|
+
rj_result = extract_rj(valid_data, method="tail_fit", min_samples=min_samples)
|
|
384
|
+
|
|
385
|
+
rj_rms = rj_result.rj_rms
|
|
386
|
+
|
|
387
|
+
# Create histogram for analysis
|
|
388
|
+
n_bins = min(100, len(valid_data) // 50)
|
|
389
|
+
hist, bin_edges = np.histogram(valid_data, bins=n_bins, density=False)
|
|
390
|
+
bin_centers = (bin_edges[:-1] + bin_edges[1:]) / 2
|
|
391
|
+
|
|
392
|
+
# DJ extraction using dual-Dirac model:
|
|
393
|
+
# The dual-Dirac model assumes DJ creates two impulses at ±δ,
|
|
394
|
+
# each convolved with Gaussian RJ.
|
|
395
|
+
# Total distribution is: 0.5*N(μ-δ, σ) + 0.5*N(μ+δ, σ) # noqa: RUF003
|
|
396
|
+
|
|
397
|
+
sorted_data = np.sort(valid_data)
|
|
398
|
+
n = len(sorted_data)
|
|
399
|
+
|
|
400
|
+
# Try to find DJ by detecting bimodal peaks in histogram
|
|
401
|
+
from scipy.ndimage import gaussian_filter1d
|
|
402
|
+
from scipy.signal import find_peaks
|
|
403
|
+
|
|
404
|
+
dj_pp = 0.0
|
|
405
|
+
peak_separation_dj = None
|
|
406
|
+
|
|
407
|
+
# Smooth histogram for peak detection
|
|
408
|
+
if len(hist) >= 5:
|
|
409
|
+
hist_smooth = gaussian_filter1d(hist.astype(float), sigma=2)
|
|
410
|
+
peaks, properties = find_peaks(hist_smooth, prominence=np.max(hist_smooth) * 0.1)
|
|
411
|
+
|
|
412
|
+
# If we find 2 clear peaks, DJ is their separation
|
|
413
|
+
if len(peaks) >= 2:
|
|
414
|
+
# Sort peaks by prominence and take top 2
|
|
415
|
+
prominences = properties.get("prominences", np.ones(len(peaks)))
|
|
416
|
+
sorted_peak_idx = np.argsort(prominences)[::-1][:2]
|
|
417
|
+
top_peaks = peaks[sorted_peak_idx]
|
|
418
|
+
top_peaks = np.sort(top_peaks)
|
|
419
|
+
|
|
420
|
+
# Peak separation in the histogram
|
|
421
|
+
peak_positions = bin_centers[top_peaks]
|
|
422
|
+
peak_separation_dj = abs(peak_positions[1] - peak_positions[0])
|
|
423
|
+
|
|
424
|
+
# If we found peaks, use that as DJ
|
|
425
|
+
if peak_separation_dj is not None and peak_separation_dj > 2 * rj_rms:
|
|
426
|
+
dj_pp = peak_separation_dj
|
|
427
|
+
else:
|
|
428
|
+
# Fallback: use quantile-based method
|
|
429
|
+
# At BER = 1e-4 (Q ≈ 3.72), we're well into the tails
|
|
430
|
+
lower_idx = max(0, int(n * 0.0001))
|
|
431
|
+
upper_idx = min(n - 1, int(n * 0.9999))
|
|
432
|
+
|
|
433
|
+
tj_at_ber = sorted_data[upper_idx] - sorted_data[lower_idx]
|
|
434
|
+
|
|
435
|
+
# For dual-Dirac + RJ: TJ = 2*Q*RJ + DJ
|
|
436
|
+
# Using Q for BER = 1e-4
|
|
437
|
+
q_factor = 3.72
|
|
438
|
+
rj_contribution = 2 * q_factor * rj_rms
|
|
439
|
+
|
|
440
|
+
# DJ is what remains after removing RJ contribution
|
|
441
|
+
dj_pp = max(0.0, tj_at_ber - rj_contribution)
|
|
442
|
+
|
|
443
|
+
# Delta is half the DJ peak-to-peak (dual-Dirac separation)
|
|
444
|
+
dj_delta = dj_pp / 2
|
|
445
|
+
|
|
446
|
+
# Confidence based on whether we found clear DJ
|
|
447
|
+
if peak_separation_dj is not None:
|
|
448
|
+
# Found bimodal peaks
|
|
449
|
+
confidence = 0.9 if len(peaks) == 2 else 0.7
|
|
450
|
+
elif dj_pp > 2 * rj_rms:
|
|
451
|
+
# Significant DJ from quantile method
|
|
452
|
+
confidence = 0.5
|
|
453
|
+
else:
|
|
454
|
+
# Little or no DJ detected
|
|
455
|
+
confidence = 0.2
|
|
456
|
+
|
|
457
|
+
return DeterministicJitterResult(
|
|
458
|
+
dj_pp=dj_pp,
|
|
459
|
+
dj_delta=dj_delta,
|
|
460
|
+
method="dual_dirac",
|
|
461
|
+
confidence=confidence,
|
|
462
|
+
histogram=hist.astype(np.float64),
|
|
463
|
+
bin_centers=bin_centers,
|
|
464
|
+
)
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def extract_pj(
|
|
468
|
+
tie_data: NDArray[np.float64],
|
|
469
|
+
sample_rate: float,
|
|
470
|
+
*,
|
|
471
|
+
min_frequency: float = 1.0,
|
|
472
|
+
max_frequency: float | None = None,
|
|
473
|
+
n_components: int = 5,
|
|
474
|
+
) -> PeriodicJitterResult:
|
|
475
|
+
"""Extract periodic jitter components via spectral analysis.
|
|
476
|
+
|
|
477
|
+
Uses FFT of TIE data to identify sinusoidal jitter components,
|
|
478
|
+
typically caused by power supply noise or EMI.
|
|
479
|
+
|
|
480
|
+
Args:
|
|
481
|
+
tie_data: Time Interval Error data array in seconds.
|
|
482
|
+
sample_rate: Sample rate of edge events (edges per second).
|
|
483
|
+
min_frequency: Minimum PJ frequency to detect (Hz).
|
|
484
|
+
max_frequency: Maximum PJ frequency (default: Nyquist).
|
|
485
|
+
n_components: Number of periodic components to extract.
|
|
486
|
+
|
|
487
|
+
Returns:
|
|
488
|
+
PeriodicJitterResult with frequency/amplitude pairs.
|
|
489
|
+
|
|
490
|
+
Example:
|
|
491
|
+
>>> pj = extract_pj(tie_data, sample_rate=1e6)
|
|
492
|
+
>>> for freq, amp in pj.components:
|
|
493
|
+
... print(f"{freq/1e3:.1f} kHz: {amp*1e12:.2f} ps")
|
|
494
|
+
|
|
495
|
+
References:
|
|
496
|
+
IEEE 2414-2020 Section 6.4
|
|
497
|
+
"""
|
|
498
|
+
valid_data = tie_data[~np.isnan(tie_data)]
|
|
499
|
+
n = len(valid_data)
|
|
500
|
+
|
|
501
|
+
if n < 32:
|
|
502
|
+
return PeriodicJitterResult(
|
|
503
|
+
components=[],
|
|
504
|
+
pj_pp=0.0,
|
|
505
|
+
dominant_frequency=None,
|
|
506
|
+
dominant_amplitude=None,
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
# Remove DC offset (mean)
|
|
510
|
+
data_centered = valid_data - np.mean(valid_data)
|
|
511
|
+
|
|
512
|
+
# Apply window to reduce spectral leakage
|
|
513
|
+
window = np.hanning(n)
|
|
514
|
+
data_windowed = data_centered * window
|
|
515
|
+
|
|
516
|
+
# Compute FFT
|
|
517
|
+
nfft = int(2 ** np.ceil(np.log2(n)))
|
|
518
|
+
spectrum = np.fft.rfft(data_windowed, n=nfft)
|
|
519
|
+
frequencies = np.fft.rfftfreq(nfft, d=1.0 / sample_rate)
|
|
520
|
+
magnitudes = np.abs(spectrum) * 2 / n # Scale for amplitude
|
|
521
|
+
|
|
522
|
+
# Set frequency range
|
|
523
|
+
if max_frequency is None:
|
|
524
|
+
max_frequency = sample_rate / 2
|
|
525
|
+
|
|
526
|
+
# Find peaks in valid frequency range
|
|
527
|
+
freq_mask = (frequencies >= min_frequency) & (frequencies <= max_frequency)
|
|
528
|
+
valid_freqs = frequencies[freq_mask]
|
|
529
|
+
valid_mags = magnitudes[freq_mask]
|
|
530
|
+
|
|
531
|
+
if len(valid_mags) < 3:
|
|
532
|
+
return PeriodicJitterResult(
|
|
533
|
+
components=[],
|
|
534
|
+
pj_pp=0.0,
|
|
535
|
+
dominant_frequency=None,
|
|
536
|
+
dominant_amplitude=None,
|
|
537
|
+
)
|
|
538
|
+
|
|
539
|
+
# Find peaks (local maxima)
|
|
540
|
+
from scipy.signal import find_peaks
|
|
541
|
+
|
|
542
|
+
# Threshold: peaks must be 3x the median magnitude
|
|
543
|
+
threshold = 3 * np.median(valid_mags)
|
|
544
|
+
peak_indices, _properties = find_peaks(
|
|
545
|
+
valid_mags,
|
|
546
|
+
height=threshold,
|
|
547
|
+
distance=3, # Minimum separation between peaks
|
|
548
|
+
)
|
|
549
|
+
|
|
550
|
+
# Sort by amplitude and take top n_components
|
|
551
|
+
if len(peak_indices) > 0:
|
|
552
|
+
peak_heights = valid_mags[peak_indices]
|
|
553
|
+
sorted_indices = np.argsort(peak_heights)[::-1][:n_components]
|
|
554
|
+
top_peaks = peak_indices[sorted_indices]
|
|
555
|
+
|
|
556
|
+
components = [(float(valid_freqs[idx]), float(valid_mags[idx])) for idx in top_peaks]
|
|
557
|
+
|
|
558
|
+
# Calculate total PJ as sum of component amplitudes (peak-to-peak)
|
|
559
|
+
pj_pp = 2 * sum(amp for _, amp in components)
|
|
560
|
+
|
|
561
|
+
dominant_frequency = components[0][0] if components else None
|
|
562
|
+
dominant_amplitude = components[0][1] if components else None
|
|
563
|
+
else:
|
|
564
|
+
components = []
|
|
565
|
+
pj_pp = 0.0
|
|
566
|
+
dominant_frequency = None
|
|
567
|
+
dominant_amplitude = None
|
|
568
|
+
|
|
569
|
+
return PeriodicJitterResult(
|
|
570
|
+
components=components,
|
|
571
|
+
pj_pp=pj_pp,
|
|
572
|
+
dominant_frequency=dominant_frequency,
|
|
573
|
+
dominant_amplitude=dominant_amplitude,
|
|
574
|
+
)
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
def extract_ddj(
|
|
578
|
+
tie_data: NDArray[np.float64],
|
|
579
|
+
bit_pattern: NDArray[np.int_] | None = None,
|
|
580
|
+
*,
|
|
581
|
+
pattern_length: int = 3,
|
|
582
|
+
) -> DataDependentJitterResult:
|
|
583
|
+
"""Extract data-dependent jitter caused by ISI effects.
|
|
584
|
+
|
|
585
|
+
Analyzes correlation between jitter and preceding bit patterns
|
|
586
|
+
to identify inter-symbol interference (ISI) induced timing variations.
|
|
587
|
+
|
|
588
|
+
Args:
|
|
589
|
+
tie_data: Time Interval Error data array in seconds.
|
|
590
|
+
bit_pattern: Associated bit pattern for each TIE sample.
|
|
591
|
+
pattern_length: Number of preceding bits to correlate.
|
|
592
|
+
|
|
593
|
+
Returns:
|
|
594
|
+
DataDependentJitterResult with pattern-correlated jitter.
|
|
595
|
+
|
|
596
|
+
Raises:
|
|
597
|
+
ValueError: If bit_pattern length does not match tie_data length.
|
|
598
|
+
|
|
599
|
+
Example:
|
|
600
|
+
>>> ddj = extract_ddj(tie_data, bit_pattern=bits, pattern_length=3)
|
|
601
|
+
>>> print(f"DDJ: {ddj.ddj_pp * 1e12:.2f} ps")
|
|
602
|
+
|
|
603
|
+
References:
|
|
604
|
+
IEEE 2414-2020 Section 6.5
|
|
605
|
+
"""
|
|
606
|
+
valid_data = tie_data[~np.isnan(tie_data)]
|
|
607
|
+
n = len(valid_data)
|
|
608
|
+
|
|
609
|
+
if bit_pattern is None:
|
|
610
|
+
# Without bit pattern data, estimate from TIE distribution
|
|
611
|
+
# Use alternating pattern assumption
|
|
612
|
+
pattern_histogram: dict[str, float] = {}
|
|
613
|
+
|
|
614
|
+
# Simple estimation: look for bimodality in TIE
|
|
615
|
+
median = np.median(valid_data)
|
|
616
|
+
above_median = valid_data > median
|
|
617
|
+
below_median = ~above_median
|
|
618
|
+
|
|
619
|
+
pattern_histogram["above_median"] = float(np.mean(valid_data[above_median]))
|
|
620
|
+
pattern_histogram["below_median"] = float(np.mean(valid_data[below_median]))
|
|
621
|
+
|
|
622
|
+
ddj_pp = abs(pattern_histogram["above_median"] - pattern_histogram["below_median"])
|
|
623
|
+
|
|
624
|
+
return DataDependentJitterResult(
|
|
625
|
+
ddj_pp=ddj_pp,
|
|
626
|
+
pattern_histogram=pattern_histogram,
|
|
627
|
+
pattern_length=pattern_length,
|
|
628
|
+
isi_coefficient=0.0, # Unknown without pattern
|
|
629
|
+
)
|
|
630
|
+
|
|
631
|
+
# With bit pattern available
|
|
632
|
+
if len(bit_pattern) != n:
|
|
633
|
+
raise ValueError("bit_pattern length must match tie_data length")
|
|
634
|
+
|
|
635
|
+
pattern_histogram = {}
|
|
636
|
+
2**pattern_length
|
|
637
|
+
|
|
638
|
+
# Create pattern strings and accumulate TIE values
|
|
639
|
+
for i in range(pattern_length - 1, n):
|
|
640
|
+
pattern_bits = bit_pattern[i - pattern_length + 1 : i + 1]
|
|
641
|
+
pattern_str = "".join(str(int(b)) for b in pattern_bits)
|
|
642
|
+
|
|
643
|
+
if pattern_str not in pattern_histogram:
|
|
644
|
+
pattern_histogram[pattern_str] = [] # type: ignore[assignment]
|
|
645
|
+
pattern_histogram[pattern_str].append(valid_data[i]) # type: ignore[attr-defined]
|
|
646
|
+
|
|
647
|
+
# Calculate mean TIE for each pattern
|
|
648
|
+
pattern_means: dict[str, float] = {}
|
|
649
|
+
for pattern, values in pattern_histogram.items():
|
|
650
|
+
if len(values) > 0: # type: ignore[arg-type]
|
|
651
|
+
pattern_means[pattern] = float(np.mean(values))
|
|
652
|
+
|
|
653
|
+
# DDJ is the range of pattern-dependent means
|
|
654
|
+
if len(pattern_means) > 1:
|
|
655
|
+
mean_values = list(pattern_means.values())
|
|
656
|
+
ddj_pp = max(mean_values) - min(mean_values)
|
|
657
|
+
else:
|
|
658
|
+
ddj_pp = 0.0
|
|
659
|
+
|
|
660
|
+
# Calculate ISI correlation coefficient
|
|
661
|
+
# Correlation between previous bit and current TIE
|
|
662
|
+
if n > 1:
|
|
663
|
+
prev_bits = bit_pattern[:-1].astype(float)
|
|
664
|
+
curr_tie = valid_data[1:]
|
|
665
|
+
correlation = np.corrcoef(prev_bits, curr_tie)[0, 1]
|
|
666
|
+
isi_coefficient = correlation if np.isfinite(correlation) else 0.0
|
|
667
|
+
else:
|
|
668
|
+
isi_coefficient = 0.0
|
|
669
|
+
|
|
670
|
+
return DataDependentJitterResult(
|
|
671
|
+
ddj_pp=ddj_pp,
|
|
672
|
+
pattern_histogram=pattern_means,
|
|
673
|
+
pattern_length=pattern_length,
|
|
674
|
+
isi_coefficient=isi_coefficient,
|
|
675
|
+
)
|
|
676
|
+
|
|
677
|
+
|
|
678
|
+
def decompose_jitter(
|
|
679
|
+
tie_data: NDArray[np.float64],
|
|
680
|
+
*,
|
|
681
|
+
edge_rate: float | None = None,
|
|
682
|
+
include_pj: bool = True,
|
|
683
|
+
include_ddj: bool = False,
|
|
684
|
+
bit_pattern: NDArray[np.int_] | None = None,
|
|
685
|
+
target_ber: float = 1e-12,
|
|
686
|
+
) -> JitterDecomposition:
|
|
687
|
+
"""Perform complete jitter decomposition.
|
|
688
|
+
|
|
689
|
+
Decomposes total jitter into its constituent components:
|
|
690
|
+
- Random Jitter (RJ): Unbounded Gaussian component
|
|
691
|
+
- Deterministic Jitter (DJ): Bounded, repeatable component
|
|
692
|
+
- Periodic Jitter (PJ): Sinusoidal components (optional)
|
|
693
|
+
- Data-Dependent Jitter (DDJ): ISI-related component (optional)
|
|
694
|
+
|
|
695
|
+
Args:
|
|
696
|
+
tie_data: Time Interval Error data array in seconds.
|
|
697
|
+
edge_rate: Rate of edges in Hz (required for PJ analysis).
|
|
698
|
+
include_pj: Include periodic jitter analysis.
|
|
699
|
+
include_ddj: Include data-dependent jitter analysis.
|
|
700
|
+
bit_pattern: Bit pattern for DDJ analysis.
|
|
701
|
+
target_ber: Target bit error rate for TJ calculation (default: 1e-12).
|
|
702
|
+
|
|
703
|
+
Returns:
|
|
704
|
+
JitterDecomposition with all component results and calculated TJ.
|
|
705
|
+
|
|
706
|
+
Example:
|
|
707
|
+
>>> decomp = decompose_jitter(tie_data, edge_rate=1e9)
|
|
708
|
+
>>> print(f"RJ: {decomp.rj.rj_rms * 1e12:.2f} ps")
|
|
709
|
+
>>> print(f"DJ: {decomp.dj.dj_pp * 1e12:.2f} ps")
|
|
710
|
+
>>> print(f"TJ: {decomp.tj_pp * 1e12:.2f} ps")
|
|
711
|
+
|
|
712
|
+
References:
|
|
713
|
+
IEEE 2414-2020 Section 6
|
|
714
|
+
Dual-Dirac model: TJ = DJ + 2*Q*RJ where Q = norm.ppf(1 - BER/2)
|
|
715
|
+
"""
|
|
716
|
+
# Extract RJ first
|
|
717
|
+
rj_result = extract_rj(tie_data)
|
|
718
|
+
|
|
719
|
+
# Extract DJ using RJ result
|
|
720
|
+
dj_result = extract_dj(tie_data, rj_result)
|
|
721
|
+
|
|
722
|
+
# Optional: Extract PJ
|
|
723
|
+
pj_result = None
|
|
724
|
+
if include_pj and edge_rate is not None:
|
|
725
|
+
pj_result = extract_pj(tie_data, edge_rate)
|
|
726
|
+
|
|
727
|
+
# Optional: Extract DDJ
|
|
728
|
+
ddj_result = None
|
|
729
|
+
if include_ddj:
|
|
730
|
+
ddj_result = extract_ddj(tie_data, bit_pattern)
|
|
731
|
+
|
|
732
|
+
# Calculate Total Jitter using dual-Dirac model
|
|
733
|
+
# TJ = DJ + 2 * Q * RJ
|
|
734
|
+
# where Q is the Q-factor for the target BER
|
|
735
|
+
q_factor = stats.norm.ppf(1 - target_ber / 2)
|
|
736
|
+
tj_pp = dj_result.dj_pp + 2 * q_factor * rj_result.rj_rms
|
|
737
|
+
|
|
738
|
+
return JitterDecomposition(
|
|
739
|
+
rj=rj_result,
|
|
740
|
+
dj=dj_result,
|
|
741
|
+
pj=pj_result,
|
|
742
|
+
ddj=ddj_result,
|
|
743
|
+
tj_pp=tj_pp,
|
|
744
|
+
ber_measured=target_ber,
|
|
745
|
+
)
|
|
746
|
+
|
|
747
|
+
|
|
748
|
+
__all__ = [
|
|
749
|
+
"DataDependentJitterResult",
|
|
750
|
+
"DeterministicJitterResult",
|
|
751
|
+
"JitterDecomposition",
|
|
752
|
+
"PeriodicJitterResult",
|
|
753
|
+
"RandomJitterResult",
|
|
754
|
+
"decompose_jitter",
|
|
755
|
+
"extract_ddj",
|
|
756
|
+
"extract_dj",
|
|
757
|
+
"extract_pj",
|
|
758
|
+
"extract_rj",
|
|
759
|
+
]
|