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,708 @@
|
|
|
1
|
+
"""Time-varying and multi-level threshold support for digital signal analysis.
|
|
2
|
+
|
|
3
|
+
- RE-THR-001: Time-Varying Threshold Support
|
|
4
|
+
- RE-THR-002: Multi-Level Logic Support
|
|
5
|
+
|
|
6
|
+
This module provides adaptive thresholding for signals with varying DC offset
|
|
7
|
+
or amplitude, and support for multi-level logic standards beyond simple
|
|
8
|
+
high/low states.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from typing import TYPE_CHECKING, Literal
|
|
15
|
+
|
|
16
|
+
import numpy as np
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from numpy.typing import NDArray
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class ThresholdConfig:
|
|
24
|
+
"""Configuration for threshold detection.
|
|
25
|
+
|
|
26
|
+
Implements RE-THR-001, RE-THR-002: Threshold configuration.
|
|
27
|
+
|
|
28
|
+
Attributes:
|
|
29
|
+
threshold_type: Type of thresholding ('fixed', 'adaptive', 'multi_level').
|
|
30
|
+
fixed_threshold: Fixed threshold value (for 'fixed' type).
|
|
31
|
+
window_size: Window size for adaptive thresholding.
|
|
32
|
+
percentile: Percentile for adaptive threshold calculation.
|
|
33
|
+
levels: Voltage levels for multi-level logic.
|
|
34
|
+
hysteresis: Hysteresis margin to prevent oscillation.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
threshold_type: Literal["fixed", "adaptive", "multi_level"] = "fixed"
|
|
38
|
+
fixed_threshold: float = 0.5
|
|
39
|
+
window_size: int = 1024
|
|
40
|
+
percentile: float = 50.0
|
|
41
|
+
levels: list[float] = field(default_factory=lambda: [0.0, 1.0])
|
|
42
|
+
hysteresis: float = 0.05
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class AdaptiveThresholdResult:
|
|
47
|
+
"""Result of adaptive threshold calculation.
|
|
48
|
+
|
|
49
|
+
Implements RE-THR-001: Time-varying threshold result.
|
|
50
|
+
|
|
51
|
+
Attributes:
|
|
52
|
+
thresholds: Array of threshold values at each sample.
|
|
53
|
+
binary_output: Digitized signal.
|
|
54
|
+
crossings: Indices of threshold crossings.
|
|
55
|
+
dc_offset: Estimated DC offset over time.
|
|
56
|
+
amplitude: Estimated signal amplitude over time.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
thresholds: NDArray[np.float64]
|
|
60
|
+
binary_output: NDArray[np.uint8]
|
|
61
|
+
crossings: list[int]
|
|
62
|
+
dc_offset: NDArray[np.float64]
|
|
63
|
+
amplitude: NDArray[np.float64]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass
|
|
67
|
+
class MultiLevelResult:
|
|
68
|
+
"""Result of multi-level logic detection.
|
|
69
|
+
|
|
70
|
+
Implements RE-THR-002: Multi-level detection result.
|
|
71
|
+
|
|
72
|
+
Attributes:
|
|
73
|
+
levels: Detected logic levels at each sample.
|
|
74
|
+
level_values: Voltage levels used.
|
|
75
|
+
transitions: List of (index, from_level, to_level) transitions.
|
|
76
|
+
level_histogram: Count of samples at each level.
|
|
77
|
+
eye_heights: Eye height for each transition.
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
levels: NDArray[np.int32]
|
|
81
|
+
level_values: list[float]
|
|
82
|
+
transitions: list[tuple[int, int, int]]
|
|
83
|
+
level_histogram: dict[int, int]
|
|
84
|
+
eye_heights: list[float]
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class AdaptiveThresholder:
|
|
88
|
+
"""Apply time-varying thresholds to signals.
|
|
89
|
+
|
|
90
|
+
Implements RE-THR-001: Time-Varying Threshold Support.
|
|
91
|
+
|
|
92
|
+
Tracks DC offset and amplitude changes to maintain accurate
|
|
93
|
+
thresholding despite signal drift.
|
|
94
|
+
|
|
95
|
+
Example:
|
|
96
|
+
>>> thresholder = AdaptiveThresholder(window_size=1000)
|
|
97
|
+
>>> result = thresholder.apply(analog_signal)
|
|
98
|
+
>>> digital = result.binary_output
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
def __init__(
|
|
102
|
+
self,
|
|
103
|
+
window_size: int = 1024,
|
|
104
|
+
percentile: float = 50.0,
|
|
105
|
+
method: Literal["median", "mean", "envelope", "otsu"] = "median",
|
|
106
|
+
hysteresis: float = 0.05,
|
|
107
|
+
) -> None:
|
|
108
|
+
"""Initialize adaptive thresholder.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
window_size: Size of sliding window for adaptation.
|
|
112
|
+
percentile: Percentile for threshold calculation.
|
|
113
|
+
method: Thresholding method.
|
|
114
|
+
hysteresis: Hysteresis margin. This is used as an absolute value
|
|
115
|
+
when amplitude-relative calculation would be too small. For
|
|
116
|
+
signals with small amplitude variations (e.g., oscillating
|
|
117
|
+
around a threshold), this value is applied directly.
|
|
118
|
+
"""
|
|
119
|
+
self.window_size = window_size
|
|
120
|
+
self.percentile = percentile
|
|
121
|
+
self.method = method
|
|
122
|
+
self.hysteresis = hysteresis
|
|
123
|
+
|
|
124
|
+
def apply(self, signal: NDArray[np.float64]) -> AdaptiveThresholdResult:
|
|
125
|
+
"""Apply adaptive thresholding to signal.
|
|
126
|
+
|
|
127
|
+
Implements RE-THR-001: Adaptive threshold application.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
signal: Input analog signal.
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
AdaptiveThresholdResult with thresholds and digitized output.
|
|
134
|
+
|
|
135
|
+
Example:
|
|
136
|
+
>>> result = thresholder.apply(analog_waveform)
|
|
137
|
+
>>> plt.plot(result.binary_output)
|
|
138
|
+
"""
|
|
139
|
+
n_samples = len(signal)
|
|
140
|
+
|
|
141
|
+
# Estimate DC offset and amplitude over time
|
|
142
|
+
dc_offset = np.zeros(n_samples)
|
|
143
|
+
amplitude = np.zeros(n_samples)
|
|
144
|
+
thresholds = np.zeros(n_samples)
|
|
145
|
+
|
|
146
|
+
half_window = self.window_size // 2
|
|
147
|
+
|
|
148
|
+
for i in range(n_samples):
|
|
149
|
+
# Window bounds
|
|
150
|
+
start = max(0, i - half_window)
|
|
151
|
+
end = min(n_samples, i + half_window)
|
|
152
|
+
window = signal[start:end]
|
|
153
|
+
|
|
154
|
+
if self.method == "median":
|
|
155
|
+
dc_offset[i] = np.median(window)
|
|
156
|
+
amplitude[i] = np.percentile(window, 95) - np.percentile(window, 5)
|
|
157
|
+
thresholds[i] = dc_offset[i]
|
|
158
|
+
|
|
159
|
+
elif self.method == "mean":
|
|
160
|
+
dc_offset[i] = np.mean(window)
|
|
161
|
+
amplitude[i] = np.std(window) * 4 # Approximate peak-to-peak
|
|
162
|
+
thresholds[i] = dc_offset[i]
|
|
163
|
+
|
|
164
|
+
elif self.method == "envelope":
|
|
165
|
+
# Use min/max envelope
|
|
166
|
+
high = np.max(window)
|
|
167
|
+
low = np.min(window)
|
|
168
|
+
dc_offset[i] = (high + low) / 2
|
|
169
|
+
amplitude[i] = high - low
|
|
170
|
+
thresholds[i] = dc_offset[i]
|
|
171
|
+
|
|
172
|
+
elif self.method == "otsu":
|
|
173
|
+
# Simplified Otsu's method
|
|
174
|
+
threshold = self._otsu_threshold(window)
|
|
175
|
+
thresholds[i] = threshold
|
|
176
|
+
dc_offset[i] = threshold
|
|
177
|
+
amplitude[i] = np.max(window) - np.min(window)
|
|
178
|
+
|
|
179
|
+
# Apply hysteresis
|
|
180
|
+
binary_output, crossings = self._apply_with_hysteresis(signal, thresholds, amplitude)
|
|
181
|
+
|
|
182
|
+
return AdaptiveThresholdResult(
|
|
183
|
+
thresholds=thresholds,
|
|
184
|
+
binary_output=binary_output,
|
|
185
|
+
crossings=crossings,
|
|
186
|
+
dc_offset=dc_offset,
|
|
187
|
+
amplitude=amplitude,
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
def calculate_threshold_profile(self, signal: NDArray[np.float64]) -> NDArray[np.float64]:
|
|
191
|
+
"""Calculate threshold values without applying.
|
|
192
|
+
|
|
193
|
+
Implements RE-THR-001: Threshold profile calculation.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
signal: Input signal.
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
Array of threshold values.
|
|
200
|
+
"""
|
|
201
|
+
result = self.apply(signal)
|
|
202
|
+
return result.thresholds
|
|
203
|
+
|
|
204
|
+
def _apply_with_hysteresis(
|
|
205
|
+
self,
|
|
206
|
+
signal: NDArray[np.float64],
|
|
207
|
+
thresholds: NDArray[np.float64],
|
|
208
|
+
amplitude: NDArray[np.float64],
|
|
209
|
+
) -> tuple[NDArray[np.uint8], list[int]]:
|
|
210
|
+
"""Apply thresholding with hysteresis.
|
|
211
|
+
|
|
212
|
+
The hysteresis prevents rapid oscillation when the signal hovers near
|
|
213
|
+
the threshold. The margin is calculated as:
|
|
214
|
+
- If amplitude is significant: hyst_margin = amplitude * hysteresis
|
|
215
|
+
- If amplitude is small: hyst_margin = hysteresis (used as absolute value)
|
|
216
|
+
|
|
217
|
+
This ensures hysteresis remains effective even for signals with very
|
|
218
|
+
small amplitude variations.
|
|
219
|
+
|
|
220
|
+
Args:
|
|
221
|
+
signal: Input signal.
|
|
222
|
+
thresholds: Threshold values.
|
|
223
|
+
amplitude: Signal amplitude at each point.
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
Tuple of (binary_output, crossings).
|
|
227
|
+
"""
|
|
228
|
+
n_samples = len(signal)
|
|
229
|
+
binary = np.zeros(n_samples, dtype=np.uint8)
|
|
230
|
+
crossings = []
|
|
231
|
+
|
|
232
|
+
# Initial state
|
|
233
|
+
current_state = 1 if signal[0] > thresholds[0] else 0
|
|
234
|
+
binary[0] = current_state
|
|
235
|
+
|
|
236
|
+
for i in range(1, n_samples):
|
|
237
|
+
threshold = thresholds[i]
|
|
238
|
+
amp = amplitude[i]
|
|
239
|
+
|
|
240
|
+
# Calculate hysteresis margin:
|
|
241
|
+
# - Use amplitude-relative margin for signals with significant amplitude
|
|
242
|
+
# - Use absolute hysteresis value when amplitude is small
|
|
243
|
+
# This prevents oscillation for signals hovering around the threshold
|
|
244
|
+
amplitude_relative_margin = amp * self.hysteresis
|
|
245
|
+
absolute_margin = self.hysteresis
|
|
246
|
+
|
|
247
|
+
# Use the larger of the two to ensure effective hysteresis
|
|
248
|
+
# When amplitude is large (e.g., > 1.0), amplitude-relative dominates
|
|
249
|
+
# When amplitude is small (e.g., 0.02), absolute hysteresis dominates
|
|
250
|
+
hyst_margin = max(amplitude_relative_margin, absolute_margin)
|
|
251
|
+
|
|
252
|
+
if current_state == 0:
|
|
253
|
+
# Currently low, need signal above threshold + hysteresis to go high
|
|
254
|
+
if signal[i] > threshold + hyst_margin:
|
|
255
|
+
current_state = 1
|
|
256
|
+
crossings.append(i)
|
|
257
|
+
else:
|
|
258
|
+
# Currently high, need signal below threshold - hysteresis to go low
|
|
259
|
+
if signal[i] < threshold - hyst_margin:
|
|
260
|
+
current_state = 0
|
|
261
|
+
crossings.append(i)
|
|
262
|
+
|
|
263
|
+
binary[i] = current_state
|
|
264
|
+
|
|
265
|
+
return binary, crossings
|
|
266
|
+
|
|
267
|
+
def _otsu_threshold(self, data: NDArray[np.float64]) -> float:
|
|
268
|
+
"""Calculate Otsu's threshold.
|
|
269
|
+
|
|
270
|
+
Args:
|
|
271
|
+
data: Data window.
|
|
272
|
+
|
|
273
|
+
Returns:
|
|
274
|
+
Optimal threshold value.
|
|
275
|
+
"""
|
|
276
|
+
# Simplified Otsu's method
|
|
277
|
+
hist, bin_edges = np.histogram(data, bins=50)
|
|
278
|
+
bin_centers = (bin_edges[:-1] + bin_edges[1:]) / 2
|
|
279
|
+
|
|
280
|
+
total = hist.sum()
|
|
281
|
+
if total == 0:
|
|
282
|
+
return float(np.mean(data))
|
|
283
|
+
|
|
284
|
+
current_max = 0
|
|
285
|
+
threshold = bin_centers[0]
|
|
286
|
+
|
|
287
|
+
sum_total = np.sum(bin_centers * hist)
|
|
288
|
+
sum_background = 0
|
|
289
|
+
weight_background = 0
|
|
290
|
+
|
|
291
|
+
for i in range(len(hist)):
|
|
292
|
+
weight_background += hist[i]
|
|
293
|
+
if weight_background == 0:
|
|
294
|
+
continue
|
|
295
|
+
|
|
296
|
+
weight_foreground = total - weight_background
|
|
297
|
+
if weight_foreground == 0:
|
|
298
|
+
break
|
|
299
|
+
|
|
300
|
+
sum_background += bin_centers[i] * hist[i]
|
|
301
|
+
|
|
302
|
+
mean_background = sum_background / weight_background
|
|
303
|
+
mean_foreground = (sum_total - sum_background) / weight_foreground
|
|
304
|
+
|
|
305
|
+
variance_between = (
|
|
306
|
+
weight_background * weight_foreground * (mean_background - mean_foreground) ** 2
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
if variance_between > current_max:
|
|
310
|
+
current_max = variance_between
|
|
311
|
+
threshold = bin_centers[i]
|
|
312
|
+
|
|
313
|
+
return float(threshold)
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
class MultiLevelDetector:
|
|
317
|
+
"""Detect multi-level logic signals.
|
|
318
|
+
|
|
319
|
+
Implements RE-THR-002: Multi-Level Logic Support.
|
|
320
|
+
|
|
321
|
+
Supports PAM-2, PAM-4, PAM-8, and custom multi-level signaling
|
|
322
|
+
where signals encode multiple bits per symbol.
|
|
323
|
+
|
|
324
|
+
Example:
|
|
325
|
+
>>> detector = MultiLevelDetector(levels=4) # PAM-4
|
|
326
|
+
>>> result = detector.detect(signal)
|
|
327
|
+
>>> symbols = result.levels
|
|
328
|
+
"""
|
|
329
|
+
|
|
330
|
+
def __init__(
|
|
331
|
+
self,
|
|
332
|
+
levels: int | list[float] = 2,
|
|
333
|
+
auto_detect_levels: bool = True,
|
|
334
|
+
hysteresis: float = 0.1,
|
|
335
|
+
) -> None:
|
|
336
|
+
"""Initialize multi-level detector.
|
|
337
|
+
|
|
338
|
+
Args:
|
|
339
|
+
levels: Number of levels or explicit voltage levels.
|
|
340
|
+
auto_detect_levels: Automatically detect level voltages.
|
|
341
|
+
hysteresis: Hysteresis fraction between levels.
|
|
342
|
+
"""
|
|
343
|
+
if isinstance(levels, int):
|
|
344
|
+
self.n_levels = levels
|
|
345
|
+
self.level_values = None
|
|
346
|
+
else:
|
|
347
|
+
self.n_levels = len(levels)
|
|
348
|
+
self.level_values = sorted(levels)
|
|
349
|
+
|
|
350
|
+
self.auto_detect_levels = auto_detect_levels
|
|
351
|
+
self.hysteresis = hysteresis
|
|
352
|
+
|
|
353
|
+
def detect(self, signal: NDArray[np.float64]) -> MultiLevelResult:
|
|
354
|
+
"""Detect multi-level logic in signal.
|
|
355
|
+
|
|
356
|
+
Implements RE-THR-002: Multi-level detection workflow.
|
|
357
|
+
|
|
358
|
+
Args:
|
|
359
|
+
signal: Input analog signal.
|
|
360
|
+
|
|
361
|
+
Returns:
|
|
362
|
+
MultiLevelResult with detected levels.
|
|
363
|
+
|
|
364
|
+
Example:
|
|
365
|
+
>>> result = detector.detect(pam4_signal)
|
|
366
|
+
>>> print(f"Detected {len(result.level_values)} levels")
|
|
367
|
+
"""
|
|
368
|
+
# Auto-detect level values if needed
|
|
369
|
+
if self.level_values is None or self.auto_detect_levels:
|
|
370
|
+
level_values = self._detect_levels(signal)
|
|
371
|
+
else:
|
|
372
|
+
level_values = self.level_values
|
|
373
|
+
|
|
374
|
+
# Calculate decision thresholds between levels
|
|
375
|
+
thresholds = [
|
|
376
|
+
(level_values[i] + level_values[i + 1]) / 2 for i in range(len(level_values) - 1)
|
|
377
|
+
]
|
|
378
|
+
|
|
379
|
+
# Apply hysteresis-aware level detection
|
|
380
|
+
levels, transitions = self._detect_with_hysteresis(signal, level_values, thresholds)
|
|
381
|
+
|
|
382
|
+
# Calculate level histogram
|
|
383
|
+
level_histogram = {}
|
|
384
|
+
for level in range(len(level_values)):
|
|
385
|
+
level_histogram[level] = int(np.sum(levels == level))
|
|
386
|
+
|
|
387
|
+
# Calculate eye heights
|
|
388
|
+
eye_heights = self._calculate_eye_heights(signal, level_values)
|
|
389
|
+
|
|
390
|
+
return MultiLevelResult(
|
|
391
|
+
levels=levels,
|
|
392
|
+
level_values=level_values,
|
|
393
|
+
transitions=transitions,
|
|
394
|
+
level_histogram=level_histogram,
|
|
395
|
+
eye_heights=eye_heights,
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
def detect_levels_from_histogram(
|
|
399
|
+
self, signal: NDArray[np.float64], n_levels: int | None = None
|
|
400
|
+
) -> list[float]:
|
|
401
|
+
"""Detect logic levels from signal histogram.
|
|
402
|
+
|
|
403
|
+
Implements RE-THR-002: Level detection.
|
|
404
|
+
|
|
405
|
+
Args:
|
|
406
|
+
signal: Input signal.
|
|
407
|
+
n_levels: Expected number of levels (auto-detect if None).
|
|
408
|
+
|
|
409
|
+
Returns:
|
|
410
|
+
List of detected voltage levels.
|
|
411
|
+
"""
|
|
412
|
+
if n_levels is None:
|
|
413
|
+
n_levels = self.n_levels
|
|
414
|
+
|
|
415
|
+
return self._detect_levels(signal, n_levels)
|
|
416
|
+
|
|
417
|
+
def calculate_eye_diagram(
|
|
418
|
+
self,
|
|
419
|
+
signal: NDArray[np.float64],
|
|
420
|
+
samples_per_symbol: int,
|
|
421
|
+
n_symbols: int = 100,
|
|
422
|
+
) -> NDArray[np.float64]:
|
|
423
|
+
"""Calculate eye diagram data for multi-level signal.
|
|
424
|
+
|
|
425
|
+
Implements RE-THR-002: Eye diagram support.
|
|
426
|
+
|
|
427
|
+
Args:
|
|
428
|
+
signal: Input signal.
|
|
429
|
+
samples_per_symbol: Samples per symbol period.
|
|
430
|
+
n_symbols: Number of symbols to overlay.
|
|
431
|
+
|
|
432
|
+
Returns:
|
|
433
|
+
2D array of overlaid symbol waveforms.
|
|
434
|
+
"""
|
|
435
|
+
n_available = len(signal) // samples_per_symbol
|
|
436
|
+
n_symbols = min(n_symbols, n_available)
|
|
437
|
+
|
|
438
|
+
# Create 2D array with overlaid symbols
|
|
439
|
+
eye_data = np.zeros((n_symbols, samples_per_symbol * 2))
|
|
440
|
+
|
|
441
|
+
for i in range(n_symbols):
|
|
442
|
+
start = i * samples_per_symbol
|
|
443
|
+
end = start + samples_per_symbol * 2
|
|
444
|
+
|
|
445
|
+
if end <= len(signal):
|
|
446
|
+
eye_data[i] = signal[start:end]
|
|
447
|
+
|
|
448
|
+
return eye_data
|
|
449
|
+
|
|
450
|
+
def _detect_levels(
|
|
451
|
+
self, signal: NDArray[np.float64], n_levels: int | None = None
|
|
452
|
+
) -> list[float]:
|
|
453
|
+
"""Detect voltage levels using clustering.
|
|
454
|
+
|
|
455
|
+
Args:
|
|
456
|
+
signal: Input signal.
|
|
457
|
+
n_levels: Expected number of levels.
|
|
458
|
+
|
|
459
|
+
Returns:
|
|
460
|
+
List of level voltage values.
|
|
461
|
+
"""
|
|
462
|
+
if n_levels is None:
|
|
463
|
+
n_levels = self.n_levels
|
|
464
|
+
|
|
465
|
+
# Use histogram-based clustering
|
|
466
|
+
hist, bin_edges = np.histogram(signal, bins=100)
|
|
467
|
+
bin_centers = (bin_edges[:-1] + bin_edges[1:]) / 2
|
|
468
|
+
|
|
469
|
+
# Find peaks in histogram
|
|
470
|
+
peaks = []
|
|
471
|
+
for i in range(1, len(hist) - 1):
|
|
472
|
+
if hist[i] > hist[i - 1] and hist[i] > hist[i + 1]:
|
|
473
|
+
peaks.append((hist[i], bin_centers[i]))
|
|
474
|
+
|
|
475
|
+
# Sort by frequency and take top n_levels
|
|
476
|
+
peaks.sort(reverse=True)
|
|
477
|
+
level_values = sorted([p[1] for p in peaks[:n_levels]])
|
|
478
|
+
|
|
479
|
+
# If not enough peaks found, use evenly spaced levels
|
|
480
|
+
if len(level_values) < n_levels:
|
|
481
|
+
min_val = np.min(signal)
|
|
482
|
+
max_val = np.max(signal)
|
|
483
|
+
level_values = list(np.linspace(min_val, max_val, n_levels))
|
|
484
|
+
|
|
485
|
+
return level_values
|
|
486
|
+
|
|
487
|
+
def _detect_with_hysteresis(
|
|
488
|
+
self,
|
|
489
|
+
signal: NDArray[np.float64],
|
|
490
|
+
level_values: list[float],
|
|
491
|
+
thresholds: list[float],
|
|
492
|
+
) -> tuple[NDArray[np.int32], list[tuple[int, int, int]]]:
|
|
493
|
+
"""Detect levels with hysteresis.
|
|
494
|
+
|
|
495
|
+
Args:
|
|
496
|
+
signal: Input signal.
|
|
497
|
+
level_values: Voltage levels.
|
|
498
|
+
thresholds: Decision thresholds.
|
|
499
|
+
|
|
500
|
+
Returns:
|
|
501
|
+
Tuple of (level_array, transitions).
|
|
502
|
+
"""
|
|
503
|
+
n_samples = len(signal)
|
|
504
|
+
levels = np.zeros(n_samples, dtype=np.int32)
|
|
505
|
+
transitions = []
|
|
506
|
+
|
|
507
|
+
# Calculate hysteresis margins
|
|
508
|
+
margins = []
|
|
509
|
+
for i in range(len(level_values) - 1):
|
|
510
|
+
margin = (level_values[i + 1] - level_values[i]) * self.hysteresis
|
|
511
|
+
margins.append(margin)
|
|
512
|
+
|
|
513
|
+
# Initial level
|
|
514
|
+
current_level = self._find_closest_level(signal[0], level_values)
|
|
515
|
+
levels[0] = current_level
|
|
516
|
+
|
|
517
|
+
for i in range(1, n_samples):
|
|
518
|
+
new_level = current_level
|
|
519
|
+
|
|
520
|
+
# Check for transitions
|
|
521
|
+
if current_level < len(level_values) - 1:
|
|
522
|
+
# Can go up
|
|
523
|
+
upper_threshold = thresholds[current_level] + margins[current_level]
|
|
524
|
+
if signal[i] > upper_threshold:
|
|
525
|
+
new_level = current_level + 1
|
|
526
|
+
|
|
527
|
+
if current_level > 0:
|
|
528
|
+
# Can go down
|
|
529
|
+
lower_threshold = thresholds[current_level - 1] - margins[current_level - 1]
|
|
530
|
+
if signal[i] < lower_threshold:
|
|
531
|
+
new_level = current_level - 1
|
|
532
|
+
|
|
533
|
+
if new_level != current_level:
|
|
534
|
+
transitions.append((i, current_level, new_level))
|
|
535
|
+
current_level = new_level
|
|
536
|
+
|
|
537
|
+
levels[i] = current_level
|
|
538
|
+
|
|
539
|
+
return levels, transitions
|
|
540
|
+
|
|
541
|
+
def _find_closest_level(self, value: float, level_values: list[float]) -> int:
|
|
542
|
+
"""Find closest level to value.
|
|
543
|
+
|
|
544
|
+
Args:
|
|
545
|
+
value: Sample value.
|
|
546
|
+
level_values: Level voltages.
|
|
547
|
+
|
|
548
|
+
Returns:
|
|
549
|
+
Level index.
|
|
550
|
+
"""
|
|
551
|
+
distances = [abs(value - lv) for lv in level_values]
|
|
552
|
+
return int(np.argmin(distances))
|
|
553
|
+
|
|
554
|
+
def _calculate_eye_heights(
|
|
555
|
+
self, signal: NDArray[np.float64], level_values: list[float]
|
|
556
|
+
) -> list[float]:
|
|
557
|
+
"""Calculate eye heights between levels.
|
|
558
|
+
|
|
559
|
+
Args:
|
|
560
|
+
signal: Input signal.
|
|
561
|
+
level_values: Level voltages.
|
|
562
|
+
|
|
563
|
+
Returns:
|
|
564
|
+
List of eye heights for each level transition.
|
|
565
|
+
"""
|
|
566
|
+
eye_heights = []
|
|
567
|
+
|
|
568
|
+
for i in range(len(level_values) - 1):
|
|
569
|
+
lower = level_values[i]
|
|
570
|
+
upper = level_values[i + 1]
|
|
571
|
+
|
|
572
|
+
# Find samples near each level
|
|
573
|
+
lower_samples = signal[np.abs(signal - lower) < (upper - lower) * 0.2]
|
|
574
|
+
upper_samples = signal[np.abs(signal - upper) < (upper - lower) * 0.2]
|
|
575
|
+
|
|
576
|
+
if len(lower_samples) > 0 and len(upper_samples) > 0:
|
|
577
|
+
# Eye height is gap between worst cases
|
|
578
|
+
worst_low = np.max(lower_samples)
|
|
579
|
+
worst_high = np.min(upper_samples)
|
|
580
|
+
eye_height = worst_high - worst_low
|
|
581
|
+
else:
|
|
582
|
+
eye_height = upper - lower
|
|
583
|
+
|
|
584
|
+
eye_heights.append(max(0, eye_height))
|
|
585
|
+
|
|
586
|
+
return eye_heights
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
# =============================================================================
|
|
590
|
+
# Convenience functions
|
|
591
|
+
# =============================================================================
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
def apply_adaptive_threshold(
|
|
595
|
+
signal: NDArray[np.float64],
|
|
596
|
+
window_size: int = 1024,
|
|
597
|
+
method: Literal["median", "mean", "envelope", "otsu"] = "median",
|
|
598
|
+
hysteresis: float = 0.05,
|
|
599
|
+
) -> AdaptiveThresholdResult:
|
|
600
|
+
"""Apply adaptive thresholding to a signal.
|
|
601
|
+
|
|
602
|
+
Implements RE-THR-001: Time-Varying Threshold Support.
|
|
603
|
+
|
|
604
|
+
Args:
|
|
605
|
+
signal: Input analog signal.
|
|
606
|
+
window_size: Adaptive window size.
|
|
607
|
+
method: Thresholding method.
|
|
608
|
+
hysteresis: Hysteresis margin.
|
|
609
|
+
|
|
610
|
+
Returns:
|
|
611
|
+
AdaptiveThresholdResult with thresholds and binary output.
|
|
612
|
+
|
|
613
|
+
Example:
|
|
614
|
+
>>> result = apply_adaptive_threshold(noisy_signal)
|
|
615
|
+
>>> digital = result.binary_output
|
|
616
|
+
"""
|
|
617
|
+
thresholder = AdaptiveThresholder(
|
|
618
|
+
window_size=window_size,
|
|
619
|
+
method=method,
|
|
620
|
+
hysteresis=hysteresis,
|
|
621
|
+
)
|
|
622
|
+
return thresholder.apply(signal)
|
|
623
|
+
|
|
624
|
+
|
|
625
|
+
def detect_multi_level(
|
|
626
|
+
signal: NDArray[np.float64],
|
|
627
|
+
n_levels: int = 4,
|
|
628
|
+
auto_detect: bool = True,
|
|
629
|
+
hysteresis: float = 0.1,
|
|
630
|
+
) -> MultiLevelResult:
|
|
631
|
+
"""Detect multi-level logic in signal.
|
|
632
|
+
|
|
633
|
+
Implements RE-THR-002: Multi-Level Logic Support.
|
|
634
|
+
|
|
635
|
+
Args:
|
|
636
|
+
signal: Input analog signal.
|
|
637
|
+
n_levels: Expected number of levels.
|
|
638
|
+
auto_detect: Automatically detect level voltages.
|
|
639
|
+
hysteresis: Hysteresis between levels.
|
|
640
|
+
|
|
641
|
+
Returns:
|
|
642
|
+
MultiLevelResult with detected levels.
|
|
643
|
+
|
|
644
|
+
Example:
|
|
645
|
+
>>> result = detect_multi_level(pam4_signal, n_levels=4)
|
|
646
|
+
>>> symbols = result.levels
|
|
647
|
+
"""
|
|
648
|
+
detector = MultiLevelDetector(
|
|
649
|
+
levels=n_levels,
|
|
650
|
+
auto_detect_levels=auto_detect,
|
|
651
|
+
hysteresis=hysteresis,
|
|
652
|
+
)
|
|
653
|
+
return detector.detect(signal)
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
def calculate_threshold_snr(
|
|
657
|
+
signal: NDArray[np.float64],
|
|
658
|
+
threshold: float | NDArray[np.float64],
|
|
659
|
+
) -> float:
|
|
660
|
+
"""Calculate signal-to-noise ratio at threshold.
|
|
661
|
+
|
|
662
|
+
Implements RE-THR-001: Threshold quality metric.
|
|
663
|
+
|
|
664
|
+
Args:
|
|
665
|
+
signal: Input signal.
|
|
666
|
+
threshold: Threshold value(s).
|
|
667
|
+
|
|
668
|
+
Returns:
|
|
669
|
+
Estimated SNR in dB.
|
|
670
|
+
"""
|
|
671
|
+
if isinstance(threshold, np.ndarray):
|
|
672
|
+
threshold = float(np.mean(threshold))
|
|
673
|
+
|
|
674
|
+
# Separate high and low samples
|
|
675
|
+
high_samples = signal[signal > threshold]
|
|
676
|
+
low_samples = signal[signal <= threshold]
|
|
677
|
+
|
|
678
|
+
if len(high_samples) == 0 or len(low_samples) == 0:
|
|
679
|
+
return 0.0
|
|
680
|
+
|
|
681
|
+
# Calculate signal power (difference between means)
|
|
682
|
+
signal_power = (np.mean(high_samples) - np.mean(low_samples)) ** 2
|
|
683
|
+
|
|
684
|
+
# Calculate noise power (variance around means)
|
|
685
|
+
noise_power = (np.var(high_samples) + np.var(low_samples)) / 2
|
|
686
|
+
|
|
687
|
+
if noise_power == 0:
|
|
688
|
+
return 100.0 # Very high SNR
|
|
689
|
+
|
|
690
|
+
snr_linear = signal_power / noise_power
|
|
691
|
+
snr_db = 10 * np.log10(snr_linear)
|
|
692
|
+
|
|
693
|
+
return float(snr_db)
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
__all__ = [
|
|
697
|
+
"AdaptiveThresholdResult",
|
|
698
|
+
# Classes
|
|
699
|
+
"AdaptiveThresholder",
|
|
700
|
+
"MultiLevelDetector",
|
|
701
|
+
"MultiLevelResult",
|
|
702
|
+
# Data classes
|
|
703
|
+
"ThresholdConfig",
|
|
704
|
+
# Functions
|
|
705
|
+
"apply_adaptive_threshold",
|
|
706
|
+
"calculate_threshold_snr",
|
|
707
|
+
"detect_multi_level",
|
|
708
|
+
]
|