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,769 @@
|
|
|
1
|
+
"""Automatic signal characterization and type detection.
|
|
2
|
+
|
|
3
|
+
This module provides intelligent signal type detection, extracting
|
|
4
|
+
characteristics without requiring user expertise.
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
>>> from oscura.discovery import characterize_signal
|
|
9
|
+
>>> result = characterize_signal(trace)
|
|
10
|
+
>>> print(f"{result.signal_type}: {result.confidence:.2f}")
|
|
11
|
+
UART: 0.94
|
|
12
|
+
|
|
13
|
+
References:
|
|
14
|
+
IEEE 181-2011: Transitional Waveform Definitions
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from dataclasses import dataclass, field
|
|
20
|
+
from typing import TYPE_CHECKING, Any, Literal
|
|
21
|
+
|
|
22
|
+
import numpy as np
|
|
23
|
+
|
|
24
|
+
from oscura.analyzers.statistics.basic import basic_stats
|
|
25
|
+
from oscura.core.types import DigitalTrace, WaveformTrace
|
|
26
|
+
|
|
27
|
+
if TYPE_CHECKING:
|
|
28
|
+
from numpy.typing import NDArray
|
|
29
|
+
|
|
30
|
+
SignalType = Literal["digital", "analog", "pwm", "uart", "spi", "i2c", "unknown"]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class SignalCharacterization:
|
|
35
|
+
"""Result of automatic signal characterization.
|
|
36
|
+
|
|
37
|
+
Contains detected signal type, confidence score, and extracted parameters.
|
|
38
|
+
|
|
39
|
+
Attributes:
|
|
40
|
+
signal_type: Detected signal type.
|
|
41
|
+
confidence: Confidence score (0.0-1.0).
|
|
42
|
+
voltage_low: Low voltage level in volts.
|
|
43
|
+
voltage_high: High voltage level in volts.
|
|
44
|
+
frequency_hz: Dominant frequency in Hz.
|
|
45
|
+
parameters: Additional signal-specific parameters.
|
|
46
|
+
quality_metrics: Signal quality measurements.
|
|
47
|
+
alternatives: Alternative signal type suggestions.
|
|
48
|
+
|
|
49
|
+
Example:
|
|
50
|
+
>>> result = characterize_signal(trace)
|
|
51
|
+
>>> if result.confidence >= 0.8:
|
|
52
|
+
... print(f"High confidence: {result.signal_type}")
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
signal_type: SignalType
|
|
56
|
+
confidence: float
|
|
57
|
+
voltage_low: float
|
|
58
|
+
voltage_high: float
|
|
59
|
+
frequency_hz: float
|
|
60
|
+
parameters: dict[str, Any] = field(default_factory=dict)
|
|
61
|
+
quality_metrics: dict[str, float] = field(default_factory=dict)
|
|
62
|
+
alternatives: list[tuple[SignalType, float]] = field(default_factory=list)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def characterize_signal(
|
|
66
|
+
trace: WaveformTrace | DigitalTrace,
|
|
67
|
+
*,
|
|
68
|
+
confidence_threshold: float = 0.6,
|
|
69
|
+
include_alternatives: bool = False,
|
|
70
|
+
min_alternatives: int = 3,
|
|
71
|
+
) -> SignalCharacterization:
|
|
72
|
+
"""Automatically characterize signal type and properties.
|
|
73
|
+
|
|
74
|
+
Analyzes waveform to detect signal type (digital, analog, PWM, UART, SPI, I2C)
|
|
75
|
+
and extract key parameters without requiring manual configuration.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
trace: Input waveform or digital trace.
|
|
79
|
+
confidence_threshold: Minimum confidence for primary detection (0.0-1.0).
|
|
80
|
+
include_alternatives: Whether to include alternative suggestions.
|
|
81
|
+
min_alternatives: Minimum number of alternatives when confidence is low.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
SignalCharacterization with detected type and parameters.
|
|
85
|
+
|
|
86
|
+
Raises:
|
|
87
|
+
ValueError: If trace is empty or invalid.
|
|
88
|
+
|
|
89
|
+
Example:
|
|
90
|
+
>>> result = characterize_signal(trace, confidence_threshold=0.8)
|
|
91
|
+
>>> print(f"Signal: {result.signal_type}")
|
|
92
|
+
>>> print(f"Confidence: {result.confidence:.2f}")
|
|
93
|
+
>>> print(f"Voltage: {result.voltage_low:.2f}V to {result.voltage_high:.2f}V")
|
|
94
|
+
Signal: UART
|
|
95
|
+
Confidence: 0.94
|
|
96
|
+
Voltage: 0.02V to 3.28V
|
|
97
|
+
|
|
98
|
+
References:
|
|
99
|
+
DISC-001: Automatic Signal Characterization
|
|
100
|
+
"""
|
|
101
|
+
# Validate input
|
|
102
|
+
if len(trace) == 0:
|
|
103
|
+
raise ValueError("Cannot characterize empty trace")
|
|
104
|
+
|
|
105
|
+
# Get signal data
|
|
106
|
+
if isinstance(trace, WaveformTrace):
|
|
107
|
+
data = trace.data
|
|
108
|
+
sample_rate = trace.metadata.sample_rate
|
|
109
|
+
is_analog = True
|
|
110
|
+
else:
|
|
111
|
+
data = trace.data.astype(np.float64)
|
|
112
|
+
sample_rate = trace.metadata.sample_rate
|
|
113
|
+
is_analog = False
|
|
114
|
+
|
|
115
|
+
# Compute basic statistics
|
|
116
|
+
stats = basic_stats(data)
|
|
117
|
+
|
|
118
|
+
# Determine voltage levels using percentiles to be robust to noise
|
|
119
|
+
# Use 5th and 95th percentiles to ignore outliers from noise
|
|
120
|
+
voltage_low = float(np.percentile(data, 5))
|
|
121
|
+
voltage_high = float(np.percentile(data, 95))
|
|
122
|
+
voltage_swing = voltage_high - voltage_low
|
|
123
|
+
|
|
124
|
+
# Analyze signal characteristics
|
|
125
|
+
candidates: dict[SignalType, float] = {}
|
|
126
|
+
|
|
127
|
+
# Check for digital signal (bimodal distribution)
|
|
128
|
+
digital_confidence = _detect_digital(data, voltage_swing)
|
|
129
|
+
candidates["digital"] = digital_confidence
|
|
130
|
+
|
|
131
|
+
# Check for analog signal (continuous distribution)
|
|
132
|
+
analog_confidence = _detect_analog(data, voltage_swing, is_analog)
|
|
133
|
+
candidates["analog"] = analog_confidence
|
|
134
|
+
|
|
135
|
+
# Check for PWM (periodic square wave with varying duty cycle)
|
|
136
|
+
pwm_confidence = _detect_pwm(data, sample_rate, voltage_swing)
|
|
137
|
+
candidates["pwm"] = pwm_confidence
|
|
138
|
+
|
|
139
|
+
# Check for UART (asynchronous serial with start/stop bits)
|
|
140
|
+
uart_confidence = _detect_uart(data, sample_rate, voltage_swing)
|
|
141
|
+
candidates["uart"] = uart_confidence
|
|
142
|
+
|
|
143
|
+
# Check for SPI (synchronous with clock and data)
|
|
144
|
+
spi_confidence = _detect_spi(data, sample_rate, voltage_swing)
|
|
145
|
+
candidates["spi"] = spi_confidence
|
|
146
|
+
|
|
147
|
+
# Check for I2C (two-wire with specific patterns)
|
|
148
|
+
i2c_confidence = _detect_i2c(data, sample_rate, voltage_swing)
|
|
149
|
+
candidates["i2c"] = i2c_confidence
|
|
150
|
+
|
|
151
|
+
# Select best match
|
|
152
|
+
sorted_candidates = sorted(candidates.items(), key=lambda x: x[1], reverse=True)
|
|
153
|
+
best_type, best_confidence = sorted_candidates[0]
|
|
154
|
+
|
|
155
|
+
# If confidence is too low, mark as unknown
|
|
156
|
+
if best_confidence < 0.5:
|
|
157
|
+
best_type = "unknown"
|
|
158
|
+
|
|
159
|
+
# If analog won but digital score is meaningful, prefer digital/unknown
|
|
160
|
+
# This handles noisy digital signals that look analog-ish
|
|
161
|
+
if best_type == "analog":
|
|
162
|
+
# Check if any protocol detector had reasonable confidence
|
|
163
|
+
protocol_confidence = max(
|
|
164
|
+
candidates.get("uart", 0),
|
|
165
|
+
candidates.get("spi", 0),
|
|
166
|
+
candidates.get("pwm", 0),
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
# If digital or protocol detectors have some confidence, don't call it purely analog
|
|
170
|
+
if digital_confidence > 0.3 or protocol_confidence > 0.2:
|
|
171
|
+
# Signal has digital characteristics - don't call it analog
|
|
172
|
+
if protocol_confidence > 0.3:
|
|
173
|
+
best_type = "unknown" # Too noisy/ambiguous to classify as specific protocol
|
|
174
|
+
best_confidence = protocol_confidence
|
|
175
|
+
elif digital_confidence > 0.4:
|
|
176
|
+
best_type = "digital" # Generic digital signal
|
|
177
|
+
best_confidence = digital_confidence
|
|
178
|
+
else:
|
|
179
|
+
best_type = "unknown" # Too ambiguous
|
|
180
|
+
best_confidence = max(digital_confidence, protocol_confidence, analog_confidence)
|
|
181
|
+
|
|
182
|
+
# Estimate dominant frequency
|
|
183
|
+
frequency_hz = _estimate_frequency(data, sample_rate)
|
|
184
|
+
|
|
185
|
+
# Extract type-specific parameters
|
|
186
|
+
parameters = _extract_parameters(best_type, data, sample_rate, voltage_low, voltage_high)
|
|
187
|
+
|
|
188
|
+
# Calculate quality metrics with improved noise estimation
|
|
189
|
+
noise_level = _estimate_noise_level(data, voltage_low, voltage_high, digital_confidence)
|
|
190
|
+
quality_metrics = {
|
|
191
|
+
"snr_db": _estimate_snr(data, stats),
|
|
192
|
+
"jitter_ns": _estimate_jitter(data, sample_rate) * 1e9,
|
|
193
|
+
"noise_level": noise_level,
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
# Prepare alternatives
|
|
197
|
+
alternatives: list[tuple[SignalType, float]] = []
|
|
198
|
+
if include_alternatives or best_confidence < confidence_threshold:
|
|
199
|
+
# Include top alternatives (excluding the winner)
|
|
200
|
+
for sig_type, conf in sorted_candidates[1:]:
|
|
201
|
+
if len(alternatives) >= min_alternatives:
|
|
202
|
+
break
|
|
203
|
+
if conf >= 0.3: # Only include reasonable alternatives
|
|
204
|
+
alternatives.append((sig_type, conf))
|
|
205
|
+
|
|
206
|
+
return SignalCharacterization(
|
|
207
|
+
signal_type=best_type,
|
|
208
|
+
confidence=round(best_confidence, 2),
|
|
209
|
+
voltage_low=voltage_low,
|
|
210
|
+
voltage_high=voltage_high,
|
|
211
|
+
frequency_hz=frequency_hz,
|
|
212
|
+
parameters=parameters,
|
|
213
|
+
quality_metrics=quality_metrics,
|
|
214
|
+
alternatives=alternatives,
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _estimate_noise_level(
|
|
219
|
+
data: NDArray[np.floating[Any]],
|
|
220
|
+
voltage_low: float,
|
|
221
|
+
voltage_high: float,
|
|
222
|
+
digital_confidence: float,
|
|
223
|
+
) -> float:
|
|
224
|
+
"""Estimate noise level in signal.
|
|
225
|
+
|
|
226
|
+
For digital signals, measures deviation from ideal logic levels.
|
|
227
|
+
For analog signals, uses normalized std.
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
data: Signal data array.
|
|
231
|
+
voltage_low: Low voltage level.
|
|
232
|
+
voltage_high: High voltage level.
|
|
233
|
+
digital_confidence: Confidence that signal is digital.
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
Noise level as fraction of voltage swing (0.0-1.0).
|
|
237
|
+
"""
|
|
238
|
+
voltage_swing = voltage_high - voltage_low
|
|
239
|
+
if voltage_swing == 0:
|
|
240
|
+
return 0.0
|
|
241
|
+
|
|
242
|
+
# For digital signals, estimate noise from deviation around logic levels
|
|
243
|
+
if digital_confidence >= 0.5:
|
|
244
|
+
threshold = (voltage_high + voltage_low) / 2
|
|
245
|
+
low_samples = data[data < threshold]
|
|
246
|
+
high_samples = data[data >= threshold]
|
|
247
|
+
|
|
248
|
+
noise_estimates = []
|
|
249
|
+
if len(low_samples) > 0:
|
|
250
|
+
# Deviation from the low level
|
|
251
|
+
low_level = np.min(data)
|
|
252
|
+
low_noise = np.std(low_samples - low_level)
|
|
253
|
+
noise_estimates.append(low_noise)
|
|
254
|
+
if len(high_samples) > 0:
|
|
255
|
+
# Deviation from the high level
|
|
256
|
+
high_level = np.max(data)
|
|
257
|
+
high_noise = np.std(high_samples - high_level)
|
|
258
|
+
noise_estimates.append(high_noise)
|
|
259
|
+
|
|
260
|
+
if noise_estimates:
|
|
261
|
+
avg_noise = np.mean(noise_estimates)
|
|
262
|
+
return float(avg_noise / voltage_swing)
|
|
263
|
+
|
|
264
|
+
# For analog signals, use std as fraction of range
|
|
265
|
+
# But cap it at 0.5 to indicate high variability, not noise
|
|
266
|
+
std_noise = float(np.std(data) / voltage_swing)
|
|
267
|
+
return min(0.5, std_noise)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _detect_digital(data: NDArray[np.floating[Any]], voltage_swing: float) -> float:
|
|
271
|
+
"""Detect digital signal characteristics.
|
|
272
|
+
|
|
273
|
+
Args:
|
|
274
|
+
data: Signal data array.
|
|
275
|
+
voltage_swing: Peak-to-peak voltage swing.
|
|
276
|
+
|
|
277
|
+
Returns:
|
|
278
|
+
Confidence score (0.0-1.0).
|
|
279
|
+
"""
|
|
280
|
+
if voltage_swing == 0:
|
|
281
|
+
return 0.0
|
|
282
|
+
|
|
283
|
+
# Check for bimodal distribution (two distinct levels)
|
|
284
|
+
hist, bin_edges = np.histogram(data, bins=50)
|
|
285
|
+
|
|
286
|
+
# Normalize histogram
|
|
287
|
+
hist = hist / np.sum(hist)
|
|
288
|
+
|
|
289
|
+
# Find peaks in histogram (should have 2 for digital)
|
|
290
|
+
peak_threshold = np.max(hist) * 0.3
|
|
291
|
+
peaks = np.where(hist > peak_threshold)[0]
|
|
292
|
+
|
|
293
|
+
if len(peaks) < 2:
|
|
294
|
+
return 0.3 # Low confidence
|
|
295
|
+
|
|
296
|
+
# Check if peaks are well separated
|
|
297
|
+
peak_separation = (bin_edges[peaks[-1]] - bin_edges[peaks[0]]) / voltage_swing
|
|
298
|
+
|
|
299
|
+
# Digital signals spend most time at rails
|
|
300
|
+
edge_bins = hist[:5].sum() + hist[-5:].sum()
|
|
301
|
+
|
|
302
|
+
# Combine factors
|
|
303
|
+
bimodal_score = min(1.0, len(peaks) / 2.0) # Closer to 2 peaks is better
|
|
304
|
+
separation_score = min(1.0, peak_separation)
|
|
305
|
+
rail_score = min(1.0, edge_bins * 2) # More time at rails is better
|
|
306
|
+
|
|
307
|
+
confidence = bimodal_score * 0.4 + separation_score * 0.3 + rail_score * 0.3
|
|
308
|
+
return min(0.95, confidence) # type: ignore[no-any-return]
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _detect_analog(data: NDArray[np.floating[Any]], voltage_swing: float, is_analog: bool) -> float:
|
|
312
|
+
"""Detect analog signal characteristics.
|
|
313
|
+
|
|
314
|
+
Args:
|
|
315
|
+
data: Signal data array.
|
|
316
|
+
voltage_swing: Peak-to-peak voltage swing.
|
|
317
|
+
is_analog: Whether input is from analog trace.
|
|
318
|
+
|
|
319
|
+
Returns:
|
|
320
|
+
Confidence score (0.0-1.0).
|
|
321
|
+
"""
|
|
322
|
+
if voltage_swing == 0:
|
|
323
|
+
return 0.0
|
|
324
|
+
|
|
325
|
+
# Check if signal has strong digital characteristics first
|
|
326
|
+
# If it does, this is NOT analog - reduce confidence significantly
|
|
327
|
+
digital_confidence = _detect_digital(data, voltage_swing)
|
|
328
|
+
if digital_confidence >= 0.6:
|
|
329
|
+
# Strong digital signal - very low analog confidence
|
|
330
|
+
return max(0.0, 0.4 - digital_confidence * 0.3)
|
|
331
|
+
|
|
332
|
+
# Analog signals have continuous distribution
|
|
333
|
+
hist, _ = np.histogram(data, bins=50)
|
|
334
|
+
hist = hist / np.sum(hist)
|
|
335
|
+
|
|
336
|
+
# Check for uniform or Gaussian-like distribution
|
|
337
|
+
uniform_score = 1.0 - np.std(hist)
|
|
338
|
+
|
|
339
|
+
# Check for smooth transitions (not many abrupt changes)
|
|
340
|
+
diff = np.diff(data)
|
|
341
|
+
smooth_score = 1.0 - min(1.0, np.mean(np.abs(diff)) / voltage_swing)
|
|
342
|
+
|
|
343
|
+
# Analog traces get boost
|
|
344
|
+
source_score = 1.0 if is_analog else 0.5 # Reduced from 0.7
|
|
345
|
+
|
|
346
|
+
confidence = uniform_score * 0.4 + smooth_score * 0.3 + source_score * 0.3
|
|
347
|
+
|
|
348
|
+
# Further reduce if there's any digital characteristics
|
|
349
|
+
if digital_confidence > 0.3:
|
|
350
|
+
confidence *= 1.0 - digital_confidence * 0.5
|
|
351
|
+
|
|
352
|
+
return min(0.9, confidence) # type: ignore[no-any-return]
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def _detect_pwm(
|
|
356
|
+
data: NDArray[np.floating[Any]],
|
|
357
|
+
sample_rate: float,
|
|
358
|
+
voltage_swing: float,
|
|
359
|
+
) -> float:
|
|
360
|
+
"""Detect PWM signal characteristics.
|
|
361
|
+
|
|
362
|
+
Args:
|
|
363
|
+
data: Signal data array.
|
|
364
|
+
sample_rate: Sample rate in Hz.
|
|
365
|
+
voltage_swing: Peak-to-peak voltage swing.
|
|
366
|
+
|
|
367
|
+
Returns:
|
|
368
|
+
Confidence score (0.0-1.0).
|
|
369
|
+
"""
|
|
370
|
+
if voltage_swing == 0 or len(data) < 100:
|
|
371
|
+
return 0.0
|
|
372
|
+
|
|
373
|
+
# PWM should have digital levels
|
|
374
|
+
digital_score = _detect_digital(data, voltage_swing)
|
|
375
|
+
|
|
376
|
+
if digital_score < 0.5:
|
|
377
|
+
return 0.0
|
|
378
|
+
|
|
379
|
+
# Threshold signal
|
|
380
|
+
threshold = (np.max(data) + np.min(data)) / 2
|
|
381
|
+
digital = data > threshold
|
|
382
|
+
|
|
383
|
+
# Find transitions
|
|
384
|
+
transitions = np.diff(digital.astype(int))
|
|
385
|
+
rising = np.where(transitions > 0)[0]
|
|
386
|
+
falling = np.where(transitions < 0)[0]
|
|
387
|
+
|
|
388
|
+
if len(rising) < 3 or len(falling) < 3:
|
|
389
|
+
return 0.0
|
|
390
|
+
|
|
391
|
+
# Check for periodic transitions
|
|
392
|
+
rising_periods = np.diff(rising)
|
|
393
|
+
period_std = np.std(rising_periods) if len(rising_periods) > 0 else 0
|
|
394
|
+
mean_period = np.mean(rising_periods) if len(rising_periods) > 0 else 1
|
|
395
|
+
|
|
396
|
+
periodicity_score = 1.0 - min(1.0, period_std / (mean_period + 1e-10))
|
|
397
|
+
|
|
398
|
+
# PWM should have varying duty cycle
|
|
399
|
+
duty_cycles = []
|
|
400
|
+
for i in range(min(len(rising), len(falling))):
|
|
401
|
+
if i < len(falling) and falling[i] > rising[i]:
|
|
402
|
+
duty = (falling[i] - rising[i]) / (mean_period + 1e-10)
|
|
403
|
+
duty_cycles.append(duty)
|
|
404
|
+
|
|
405
|
+
duty_variation = np.std(duty_cycles) if len(duty_cycles) > 1 else 0
|
|
406
|
+
variation_score = min(1.0, duty_variation * 5) # Some variation expected
|
|
407
|
+
|
|
408
|
+
confidence = digital_score * 0.3 + periodicity_score * 0.5 + variation_score * 0.2
|
|
409
|
+
|
|
410
|
+
# Boost if strong periodicity with variation (classic PWM signature)
|
|
411
|
+
if periodicity_score > 0.7 and variation_score > 0.3:
|
|
412
|
+
confidence = min(0.94, confidence * 1.1)
|
|
413
|
+
|
|
414
|
+
return min(0.94, confidence)
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def _detect_uart(
|
|
418
|
+
data: NDArray[np.floating[Any]], sample_rate: float, voltage_swing: float
|
|
419
|
+
) -> float:
|
|
420
|
+
"""Detect UART signal characteristics.
|
|
421
|
+
|
|
422
|
+
Args:
|
|
423
|
+
data: Signal data array.
|
|
424
|
+
sample_rate: Sample rate in Hz.
|
|
425
|
+
voltage_swing: Peak-to-peak voltage swing.
|
|
426
|
+
|
|
427
|
+
Returns:
|
|
428
|
+
Confidence score (0.0-1.0).
|
|
429
|
+
"""
|
|
430
|
+
if voltage_swing == 0 or len(data) < 200:
|
|
431
|
+
return 0.0
|
|
432
|
+
|
|
433
|
+
# UART should be digital
|
|
434
|
+
digital_score = _detect_digital(data, voltage_swing)
|
|
435
|
+
if digital_score < 0.7: # Strict threshold for UART
|
|
436
|
+
return 0.0
|
|
437
|
+
|
|
438
|
+
# Check for bimodal (two-level) distribution
|
|
439
|
+
# UART should have primarily two voltage levels, not continuous values
|
|
440
|
+
hist, _ = np.histogram(data, bins=50)
|
|
441
|
+
hist = hist / np.sum(hist)
|
|
442
|
+
|
|
443
|
+
# Count significant histogram bins (>5% of samples)
|
|
444
|
+
significant_bins = np.sum(hist > 0.05)
|
|
445
|
+
|
|
446
|
+
# UART should have at most 2-4 significant bins (low and high with some noise)
|
|
447
|
+
# Sine wave will have many bins
|
|
448
|
+
if significant_bins > 6:
|
|
449
|
+
return 0.0
|
|
450
|
+
|
|
451
|
+
# Threshold signal
|
|
452
|
+
threshold = (np.max(data) + np.min(data)) / 2
|
|
453
|
+
digital = data > threshold
|
|
454
|
+
|
|
455
|
+
# Find edges
|
|
456
|
+
transitions = np.diff(digital.astype(int))
|
|
457
|
+
edges = np.where(np.abs(transitions) > 0)[0]
|
|
458
|
+
|
|
459
|
+
if len(edges) < 10:
|
|
460
|
+
return 0.0
|
|
461
|
+
|
|
462
|
+
# UART has consistent bit timing
|
|
463
|
+
edge_intervals = np.diff(edges)
|
|
464
|
+
|
|
465
|
+
# Look for common baud rates
|
|
466
|
+
common_bauds = [9600, 19200, 38400, 57600, 115200]
|
|
467
|
+
baud_scores = []
|
|
468
|
+
|
|
469
|
+
for baud in common_bauds:
|
|
470
|
+
bit_period_samples = sample_rate / baud
|
|
471
|
+
# Count edges that align with this baud rate (stricter alignment)
|
|
472
|
+
aligned = np.sum(np.abs(edge_intervals % bit_period_samples) < bit_period_samples * 0.15)
|
|
473
|
+
baud_scores.append(aligned / len(edge_intervals))
|
|
474
|
+
|
|
475
|
+
timing_score = max(baud_scores) if baud_scores else 0.0
|
|
476
|
+
|
|
477
|
+
# UART requires strong timing alignment
|
|
478
|
+
if timing_score < 0.4:
|
|
479
|
+
return 0.0
|
|
480
|
+
|
|
481
|
+
# UART idles high typically
|
|
482
|
+
idle_score = np.mean(digital[-100:])
|
|
483
|
+
|
|
484
|
+
confidence = digital_score * 0.3 + timing_score * 0.6 + idle_score * 0.1
|
|
485
|
+
|
|
486
|
+
# Boost confidence if timing alignment is strong
|
|
487
|
+
if timing_score > 0.7:
|
|
488
|
+
confidence = min(0.96, confidence * 1.1)
|
|
489
|
+
|
|
490
|
+
return min(0.96, confidence) # type: ignore[no-any-return]
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def _detect_spi(
|
|
494
|
+
data: NDArray[np.floating[Any]],
|
|
495
|
+
sample_rate: float,
|
|
496
|
+
voltage_swing: float,
|
|
497
|
+
) -> float:
|
|
498
|
+
"""Detect SPI signal characteristics.
|
|
499
|
+
|
|
500
|
+
Args:
|
|
501
|
+
data: Signal data array.
|
|
502
|
+
sample_rate: Sample rate in Hz.
|
|
503
|
+
voltage_swing: Peak-to-peak voltage swing.
|
|
504
|
+
|
|
505
|
+
Returns:
|
|
506
|
+
Confidence score (0.0-1.0).
|
|
507
|
+
"""
|
|
508
|
+
if voltage_swing == 0 or len(data) < 200:
|
|
509
|
+
return 0.0
|
|
510
|
+
|
|
511
|
+
# SPI should be digital
|
|
512
|
+
digital_score = _detect_digital(data, voltage_swing)
|
|
513
|
+
if digital_score < 0.6:
|
|
514
|
+
return 0.0
|
|
515
|
+
|
|
516
|
+
# Threshold signal
|
|
517
|
+
threshold = (np.max(data) + np.min(data)) / 2
|
|
518
|
+
digital = data > threshold
|
|
519
|
+
|
|
520
|
+
# Find edges
|
|
521
|
+
transitions = np.diff(digital.astype(int))
|
|
522
|
+
edges = np.where(np.abs(transitions) > 0)[0]
|
|
523
|
+
|
|
524
|
+
if len(edges) < 20:
|
|
525
|
+
return 0.0
|
|
526
|
+
|
|
527
|
+
# SPI typically has bursts of regular clock transitions
|
|
528
|
+
edge_intervals = np.diff(edges)
|
|
529
|
+
|
|
530
|
+
# Check for consistent clock period
|
|
531
|
+
median_interval = np.median(edge_intervals)
|
|
532
|
+
interval_std = np.std(edge_intervals)
|
|
533
|
+
consistency_score = 1.0 - min(1.0, interval_std / (median_interval + 1e-10))
|
|
534
|
+
|
|
535
|
+
# SPI has many transitions (clock toggling)
|
|
536
|
+
transition_density = len(edges) / len(data)
|
|
537
|
+
density_score = min(1.0, transition_density * 20)
|
|
538
|
+
|
|
539
|
+
confidence = digital_score * 0.3 + consistency_score * 0.5 + density_score * 0.2
|
|
540
|
+
|
|
541
|
+
# Boost confidence if consistency is very high (strong clock signal)
|
|
542
|
+
if consistency_score > 0.8 and density_score > 0.5:
|
|
543
|
+
confidence = min(0.95, confidence * 1.15)
|
|
544
|
+
|
|
545
|
+
return min(0.95, confidence) # type: ignore[no-any-return]
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
def _detect_i2c(
|
|
549
|
+
data: NDArray[np.floating[Any]],
|
|
550
|
+
sample_rate: float,
|
|
551
|
+
voltage_swing: float,
|
|
552
|
+
) -> float:
|
|
553
|
+
"""Detect I2C signal characteristics.
|
|
554
|
+
|
|
555
|
+
Args:
|
|
556
|
+
data: Signal data array.
|
|
557
|
+
sample_rate: Sample rate in Hz.
|
|
558
|
+
voltage_swing: Peak-to-peak voltage swing.
|
|
559
|
+
|
|
560
|
+
Returns:
|
|
561
|
+
Confidence score (0.0-1.0).
|
|
562
|
+
"""
|
|
563
|
+
# I2C detection requires both SDA and SCL, single channel is limited
|
|
564
|
+
# This is a placeholder that gives low confidence
|
|
565
|
+
digital_score = _detect_digital(data, voltage_swing)
|
|
566
|
+
return min(0.6, digital_score * 0.5)
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
def _estimate_frequency(data: NDArray[np.floating[Any]], sample_rate: float) -> float:
|
|
570
|
+
"""Estimate dominant frequency in signal.
|
|
571
|
+
|
|
572
|
+
Args:
|
|
573
|
+
data: Signal data array.
|
|
574
|
+
sample_rate: Sample rate in Hz.
|
|
575
|
+
|
|
576
|
+
Returns:
|
|
577
|
+
Dominant frequency in Hz.
|
|
578
|
+
"""
|
|
579
|
+
if len(data) < 10:
|
|
580
|
+
return 0.0
|
|
581
|
+
|
|
582
|
+
# Simple zero-crossing based frequency estimate
|
|
583
|
+
mean_val = np.mean(data)
|
|
584
|
+
crossings = np.where(np.diff(np.sign(data - mean_val)) != 0)[0]
|
|
585
|
+
|
|
586
|
+
if len(crossings) < 2:
|
|
587
|
+
return 0.0
|
|
588
|
+
|
|
589
|
+
# Average period between crossings (half periods)
|
|
590
|
+
avg_half_period = np.mean(np.diff(crossings))
|
|
591
|
+
period_samples = avg_half_period * 2
|
|
592
|
+
|
|
593
|
+
frequency = sample_rate / period_samples if period_samples > 0 else 0.0
|
|
594
|
+
return frequency
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
def _estimate_snr(data: NDArray[np.floating[Any]], stats: dict[str, float]) -> float:
|
|
598
|
+
"""Estimate signal-to-noise ratio.
|
|
599
|
+
|
|
600
|
+
Args:
|
|
601
|
+
data: Signal data array.
|
|
602
|
+
stats: Basic statistics dictionary.
|
|
603
|
+
|
|
604
|
+
Returns:
|
|
605
|
+
Estimated SNR in dB.
|
|
606
|
+
"""
|
|
607
|
+
signal_power = stats["mean"] ** 2
|
|
608
|
+
noise_power = stats["variance"]
|
|
609
|
+
|
|
610
|
+
if noise_power == 0:
|
|
611
|
+
return 100.0 # Very high SNR
|
|
612
|
+
|
|
613
|
+
snr = signal_power / noise_power
|
|
614
|
+
snr_db = 10 * np.log10(snr) if snr > 0 else 0.0
|
|
615
|
+
|
|
616
|
+
return max(0.0, min(100.0, snr_db))
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
def _estimate_jitter(data: NDArray[np.floating[Any]], sample_rate: float) -> float:
|
|
620
|
+
"""Estimate timing jitter.
|
|
621
|
+
|
|
622
|
+
Args:
|
|
623
|
+
data: Signal data array.
|
|
624
|
+
sample_rate: Sample rate in Hz.
|
|
625
|
+
|
|
626
|
+
Returns:
|
|
627
|
+
Estimated jitter in seconds.
|
|
628
|
+
"""
|
|
629
|
+
# Simple edge-to-edge jitter estimate
|
|
630
|
+
threshold = (np.max(data) + np.min(data)) / 2
|
|
631
|
+
digital = data > threshold
|
|
632
|
+
edges = np.where(np.diff(digital.astype(int)) != 0)[0]
|
|
633
|
+
|
|
634
|
+
if len(edges) < 3:
|
|
635
|
+
return 0.0
|
|
636
|
+
|
|
637
|
+
edge_intervals = np.diff(edges)
|
|
638
|
+
jitter_samples = np.std(edge_intervals)
|
|
639
|
+
jitter_seconds = jitter_samples / sample_rate
|
|
640
|
+
|
|
641
|
+
return jitter_seconds # type: ignore[no-any-return]
|
|
642
|
+
|
|
643
|
+
|
|
644
|
+
def _extract_parameters(
|
|
645
|
+
signal_type: SignalType,
|
|
646
|
+
data: NDArray[np.floating[Any]],
|
|
647
|
+
sample_rate: float,
|
|
648
|
+
voltage_low: float,
|
|
649
|
+
voltage_high: float,
|
|
650
|
+
) -> dict[str, Any]:
|
|
651
|
+
"""Extract signal-specific parameters.
|
|
652
|
+
|
|
653
|
+
Args:
|
|
654
|
+
signal_type: Detected signal type.
|
|
655
|
+
data: Signal data array.
|
|
656
|
+
sample_rate: Sample rate in Hz.
|
|
657
|
+
voltage_low: Low voltage level.
|
|
658
|
+
voltage_high: High voltage level.
|
|
659
|
+
|
|
660
|
+
Returns:
|
|
661
|
+
Dictionary of parameters specific to signal type.
|
|
662
|
+
"""
|
|
663
|
+
params: dict[str, Any] = {}
|
|
664
|
+
|
|
665
|
+
if signal_type in ("digital", "uart", "spi", "i2c"):
|
|
666
|
+
# Add logic level parameters
|
|
667
|
+
logic_family = _guess_logic_family(voltage_low, voltage_high)
|
|
668
|
+
if logic_family != "Unknown":
|
|
669
|
+
params["logic_family"] = logic_family
|
|
670
|
+
|
|
671
|
+
if signal_type == "pwm":
|
|
672
|
+
# Calculate duty cycle
|
|
673
|
+
threshold = (voltage_high + voltage_low) / 2
|
|
674
|
+
digital = data > threshold
|
|
675
|
+
duty_cycle = np.mean(digital)
|
|
676
|
+
params["duty_cycle"] = round(duty_cycle, 3)
|
|
677
|
+
|
|
678
|
+
if signal_type == "uart":
|
|
679
|
+
# Estimate baud rate
|
|
680
|
+
params["estimated_baud"] = _estimate_baud_rate(data, sample_rate)
|
|
681
|
+
|
|
682
|
+
return params
|
|
683
|
+
|
|
684
|
+
|
|
685
|
+
def _guess_logic_family(voltage_low: float, voltage_high: float) -> str:
|
|
686
|
+
"""Guess logic family from voltage levels.
|
|
687
|
+
|
|
688
|
+
Args:
|
|
689
|
+
voltage_low: Low voltage level in volts.
|
|
690
|
+
voltage_high: High voltage level in volts.
|
|
691
|
+
|
|
692
|
+
Returns:
|
|
693
|
+
Logic family name.
|
|
694
|
+
"""
|
|
695
|
+
voltage_swing = voltage_high - voltage_low
|
|
696
|
+
|
|
697
|
+
# Match to closest standard voltage level
|
|
698
|
+
# This handles noise better than fixed ranges
|
|
699
|
+
standard_levels = [
|
|
700
|
+
(1.8, "1.8V LVCMOS"),
|
|
701
|
+
(3.3, "3.3V LVCMOS"),
|
|
702
|
+
(5.0, "5V TTL/CMOS"),
|
|
703
|
+
]
|
|
704
|
+
|
|
705
|
+
# Find closest match
|
|
706
|
+
closest_diff = float("inf")
|
|
707
|
+
second_closest_diff = float("inf")
|
|
708
|
+
closest_family = "Unknown"
|
|
709
|
+
closest_level = 0.0
|
|
710
|
+
|
|
711
|
+
for level, family in standard_levels:
|
|
712
|
+
diff = abs(voltage_swing - level)
|
|
713
|
+
if diff < closest_diff:
|
|
714
|
+
second_closest_diff = closest_diff
|
|
715
|
+
closest_diff = diff
|
|
716
|
+
closest_family = family
|
|
717
|
+
closest_level = level
|
|
718
|
+
elif diff < second_closest_diff:
|
|
719
|
+
second_closest_diff = diff
|
|
720
|
+
|
|
721
|
+
# Only return a match if:
|
|
722
|
+
# 1. Closest match is within 50% tolerance
|
|
723
|
+
# 2. AND it's significantly closer than second-best (not ambiguous)
|
|
724
|
+
if closest_diff == float("inf") or closest_diff > closest_level * 0.5:
|
|
725
|
+
return "Unknown"
|
|
726
|
+
|
|
727
|
+
# Check if ambiguous (second closest is also pretty close)
|
|
728
|
+
# If second-best is within 20% more distance, it's too ambiguous
|
|
729
|
+
if second_closest_diff < closest_diff * 1.2:
|
|
730
|
+
return "Unknown" # Too ambiguous
|
|
731
|
+
|
|
732
|
+
return closest_family
|
|
733
|
+
|
|
734
|
+
|
|
735
|
+
def _estimate_baud_rate(data: NDArray[np.floating[Any]], sample_rate: float) -> int:
|
|
736
|
+
"""Estimate UART baud rate.
|
|
737
|
+
|
|
738
|
+
Args:
|
|
739
|
+
data: Signal data array.
|
|
740
|
+
sample_rate: Sample rate in Hz.
|
|
741
|
+
|
|
742
|
+
Returns:
|
|
743
|
+
Estimated baud rate in bps.
|
|
744
|
+
"""
|
|
745
|
+
# Find bit period from edge intervals
|
|
746
|
+
threshold = (np.max(data) + np.min(data)) / 2
|
|
747
|
+
digital = data > threshold
|
|
748
|
+
edges = np.where(np.diff(digital.astype(int)) != 0)[0]
|
|
749
|
+
|
|
750
|
+
if len(edges) < 10:
|
|
751
|
+
return 9600 # Default fallback
|
|
752
|
+
|
|
753
|
+
edge_intervals = np.diff(edges)
|
|
754
|
+
# Use median to be robust to outliers
|
|
755
|
+
median_interval = np.median(edge_intervals)
|
|
756
|
+
estimated_baud = int(sample_rate / median_interval)
|
|
757
|
+
|
|
758
|
+
# Snap to common baud rates
|
|
759
|
+
common_bauds = [9600, 19200, 38400, 57600, 115200, 230400, 460800, 921600]
|
|
760
|
+
closest_baud = min(common_bauds, key=lambda x: abs(x - estimated_baud))
|
|
761
|
+
|
|
762
|
+
return closest_baud
|
|
763
|
+
|
|
764
|
+
|
|
765
|
+
__all__ = [
|
|
766
|
+
"SignalCharacterization",
|
|
767
|
+
"SignalType",
|
|
768
|
+
"characterize_signal",
|
|
769
|
+
]
|