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,632 @@
|
|
|
1
|
+
"""Edge detection with sub-sample precision and timing analysis.
|
|
2
|
+
|
|
3
|
+
This module provides edge detection with interpolation for sub-sample precision,
|
|
4
|
+
timing measurements between edges, and timing constraint validation for digital
|
|
5
|
+
signal analysis.
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
Example:
|
|
9
|
+
>>> import numpy as np
|
|
10
|
+
>>> from oscura.analyzers.digital.edges import detect_edges, measure_edge_timing
|
|
11
|
+
>>> # Generate test signal
|
|
12
|
+
>>> signal = np.array([0, 0, 0.5, 1.0, 1.0, 1.0, 0.5, 0, 0])
|
|
13
|
+
>>> # Detect edges
|
|
14
|
+
>>> edges = detect_edges(signal, edge_type='both', sample_rate=100e6)
|
|
15
|
+
>>> # Measure timing
|
|
16
|
+
>>> timing = measure_edge_timing(edges, sample_rate=100e6)
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from dataclasses import dataclass
|
|
22
|
+
from typing import TYPE_CHECKING, Literal
|
|
23
|
+
|
|
24
|
+
import numpy as np
|
|
25
|
+
|
|
26
|
+
from oscura.core.memoize import memoize_analysis
|
|
27
|
+
|
|
28
|
+
if TYPE_CHECKING:
|
|
29
|
+
from numpy.typing import NDArray
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class Edge:
|
|
34
|
+
"""A detected edge in the signal.
|
|
35
|
+
|
|
36
|
+
Attributes:
|
|
37
|
+
sample_index: Sample index where edge was detected.
|
|
38
|
+
time: Interpolated edge time in seconds.
|
|
39
|
+
edge_type: Type of edge ('rising' or 'falling').
|
|
40
|
+
amplitude: Transition amplitude in signal units (volts).
|
|
41
|
+
slew_rate: Edge slew rate (signal units per second).
|
|
42
|
+
quality: Edge quality classification.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
sample_index: int
|
|
46
|
+
time: float # Interpolated time
|
|
47
|
+
edge_type: Literal["rising", "falling"]
|
|
48
|
+
amplitude: float # Transition amplitude
|
|
49
|
+
slew_rate: float # V/s or samples/s
|
|
50
|
+
quality: Literal["clean", "slow", "noisy", "glitch"]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass
|
|
54
|
+
class EdgeTiming:
|
|
55
|
+
"""Timing measurements from edge analysis.
|
|
56
|
+
|
|
57
|
+
Attributes:
|
|
58
|
+
periods: Array of edge-to-edge periods in seconds.
|
|
59
|
+
mean_period: Mean period in seconds.
|
|
60
|
+
std_period: Standard deviation of period in seconds.
|
|
61
|
+
min_period: Minimum period in seconds.
|
|
62
|
+
max_period: Maximum period in seconds.
|
|
63
|
+
duty_cycles: Array of duty cycle ratios (0-1).
|
|
64
|
+
mean_duty_cycle: Mean duty cycle ratio.
|
|
65
|
+
jitter_rms: RMS jitter in seconds.
|
|
66
|
+
jitter_pp: Peak-to-peak jitter in seconds.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
periods: NDArray[np.float64] # Edge-to-edge periods
|
|
70
|
+
mean_period: float
|
|
71
|
+
std_period: float
|
|
72
|
+
min_period: float
|
|
73
|
+
max_period: float
|
|
74
|
+
duty_cycles: NDArray[np.float64]
|
|
75
|
+
mean_duty_cycle: float
|
|
76
|
+
jitter_rms: float
|
|
77
|
+
jitter_pp: float
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass
|
|
81
|
+
class TimingConstraint:
|
|
82
|
+
"""Timing constraint for validation.
|
|
83
|
+
|
|
84
|
+
Attributes:
|
|
85
|
+
name: Descriptive name for the constraint.
|
|
86
|
+
min_time: Minimum allowed time in seconds (None for no minimum).
|
|
87
|
+
max_time: Maximum allowed time in seconds (None for no maximum).
|
|
88
|
+
reference: Which edges to check ('rising', 'falling', or 'both').
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
name: str
|
|
92
|
+
min_time: float | None = None
|
|
93
|
+
max_time: float | None = None
|
|
94
|
+
reference: str | None = None # 'rising', 'falling', 'both'
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@dataclass
|
|
98
|
+
class TimingViolation:
|
|
99
|
+
"""A timing constraint violation.
|
|
100
|
+
|
|
101
|
+
Attributes:
|
|
102
|
+
constraint: The violated constraint.
|
|
103
|
+
measured_time: The measured time that violated the constraint.
|
|
104
|
+
edge_index: Index of the edge that violated the constraint.
|
|
105
|
+
sample_index: Sample index where violation occurred.
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
constraint: TimingConstraint
|
|
109
|
+
measured_time: float
|
|
110
|
+
edge_index: int
|
|
111
|
+
sample_index: int
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@memoize_analysis(maxsize=32)
|
|
115
|
+
def detect_edges(
|
|
116
|
+
trace: NDArray[np.float64],
|
|
117
|
+
edge_type: Literal["rising", "falling", "both"] = "both",
|
|
118
|
+
threshold: float | Literal["auto"] = "auto",
|
|
119
|
+
hysteresis: float = 0.0,
|
|
120
|
+
sample_rate: float = 1.0,
|
|
121
|
+
) -> list[Edge]:
|
|
122
|
+
"""Detect signal edges with configurable threshold.
|
|
123
|
+
|
|
124
|
+
Detects rising and/or falling edges in a digital or analog signal with
|
|
125
|
+
optional hysteresis for noise immunity.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
trace: Input signal trace (analog or digital).
|
|
129
|
+
edge_type: Type of edges to detect ('rising', 'falling', or 'both').
|
|
130
|
+
threshold: Detection threshold. 'auto' computes from signal midpoint.
|
|
131
|
+
hysteresis: Hysteresis amount for noise immunity (signal units).
|
|
132
|
+
sample_rate: Sample rate in Hz for time calculation.
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
List of Edge objects with detected edges.
|
|
136
|
+
|
|
137
|
+
Example:
|
|
138
|
+
>>> signal = np.array([0, 0, 1, 1, 0, 0])
|
|
139
|
+
>>> edges = detect_edges(signal, edge_type='rising')
|
|
140
|
+
>>> len(edges)
|
|
141
|
+
1
|
|
142
|
+
"""
|
|
143
|
+
if len(trace) < 2:
|
|
144
|
+
return []
|
|
145
|
+
|
|
146
|
+
trace = np.asarray(trace)
|
|
147
|
+
|
|
148
|
+
# Compute threshold if auto
|
|
149
|
+
thresh_val: float
|
|
150
|
+
if threshold == "auto":
|
|
151
|
+
thresh_val = float((np.max(trace) + np.min(trace)) / 2.0)
|
|
152
|
+
else:
|
|
153
|
+
thresh_val = threshold
|
|
154
|
+
|
|
155
|
+
# Apply hysteresis if specified
|
|
156
|
+
if hysteresis > 0:
|
|
157
|
+
thresh_high = thresh_val + hysteresis / 2.0
|
|
158
|
+
thresh_low = thresh_val - hysteresis / 2.0
|
|
159
|
+
else:
|
|
160
|
+
thresh_high = thresh_val
|
|
161
|
+
thresh_low = thresh_val
|
|
162
|
+
|
|
163
|
+
edges: list[Edge] = []
|
|
164
|
+
time_base = 1.0 / sample_rate
|
|
165
|
+
|
|
166
|
+
# State machine for hysteresis
|
|
167
|
+
state = trace[0] > thresh_val # Initial state
|
|
168
|
+
|
|
169
|
+
for i in range(1, len(trace)):
|
|
170
|
+
prev_val = trace[i - 1]
|
|
171
|
+
curr_val = trace[i]
|
|
172
|
+
|
|
173
|
+
# Detect transitions with hysteresis
|
|
174
|
+
if not state and curr_val > thresh_high:
|
|
175
|
+
# Rising edge
|
|
176
|
+
if edge_type in ["rising", "both"]:
|
|
177
|
+
# Interpolate edge time
|
|
178
|
+
interp_time = interpolate_edge_time(trace, i - 1, method="linear")
|
|
179
|
+
time = (i - 1 + interp_time) * time_base
|
|
180
|
+
|
|
181
|
+
# Calculate edge properties
|
|
182
|
+
amplitude = curr_val - prev_val
|
|
183
|
+
slew_rate = amplitude * sample_rate
|
|
184
|
+
|
|
185
|
+
# Classify quality (simple heuristic)
|
|
186
|
+
quality = classify_edge_quality(trace, i, sample_rate)
|
|
187
|
+
|
|
188
|
+
edges.append(
|
|
189
|
+
Edge(
|
|
190
|
+
sample_index=i,
|
|
191
|
+
time=time,
|
|
192
|
+
edge_type="rising",
|
|
193
|
+
amplitude=abs(amplitude),
|
|
194
|
+
slew_rate=slew_rate,
|
|
195
|
+
quality=quality,
|
|
196
|
+
)
|
|
197
|
+
)
|
|
198
|
+
state = True
|
|
199
|
+
|
|
200
|
+
elif state and curr_val < thresh_low:
|
|
201
|
+
# Falling edge
|
|
202
|
+
if edge_type in ["falling", "both"]:
|
|
203
|
+
# Interpolate edge time
|
|
204
|
+
interp_time = interpolate_edge_time(trace, i - 1, method="linear")
|
|
205
|
+
time = (i - 1 + interp_time) * time_base
|
|
206
|
+
|
|
207
|
+
# Calculate edge properties
|
|
208
|
+
amplitude = prev_val - curr_val
|
|
209
|
+
slew_rate = -amplitude * sample_rate
|
|
210
|
+
|
|
211
|
+
# Classify quality (simple heuristic)
|
|
212
|
+
quality = classify_edge_quality(trace, i, sample_rate)
|
|
213
|
+
|
|
214
|
+
edges.append(
|
|
215
|
+
Edge(
|
|
216
|
+
sample_index=i,
|
|
217
|
+
time=time,
|
|
218
|
+
edge_type="falling",
|
|
219
|
+
amplitude=abs(amplitude),
|
|
220
|
+
slew_rate=slew_rate,
|
|
221
|
+
quality=quality,
|
|
222
|
+
)
|
|
223
|
+
)
|
|
224
|
+
state = False
|
|
225
|
+
|
|
226
|
+
return edges
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def interpolate_edge_time(
|
|
230
|
+
trace: NDArray[np.float64], sample_index: int, method: Literal["linear", "quadratic"] = "linear"
|
|
231
|
+
) -> float:
|
|
232
|
+
"""Interpolate edge time for sub-sample precision.
|
|
233
|
+
|
|
234
|
+
Uses linear or quadratic interpolation to estimate the fractional sample
|
|
235
|
+
position where an edge crosses the threshold.
|
|
236
|
+
|
|
237
|
+
Args:
|
|
238
|
+
trace: Input signal trace.
|
|
239
|
+
sample_index: Sample index just before the edge.
|
|
240
|
+
method: Interpolation method ('linear' or 'quadratic').
|
|
241
|
+
|
|
242
|
+
Returns:
|
|
243
|
+
Fractional sample offset (0.0 to 1.0) from sample_index.
|
|
244
|
+
|
|
245
|
+
Example:
|
|
246
|
+
>>> trace = np.array([0, 0.3, 0.8, 1.0])
|
|
247
|
+
>>> offset = interpolate_edge_time(trace, 1, method='linear')
|
|
248
|
+
"""
|
|
249
|
+
if sample_index < 0 or sample_index >= len(trace) - 1:
|
|
250
|
+
return 0.0
|
|
251
|
+
|
|
252
|
+
if method == "linear":
|
|
253
|
+
# Linear interpolation between two points
|
|
254
|
+
v0 = trace[sample_index]
|
|
255
|
+
v1 = trace[sample_index + 1]
|
|
256
|
+
|
|
257
|
+
if abs(v1 - v0) < 1e-10:
|
|
258
|
+
return 0.5 # Avoid division by zero
|
|
259
|
+
|
|
260
|
+
# Find midpoint crossing
|
|
261
|
+
threshold = (v0 + v1) / 2.0
|
|
262
|
+
fraction = (threshold - v0) / (v1 - v0)
|
|
263
|
+
|
|
264
|
+
# Clamp to valid range
|
|
265
|
+
return float(np.clip(fraction, 0.0, 1.0))
|
|
266
|
+
|
|
267
|
+
elif method == "quadratic":
|
|
268
|
+
# Quadratic interpolation using 3 points
|
|
269
|
+
if sample_index < 1 or sample_index >= len(trace) - 1:
|
|
270
|
+
# Fall back to linear
|
|
271
|
+
return interpolate_edge_time(trace, sample_index, method="linear")
|
|
272
|
+
|
|
273
|
+
# Use points before, at, and after edge
|
|
274
|
+
_v_prev = trace[sample_index - 1]
|
|
275
|
+
v0 = trace[sample_index]
|
|
276
|
+
v1 = trace[sample_index + 1]
|
|
277
|
+
|
|
278
|
+
# Fit parabola and find threshold crossing
|
|
279
|
+
# Simplified: use linear for now (full quadratic fit is complex)
|
|
280
|
+
return interpolate_edge_time(trace, sample_index, method="linear")
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def measure_edge_timing(edges: list[Edge], sample_rate: float = 1.0) -> EdgeTiming:
|
|
284
|
+
"""Measure timing between edges.
|
|
285
|
+
|
|
286
|
+
Computes period, duty cycle, and jitter statistics from a list of detected edges.
|
|
287
|
+
|
|
288
|
+
Args:
|
|
289
|
+
edges: List of Edge objects from detect_edges().
|
|
290
|
+
sample_rate: Sample rate in Hz (for time base).
|
|
291
|
+
|
|
292
|
+
Returns:
|
|
293
|
+
EdgeTiming object with timing measurements.
|
|
294
|
+
|
|
295
|
+
Example:
|
|
296
|
+
>>> edges = detect_edges(signal, edge_type='both', sample_rate=100e6)
|
|
297
|
+
>>> timing = measure_edge_timing(edges, sample_rate=100e6)
|
|
298
|
+
"""
|
|
299
|
+
if len(edges) < 2:
|
|
300
|
+
# Not enough edges for timing analysis
|
|
301
|
+
return EdgeTiming(
|
|
302
|
+
periods=np.array([]),
|
|
303
|
+
mean_period=0.0,
|
|
304
|
+
std_period=0.0,
|
|
305
|
+
min_period=0.0,
|
|
306
|
+
max_period=0.0,
|
|
307
|
+
duty_cycles=np.array([]),
|
|
308
|
+
mean_duty_cycle=0.0,
|
|
309
|
+
jitter_rms=0.0,
|
|
310
|
+
jitter_pp=0.0,
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
# Calculate periods (time between consecutive edges)
|
|
314
|
+
edge_times = np.array([e.time for e in edges])
|
|
315
|
+
periods = np.diff(edge_times)
|
|
316
|
+
|
|
317
|
+
# Calculate duty cycles (ratio of high time to period)
|
|
318
|
+
duty_cycles = []
|
|
319
|
+
rising_edges = [e for e in edges if e.edge_type == "rising"]
|
|
320
|
+
falling_edges = [e for e in edges if e.edge_type == "falling"]
|
|
321
|
+
|
|
322
|
+
# Match rising and falling edges to compute duty cycles
|
|
323
|
+
for i in range(min(len(rising_edges), len(falling_edges))):
|
|
324
|
+
rise_time = rising_edges[i].time
|
|
325
|
+
fall_time = falling_edges[i].time
|
|
326
|
+
|
|
327
|
+
# Find next edge of opposite type
|
|
328
|
+
if i + 1 < len(rising_edges):
|
|
329
|
+
next_rise = rising_edges[i + 1].time
|
|
330
|
+
period = next_rise - rise_time
|
|
331
|
+
if period > 0:
|
|
332
|
+
high_time = fall_time - rise_time
|
|
333
|
+
duty_cycle = high_time / period
|
|
334
|
+
duty_cycles.append(np.clip(duty_cycle, 0.0, 1.0))
|
|
335
|
+
|
|
336
|
+
duty_cycles_arr = np.array(duty_cycles) if duty_cycles else np.array([])
|
|
337
|
+
|
|
338
|
+
# Calculate jitter
|
|
339
|
+
if len(periods) > 1:
|
|
340
|
+
mean_period = np.mean(periods)
|
|
341
|
+
jitter_rms = np.std(periods)
|
|
342
|
+
jitter_pp = np.max(periods) - np.min(periods)
|
|
343
|
+
else:
|
|
344
|
+
mean_period = periods[0] if len(periods) > 0 else 0.0
|
|
345
|
+
jitter_rms = 0.0
|
|
346
|
+
jitter_pp = 0.0
|
|
347
|
+
|
|
348
|
+
return EdgeTiming(
|
|
349
|
+
periods=periods,
|
|
350
|
+
mean_period=float(mean_period),
|
|
351
|
+
std_period=float(np.std(periods)) if len(periods) > 0 else 0.0,
|
|
352
|
+
min_period=float(np.min(periods)) if len(periods) > 0 else 0.0,
|
|
353
|
+
max_period=float(np.max(periods)) if len(periods) > 0 else 0.0,
|
|
354
|
+
duty_cycles=duty_cycles_arr,
|
|
355
|
+
mean_duty_cycle=float(np.mean(duty_cycles_arr)) if len(duty_cycles_arr) > 0 else 0.0,
|
|
356
|
+
jitter_rms=float(jitter_rms),
|
|
357
|
+
jitter_pp=float(jitter_pp),
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def check_timing_constraints(
|
|
362
|
+
edges: list[Edge], constraints: list[TimingConstraint], sample_rate: float = 1.0
|
|
363
|
+
) -> list[TimingViolation]:
|
|
364
|
+
"""Check edges against timing constraints.
|
|
365
|
+
|
|
366
|
+
Validates edge timing against specified constraints and reports violations.
|
|
367
|
+
|
|
368
|
+
Args:
|
|
369
|
+
edges: List of Edge objects to check.
|
|
370
|
+
constraints: List of TimingConstraint objects defining limits.
|
|
371
|
+
sample_rate: Sample rate in Hz.
|
|
372
|
+
|
|
373
|
+
Returns:
|
|
374
|
+
List of TimingViolation objects for any violations found.
|
|
375
|
+
|
|
376
|
+
Example:
|
|
377
|
+
>>> constraint = TimingConstraint(name="min_period", min_time=10e-9)
|
|
378
|
+
>>> violations = check_timing_constraints(edges, [constraint])
|
|
379
|
+
"""
|
|
380
|
+
violations: list[TimingViolation] = []
|
|
381
|
+
|
|
382
|
+
if len(edges) < 2:
|
|
383
|
+
return violations
|
|
384
|
+
|
|
385
|
+
# Calculate periods between edges
|
|
386
|
+
for i in range(len(edges) - 1):
|
|
387
|
+
edge_time = edges[i].time
|
|
388
|
+
next_time = edges[i + 1].time
|
|
389
|
+
period = next_time - edge_time
|
|
390
|
+
|
|
391
|
+
for constraint in constraints:
|
|
392
|
+
# Check if constraint applies to this edge type
|
|
393
|
+
if constraint.reference:
|
|
394
|
+
if constraint.reference == "rising" and edges[i].edge_type != "rising":
|
|
395
|
+
continue
|
|
396
|
+
if constraint.reference == "falling" and edges[i].edge_type != "falling":
|
|
397
|
+
continue
|
|
398
|
+
|
|
399
|
+
# Check timing constraints
|
|
400
|
+
violated = False
|
|
401
|
+
|
|
402
|
+
if constraint.min_time is not None and period < constraint.min_time:
|
|
403
|
+
violated = True
|
|
404
|
+
|
|
405
|
+
if constraint.max_time is not None and period > constraint.max_time:
|
|
406
|
+
violated = True
|
|
407
|
+
|
|
408
|
+
if violated:
|
|
409
|
+
violations.append(
|
|
410
|
+
TimingViolation(
|
|
411
|
+
constraint=constraint,
|
|
412
|
+
measured_time=period,
|
|
413
|
+
edge_index=i,
|
|
414
|
+
sample_index=edges[i].sample_index,
|
|
415
|
+
)
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
return violations
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def classify_edge_quality(
|
|
422
|
+
trace: NDArray[np.float64], edge_index: int, sample_rate: float
|
|
423
|
+
) -> Literal["clean", "slow", "noisy", "glitch"]:
|
|
424
|
+
"""Classify edge quality.
|
|
425
|
+
|
|
426
|
+
Analyzes the edge transition to classify its quality based on slew rate,
|
|
427
|
+
noise, and duration.
|
|
428
|
+
|
|
429
|
+
Args:
|
|
430
|
+
trace: Input signal trace.
|
|
431
|
+
edge_index: Sample index of the edge.
|
|
432
|
+
sample_rate: Sample rate in Hz.
|
|
433
|
+
|
|
434
|
+
Returns:
|
|
435
|
+
Quality classification: 'clean', 'slow', 'noisy', or 'glitch'.
|
|
436
|
+
|
|
437
|
+
Example:
|
|
438
|
+
>>> quality = classify_edge_quality(trace, 10, 100e6)
|
|
439
|
+
"""
|
|
440
|
+
if edge_index < 1 or edge_index >= len(trace) - 1:
|
|
441
|
+
return "clean"
|
|
442
|
+
|
|
443
|
+
# Get window around edge
|
|
444
|
+
window_size = min(10, edge_index, len(trace) - edge_index - 1)
|
|
445
|
+
window = trace[edge_index - window_size : edge_index + window_size + 1]
|
|
446
|
+
|
|
447
|
+
# Calculate transition amplitude
|
|
448
|
+
v_before = trace[edge_index - 1]
|
|
449
|
+
v_after = trace[edge_index]
|
|
450
|
+
amplitude = abs(v_after - v_before)
|
|
451
|
+
|
|
452
|
+
# Check for glitch (very short duration)
|
|
453
|
+
if window_size < 3:
|
|
454
|
+
return "glitch"
|
|
455
|
+
|
|
456
|
+
# Calculate noise (std dev in window)
|
|
457
|
+
noise = np.std(window)
|
|
458
|
+
|
|
459
|
+
# Calculate slew rate
|
|
460
|
+
_slew_rate = amplitude * sample_rate
|
|
461
|
+
|
|
462
|
+
# Simple heuristic classification
|
|
463
|
+
signal_range = np.max(trace) - np.min(trace)
|
|
464
|
+
|
|
465
|
+
if amplitude < signal_range * 0.1:
|
|
466
|
+
return "glitch"
|
|
467
|
+
|
|
468
|
+
if noise > amplitude * 0.2:
|
|
469
|
+
return "noisy"
|
|
470
|
+
|
|
471
|
+
# Check if transition is slow (takes many samples)
|
|
472
|
+
transition_samples = 0
|
|
473
|
+
_threshold = (v_before + v_after) / 2.0
|
|
474
|
+
|
|
475
|
+
for i in range(max(0, edge_index - window_size), min(len(trace), edge_index + window_size)):
|
|
476
|
+
val = trace[i]
|
|
477
|
+
if v_before < v_after: # Rising
|
|
478
|
+
if v_before <= val <= v_after:
|
|
479
|
+
transition_samples += 1
|
|
480
|
+
else: # Falling
|
|
481
|
+
if v_after <= val <= v_before:
|
|
482
|
+
transition_samples += 1
|
|
483
|
+
|
|
484
|
+
if transition_samples > 5:
|
|
485
|
+
return "slow"
|
|
486
|
+
|
|
487
|
+
return "clean"
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
class EdgeDetector:
|
|
491
|
+
"""Object-oriented wrapper for edge detection functionality.
|
|
492
|
+
|
|
493
|
+
Provides a class-based interface for edge detection operations,
|
|
494
|
+
wrapping the functional API for consistency with test expectations.
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
Example:
|
|
499
|
+
>>> detector = EdgeDetector()
|
|
500
|
+
>>> rising, falling = detector.detect_all_edges(signal_data)
|
|
501
|
+
"""
|
|
502
|
+
|
|
503
|
+
def __init__(
|
|
504
|
+
self,
|
|
505
|
+
threshold: float | Literal["auto"] = "auto",
|
|
506
|
+
hysteresis: float = 0.0,
|
|
507
|
+
sample_rate: float = 1.0,
|
|
508
|
+
min_pulse_width: int | None = None,
|
|
509
|
+
):
|
|
510
|
+
"""Initialize edge detector.
|
|
511
|
+
|
|
512
|
+
Args:
|
|
513
|
+
threshold: Detection threshold. 'auto' computes from signal midpoint.
|
|
514
|
+
hysteresis: Hysteresis amount for noise immunity (signal units).
|
|
515
|
+
sample_rate: Sample rate in Hz for time calculation.
|
|
516
|
+
min_pulse_width: Minimum pulse width in samples to filter noise.
|
|
517
|
+
"""
|
|
518
|
+
self.threshold = threshold
|
|
519
|
+
self.hysteresis = hysteresis
|
|
520
|
+
self.sample_rate = sample_rate
|
|
521
|
+
self.min_pulse_width = min_pulse_width
|
|
522
|
+
|
|
523
|
+
def detect_all_edges(
|
|
524
|
+
self, trace: NDArray[np.float64]
|
|
525
|
+
) -> tuple[NDArray[np.intp], NDArray[np.intp]]:
|
|
526
|
+
"""Detect all rising and falling edges.
|
|
527
|
+
|
|
528
|
+
Args:
|
|
529
|
+
trace: Input signal trace (analog or digital).
|
|
530
|
+
|
|
531
|
+
Returns:
|
|
532
|
+
Tuple of (rising_edge_indices, falling_edge_indices).
|
|
533
|
+
|
|
534
|
+
Example:
|
|
535
|
+
>>> detector = EdgeDetector(sample_rate=100e6)
|
|
536
|
+
>>> rising, falling = detector.detect_all_edges(signal)
|
|
537
|
+
"""
|
|
538
|
+
edges = detect_edges(
|
|
539
|
+
trace,
|
|
540
|
+
edge_type="both",
|
|
541
|
+
threshold=self.threshold,
|
|
542
|
+
hysteresis=self.hysteresis,
|
|
543
|
+
sample_rate=self.sample_rate,
|
|
544
|
+
)
|
|
545
|
+
|
|
546
|
+
# Filter by min_pulse_width if specified
|
|
547
|
+
if self.min_pulse_width is not None and len(edges) > 1:
|
|
548
|
+
filtered_edges = []
|
|
549
|
+
for i, edge in enumerate(edges):
|
|
550
|
+
if i == 0:
|
|
551
|
+
filtered_edges.append(edge)
|
|
552
|
+
continue
|
|
553
|
+
# Check distance to previous edge
|
|
554
|
+
dist = edge.sample_index - edges[i - 1].sample_index
|
|
555
|
+
if dist >= self.min_pulse_width:
|
|
556
|
+
filtered_edges.append(edge)
|
|
557
|
+
edges = filtered_edges
|
|
558
|
+
|
|
559
|
+
rising_indices = np.array(
|
|
560
|
+
[e.sample_index for e in edges if e.edge_type == "rising"], dtype=np.int64
|
|
561
|
+
)
|
|
562
|
+
falling_indices = np.array(
|
|
563
|
+
[e.sample_index for e in edges if e.edge_type == "falling"], dtype=np.int64
|
|
564
|
+
)
|
|
565
|
+
|
|
566
|
+
return rising_indices, falling_indices
|
|
567
|
+
|
|
568
|
+
def detect_rising_edges(self, trace: NDArray[np.float64]) -> list[Edge]:
|
|
569
|
+
"""Detect only rising edges.
|
|
570
|
+
|
|
571
|
+
Args:
|
|
572
|
+
trace: Input signal trace.
|
|
573
|
+
|
|
574
|
+
Returns:
|
|
575
|
+
List of Edge objects for rising edges.
|
|
576
|
+
"""
|
|
577
|
+
return detect_edges(
|
|
578
|
+
trace,
|
|
579
|
+
edge_type="rising",
|
|
580
|
+
threshold=self.threshold,
|
|
581
|
+
hysteresis=self.hysteresis,
|
|
582
|
+
sample_rate=self.sample_rate,
|
|
583
|
+
)
|
|
584
|
+
|
|
585
|
+
def detect_falling_edges(self, trace: NDArray[np.float64]) -> list[Edge]:
|
|
586
|
+
"""Detect only falling edges.
|
|
587
|
+
|
|
588
|
+
Args:
|
|
589
|
+
trace: Input signal trace.
|
|
590
|
+
|
|
591
|
+
Returns:
|
|
592
|
+
List of Edge objects for falling edges.
|
|
593
|
+
"""
|
|
594
|
+
return detect_edges(
|
|
595
|
+
trace,
|
|
596
|
+
edge_type="falling",
|
|
597
|
+
threshold=self.threshold,
|
|
598
|
+
hysteresis=self.hysteresis,
|
|
599
|
+
sample_rate=self.sample_rate,
|
|
600
|
+
)
|
|
601
|
+
|
|
602
|
+
def measure_timing(self, trace: NDArray[np.float64]) -> EdgeTiming:
|
|
603
|
+
"""Detect edges and measure timing.
|
|
604
|
+
|
|
605
|
+
Args:
|
|
606
|
+
trace: Input signal trace.
|
|
607
|
+
|
|
608
|
+
Returns:
|
|
609
|
+
EdgeTiming object with timing measurements.
|
|
610
|
+
"""
|
|
611
|
+
edges = detect_edges(
|
|
612
|
+
trace,
|
|
613
|
+
edge_type="both",
|
|
614
|
+
threshold=self.threshold,
|
|
615
|
+
hysteresis=self.hysteresis,
|
|
616
|
+
sample_rate=self.sample_rate,
|
|
617
|
+
)
|
|
618
|
+
return measure_edge_timing(edges, self.sample_rate)
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
__all__ = [
|
|
622
|
+
"Edge",
|
|
623
|
+
"EdgeDetector",
|
|
624
|
+
"EdgeTiming",
|
|
625
|
+
"TimingConstraint",
|
|
626
|
+
"TimingViolation",
|
|
627
|
+
"check_timing_constraints",
|
|
628
|
+
"classify_edge_quality",
|
|
629
|
+
"detect_edges",
|
|
630
|
+
"interpolate_edge_time",
|
|
631
|
+
"measure_edge_timing",
|
|
632
|
+
]
|