oscura 0.0.1__py3-none-any.whl → 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- oscura/__init__.py +813 -8
- oscura/__main__.py +392 -0
- oscura/analyzers/__init__.py +37 -0
- oscura/analyzers/digital/__init__.py +177 -0
- oscura/analyzers/digital/bus.py +691 -0
- oscura/analyzers/digital/clock.py +805 -0
- oscura/analyzers/digital/correlation.py +720 -0
- oscura/analyzers/digital/edges.py +632 -0
- oscura/analyzers/digital/extraction.py +413 -0
- oscura/analyzers/digital/quality.py +878 -0
- oscura/analyzers/digital/signal_quality.py +877 -0
- oscura/analyzers/digital/thresholds.py +708 -0
- oscura/analyzers/digital/timing.py +1104 -0
- oscura/analyzers/eye/__init__.py +46 -0
- oscura/analyzers/eye/diagram.py +434 -0
- oscura/analyzers/eye/metrics.py +555 -0
- oscura/analyzers/jitter/__init__.py +83 -0
- oscura/analyzers/jitter/ber.py +333 -0
- oscura/analyzers/jitter/decomposition.py +759 -0
- oscura/analyzers/jitter/measurements.py +413 -0
- oscura/analyzers/jitter/spectrum.py +220 -0
- oscura/analyzers/measurements.py +40 -0
- oscura/analyzers/packet/__init__.py +171 -0
- oscura/analyzers/packet/daq.py +1077 -0
- oscura/analyzers/packet/metrics.py +437 -0
- oscura/analyzers/packet/parser.py +327 -0
- oscura/analyzers/packet/payload.py +2156 -0
- oscura/analyzers/packet/payload_analysis.py +1312 -0
- oscura/analyzers/packet/payload_extraction.py +236 -0
- oscura/analyzers/packet/payload_patterns.py +670 -0
- oscura/analyzers/packet/stream.py +359 -0
- oscura/analyzers/patterns/__init__.py +266 -0
- oscura/analyzers/patterns/clustering.py +1036 -0
- oscura/analyzers/patterns/discovery.py +539 -0
- oscura/analyzers/patterns/learning.py +797 -0
- oscura/analyzers/patterns/matching.py +1091 -0
- oscura/analyzers/patterns/periodic.py +650 -0
- oscura/analyzers/patterns/sequences.py +767 -0
- oscura/analyzers/power/__init__.py +116 -0
- oscura/analyzers/power/ac_power.py +391 -0
- oscura/analyzers/power/basic.py +383 -0
- oscura/analyzers/power/conduction.py +314 -0
- oscura/analyzers/power/efficiency.py +297 -0
- oscura/analyzers/power/ripple.py +356 -0
- oscura/analyzers/power/soa.py +372 -0
- oscura/analyzers/power/switching.py +479 -0
- oscura/analyzers/protocol/__init__.py +150 -0
- oscura/analyzers/protocols/__init__.py +150 -0
- oscura/analyzers/protocols/base.py +500 -0
- oscura/analyzers/protocols/can.py +620 -0
- oscura/analyzers/protocols/can_fd.py +448 -0
- oscura/analyzers/protocols/flexray.py +405 -0
- oscura/analyzers/protocols/hdlc.py +399 -0
- oscura/analyzers/protocols/i2c.py +368 -0
- oscura/analyzers/protocols/i2s.py +296 -0
- oscura/analyzers/protocols/jtag.py +393 -0
- oscura/analyzers/protocols/lin.py +445 -0
- oscura/analyzers/protocols/manchester.py +333 -0
- oscura/analyzers/protocols/onewire.py +501 -0
- oscura/analyzers/protocols/spi.py +334 -0
- oscura/analyzers/protocols/swd.py +325 -0
- oscura/analyzers/protocols/uart.py +393 -0
- oscura/analyzers/protocols/usb.py +495 -0
- oscura/analyzers/signal_integrity/__init__.py +63 -0
- oscura/analyzers/signal_integrity/embedding.py +294 -0
- oscura/analyzers/signal_integrity/equalization.py +370 -0
- oscura/analyzers/signal_integrity/sparams.py +484 -0
- oscura/analyzers/spectral/__init__.py +53 -0
- oscura/analyzers/spectral/chunked.py +273 -0
- oscura/analyzers/spectral/chunked_fft.py +571 -0
- oscura/analyzers/spectral/chunked_wavelet.py +391 -0
- oscura/analyzers/spectral/fft.py +92 -0
- oscura/analyzers/statistical/__init__.py +250 -0
- oscura/analyzers/statistical/checksum.py +923 -0
- oscura/analyzers/statistical/chunked_corr.py +228 -0
- oscura/analyzers/statistical/classification.py +778 -0
- oscura/analyzers/statistical/entropy.py +1113 -0
- oscura/analyzers/statistical/ngrams.py +614 -0
- oscura/analyzers/statistics/__init__.py +119 -0
- oscura/analyzers/statistics/advanced.py +885 -0
- oscura/analyzers/statistics/basic.py +263 -0
- oscura/analyzers/statistics/correlation.py +630 -0
- oscura/analyzers/statistics/distribution.py +298 -0
- oscura/analyzers/statistics/outliers.py +463 -0
- oscura/analyzers/statistics/streaming.py +93 -0
- oscura/analyzers/statistics/trend.py +520 -0
- oscura/analyzers/validation.py +598 -0
- oscura/analyzers/waveform/__init__.py +36 -0
- oscura/analyzers/waveform/measurements.py +943 -0
- oscura/analyzers/waveform/measurements_with_uncertainty.py +371 -0
- oscura/analyzers/waveform/spectral.py +1689 -0
- oscura/analyzers/waveform/wavelets.py +298 -0
- oscura/api/__init__.py +62 -0
- oscura/api/dsl.py +538 -0
- oscura/api/fluent.py +571 -0
- oscura/api/operators.py +498 -0
- oscura/api/optimization.py +392 -0
- oscura/api/profiling.py +396 -0
- oscura/automotive/__init__.py +73 -0
- oscura/automotive/can/__init__.py +52 -0
- oscura/automotive/can/analysis.py +356 -0
- oscura/automotive/can/checksum.py +250 -0
- oscura/automotive/can/correlation.py +212 -0
- oscura/automotive/can/discovery.py +355 -0
- oscura/automotive/can/message_wrapper.py +375 -0
- oscura/automotive/can/models.py +385 -0
- oscura/automotive/can/patterns.py +381 -0
- oscura/automotive/can/session.py +452 -0
- oscura/automotive/can/state_machine.py +300 -0
- oscura/automotive/can/stimulus_response.py +461 -0
- oscura/automotive/dbc/__init__.py +15 -0
- oscura/automotive/dbc/generator.py +156 -0
- oscura/automotive/dbc/parser.py +146 -0
- oscura/automotive/dtc/__init__.py +30 -0
- oscura/automotive/dtc/database.py +3036 -0
- oscura/automotive/j1939/__init__.py +14 -0
- oscura/automotive/j1939/decoder.py +745 -0
- oscura/automotive/loaders/__init__.py +35 -0
- oscura/automotive/loaders/asc.py +98 -0
- oscura/automotive/loaders/blf.py +77 -0
- oscura/automotive/loaders/csv_can.py +136 -0
- oscura/automotive/loaders/dispatcher.py +136 -0
- oscura/automotive/loaders/mdf.py +331 -0
- oscura/automotive/loaders/pcap.py +132 -0
- oscura/automotive/obd/__init__.py +14 -0
- oscura/automotive/obd/decoder.py +707 -0
- oscura/automotive/uds/__init__.py +48 -0
- oscura/automotive/uds/decoder.py +265 -0
- oscura/automotive/uds/models.py +64 -0
- oscura/automotive/visualization.py +369 -0
- oscura/batch/__init__.py +55 -0
- oscura/batch/advanced.py +627 -0
- oscura/batch/aggregate.py +300 -0
- oscura/batch/analyze.py +139 -0
- oscura/batch/logging.py +487 -0
- oscura/batch/metrics.py +556 -0
- oscura/builders/__init__.py +41 -0
- oscura/builders/signal_builder.py +1131 -0
- oscura/cli/__init__.py +14 -0
- oscura/cli/batch.py +339 -0
- oscura/cli/characterize.py +273 -0
- oscura/cli/compare.py +775 -0
- oscura/cli/decode.py +551 -0
- oscura/cli/main.py +247 -0
- oscura/cli/shell.py +350 -0
- oscura/comparison/__init__.py +66 -0
- oscura/comparison/compare.py +397 -0
- oscura/comparison/golden.py +487 -0
- oscura/comparison/limits.py +391 -0
- oscura/comparison/mask.py +434 -0
- oscura/comparison/trace_diff.py +30 -0
- oscura/comparison/visualization.py +481 -0
- oscura/compliance/__init__.py +70 -0
- oscura/compliance/advanced.py +756 -0
- oscura/compliance/masks.py +363 -0
- oscura/compliance/reporting.py +483 -0
- oscura/compliance/testing.py +298 -0
- oscura/component/__init__.py +38 -0
- oscura/component/impedance.py +365 -0
- oscura/component/reactive.py +598 -0
- oscura/component/transmission_line.py +312 -0
- oscura/config/__init__.py +191 -0
- oscura/config/defaults.py +254 -0
- oscura/config/loader.py +348 -0
- oscura/config/memory.py +271 -0
- oscura/config/migration.py +458 -0
- oscura/config/pipeline.py +1077 -0
- oscura/config/preferences.py +530 -0
- oscura/config/protocol.py +875 -0
- oscura/config/schema.py +713 -0
- oscura/config/settings.py +420 -0
- oscura/config/thresholds.py +599 -0
- oscura/convenience.py +457 -0
- oscura/core/__init__.py +299 -0
- oscura/core/audit.py +457 -0
- oscura/core/backend_selector.py +405 -0
- oscura/core/cache.py +590 -0
- oscura/core/cancellation.py +439 -0
- oscura/core/confidence.py +225 -0
- oscura/core/config.py +506 -0
- oscura/core/correlation.py +216 -0
- oscura/core/cross_domain.py +422 -0
- oscura/core/debug.py +301 -0
- oscura/core/edge_cases.py +541 -0
- oscura/core/exceptions.py +535 -0
- oscura/core/gpu_backend.py +523 -0
- oscura/core/lazy.py +832 -0
- oscura/core/log_query.py +540 -0
- oscura/core/logging.py +931 -0
- oscura/core/logging_advanced.py +952 -0
- oscura/core/memoize.py +171 -0
- oscura/core/memory_check.py +274 -0
- oscura/core/memory_guard.py +290 -0
- oscura/core/memory_limits.py +336 -0
- oscura/core/memory_monitor.py +453 -0
- oscura/core/memory_progress.py +465 -0
- oscura/core/memory_warnings.py +315 -0
- oscura/core/numba_backend.py +362 -0
- oscura/core/performance.py +352 -0
- oscura/core/progress.py +524 -0
- oscura/core/provenance.py +358 -0
- oscura/core/results.py +331 -0
- oscura/core/types.py +504 -0
- oscura/core/uncertainty.py +383 -0
- oscura/discovery/__init__.py +52 -0
- oscura/discovery/anomaly_detector.py +672 -0
- oscura/discovery/auto_decoder.py +415 -0
- oscura/discovery/comparison.py +497 -0
- oscura/discovery/quality_validator.py +528 -0
- oscura/discovery/signal_detector.py +769 -0
- oscura/dsl/__init__.py +73 -0
- oscura/dsl/commands.py +246 -0
- oscura/dsl/interpreter.py +455 -0
- oscura/dsl/parser.py +689 -0
- oscura/dsl/repl.py +172 -0
- oscura/exceptions.py +59 -0
- oscura/exploratory/__init__.py +111 -0
- oscura/exploratory/error_recovery.py +642 -0
- oscura/exploratory/fuzzy.py +513 -0
- oscura/exploratory/fuzzy_advanced.py +786 -0
- oscura/exploratory/legacy.py +831 -0
- oscura/exploratory/parse.py +358 -0
- oscura/exploratory/recovery.py +275 -0
- oscura/exploratory/sync.py +382 -0
- oscura/exploratory/unknown.py +707 -0
- oscura/export/__init__.py +25 -0
- oscura/export/wireshark/README.md +265 -0
- oscura/export/wireshark/__init__.py +47 -0
- oscura/export/wireshark/generator.py +312 -0
- oscura/export/wireshark/lua_builder.py +159 -0
- oscura/export/wireshark/templates/dissector.lua.j2 +92 -0
- oscura/export/wireshark/type_mapping.py +165 -0
- oscura/export/wireshark/validator.py +105 -0
- oscura/exporters/__init__.py +94 -0
- oscura/exporters/csv.py +303 -0
- oscura/exporters/exporters.py +44 -0
- oscura/exporters/hdf5.py +219 -0
- oscura/exporters/html_export.py +701 -0
- oscura/exporters/json_export.py +291 -0
- oscura/exporters/markdown_export.py +367 -0
- oscura/exporters/matlab_export.py +354 -0
- oscura/exporters/npz_export.py +219 -0
- oscura/exporters/spice_export.py +210 -0
- oscura/extensibility/__init__.py +131 -0
- oscura/extensibility/docs.py +752 -0
- oscura/extensibility/extensions.py +1125 -0
- oscura/extensibility/logging.py +259 -0
- oscura/extensibility/measurements.py +485 -0
- oscura/extensibility/plugins.py +414 -0
- oscura/extensibility/registry.py +346 -0
- oscura/extensibility/templates.py +913 -0
- oscura/extensibility/validation.py +651 -0
- oscura/filtering/__init__.py +89 -0
- oscura/filtering/base.py +563 -0
- oscura/filtering/convenience.py +564 -0
- oscura/filtering/design.py +725 -0
- oscura/filtering/filters.py +32 -0
- oscura/filtering/introspection.py +605 -0
- oscura/guidance/__init__.py +24 -0
- oscura/guidance/recommender.py +429 -0
- oscura/guidance/wizard.py +518 -0
- oscura/inference/__init__.py +251 -0
- oscura/inference/active_learning/README.md +153 -0
- oscura/inference/active_learning/__init__.py +38 -0
- oscura/inference/active_learning/lstar.py +257 -0
- oscura/inference/active_learning/observation_table.py +230 -0
- oscura/inference/active_learning/oracle.py +78 -0
- oscura/inference/active_learning/teachers/__init__.py +15 -0
- oscura/inference/active_learning/teachers/simulator.py +192 -0
- oscura/inference/adaptive_tuning.py +453 -0
- oscura/inference/alignment.py +653 -0
- oscura/inference/bayesian.py +943 -0
- oscura/inference/binary.py +1016 -0
- oscura/inference/crc_reverse.py +711 -0
- oscura/inference/logic.py +288 -0
- oscura/inference/message_format.py +1305 -0
- oscura/inference/protocol.py +417 -0
- oscura/inference/protocol_dsl.py +1084 -0
- oscura/inference/protocol_library.py +1230 -0
- oscura/inference/sequences.py +809 -0
- oscura/inference/signal_intelligence.py +1509 -0
- oscura/inference/spectral.py +215 -0
- oscura/inference/state_machine.py +634 -0
- oscura/inference/stream.py +918 -0
- oscura/integrations/__init__.py +59 -0
- oscura/integrations/llm.py +1827 -0
- oscura/jupyter/__init__.py +32 -0
- oscura/jupyter/display.py +268 -0
- oscura/jupyter/magic.py +334 -0
- oscura/loaders/__init__.py +526 -0
- oscura/loaders/binary.py +69 -0
- oscura/loaders/configurable.py +1255 -0
- oscura/loaders/csv.py +26 -0
- oscura/loaders/csv_loader.py +473 -0
- oscura/loaders/hdf5.py +9 -0
- oscura/loaders/hdf5_loader.py +510 -0
- oscura/loaders/lazy.py +370 -0
- oscura/loaders/mmap_loader.py +583 -0
- oscura/loaders/numpy_loader.py +436 -0
- oscura/loaders/pcap.py +432 -0
- oscura/loaders/preprocessing.py +368 -0
- oscura/loaders/rigol.py +287 -0
- oscura/loaders/sigrok.py +321 -0
- oscura/loaders/tdms.py +367 -0
- oscura/loaders/tektronix.py +711 -0
- oscura/loaders/validation.py +584 -0
- oscura/loaders/vcd.py +464 -0
- oscura/loaders/wav.py +233 -0
- oscura/math/__init__.py +45 -0
- oscura/math/arithmetic.py +824 -0
- oscura/math/interpolation.py +413 -0
- oscura/onboarding/__init__.py +39 -0
- oscura/onboarding/help.py +498 -0
- oscura/onboarding/tutorials.py +405 -0
- oscura/onboarding/wizard.py +466 -0
- oscura/optimization/__init__.py +19 -0
- oscura/optimization/parallel.py +440 -0
- oscura/optimization/search.py +532 -0
- oscura/pipeline/__init__.py +43 -0
- oscura/pipeline/base.py +338 -0
- oscura/pipeline/composition.py +242 -0
- oscura/pipeline/parallel.py +448 -0
- oscura/pipeline/pipeline.py +375 -0
- oscura/pipeline/reverse_engineering.py +1119 -0
- oscura/plugins/__init__.py +122 -0
- oscura/plugins/base.py +272 -0
- oscura/plugins/cli.py +497 -0
- oscura/plugins/discovery.py +411 -0
- oscura/plugins/isolation.py +418 -0
- oscura/plugins/lifecycle.py +959 -0
- oscura/plugins/manager.py +493 -0
- oscura/plugins/registry.py +421 -0
- oscura/plugins/versioning.py +372 -0
- oscura/py.typed +0 -0
- oscura/quality/__init__.py +65 -0
- oscura/quality/ensemble.py +740 -0
- oscura/quality/explainer.py +338 -0
- oscura/quality/scoring.py +616 -0
- oscura/quality/warnings.py +456 -0
- oscura/reporting/__init__.py +248 -0
- oscura/reporting/advanced.py +1234 -0
- oscura/reporting/analyze.py +448 -0
- oscura/reporting/argument_preparer.py +596 -0
- oscura/reporting/auto_report.py +507 -0
- oscura/reporting/batch.py +615 -0
- oscura/reporting/chart_selection.py +223 -0
- oscura/reporting/comparison.py +330 -0
- oscura/reporting/config.py +615 -0
- oscura/reporting/content/__init__.py +39 -0
- oscura/reporting/content/executive.py +127 -0
- oscura/reporting/content/filtering.py +191 -0
- oscura/reporting/content/minimal.py +257 -0
- oscura/reporting/content/verbosity.py +162 -0
- oscura/reporting/core.py +508 -0
- oscura/reporting/core_formats/__init__.py +17 -0
- oscura/reporting/core_formats/multi_format.py +210 -0
- oscura/reporting/engine.py +836 -0
- oscura/reporting/export.py +366 -0
- oscura/reporting/formatting/__init__.py +129 -0
- oscura/reporting/formatting/emphasis.py +81 -0
- oscura/reporting/formatting/numbers.py +403 -0
- oscura/reporting/formatting/standards.py +55 -0
- oscura/reporting/formatting.py +466 -0
- oscura/reporting/html.py +578 -0
- oscura/reporting/index.py +590 -0
- oscura/reporting/multichannel.py +296 -0
- oscura/reporting/output.py +379 -0
- oscura/reporting/pdf.py +373 -0
- oscura/reporting/plots.py +731 -0
- oscura/reporting/pptx_export.py +360 -0
- oscura/reporting/renderers/__init__.py +11 -0
- oscura/reporting/renderers/pdf.py +94 -0
- oscura/reporting/sections.py +471 -0
- oscura/reporting/standards.py +680 -0
- oscura/reporting/summary_generator.py +368 -0
- oscura/reporting/tables.py +397 -0
- oscura/reporting/template_system.py +724 -0
- oscura/reporting/templates/__init__.py +15 -0
- oscura/reporting/templates/definition.py +205 -0
- oscura/reporting/templates/index.html +649 -0
- oscura/reporting/templates/index.md +173 -0
- oscura/schemas/__init__.py +158 -0
- oscura/schemas/bus_configuration.json +322 -0
- oscura/schemas/device_mapping.json +182 -0
- oscura/schemas/packet_format.json +418 -0
- oscura/schemas/protocol_definition.json +363 -0
- oscura/search/__init__.py +16 -0
- oscura/search/anomaly.py +292 -0
- oscura/search/context.py +149 -0
- oscura/search/pattern.py +160 -0
- oscura/session/__init__.py +34 -0
- oscura/session/annotations.py +289 -0
- oscura/session/history.py +313 -0
- oscura/session/session.py +445 -0
- oscura/streaming/__init__.py +43 -0
- oscura/streaming/chunked.py +611 -0
- oscura/streaming/progressive.py +393 -0
- oscura/streaming/realtime.py +622 -0
- oscura/testing/__init__.py +54 -0
- oscura/testing/synthetic.py +808 -0
- oscura/triggering/__init__.py +68 -0
- oscura/triggering/base.py +229 -0
- oscura/triggering/edge.py +353 -0
- oscura/triggering/pattern.py +344 -0
- oscura/triggering/pulse.py +581 -0
- oscura/triggering/window.py +453 -0
- oscura/ui/__init__.py +48 -0
- oscura/ui/formatters.py +526 -0
- oscura/ui/progressive_display.py +340 -0
- oscura/utils/__init__.py +99 -0
- oscura/utils/autodetect.py +338 -0
- oscura/utils/buffer.py +389 -0
- oscura/utils/lazy.py +407 -0
- oscura/utils/lazy_imports.py +147 -0
- oscura/utils/memory.py +836 -0
- oscura/utils/memory_advanced.py +1326 -0
- oscura/utils/memory_extensions.py +465 -0
- oscura/utils/progressive.py +352 -0
- oscura/utils/windowing.py +362 -0
- oscura/visualization/__init__.py +321 -0
- oscura/visualization/accessibility.py +526 -0
- oscura/visualization/annotations.py +374 -0
- oscura/visualization/axis_scaling.py +305 -0
- oscura/visualization/colors.py +453 -0
- oscura/visualization/digital.py +337 -0
- oscura/visualization/eye.py +420 -0
- oscura/visualization/histogram.py +281 -0
- oscura/visualization/interactive.py +858 -0
- oscura/visualization/jitter.py +702 -0
- oscura/visualization/keyboard.py +394 -0
- oscura/visualization/layout.py +365 -0
- oscura/visualization/optimization.py +1028 -0
- oscura/visualization/palettes.py +446 -0
- oscura/visualization/plot.py +92 -0
- oscura/visualization/power.py +290 -0
- oscura/visualization/power_extended.py +626 -0
- oscura/visualization/presets.py +467 -0
- oscura/visualization/protocols.py +932 -0
- oscura/visualization/render.py +207 -0
- oscura/visualization/rendering.py +444 -0
- oscura/visualization/reverse_engineering.py +791 -0
- oscura/visualization/signal_integrity.py +808 -0
- oscura/visualization/specialized.py +553 -0
- oscura/visualization/spectral.py +811 -0
- oscura/visualization/styles.py +381 -0
- oscura/visualization/thumbnails.py +311 -0
- oscura/visualization/time_axis.py +351 -0
- oscura/visualization/waveform.py +367 -0
- oscura/workflow/__init__.py +13 -0
- oscura/workflow/dag.py +377 -0
- oscura/workflows/__init__.py +58 -0
- oscura/workflows/compliance.py +280 -0
- oscura/workflows/digital.py +272 -0
- oscura/workflows/multi_trace.py +502 -0
- oscura/workflows/power.py +178 -0
- oscura/workflows/protocol.py +492 -0
- oscura/workflows/reverse_engineering.py +639 -0
- oscura/workflows/signal_integrity.py +227 -0
- oscura-0.1.0.dist-info/METADATA +300 -0
- oscura-0.1.0.dist-info/RECORD +463 -0
- oscura-0.1.0.dist-info/entry_points.txt +2 -0
- {oscura-0.0.1.dist-info → oscura-0.1.0.dist-info}/licenses/LICENSE +1 -1
- oscura-0.0.1.dist-info/METADATA +0 -63
- oscura-0.0.1.dist-info/RECORD +0 -5
- {oscura-0.0.1.dist-info → oscura-0.1.0.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,805 @@
|
|
|
1
|
+
"""Advanced clock recovery for digital signals.
|
|
2
|
+
|
|
3
|
+
This module provides comprehensive clock recovery and analysis tools for digital
|
|
4
|
+
signals, including frequency detection, clock reconstruction, baud rate detection,
|
|
5
|
+
and jitter measurement.
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
Example:
|
|
9
|
+
>>> from oscura.analyzers.digital.clock import detect_clock_frequency, recover_clock
|
|
10
|
+
>>> freq = detect_clock_frequency(data_trace, sample_rate=1e9)
|
|
11
|
+
>>> print(f"Detected clock: {freq/1e6:.2f} MHz")
|
|
12
|
+
>>> clock = recover_clock(data_trace, sample_rate=1e9, method='edge')
|
|
13
|
+
>>> metrics = measure_clock_jitter(clock, sample_rate=1e9)
|
|
14
|
+
|
|
15
|
+
References:
|
|
16
|
+
Gardner, F.M.: "Phaselock Techniques" (3rd Ed), Wiley, 2005
|
|
17
|
+
Lee, E.A. & Messerschmitt, D.G.: "Digital Communication" (2nd Ed), 1994
|
|
18
|
+
IEEE 1241-2010: Standard for Terminology and Test Methods for ADCs
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from dataclasses import dataclass
|
|
24
|
+
from typing import TYPE_CHECKING, Any, ClassVar, Literal
|
|
25
|
+
|
|
26
|
+
import numpy as np
|
|
27
|
+
from scipy import signal
|
|
28
|
+
|
|
29
|
+
from oscura.core.exceptions import InsufficientDataError, ValidationError
|
|
30
|
+
|
|
31
|
+
if TYPE_CHECKING:
|
|
32
|
+
from numpy.typing import NDArray
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class ClockMetrics:
|
|
37
|
+
"""Clock signal quality metrics.
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
Attributes:
|
|
42
|
+
frequency: Detected frequency in Hz.
|
|
43
|
+
period_samples: Period in samples.
|
|
44
|
+
period_seconds: Period in seconds.
|
|
45
|
+
jitter_rms: RMS jitter in seconds.
|
|
46
|
+
jitter_pp: Peak-to-peak jitter in seconds.
|
|
47
|
+
duty_cycle: Duty cycle (0.0 to 1.0).
|
|
48
|
+
stability: Stability score (0.0 to 1.0).
|
|
49
|
+
confidence: Detection confidence (0.0 to 1.0).
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
frequency: float
|
|
53
|
+
period_samples: float
|
|
54
|
+
period_seconds: float
|
|
55
|
+
jitter_rms: float
|
|
56
|
+
jitter_pp: float
|
|
57
|
+
duty_cycle: float
|
|
58
|
+
stability: float
|
|
59
|
+
confidence: float
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass
|
|
63
|
+
class BaudRateResult:
|
|
64
|
+
"""Result of baud rate detection.
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
Attributes:
|
|
69
|
+
baud_rate: Detected baud rate in bits per second.
|
|
70
|
+
bit_period_samples: Bit period in samples.
|
|
71
|
+
confidence: Detection confidence (0.0 to 1.0).
|
|
72
|
+
method: Method used for detection.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
baud_rate: int
|
|
76
|
+
bit_period_samples: float
|
|
77
|
+
confidence: float
|
|
78
|
+
method: str
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class ClockRecovery:
|
|
82
|
+
"""Recover clock signal from data.
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
This class provides multiple methods for clock recovery including edge-based,
|
|
87
|
+
FFT-based, and autocorrelation-based detection, as well as PLL tracking and
|
|
88
|
+
baud rate detection for asynchronous protocols.
|
|
89
|
+
|
|
90
|
+
Can be initialized with or without sample_rate:
|
|
91
|
+
- With sample_rate: ClockRecovery(sample_rate=1e9)
|
|
92
|
+
- Without: ClockRecovery() - sample_rate extracted from trace metadata
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
# Standard baud rates for async protocols
|
|
96
|
+
STANDARD_BAUD_RATES: ClassVar[list[int]] = [
|
|
97
|
+
300,
|
|
98
|
+
600,
|
|
99
|
+
1200,
|
|
100
|
+
2400,
|
|
101
|
+
4800,
|
|
102
|
+
9600,
|
|
103
|
+
14400,
|
|
104
|
+
19200,
|
|
105
|
+
28800,
|
|
106
|
+
38400,
|
|
107
|
+
57600,
|
|
108
|
+
115200,
|
|
109
|
+
230400,
|
|
110
|
+
460800,
|
|
111
|
+
921600,
|
|
112
|
+
1000000,
|
|
113
|
+
2000000,
|
|
114
|
+
]
|
|
115
|
+
|
|
116
|
+
def __init__(self, sample_rate: float | None = None):
|
|
117
|
+
"""Initialize with optional sample rate.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
sample_rate: Sample rate in Hz. If None, will be extracted from trace metadata.
|
|
121
|
+
|
|
122
|
+
Raises:
|
|
123
|
+
ValidationError: If sample rate is provided and invalid.
|
|
124
|
+
"""
|
|
125
|
+
if sample_rate is not None and sample_rate <= 0:
|
|
126
|
+
raise ValidationError(f"Sample rate must be positive, got {sample_rate}")
|
|
127
|
+
|
|
128
|
+
self.sample_rate: float | None = float(sample_rate) if sample_rate is not None else None
|
|
129
|
+
|
|
130
|
+
def _get_sample_rate(self, trace: Any) -> float:
|
|
131
|
+
"""Extract sample rate from trace or use stored value.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
trace: A DigitalTrace/WaveformTrace with metadata, or a numpy array.
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
Sample rate in Hz.
|
|
138
|
+
|
|
139
|
+
Raises:
|
|
140
|
+
ValidationError: If sample rate cannot be determined.
|
|
141
|
+
"""
|
|
142
|
+
if self.sample_rate is not None:
|
|
143
|
+
return self.sample_rate
|
|
144
|
+
|
|
145
|
+
# Try to extract from trace metadata
|
|
146
|
+
if hasattr(trace, "metadata") and hasattr(trace.metadata, "sample_rate"):
|
|
147
|
+
return float(trace.metadata.sample_rate)
|
|
148
|
+
|
|
149
|
+
raise ValidationError(
|
|
150
|
+
"Sample rate not set and cannot be extracted from trace. "
|
|
151
|
+
"Either provide sample_rate to constructor or use a trace with metadata."
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
def _get_trace_data(self, trace: Any) -> NDArray[np.float64]:
|
|
155
|
+
"""Extract numpy array from trace object.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
trace: A DigitalTrace/WaveformTrace or numpy array.
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
Numpy array of signal data.
|
|
162
|
+
"""
|
|
163
|
+
if hasattr(trace, "data"):
|
|
164
|
+
return np.asarray(trace.data, dtype=np.float64)
|
|
165
|
+
return np.asarray(trace, dtype=np.float64)
|
|
166
|
+
|
|
167
|
+
def detect_frequency(
|
|
168
|
+
self, trace: Any, method: Literal["edge", "fft", "autocorr"] = "edge"
|
|
169
|
+
) -> float:
|
|
170
|
+
"""Detect clock frequency from signal (supports DigitalTrace).
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
This method supports both raw numpy arrays and DigitalTrace objects.
|
|
175
|
+
Sample rate is extracted from trace metadata if not set in constructor.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
trace: Signal trace data (DigitalTrace or numpy array).
|
|
179
|
+
method: Detection method to use.
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
Detected frequency in Hz.
|
|
183
|
+
|
|
184
|
+
Example:
|
|
185
|
+
>>> recovery = ClockRecovery()
|
|
186
|
+
>>> freq = recovery.detect_frequency(digital_trace)
|
|
187
|
+
"""
|
|
188
|
+
sample_rate = self._get_sample_rate(trace)
|
|
189
|
+
data = self._get_trace_data(trace)
|
|
190
|
+
|
|
191
|
+
# Temporarily set sample rate for internal methods
|
|
192
|
+
old_rate = self.sample_rate
|
|
193
|
+
self.sample_rate = sample_rate
|
|
194
|
+
|
|
195
|
+
try:
|
|
196
|
+
return self.detect_clock_frequency(data, method)
|
|
197
|
+
finally:
|
|
198
|
+
self.sample_rate = old_rate
|
|
199
|
+
|
|
200
|
+
def detect_clock_frequency(
|
|
201
|
+
self, trace: NDArray[np.float64], method: Literal["edge", "fft", "autocorr"] = "edge"
|
|
202
|
+
) -> float:
|
|
203
|
+
"""Detect clock frequency from signal.
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
Detects the dominant clock frequency using the specified method.
|
|
208
|
+
Each method has different strengths:
|
|
209
|
+
- edge: Best for clean digital signals with clear transitions
|
|
210
|
+
- fft: Best for noisy signals or periodic analog waveforms
|
|
211
|
+
- autocorr: Best for periodic patterns with timing jitter
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
trace: Signal trace data.
|
|
215
|
+
method: Detection method to use.
|
|
216
|
+
|
|
217
|
+
Returns:
|
|
218
|
+
Detected frequency in Hz.
|
|
219
|
+
|
|
220
|
+
Raises:
|
|
221
|
+
InsufficientDataError: If trace is too short.
|
|
222
|
+
ValidationError: If method is invalid or detection fails.
|
|
223
|
+
"""
|
|
224
|
+
if len(trace) < 10:
|
|
225
|
+
raise InsufficientDataError("Trace must have at least 10 samples")
|
|
226
|
+
|
|
227
|
+
if self.sample_rate is None:
|
|
228
|
+
raise ValidationError(
|
|
229
|
+
"Sample rate not set. Use detect_frequency() with trace metadata."
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
if method == "edge":
|
|
233
|
+
return self._detect_frequency_edge(trace)
|
|
234
|
+
elif method == "fft":
|
|
235
|
+
return self._detect_frequency_fft(trace)
|
|
236
|
+
elif method == "autocorr":
|
|
237
|
+
return self._detect_frequency_autocorr(trace)
|
|
238
|
+
else:
|
|
239
|
+
raise ValidationError(f"Unknown method: {method}")
|
|
240
|
+
|
|
241
|
+
def recover_clock(
|
|
242
|
+
self, data_trace: NDArray[np.float64], method: Literal["edge", "pll", "fft"] = "edge"
|
|
243
|
+
) -> NDArray[np.float64]:
|
|
244
|
+
"""Recover clock signal from data.
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
Reconstructs a clock signal from the data trace. The recovered clock
|
|
249
|
+
is a square wave aligned to the detected clock transitions.
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
data_trace: Data signal trace.
|
|
253
|
+
method: Recovery method to use.
|
|
254
|
+
|
|
255
|
+
Returns:
|
|
256
|
+
Recovered clock trace (same length as input).
|
|
257
|
+
|
|
258
|
+
Raises:
|
|
259
|
+
InsufficientDataError: If trace is too short.
|
|
260
|
+
ValidationError: If method is invalid or recovery fails.
|
|
261
|
+
"""
|
|
262
|
+
if len(data_trace) < 10:
|
|
263
|
+
raise InsufficientDataError("Trace must have at least 10 samples")
|
|
264
|
+
|
|
265
|
+
if self.sample_rate is None:
|
|
266
|
+
raise ValidationError("Sample rate not set")
|
|
267
|
+
|
|
268
|
+
# Detect clock frequency first
|
|
269
|
+
freq = self.detect_clock_frequency(data_trace, method=method if method != "pll" else "edge")
|
|
270
|
+
|
|
271
|
+
if freq <= 0:
|
|
272
|
+
raise ValidationError("Failed to detect valid clock frequency")
|
|
273
|
+
|
|
274
|
+
if method == "pll":
|
|
275
|
+
# Use PLL tracking for robust recovery
|
|
276
|
+
return self._pll_track(data_trace, freq)
|
|
277
|
+
else:
|
|
278
|
+
# Generate ideal square wave at detected frequency
|
|
279
|
+
_period_samples = self.sample_rate / freq
|
|
280
|
+
n_samples = len(data_trace)
|
|
281
|
+
t = np.arange(n_samples)
|
|
282
|
+
|
|
283
|
+
# Generate square wave (50% duty cycle)
|
|
284
|
+
clock_raw = signal.square(2 * np.pi * freq * t / self.sample_rate)
|
|
285
|
+
|
|
286
|
+
# Normalize to 0-1 range
|
|
287
|
+
clock = (clock_raw + 1.0) / 2.0
|
|
288
|
+
|
|
289
|
+
return np.asarray(clock, dtype=np.float64)
|
|
290
|
+
|
|
291
|
+
def detect_baud_rate(
|
|
292
|
+
self, trace: NDArray[np.float64], candidates: list[int] | None = None
|
|
293
|
+
) -> BaudRateResult:
|
|
294
|
+
"""Auto-detect baud rate for async protocols.
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
Detects the baud rate by analyzing bit timing. Works best with traces
|
|
299
|
+
containing start bits or transitions between different bit values.
|
|
300
|
+
|
|
301
|
+
Args:
|
|
302
|
+
trace: Signal trace data.
|
|
303
|
+
candidates: List of candidate baud rates to test. If None, uses
|
|
304
|
+
standard rates.
|
|
305
|
+
|
|
306
|
+
Returns:
|
|
307
|
+
BaudRateResult with detected baud rate and confidence.
|
|
308
|
+
|
|
309
|
+
Raises:
|
|
310
|
+
InsufficientDataError: If trace is too short or not enough edges found.
|
|
311
|
+
ValidationError: If sample rate is not set.
|
|
312
|
+
"""
|
|
313
|
+
if len(trace) < 100:
|
|
314
|
+
raise InsufficientDataError("Need at least 100 samples for baud rate detection")
|
|
315
|
+
|
|
316
|
+
if self.sample_rate is None:
|
|
317
|
+
raise ValidationError("Sample rate not set")
|
|
318
|
+
|
|
319
|
+
if candidates is None:
|
|
320
|
+
candidates = self.STANDARD_BAUD_RATES
|
|
321
|
+
|
|
322
|
+
# Detect edges to find bit transitions
|
|
323
|
+
edges = self._detect_edges_simple(trace)
|
|
324
|
+
|
|
325
|
+
if len(edges) < 3:
|
|
326
|
+
raise InsufficientDataError("Not enough edges to detect baud rate")
|
|
327
|
+
|
|
328
|
+
# Calculate inter-edge intervals
|
|
329
|
+
intervals = np.diff(edges)
|
|
330
|
+
|
|
331
|
+
# The minimum interval should be close to one bit period
|
|
332
|
+
# (assuming we have at least some single-bit pulses)
|
|
333
|
+
# Use histogram to find most common interval
|
|
334
|
+
hist, bin_edges = np.histogram(intervals, bins=50)
|
|
335
|
+
most_common_interval = bin_edges[np.argmax(hist)]
|
|
336
|
+
|
|
337
|
+
# Convert to frequency
|
|
338
|
+
detected_freq = self.sample_rate / most_common_interval
|
|
339
|
+
|
|
340
|
+
# Find closest standard baud rate
|
|
341
|
+
candidates_array = np.array(candidates)
|
|
342
|
+
errors = np.abs(candidates_array - detected_freq)
|
|
343
|
+
best_idx = np.argmin(errors)
|
|
344
|
+
best_baud = candidates_array[best_idx]
|
|
345
|
+
|
|
346
|
+
# Calculate confidence based on how close we are to standard rate
|
|
347
|
+
relative_error = errors[best_idx] / best_baud
|
|
348
|
+
confidence = max(0.0, 1.0 - relative_error * 10)
|
|
349
|
+
|
|
350
|
+
bit_period_samples = self.sample_rate / best_baud
|
|
351
|
+
|
|
352
|
+
return BaudRateResult(
|
|
353
|
+
baud_rate=int(best_baud),
|
|
354
|
+
bit_period_samples=float(bit_period_samples),
|
|
355
|
+
confidence=float(confidence),
|
|
356
|
+
method="edge_histogram",
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
def measure_clock_jitter(self, clock_trace: NDArray[np.float64]) -> ClockMetrics:
|
|
360
|
+
"""Measure clock jitter and quality metrics.
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
Analyzes a clock signal to measure jitter, duty cycle, and stability.
|
|
365
|
+
Works best with recovered or measured clock signals.
|
|
366
|
+
|
|
367
|
+
Args:
|
|
368
|
+
clock_trace: Clock signal trace.
|
|
369
|
+
|
|
370
|
+
Returns:
|
|
371
|
+
ClockMetrics with comprehensive quality measurements.
|
|
372
|
+
|
|
373
|
+
Raises:
|
|
374
|
+
InsufficientDataError: If trace is too short or has too few edges.
|
|
375
|
+
ValidationError: If sample rate is not set.
|
|
376
|
+
"""
|
|
377
|
+
if len(clock_trace) < 10:
|
|
378
|
+
raise InsufficientDataError("Trace must have at least 10 samples")
|
|
379
|
+
|
|
380
|
+
if self.sample_rate is None:
|
|
381
|
+
raise ValidationError("Sample rate not set")
|
|
382
|
+
|
|
383
|
+
# Detect rising and falling edges
|
|
384
|
+
rising_edges = self._detect_edges_by_type(clock_trace, "rising")
|
|
385
|
+
falling_edges = self._detect_edges_by_type(clock_trace, "falling")
|
|
386
|
+
|
|
387
|
+
if len(rising_edges) < 3:
|
|
388
|
+
raise InsufficientDataError("Need at least 3 rising edges for jitter measurement")
|
|
389
|
+
|
|
390
|
+
# Calculate periods from rising edge to rising edge
|
|
391
|
+
periods = np.diff(rising_edges)
|
|
392
|
+
|
|
393
|
+
if len(periods) == 0:
|
|
394
|
+
raise InsufficientDataError("Cannot calculate period from single edge")
|
|
395
|
+
|
|
396
|
+
# Mean period
|
|
397
|
+
mean_period_samples = np.mean(periods)
|
|
398
|
+
mean_period_seconds = mean_period_samples / self.sample_rate
|
|
399
|
+
frequency = 1.0 / mean_period_seconds
|
|
400
|
+
|
|
401
|
+
# RMS jitter (standard deviation of periods)
|
|
402
|
+
jitter_rms_samples = np.std(periods)
|
|
403
|
+
jitter_rms = jitter_rms_samples / self.sample_rate
|
|
404
|
+
|
|
405
|
+
# Peak-to-peak jitter
|
|
406
|
+
jitter_pp_samples = np.ptp(periods)
|
|
407
|
+
jitter_pp = jitter_pp_samples / self.sample_rate
|
|
408
|
+
|
|
409
|
+
# Duty cycle (high time / period)
|
|
410
|
+
if len(falling_edges) >= len(rising_edges):
|
|
411
|
+
# Can measure duty cycle
|
|
412
|
+
high_times = []
|
|
413
|
+
for _i, rise in enumerate(rising_edges):
|
|
414
|
+
# Find next falling edge
|
|
415
|
+
fall_idx = np.searchsorted(falling_edges, rise)
|
|
416
|
+
if fall_idx < len(falling_edges):
|
|
417
|
+
high_time = falling_edges[fall_idx] - rise
|
|
418
|
+
high_times.append(high_time)
|
|
419
|
+
|
|
420
|
+
if high_times:
|
|
421
|
+
mean_high_time = np.mean(high_times)
|
|
422
|
+
duty_cycle = mean_high_time / mean_period_samples
|
|
423
|
+
else:
|
|
424
|
+
duty_cycle = 0.5 # Assume 50% if cannot measure
|
|
425
|
+
else:
|
|
426
|
+
duty_cycle = 0.5
|
|
427
|
+
|
|
428
|
+
# Stability score (inverse of relative jitter)
|
|
429
|
+
relative_jitter = (
|
|
430
|
+
jitter_rms_samples / mean_period_samples if mean_period_samples > 0 else 1.0
|
|
431
|
+
)
|
|
432
|
+
stability = max(0.0, 1.0 - relative_jitter * 10)
|
|
433
|
+
|
|
434
|
+
# Confidence based on number of periods and stability
|
|
435
|
+
confidence = min(1.0, len(periods) / 100.0) * stability
|
|
436
|
+
|
|
437
|
+
return ClockMetrics(
|
|
438
|
+
frequency=float(frequency),
|
|
439
|
+
period_samples=float(mean_period_samples),
|
|
440
|
+
period_seconds=float(mean_period_seconds),
|
|
441
|
+
jitter_rms=float(jitter_rms),
|
|
442
|
+
jitter_pp=float(jitter_pp),
|
|
443
|
+
duty_cycle=float(np.clip(duty_cycle, 0.0, 1.0)),
|
|
444
|
+
stability=float(stability),
|
|
445
|
+
confidence=float(confidence),
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
def _detect_frequency_edge(self, trace: NDArray[np.float64]) -> float:
|
|
449
|
+
"""Detect frequency using edge timing histogram.
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
Args:
|
|
454
|
+
trace: Signal trace.
|
|
455
|
+
|
|
456
|
+
Returns:
|
|
457
|
+
Detected frequency in Hz.
|
|
458
|
+
|
|
459
|
+
Raises:
|
|
460
|
+
ValidationError: If not enough edges found to detect frequency.
|
|
461
|
+
"""
|
|
462
|
+
edges = self._detect_edges_simple(trace)
|
|
463
|
+
|
|
464
|
+
if len(edges) < 3:
|
|
465
|
+
raise ValidationError("Not enough edges to detect frequency")
|
|
466
|
+
|
|
467
|
+
# Calculate inter-edge intervals
|
|
468
|
+
intervals = np.diff(edges)
|
|
469
|
+
|
|
470
|
+
# Build histogram of intervals
|
|
471
|
+
# The peak should correspond to half the period (edge to edge)
|
|
472
|
+
hist, bin_edges = np.histogram(intervals, bins=50)
|
|
473
|
+
_peak_interval = bin_edges[np.argmax(hist)]
|
|
474
|
+
|
|
475
|
+
# Frequency is sample_rate / (2 * interval) for edge-to-edge
|
|
476
|
+
# But we need to check if these are half-periods or full periods
|
|
477
|
+
# Use median interval as robust estimator
|
|
478
|
+
median_interval = np.median(intervals)
|
|
479
|
+
|
|
480
|
+
# Assume median represents half-period (rising to falling or vice versa)
|
|
481
|
+
# So full period is 2x median interval
|
|
482
|
+
period_samples = 2 * median_interval
|
|
483
|
+
frequency = self.sample_rate / period_samples
|
|
484
|
+
|
|
485
|
+
return float(frequency)
|
|
486
|
+
|
|
487
|
+
def _detect_frequency_fft(self, trace: NDArray[np.float64]) -> float:
|
|
488
|
+
"""Detect frequency using FFT spectral analysis.
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
Args:
|
|
493
|
+
trace: Signal trace.
|
|
494
|
+
|
|
495
|
+
Returns:
|
|
496
|
+
Detected frequency in Hz.
|
|
497
|
+
|
|
498
|
+
Raises:
|
|
499
|
+
ValidationError: If sample rate is not set.
|
|
500
|
+
"""
|
|
501
|
+
# Remove DC component
|
|
502
|
+
trace_ac = trace - np.mean(trace)
|
|
503
|
+
|
|
504
|
+
# Apply window to reduce spectral leakage
|
|
505
|
+
window = signal.windows.hann(len(trace_ac))
|
|
506
|
+
trace_windowed = trace_ac * window
|
|
507
|
+
|
|
508
|
+
# Compute FFT
|
|
509
|
+
fft = np.fft.rfft(trace_windowed)
|
|
510
|
+
if self.sample_rate is None:
|
|
511
|
+
raise ValidationError("Sample rate not set")
|
|
512
|
+
freqs = np.fft.rfftfreq(len(trace_windowed), 1.0 / self.sample_rate)
|
|
513
|
+
|
|
514
|
+
# Find peak in magnitude spectrum
|
|
515
|
+
magnitude = np.abs(fft)
|
|
516
|
+
|
|
517
|
+
# Ignore DC and very low frequencies (below 10 Hz)
|
|
518
|
+
min_freq_hz = 10.0
|
|
519
|
+
min_freq_idx = np.searchsorted(freqs, min_freq_hz)
|
|
520
|
+
if min_freq_idx >= len(magnitude):
|
|
521
|
+
min_freq_idx = np.intp(1)
|
|
522
|
+
|
|
523
|
+
peak_idx = min_freq_idx + np.argmax(magnitude[min_freq_idx:])
|
|
524
|
+
frequency = freqs[peak_idx]
|
|
525
|
+
|
|
526
|
+
return float(frequency)
|
|
527
|
+
|
|
528
|
+
def _detect_frequency_autocorr(self, trace: NDArray[np.float64]) -> float:
|
|
529
|
+
"""Detect frequency using autocorrelation.
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
Args:
|
|
534
|
+
trace: Signal trace.
|
|
535
|
+
|
|
536
|
+
Returns:
|
|
537
|
+
Detected frequency in Hz.
|
|
538
|
+
|
|
539
|
+
Raises:
|
|
540
|
+
ValidationError: If no periodic pattern detected or sample rate not set.
|
|
541
|
+
"""
|
|
542
|
+
# Remove mean
|
|
543
|
+
trace_centered = trace - np.mean(trace)
|
|
544
|
+
|
|
545
|
+
# Compute autocorrelation
|
|
546
|
+
autocorr = signal.correlate(trace_centered, trace_centered, mode="full")
|
|
547
|
+
autocorr = autocorr[len(autocorr) // 2 :] # Keep only positive lags
|
|
548
|
+
|
|
549
|
+
# Normalize
|
|
550
|
+
autocorr = autocorr / autocorr[0]
|
|
551
|
+
|
|
552
|
+
# Find first peak after lag 0
|
|
553
|
+
# Look for peaks in autocorrelation
|
|
554
|
+
peaks, _ = signal.find_peaks(autocorr, height=0.3)
|
|
555
|
+
|
|
556
|
+
if len(peaks) == 0:
|
|
557
|
+
raise ValidationError("No periodic pattern detected in autocorrelation")
|
|
558
|
+
|
|
559
|
+
# First peak corresponds to period
|
|
560
|
+
period_samples = peaks[0]
|
|
561
|
+
if self.sample_rate is None:
|
|
562
|
+
raise ValidationError("Sample rate not set")
|
|
563
|
+
frequency = self.sample_rate / period_samples
|
|
564
|
+
|
|
565
|
+
return float(frequency)
|
|
566
|
+
|
|
567
|
+
def _pll_track(
|
|
568
|
+
self, trace: NDArray[np.float64], initial_freq: float, bandwidth: float = 0.01
|
|
569
|
+
) -> NDArray[np.float64]:
|
|
570
|
+
"""Software PLL for phase tracking.
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
Implements a simple digital PLL for tracking phase and frequency
|
|
575
|
+
variations in the input signal.
|
|
576
|
+
|
|
577
|
+
Args:
|
|
578
|
+
trace: Input data trace.
|
|
579
|
+
initial_freq: Initial frequency estimate in Hz.
|
|
580
|
+
bandwidth: Loop bandwidth (0.0 to 1.0), lower = more filtering.
|
|
581
|
+
|
|
582
|
+
Returns:
|
|
583
|
+
Recovered clock signal.
|
|
584
|
+
|
|
585
|
+
Raises:
|
|
586
|
+
ValidationError: If sample rate is not set.
|
|
587
|
+
"""
|
|
588
|
+
n_samples = len(trace)
|
|
589
|
+
clock = np.zeros(n_samples)
|
|
590
|
+
|
|
591
|
+
# PLL state
|
|
592
|
+
phase = 0.0
|
|
593
|
+
freq = initial_freq
|
|
594
|
+
if self.sample_rate is None:
|
|
595
|
+
raise ValidationError("Sample rate not set")
|
|
596
|
+
omega = 2 * np.pi * freq / self.sample_rate
|
|
597
|
+
|
|
598
|
+
# Loop filter gains (proportional + integral)
|
|
599
|
+
kp = 2 * bandwidth # Proportional gain
|
|
600
|
+
ki = bandwidth**2 # Integral gain
|
|
601
|
+
|
|
602
|
+
# Detect edges for phase error calculation
|
|
603
|
+
threshold = (np.max(trace) + np.min(trace)) / 2.0
|
|
604
|
+
prev_sample = trace[0]
|
|
605
|
+
|
|
606
|
+
for i in range(n_samples):
|
|
607
|
+
# Generate clock output
|
|
608
|
+
clock[i] = 1.0 if np.cos(phase) > 0 else 0.0
|
|
609
|
+
|
|
610
|
+
# Detect phase error at edges
|
|
611
|
+
current_sample = trace[i]
|
|
612
|
+
phase_error = 0.0
|
|
613
|
+
|
|
614
|
+
# Simple phase detector: check if edge coincides with clock transition
|
|
615
|
+
if (prev_sample < threshold <= current_sample) or (
|
|
616
|
+
prev_sample > threshold >= current_sample
|
|
617
|
+
):
|
|
618
|
+
# Edge detected
|
|
619
|
+
clock_value = np.cos(phase)
|
|
620
|
+
# Phase error is sign of clock at edge
|
|
621
|
+
phase_error = np.sign(clock_value) * 0.1
|
|
622
|
+
|
|
623
|
+
# Update frequency and phase with loop filter
|
|
624
|
+
_freq_adjust = kp * phase_error
|
|
625
|
+
omega += ki * phase_error
|
|
626
|
+
|
|
627
|
+
# Update phase
|
|
628
|
+
phase += omega
|
|
629
|
+
phase = phase % (2 * np.pi)
|
|
630
|
+
|
|
631
|
+
prev_sample = current_sample
|
|
632
|
+
|
|
633
|
+
return clock
|
|
634
|
+
|
|
635
|
+
def _detect_edges_simple(self, trace: NDArray[np.float64]) -> NDArray[np.intp]:
|
|
636
|
+
"""Detect all edges in trace (both rising and falling).
|
|
637
|
+
|
|
638
|
+
Args:
|
|
639
|
+
trace: Signal trace.
|
|
640
|
+
|
|
641
|
+
Returns:
|
|
642
|
+
Array of edge indices.
|
|
643
|
+
"""
|
|
644
|
+
threshold = (np.max(trace) + np.min(trace)) / 2.0
|
|
645
|
+
rising = np.where((trace[:-1] < threshold) & (trace[1:] >= threshold))[0]
|
|
646
|
+
falling = np.where((trace[:-1] > threshold) & (trace[1:] <= threshold))[0]
|
|
647
|
+
|
|
648
|
+
# Combine and sort
|
|
649
|
+
all_edges = np.concatenate([rising, falling])
|
|
650
|
+
all_edges.sort()
|
|
651
|
+
|
|
652
|
+
return all_edges
|
|
653
|
+
|
|
654
|
+
def _detect_edges_by_type(
|
|
655
|
+
self, trace: NDArray[np.float64], edge_type: Literal["rising", "falling"]
|
|
656
|
+
) -> NDArray[np.intp]:
|
|
657
|
+
"""Detect edges of specific type.
|
|
658
|
+
|
|
659
|
+
Args:
|
|
660
|
+
trace: Signal trace.
|
|
661
|
+
edge_type: Type of edge to detect.
|
|
662
|
+
|
|
663
|
+
Returns:
|
|
664
|
+
Array of edge indices.
|
|
665
|
+
"""
|
|
666
|
+
threshold = (np.max(trace) + np.min(trace)) / 2.0
|
|
667
|
+
|
|
668
|
+
if edge_type == "rising":
|
|
669
|
+
edges = np.where((trace[:-1] < threshold) & (trace[1:] >= threshold))[0]
|
|
670
|
+
else: # falling
|
|
671
|
+
edges = np.where((trace[:-1] > threshold) & (trace[1:] <= threshold))[0]
|
|
672
|
+
|
|
673
|
+
return edges + 1 # Return index after crossing
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
# Convenience functions
|
|
677
|
+
|
|
678
|
+
|
|
679
|
+
def detect_clock_frequency(
|
|
680
|
+
trace: NDArray[np.float64],
|
|
681
|
+
sample_rate: float,
|
|
682
|
+
method: Literal["edge", "fft", "autocorr"] = "edge",
|
|
683
|
+
) -> float:
|
|
684
|
+
"""Detect clock frequency from signal.
|
|
685
|
+
|
|
686
|
+
|
|
687
|
+
|
|
688
|
+
Convenience function for detecting clock frequency without creating
|
|
689
|
+
a ClockRecovery instance.
|
|
690
|
+
|
|
691
|
+
Args:
|
|
692
|
+
trace: Signal trace data.
|
|
693
|
+
sample_rate: Sample rate in Hz.
|
|
694
|
+
method: Detection method ('edge', 'fft', or 'autocorr').
|
|
695
|
+
|
|
696
|
+
Returns:
|
|
697
|
+
Detected frequency in Hz.
|
|
698
|
+
|
|
699
|
+
Example:
|
|
700
|
+
>>> freq = detect_clock_frequency(data, sample_rate=1e9, method='edge')
|
|
701
|
+
>>> print(f"Clock: {freq/1e6:.2f} MHz")
|
|
702
|
+
"""
|
|
703
|
+
recovery = ClockRecovery(sample_rate)
|
|
704
|
+
return recovery.detect_clock_frequency(trace, method)
|
|
705
|
+
|
|
706
|
+
|
|
707
|
+
def recover_clock(
|
|
708
|
+
data_trace: NDArray[np.float64],
|
|
709
|
+
sample_rate: float,
|
|
710
|
+
method: Literal["edge", "pll", "fft"] = "edge",
|
|
711
|
+
) -> NDArray[np.float64]:
|
|
712
|
+
"""Recover clock signal from data.
|
|
713
|
+
|
|
714
|
+
|
|
715
|
+
|
|
716
|
+
Convenience function for recovering clock signal without creating
|
|
717
|
+
a ClockRecovery instance.
|
|
718
|
+
|
|
719
|
+
Args:
|
|
720
|
+
data_trace: Data signal trace.
|
|
721
|
+
sample_rate: Sample rate in Hz.
|
|
722
|
+
method: Recovery method ('edge', 'pll', or 'fft').
|
|
723
|
+
|
|
724
|
+
Returns:
|
|
725
|
+
Recovered clock trace.
|
|
726
|
+
|
|
727
|
+
Example:
|
|
728
|
+
>>> clock = recover_clock(data, sample_rate=1e9, method='pll')
|
|
729
|
+
"""
|
|
730
|
+
recovery = ClockRecovery(sample_rate)
|
|
731
|
+
return recovery.recover_clock(data_trace, method)
|
|
732
|
+
|
|
733
|
+
|
|
734
|
+
def detect_baud_rate(
|
|
735
|
+
trace: Any, sample_rate: float | None = None, candidates: list[int] | None = None
|
|
736
|
+
) -> int | BaudRateResult:
|
|
737
|
+
"""Auto-detect baud rate.
|
|
738
|
+
|
|
739
|
+
|
|
740
|
+
|
|
741
|
+
Convenience function for baud rate detection. Supports both DigitalTrace
|
|
742
|
+
objects (with metadata) and raw numpy arrays (requiring sample_rate).
|
|
743
|
+
|
|
744
|
+
Args:
|
|
745
|
+
trace: Signal trace data (DigitalTrace or numpy array).
|
|
746
|
+
sample_rate: Sample rate in Hz (optional if trace has metadata).
|
|
747
|
+
candidates: List of candidate baud rates. If None, uses standard rates.
|
|
748
|
+
|
|
749
|
+
Returns:
|
|
750
|
+
Detected baud rate as int (for DigitalTrace) or BaudRateResult.
|
|
751
|
+
|
|
752
|
+
Raises:
|
|
753
|
+
ValidationError: If sample_rate is required but not provided.
|
|
754
|
+
|
|
755
|
+
Example:
|
|
756
|
+
>>> baud = detect_baud_rate(digital_trace) # Uses metadata
|
|
757
|
+
>>> result = detect_baud_rate(data_array, sample_rate=1e6) # Explicit rate
|
|
758
|
+
"""
|
|
759
|
+
# Check if trace is a DigitalTrace with metadata
|
|
760
|
+
if hasattr(trace, "metadata") and hasattr(trace.metadata, "sample_rate"):
|
|
761
|
+
rate = trace.metadata.sample_rate
|
|
762
|
+
data = np.asarray(trace.data, dtype=np.float64)
|
|
763
|
+
recovery = ClockRecovery(rate)
|
|
764
|
+
result = recovery.detect_baud_rate(data, candidates)
|
|
765
|
+
return result.baud_rate # Return just the baud rate for DigitalTrace
|
|
766
|
+
elif sample_rate is not None:
|
|
767
|
+
data = np.asarray(trace, dtype=np.float64)
|
|
768
|
+
recovery = ClockRecovery(sample_rate)
|
|
769
|
+
return recovery.detect_baud_rate(data, candidates)
|
|
770
|
+
else:
|
|
771
|
+
raise ValidationError("sample_rate required when trace is not a DigitalTrace with metadata")
|
|
772
|
+
|
|
773
|
+
|
|
774
|
+
def measure_clock_jitter(clock_trace: NDArray[np.float64], sample_rate: float) -> ClockMetrics:
|
|
775
|
+
"""Measure clock jitter.
|
|
776
|
+
|
|
777
|
+
|
|
778
|
+
|
|
779
|
+
Convenience function for jitter measurement without creating
|
|
780
|
+
a ClockRecovery instance.
|
|
781
|
+
|
|
782
|
+
Args:
|
|
783
|
+
clock_trace: Clock signal trace.
|
|
784
|
+
sample_rate: Sample rate in Hz.
|
|
785
|
+
|
|
786
|
+
Returns:
|
|
787
|
+
ClockMetrics with jitter and quality measurements.
|
|
788
|
+
|
|
789
|
+
Example:
|
|
790
|
+
>>> metrics = measure_clock_jitter(clock, sample_rate=1e9)
|
|
791
|
+
>>> print(f"RMS jitter: {metrics.jitter_rms*1e12:.2f} ps")
|
|
792
|
+
"""
|
|
793
|
+
recovery = ClockRecovery(sample_rate)
|
|
794
|
+
return recovery.measure_clock_jitter(clock_trace)
|
|
795
|
+
|
|
796
|
+
|
|
797
|
+
__all__ = [
|
|
798
|
+
"BaudRateResult",
|
|
799
|
+
"ClockMetrics",
|
|
800
|
+
"ClockRecovery",
|
|
801
|
+
"detect_baud_rate",
|
|
802
|
+
"detect_clock_frequency",
|
|
803
|
+
"measure_clock_jitter",
|
|
804
|
+
"recover_clock",
|
|
805
|
+
]
|