oscura 0.5.0__py3-none-any.whl → 0.6.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 +169 -167
- oscura/analyzers/__init__.py +3 -0
- oscura/analyzers/classification.py +659 -0
- oscura/analyzers/digital/__init__.py +0 -48
- oscura/analyzers/digital/edges.py +325 -65
- oscura/analyzers/digital/extraction.py +0 -195
- oscura/analyzers/digital/quality.py +293 -166
- oscura/analyzers/digital/timing.py +260 -115
- oscura/analyzers/digital/timing_numba.py +334 -0
- oscura/analyzers/entropy.py +605 -0
- oscura/analyzers/eye/diagram.py +176 -109
- oscura/analyzers/eye/metrics.py +5 -5
- oscura/analyzers/jitter/__init__.py +6 -4
- oscura/analyzers/jitter/ber.py +52 -52
- oscura/analyzers/jitter/classification.py +156 -0
- oscura/analyzers/jitter/decomposition.py +163 -113
- oscura/analyzers/jitter/spectrum.py +80 -64
- oscura/analyzers/ml/__init__.py +39 -0
- oscura/analyzers/ml/features.py +600 -0
- oscura/analyzers/ml/signal_classifier.py +604 -0
- oscura/analyzers/packet/daq.py +246 -158
- oscura/analyzers/packet/parser.py +12 -1
- oscura/analyzers/packet/payload.py +50 -2110
- oscura/analyzers/packet/payload_analysis.py +361 -181
- oscura/analyzers/packet/payload_patterns.py +133 -70
- oscura/analyzers/packet/stream.py +84 -23
- oscura/analyzers/patterns/__init__.py +26 -5
- oscura/analyzers/patterns/anomaly_detection.py +908 -0
- oscura/analyzers/patterns/clustering.py +169 -108
- oscura/analyzers/patterns/clustering_optimized.py +227 -0
- oscura/analyzers/patterns/discovery.py +1 -1
- oscura/analyzers/patterns/matching.py +581 -197
- oscura/analyzers/patterns/pattern_mining.py +778 -0
- oscura/analyzers/patterns/periodic.py +121 -38
- oscura/analyzers/patterns/sequences.py +175 -78
- oscura/analyzers/power/conduction.py +1 -1
- oscura/analyzers/power/soa.py +6 -6
- oscura/analyzers/power/switching.py +250 -110
- oscura/analyzers/protocol/__init__.py +17 -1
- oscura/analyzers/protocols/__init__.py +1 -22
- oscura/analyzers/protocols/base.py +6 -6
- oscura/analyzers/protocols/ble/__init__.py +38 -0
- oscura/analyzers/protocols/ble/analyzer.py +809 -0
- oscura/analyzers/protocols/ble/uuids.py +288 -0
- oscura/analyzers/protocols/can.py +257 -127
- oscura/analyzers/protocols/can_fd.py +107 -80
- oscura/analyzers/protocols/flexray.py +139 -80
- oscura/analyzers/protocols/hdlc.py +93 -58
- oscura/analyzers/protocols/i2c.py +247 -106
- oscura/analyzers/protocols/i2s.py +138 -86
- oscura/analyzers/protocols/industrial/__init__.py +40 -0
- oscura/analyzers/protocols/industrial/bacnet/__init__.py +33 -0
- oscura/analyzers/protocols/industrial/bacnet/analyzer.py +708 -0
- oscura/analyzers/protocols/industrial/bacnet/encoding.py +412 -0
- oscura/analyzers/protocols/industrial/bacnet/services.py +622 -0
- oscura/analyzers/protocols/industrial/ethercat/__init__.py +30 -0
- oscura/analyzers/protocols/industrial/ethercat/analyzer.py +474 -0
- oscura/analyzers/protocols/industrial/ethercat/mailbox.py +339 -0
- oscura/analyzers/protocols/industrial/ethercat/topology.py +166 -0
- oscura/analyzers/protocols/industrial/modbus/__init__.py +31 -0
- oscura/analyzers/protocols/industrial/modbus/analyzer.py +525 -0
- oscura/analyzers/protocols/industrial/modbus/crc.py +79 -0
- oscura/analyzers/protocols/industrial/modbus/functions.py +436 -0
- oscura/analyzers/protocols/industrial/opcua/__init__.py +21 -0
- oscura/analyzers/protocols/industrial/opcua/analyzer.py +552 -0
- oscura/analyzers/protocols/industrial/opcua/datatypes.py +446 -0
- oscura/analyzers/protocols/industrial/opcua/services.py +264 -0
- oscura/analyzers/protocols/industrial/profinet/__init__.py +23 -0
- oscura/analyzers/protocols/industrial/profinet/analyzer.py +441 -0
- oscura/analyzers/protocols/industrial/profinet/dcp.py +263 -0
- oscura/analyzers/protocols/industrial/profinet/ptcp.py +200 -0
- oscura/analyzers/protocols/jtag.py +180 -98
- oscura/analyzers/protocols/lin.py +219 -114
- oscura/analyzers/protocols/manchester.py +4 -4
- oscura/analyzers/protocols/onewire.py +253 -149
- oscura/analyzers/protocols/parallel_bus/__init__.py +20 -0
- oscura/analyzers/protocols/parallel_bus/centronics.py +92 -0
- oscura/analyzers/protocols/parallel_bus/gpib.py +137 -0
- oscura/analyzers/protocols/spi.py +192 -95
- oscura/analyzers/protocols/swd.py +321 -167
- oscura/analyzers/protocols/uart.py +267 -125
- oscura/analyzers/protocols/usb.py +235 -131
- oscura/analyzers/side_channel/power.py +17 -12
- oscura/analyzers/signal/__init__.py +15 -0
- oscura/analyzers/signal/timing_analysis.py +1086 -0
- oscura/analyzers/signal_integrity/__init__.py +4 -1
- oscura/analyzers/signal_integrity/sparams.py +2 -19
- oscura/analyzers/spectral/chunked.py +129 -60
- oscura/analyzers/spectral/chunked_fft.py +300 -94
- oscura/analyzers/spectral/chunked_wavelet.py +100 -80
- oscura/analyzers/statistical/checksum.py +376 -217
- oscura/analyzers/statistical/classification.py +229 -107
- oscura/analyzers/statistical/entropy.py +78 -53
- oscura/analyzers/statistics/correlation.py +407 -211
- oscura/analyzers/statistics/outliers.py +2 -2
- oscura/analyzers/statistics/streaming.py +30 -5
- oscura/analyzers/validation.py +216 -101
- oscura/analyzers/waveform/measurements.py +9 -0
- oscura/analyzers/waveform/measurements_with_uncertainty.py +31 -15
- oscura/analyzers/waveform/spectral.py +500 -228
- oscura/api/__init__.py +31 -5
- oscura/api/dsl/__init__.py +582 -0
- oscura/{dsl → api/dsl}/commands.py +43 -76
- oscura/{dsl → api/dsl}/interpreter.py +26 -51
- oscura/{dsl → api/dsl}/parser.py +107 -77
- oscura/{dsl → api/dsl}/repl.py +2 -2
- oscura/api/dsl.py +1 -1
- oscura/{integrations → api/integrations}/__init__.py +1 -1
- oscura/{integrations → api/integrations}/llm.py +201 -102
- oscura/api/operators.py +3 -3
- oscura/api/optimization.py +144 -30
- oscura/api/rest_server.py +921 -0
- oscura/api/server/__init__.py +17 -0
- oscura/api/server/dashboard.py +850 -0
- oscura/api/server/static/README.md +34 -0
- oscura/api/server/templates/base.html +181 -0
- oscura/api/server/templates/export.html +120 -0
- oscura/api/server/templates/home.html +284 -0
- oscura/api/server/templates/protocols.html +58 -0
- oscura/api/server/templates/reports.html +43 -0
- oscura/api/server/templates/session_detail.html +89 -0
- oscura/api/server/templates/sessions.html +83 -0
- oscura/api/server/templates/waveforms.html +73 -0
- oscura/automotive/__init__.py +8 -1
- oscura/automotive/can/__init__.py +10 -0
- oscura/automotive/can/checksum.py +3 -1
- oscura/automotive/can/dbc_generator.py +590 -0
- oscura/automotive/can/message_wrapper.py +121 -74
- oscura/automotive/can/patterns.py +98 -21
- oscura/automotive/can/session.py +292 -56
- oscura/automotive/can/state_machine.py +6 -3
- oscura/automotive/can/stimulus_response.py +97 -75
- oscura/automotive/dbc/__init__.py +10 -2
- oscura/automotive/dbc/generator.py +84 -56
- oscura/automotive/dbc/parser.py +6 -6
- oscura/automotive/dtc/data.json +2763 -0
- oscura/automotive/dtc/database.py +2 -2
- oscura/automotive/flexray/__init__.py +31 -0
- oscura/automotive/flexray/analyzer.py +504 -0
- oscura/automotive/flexray/crc.py +185 -0
- oscura/automotive/flexray/fibex.py +449 -0
- oscura/automotive/j1939/__init__.py +45 -8
- oscura/automotive/j1939/analyzer.py +605 -0
- oscura/automotive/j1939/spns.py +326 -0
- oscura/automotive/j1939/transport.py +306 -0
- oscura/automotive/lin/__init__.py +47 -0
- oscura/automotive/lin/analyzer.py +612 -0
- oscura/automotive/loaders/blf.py +13 -2
- oscura/automotive/loaders/csv_can.py +143 -72
- oscura/automotive/loaders/dispatcher.py +50 -2
- oscura/automotive/loaders/mdf.py +86 -45
- oscura/automotive/loaders/pcap.py +111 -61
- oscura/automotive/uds/__init__.py +4 -0
- oscura/automotive/uds/analyzer.py +725 -0
- oscura/automotive/uds/decoder.py +140 -58
- oscura/automotive/uds/models.py +7 -1
- oscura/automotive/visualization.py +1 -1
- oscura/cli/analyze.py +348 -0
- oscura/cli/batch.py +142 -122
- oscura/cli/benchmark.py +275 -0
- oscura/cli/characterize.py +137 -82
- oscura/cli/compare.py +224 -131
- oscura/cli/completion.py +250 -0
- oscura/cli/config_cmd.py +361 -0
- oscura/cli/decode.py +164 -87
- oscura/cli/export.py +286 -0
- oscura/cli/main.py +115 -31
- oscura/{onboarding → cli/onboarding}/__init__.py +3 -3
- oscura/{onboarding → cli/onboarding}/help.py +80 -58
- oscura/{onboarding → cli/onboarding}/tutorials.py +97 -72
- oscura/{onboarding → cli/onboarding}/wizard.py +55 -36
- oscura/cli/progress.py +147 -0
- oscura/cli/shell.py +157 -135
- oscura/cli/validate_cmd.py +204 -0
- oscura/cli/visualize.py +158 -0
- oscura/convenience.py +125 -79
- oscura/core/__init__.py +4 -2
- oscura/core/backend_selector.py +3 -3
- oscura/core/cache.py +126 -15
- oscura/core/cancellation.py +1 -1
- oscura/{config → core/config}/__init__.py +20 -11
- oscura/{config → core/config}/defaults.py +1 -1
- oscura/{config → core/config}/loader.py +7 -5
- oscura/{config → core/config}/memory.py +5 -5
- oscura/{config → core/config}/migration.py +1 -1
- oscura/{config → core/config}/pipeline.py +99 -23
- oscura/{config → core/config}/preferences.py +1 -1
- oscura/{config → core/config}/protocol.py +3 -3
- oscura/{config → core/config}/schema.py +426 -272
- oscura/{config → core/config}/settings.py +1 -1
- oscura/{config → core/config}/thresholds.py +195 -153
- oscura/core/correlation.py +5 -6
- oscura/core/cross_domain.py +0 -2
- oscura/core/debug.py +9 -5
- oscura/{extensibility → core/extensibility}/docs.py +158 -70
- oscura/{extensibility → core/extensibility}/extensions.py +160 -76
- oscura/{extensibility → core/extensibility}/logging.py +1 -1
- oscura/{extensibility → core/extensibility}/measurements.py +1 -1
- oscura/{extensibility → core/extensibility}/plugins.py +1 -1
- oscura/{extensibility → core/extensibility}/templates.py +73 -3
- oscura/{extensibility → core/extensibility}/validation.py +1 -1
- oscura/core/gpu_backend.py +11 -7
- oscura/core/log_query.py +101 -11
- oscura/core/logging.py +126 -54
- oscura/core/logging_advanced.py +5 -5
- oscura/core/memory_limits.py +108 -70
- oscura/core/memory_monitor.py +2 -2
- oscura/core/memory_progress.py +7 -7
- oscura/core/memory_warnings.py +1 -1
- oscura/core/numba_backend.py +13 -13
- oscura/{plugins → core/plugins}/__init__.py +9 -9
- oscura/{plugins → core/plugins}/base.py +7 -7
- oscura/{plugins → core/plugins}/cli.py +3 -3
- oscura/{plugins → core/plugins}/discovery.py +186 -106
- oscura/{plugins → core/plugins}/lifecycle.py +1 -1
- oscura/{plugins → core/plugins}/manager.py +7 -7
- oscura/{plugins → core/plugins}/registry.py +3 -3
- oscura/{plugins → core/plugins}/versioning.py +1 -1
- oscura/core/progress.py +16 -1
- oscura/core/provenance.py +8 -2
- oscura/{schemas → core/schemas}/__init__.py +2 -2
- oscura/core/schemas/bus_configuration.json +322 -0
- oscura/core/schemas/device_mapping.json +182 -0
- oscura/core/schemas/packet_format.json +418 -0
- oscura/core/schemas/protocol_definition.json +363 -0
- oscura/core/types.py +4 -0
- oscura/core/uncertainty.py +3 -3
- oscura/correlation/__init__.py +52 -0
- oscura/correlation/multi_protocol.py +811 -0
- oscura/discovery/auto_decoder.py +117 -35
- oscura/discovery/comparison.py +191 -86
- oscura/discovery/quality_validator.py +155 -68
- oscura/discovery/signal_detector.py +196 -79
- oscura/export/__init__.py +18 -20
- oscura/export/kaitai_struct.py +513 -0
- oscura/export/scapy_layer.py +801 -0
- oscura/export/wireshark/README.md +15 -15
- oscura/export/wireshark/generator.py +1 -1
- oscura/export/wireshark/templates/dissector.lua.j2 +2 -2
- oscura/export/wireshark_dissector.py +746 -0
- oscura/guidance/wizard.py +207 -111
- oscura/hardware/__init__.py +19 -0
- oscura/{acquisition → hardware/acquisition}/__init__.py +4 -4
- oscura/{acquisition → hardware/acquisition}/file.py +2 -2
- oscura/{acquisition → hardware/acquisition}/hardware.py +7 -7
- oscura/{acquisition → hardware/acquisition}/saleae.py +15 -12
- oscura/{acquisition → hardware/acquisition}/socketcan.py +1 -1
- oscura/{acquisition → hardware/acquisition}/streaming.py +2 -2
- oscura/{acquisition → hardware/acquisition}/synthetic.py +3 -3
- oscura/{acquisition → hardware/acquisition}/visa.py +33 -11
- oscura/hardware/firmware/__init__.py +29 -0
- oscura/hardware/firmware/pattern_recognition.py +874 -0
- oscura/hardware/hal_detector.py +736 -0
- oscura/hardware/security/__init__.py +37 -0
- oscura/hardware/security/side_channel_detector.py +1126 -0
- oscura/inference/__init__.py +4 -0
- oscura/inference/active_learning/README.md +7 -7
- oscura/inference/active_learning/observation_table.py +4 -1
- oscura/inference/alignment.py +216 -123
- oscura/inference/bayesian.py +113 -33
- oscura/inference/crc_reverse.py +101 -55
- oscura/inference/logic.py +6 -2
- oscura/inference/message_format.py +342 -183
- oscura/inference/protocol.py +95 -44
- oscura/inference/protocol_dsl.py +180 -82
- oscura/inference/signal_intelligence.py +1439 -706
- oscura/inference/spectral.py +99 -57
- oscura/inference/state_machine.py +810 -158
- oscura/inference/stream.py +270 -110
- oscura/iot/__init__.py +34 -0
- oscura/iot/coap/__init__.py +32 -0
- oscura/iot/coap/analyzer.py +668 -0
- oscura/iot/coap/options.py +212 -0
- oscura/iot/lorawan/__init__.py +21 -0
- oscura/iot/lorawan/crypto.py +206 -0
- oscura/iot/lorawan/decoder.py +801 -0
- oscura/iot/lorawan/mac_commands.py +341 -0
- oscura/iot/mqtt/__init__.py +27 -0
- oscura/iot/mqtt/analyzer.py +999 -0
- oscura/iot/mqtt/properties.py +315 -0
- oscura/iot/zigbee/__init__.py +31 -0
- oscura/iot/zigbee/analyzer.py +615 -0
- oscura/iot/zigbee/security.py +153 -0
- oscura/iot/zigbee/zcl.py +349 -0
- oscura/jupyter/display.py +125 -45
- oscura/{exploratory → jupyter/exploratory}/__init__.py +8 -8
- oscura/{exploratory → jupyter/exploratory}/error_recovery.py +298 -141
- oscura/jupyter/exploratory/fuzzy.py +746 -0
- oscura/{exploratory → jupyter/exploratory}/fuzzy_advanced.py +258 -100
- oscura/{exploratory → jupyter/exploratory}/legacy.py +464 -242
- oscura/{exploratory → jupyter/exploratory}/parse.py +167 -145
- oscura/{exploratory → jupyter/exploratory}/recovery.py +119 -87
- oscura/jupyter/exploratory/sync.py +612 -0
- oscura/{exploratory → jupyter/exploratory}/unknown.py +299 -176
- oscura/jupyter/magic.py +4 -4
- oscura/{ui → jupyter/ui}/__init__.py +2 -2
- oscura/{ui → jupyter/ui}/formatters.py +3 -3
- oscura/{ui → jupyter/ui}/progressive_display.py +153 -82
- oscura/loaders/__init__.py +171 -63
- oscura/loaders/binary.py +88 -1
- oscura/loaders/chipwhisperer.py +153 -137
- oscura/loaders/configurable.py +208 -86
- oscura/loaders/csv_loader.py +458 -215
- oscura/loaders/hdf5_loader.py +278 -119
- oscura/loaders/lazy.py +87 -54
- oscura/loaders/mmap_loader.py +1 -1
- oscura/loaders/numpy_loader.py +253 -116
- oscura/loaders/pcap.py +226 -151
- oscura/loaders/rigol.py +110 -49
- oscura/loaders/sigrok.py +201 -78
- oscura/loaders/tdms.py +81 -58
- oscura/loaders/tektronix.py +291 -174
- oscura/loaders/touchstone.py +182 -87
- oscura/loaders/vcd.py +215 -117
- oscura/loaders/wav.py +155 -68
- oscura/reporting/__init__.py +9 -7
- oscura/reporting/analyze.py +352 -146
- oscura/reporting/argument_preparer.py +69 -14
- oscura/reporting/auto_report.py +97 -61
- oscura/reporting/batch.py +131 -58
- oscura/reporting/chart_selection.py +57 -45
- oscura/reporting/comparison.py +63 -17
- oscura/reporting/content/executive.py +76 -24
- oscura/reporting/core_formats/multi_format.py +11 -8
- oscura/reporting/engine.py +312 -158
- oscura/reporting/enhanced_reports.py +949 -0
- oscura/reporting/export.py +86 -43
- oscura/reporting/formatting/numbers.py +69 -42
- oscura/reporting/html.py +139 -58
- oscura/reporting/index.py +137 -65
- oscura/reporting/output.py +158 -67
- oscura/reporting/pdf.py +67 -102
- oscura/reporting/plots.py +191 -112
- oscura/reporting/sections.py +88 -47
- oscura/reporting/standards.py +104 -61
- oscura/reporting/summary_generator.py +75 -55
- oscura/reporting/tables.py +138 -54
- oscura/reporting/templates/enhanced/protocol_re.html +525 -0
- oscura/reporting/templates/index.md +13 -13
- oscura/sessions/__init__.py +14 -23
- oscura/sessions/base.py +3 -3
- oscura/sessions/blackbox.py +106 -10
- oscura/sessions/generic.py +2 -2
- oscura/sessions/legacy.py +783 -0
- oscura/side_channel/__init__.py +63 -0
- oscura/side_channel/dpa.py +1025 -0
- oscura/utils/__init__.py +15 -1
- oscura/utils/autodetect.py +1 -5
- oscura/utils/bitwise.py +118 -0
- oscura/{builders → utils/builders}/__init__.py +1 -1
- oscura/{comparison → utils/comparison}/__init__.py +6 -6
- oscura/{comparison → utils/comparison}/compare.py +202 -101
- oscura/{comparison → utils/comparison}/golden.py +83 -63
- oscura/{comparison → utils/comparison}/limits.py +313 -89
- oscura/{comparison → utils/comparison}/mask.py +151 -45
- oscura/{comparison → utils/comparison}/trace_diff.py +1 -1
- oscura/{comparison → utils/comparison}/visualization.py +147 -89
- oscura/{component → utils/component}/__init__.py +3 -3
- oscura/{component → utils/component}/impedance.py +122 -58
- oscura/{component → utils/component}/reactive.py +165 -168
- oscura/{component → utils/component}/transmission_line.py +3 -3
- oscura/{filtering → utils/filtering}/__init__.py +6 -6
- oscura/{filtering → utils/filtering}/base.py +1 -1
- oscura/{filtering → utils/filtering}/convenience.py +2 -2
- oscura/{filtering → utils/filtering}/design.py +169 -93
- oscura/{filtering → utils/filtering}/filters.py +2 -2
- oscura/{filtering → utils/filtering}/introspection.py +2 -2
- oscura/utils/geometry.py +31 -0
- oscura/utils/imports.py +184 -0
- oscura/utils/lazy.py +1 -1
- oscura/{math → utils/math}/__init__.py +2 -2
- oscura/{math → utils/math}/arithmetic.py +114 -48
- oscura/{math → utils/math}/interpolation.py +139 -106
- oscura/utils/memory.py +129 -66
- oscura/utils/memory_advanced.py +92 -9
- oscura/utils/memory_extensions.py +10 -8
- oscura/{optimization → utils/optimization}/__init__.py +1 -1
- oscura/{optimization → utils/optimization}/search.py +2 -2
- oscura/utils/performance/__init__.py +58 -0
- oscura/utils/performance/caching.py +889 -0
- oscura/utils/performance/lsh_clustering.py +333 -0
- oscura/utils/performance/memory_optimizer.py +699 -0
- oscura/utils/performance/optimizations.py +675 -0
- oscura/utils/performance/parallel.py +654 -0
- oscura/utils/performance/profiling.py +661 -0
- oscura/{pipeline → utils/pipeline}/base.py +1 -1
- oscura/{pipeline → utils/pipeline}/composition.py +11 -3
- oscura/{pipeline → utils/pipeline}/parallel.py +3 -2
- oscura/{pipeline → utils/pipeline}/pipeline.py +1 -1
- oscura/{pipeline → utils/pipeline}/reverse_engineering.py +412 -221
- oscura/{search → utils/search}/__init__.py +3 -3
- oscura/{search → utils/search}/anomaly.py +188 -58
- oscura/utils/search/context.py +294 -0
- oscura/{search → utils/search}/pattern.py +138 -10
- oscura/utils/serial.py +51 -0
- oscura/utils/storage/__init__.py +61 -0
- oscura/utils/storage/database.py +1166 -0
- oscura/{streaming → utils/streaming}/chunked.py +302 -143
- oscura/{streaming → utils/streaming}/progressive.py +1 -1
- oscura/{streaming → utils/streaming}/realtime.py +3 -2
- oscura/{triggering → utils/triggering}/__init__.py +6 -6
- oscura/{triggering → utils/triggering}/base.py +6 -6
- oscura/{triggering → utils/triggering}/edge.py +2 -2
- oscura/{triggering → utils/triggering}/pattern.py +2 -2
- oscura/{triggering → utils/triggering}/pulse.py +115 -74
- oscura/{triggering → utils/triggering}/window.py +2 -2
- oscura/utils/validation.py +32 -0
- oscura/validation/__init__.py +121 -0
- oscura/{compliance → validation/compliance}/__init__.py +5 -5
- oscura/{compliance → validation/compliance}/advanced.py +5 -5
- oscura/{compliance → validation/compliance}/masks.py +1 -1
- oscura/{compliance → validation/compliance}/reporting.py +127 -53
- oscura/{compliance → validation/compliance}/testing.py +114 -52
- oscura/validation/compliance_tests.py +915 -0
- oscura/validation/fuzzer.py +990 -0
- oscura/validation/grammar_tests.py +596 -0
- oscura/validation/grammar_validator.py +904 -0
- oscura/validation/hil_testing.py +977 -0
- oscura/{quality → validation/quality}/__init__.py +4 -4
- oscura/{quality → validation/quality}/ensemble.py +251 -171
- oscura/{quality → validation/quality}/explainer.py +3 -3
- oscura/{quality → validation/quality}/scoring.py +1 -1
- oscura/{quality → validation/quality}/warnings.py +4 -4
- oscura/validation/regression_suite.py +808 -0
- oscura/validation/replay.py +788 -0
- oscura/{testing → validation/testing}/__init__.py +2 -2
- oscura/{testing → validation/testing}/synthetic.py +5 -5
- oscura/visualization/__init__.py +9 -0
- oscura/visualization/accessibility.py +1 -1
- oscura/visualization/annotations.py +64 -67
- oscura/visualization/colors.py +7 -7
- oscura/visualization/digital.py +180 -81
- oscura/visualization/eye.py +236 -85
- oscura/visualization/interactive.py +320 -143
- oscura/visualization/jitter.py +587 -247
- oscura/visualization/layout.py +169 -134
- oscura/visualization/optimization.py +103 -52
- oscura/visualization/palettes.py +1 -1
- oscura/visualization/power.py +427 -211
- oscura/visualization/power_extended.py +626 -297
- oscura/visualization/presets.py +2 -0
- oscura/visualization/protocols.py +495 -181
- oscura/visualization/render.py +79 -63
- oscura/visualization/reverse_engineering.py +171 -124
- oscura/visualization/signal_integrity.py +460 -279
- oscura/visualization/specialized.py +190 -100
- oscura/visualization/spectral.py +670 -255
- oscura/visualization/thumbnails.py +166 -137
- oscura/visualization/waveform.py +150 -63
- oscura/workflows/__init__.py +3 -0
- oscura/{batch → workflows/batch}/__init__.py +5 -5
- oscura/{batch → workflows/batch}/advanced.py +150 -75
- oscura/workflows/batch/aggregate.py +531 -0
- oscura/workflows/batch/analyze.py +236 -0
- oscura/{batch → workflows/batch}/logging.py +2 -2
- oscura/{batch → workflows/batch}/metrics.py +1 -1
- oscura/workflows/complete_re.py +1144 -0
- oscura/workflows/compliance.py +44 -54
- oscura/workflows/digital.py +197 -51
- oscura/workflows/legacy/__init__.py +12 -0
- oscura/{workflow → workflows/legacy}/dag.py +4 -1
- oscura/workflows/multi_trace.py +9 -9
- oscura/workflows/power.py +42 -62
- oscura/workflows/protocol.py +82 -49
- oscura/workflows/reverse_engineering.py +351 -150
- oscura/workflows/signal_integrity.py +157 -82
- oscura-0.6.0.dist-info/METADATA +643 -0
- oscura-0.6.0.dist-info/RECORD +590 -0
- oscura/analyzers/digital/ic_database.py +0 -498
- oscura/analyzers/digital/timing_paths.py +0 -339
- oscura/analyzers/digital/vintage.py +0 -377
- oscura/analyzers/digital/vintage_result.py +0 -148
- oscura/analyzers/protocols/parallel_bus.py +0 -449
- oscura/batch/aggregate.py +0 -300
- oscura/batch/analyze.py +0 -139
- oscura/dsl/__init__.py +0 -73
- oscura/exceptions.py +0 -59
- oscura/exploratory/fuzzy.py +0 -513
- oscura/exploratory/sync.py +0 -384
- oscura/export/wavedrom.py +0 -430
- oscura/exporters/__init__.py +0 -94
- oscura/exporters/csv.py +0 -303
- oscura/exporters/exporters.py +0 -44
- oscura/exporters/hdf5.py +0 -217
- oscura/exporters/html_export.py +0 -701
- oscura/exporters/json_export.py +0 -338
- oscura/exporters/markdown_export.py +0 -367
- oscura/exporters/matlab_export.py +0 -354
- oscura/exporters/npz_export.py +0 -219
- oscura/exporters/spice_export.py +0 -210
- oscura/exporters/vintage_logic_csv.py +0 -247
- oscura/reporting/vintage_logic_report.py +0 -523
- oscura/search/context.py +0 -149
- oscura/session/__init__.py +0 -34
- oscura/session/annotations.py +0 -289
- oscura/session/history.py +0 -313
- oscura/session/session.py +0 -520
- oscura/visualization/digital_advanced.py +0 -718
- oscura/visualization/figure_manager.py +0 -156
- oscura/workflow/__init__.py +0 -13
- oscura-0.5.0.dist-info/METADATA +0 -407
- oscura-0.5.0.dist-info/RECORD +0 -486
- /oscura/core/{config.py → config/legacy.py} +0 -0
- /oscura/{extensibility → core/extensibility}/__init__.py +0 -0
- /oscura/{extensibility → core/extensibility}/registry.py +0 -0
- /oscura/{plugins → core/plugins}/isolation.py +0 -0
- /oscura/{builders → utils/builders}/signal_builder.py +0 -0
- /oscura/{optimization → utils/optimization}/parallel.py +0 -0
- /oscura/{pipeline → utils/pipeline}/__init__.py +0 -0
- /oscura/{streaming → utils/streaming}/__init__.py +0 -0
- {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/WHEEL +0 -0
- {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/entry_points.txt +0 -0
- {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,746 @@
|
|
|
1
|
+
"""Fuzzy matching for timing and pattern analysis.
|
|
2
|
+
|
|
3
|
+
This module provides fuzzy matching capabilities for tolerating
|
|
4
|
+
timing variations and pattern deviations in real-world signals.
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
>>> from oscura.jupyter.exploratory.fuzzy import fuzzy_timing_match
|
|
9
|
+
>>> result = fuzzy_timing_match(edges, expected_period=1e-6, tolerance=0.1)
|
|
10
|
+
>>> print(f"Match confidence: {result.confidence:.1%}")
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from typing import TYPE_CHECKING, Any
|
|
17
|
+
|
|
18
|
+
import numpy as np
|
|
19
|
+
|
|
20
|
+
from oscura.core.types import WaveformTrace
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from numpy.typing import NDArray
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class FuzzyTimingResult:
|
|
28
|
+
"""Result of fuzzy timing match.
|
|
29
|
+
|
|
30
|
+
Attributes:
|
|
31
|
+
match: True if timing matches within tolerance.
|
|
32
|
+
confidence: Match confidence (0.0 to 1.0).
|
|
33
|
+
period: Detected period.
|
|
34
|
+
deviation: Deviation from expected period.
|
|
35
|
+
jitter_rms: RMS timing jitter.
|
|
36
|
+
outlier_count: Number of timing outliers.
|
|
37
|
+
outlier_indices: Indices of outlier edges.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
match: bool
|
|
41
|
+
confidence: float
|
|
42
|
+
period: float
|
|
43
|
+
deviation: float
|
|
44
|
+
jitter_rms: float
|
|
45
|
+
outlier_count: int
|
|
46
|
+
outlier_indices: list[int]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def fuzzy_timing_match(
|
|
50
|
+
trace_or_edges: WaveformTrace | NDArray[np.float64],
|
|
51
|
+
*,
|
|
52
|
+
expected_period: float | None = None,
|
|
53
|
+
tolerance: float = 0.1,
|
|
54
|
+
sample_rate: float | None = None,
|
|
55
|
+
) -> FuzzyTimingResult:
|
|
56
|
+
"""Match timing with fuzzy tolerance.
|
|
57
|
+
|
|
58
|
+
Allows timing variations while still detecting protocol patterns.
|
|
59
|
+
Useful for signals with jitter or clock drift.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
trace_or_edges: WaveformTrace or array of edge times.
|
|
63
|
+
expected_period: Expected period in seconds.
|
|
64
|
+
tolerance: Tolerance as fraction (0.1 = 10%).
|
|
65
|
+
sample_rate: Sample rate (required if trace provided).
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
FuzzyTimingResult with match information.
|
|
69
|
+
|
|
70
|
+
Raises:
|
|
71
|
+
ValueError: If sample_rate is invalid when WaveformTrace provided.
|
|
72
|
+
|
|
73
|
+
Example:
|
|
74
|
+
>>> result = fuzzy_timing_match(trace, expected_period=1e-6, tolerance=0.1)
|
|
75
|
+
>>> print(f"Period match: {result.match}")
|
|
76
|
+
>>> print(f"Actual period: {result.period:.3e} s")
|
|
77
|
+
|
|
78
|
+
References:
|
|
79
|
+
FUZZY-001: Fuzzy Timing Tolerance
|
|
80
|
+
"""
|
|
81
|
+
edges = _extract_edges_from_trace(trace_or_edges, sample_rate)
|
|
82
|
+
|
|
83
|
+
if len(edges) < 2:
|
|
84
|
+
return _create_empty_timing_result()
|
|
85
|
+
|
|
86
|
+
intervals = np.diff(edges)
|
|
87
|
+
detected_period = float(np.median(intervals))
|
|
88
|
+
expected_period = expected_period if expected_period is not None else detected_period
|
|
89
|
+
|
|
90
|
+
deviation, match = _calculate_timing_deviation(detected_period, expected_period, tolerance)
|
|
91
|
+
jitter_rms = _calculate_timing_jitter(intervals, detected_period)
|
|
92
|
+
outlier_count, outlier_indices = _find_timing_outliers(intervals, expected_period, tolerance)
|
|
93
|
+
confidence = _calculate_timing_confidence(deviation, tolerance, outlier_count, len(intervals))
|
|
94
|
+
|
|
95
|
+
return FuzzyTimingResult(
|
|
96
|
+
match=match,
|
|
97
|
+
confidence=confidence,
|
|
98
|
+
period=detected_period,
|
|
99
|
+
deviation=deviation,
|
|
100
|
+
jitter_rms=jitter_rms,
|
|
101
|
+
outlier_count=outlier_count,
|
|
102
|
+
outlier_indices=outlier_indices,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _extract_edges_from_trace(
|
|
107
|
+
trace_or_edges: WaveformTrace | NDArray[np.float64], sample_rate: float | None
|
|
108
|
+
) -> NDArray[np.float64]:
|
|
109
|
+
"""Extract edge times from trace or pass through edge array."""
|
|
110
|
+
if isinstance(trace_or_edges, WaveformTrace):
|
|
111
|
+
data = trace_or_edges.data
|
|
112
|
+
sample_rate = sample_rate or trace_or_edges.metadata.sample_rate
|
|
113
|
+
|
|
114
|
+
if sample_rate is None or sample_rate <= 0:
|
|
115
|
+
raise ValueError("Valid sample_rate required for WaveformTrace")
|
|
116
|
+
|
|
117
|
+
v_min = np.percentile(data, 5)
|
|
118
|
+
v_max = np.percentile(data, 95)
|
|
119
|
+
threshold = (v_min + v_max) / 2
|
|
120
|
+
digital = data > threshold
|
|
121
|
+
|
|
122
|
+
edge_samples = np.where(np.abs(np.diff(digital.astype(int))) > 0)[0]
|
|
123
|
+
return edge_samples / sample_rate
|
|
124
|
+
|
|
125
|
+
return trace_or_edges
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _create_empty_timing_result() -> FuzzyTimingResult:
|
|
129
|
+
"""Create empty timing result for insufficient edges."""
|
|
130
|
+
return FuzzyTimingResult(
|
|
131
|
+
match=False,
|
|
132
|
+
confidence=0.0,
|
|
133
|
+
period=0.0,
|
|
134
|
+
deviation=1.0,
|
|
135
|
+
jitter_rms=0.0,
|
|
136
|
+
outlier_count=0,
|
|
137
|
+
outlier_indices=[],
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _calculate_timing_deviation(
|
|
142
|
+
detected: float, expected: float, tolerance: float
|
|
143
|
+
) -> tuple[float, bool]:
|
|
144
|
+
"""Calculate period deviation and match status."""
|
|
145
|
+
deviation = abs(detected - expected) / expected
|
|
146
|
+
return deviation, bool(deviation <= tolerance)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _calculate_timing_jitter(intervals: NDArray[np.float64], detected_period: float) -> float:
|
|
150
|
+
"""Calculate RMS timing jitter."""
|
|
151
|
+
normalized_intervals = intervals / detected_period
|
|
152
|
+
return float(np.std(normalized_intervals - 1.0) * detected_period)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _find_timing_outliers(
|
|
156
|
+
intervals: NDArray[np.float64], expected_period: float, tolerance: float
|
|
157
|
+
) -> tuple[int, list[int]]:
|
|
158
|
+
"""Find outlier intervals exceeding tolerance threshold."""
|
|
159
|
+
outlier_threshold = expected_period * tolerance * 3
|
|
160
|
+
deviations = np.abs(intervals - expected_period)
|
|
161
|
+
outlier_mask = deviations > outlier_threshold
|
|
162
|
+
outlier_count = int(np.sum(outlier_mask))
|
|
163
|
+
outlier_indices = list(np.where(outlier_mask)[0])
|
|
164
|
+
return outlier_count, outlier_indices
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _calculate_timing_confidence(
|
|
168
|
+
deviation: float, tolerance: float, outlier_count: int, total_intervals: int
|
|
169
|
+
) -> float:
|
|
170
|
+
"""Calculate timing match confidence score."""
|
|
171
|
+
confidence = max(0.0, 1.0 - deviation / tolerance)
|
|
172
|
+
confidence *= max(0.0, 1.0 - outlier_count / max(total_intervals, 1))
|
|
173
|
+
return min(1.0, confidence)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
@dataclass
|
|
177
|
+
class FuzzyPatternResult:
|
|
178
|
+
"""Result of fuzzy pattern match.
|
|
179
|
+
|
|
180
|
+
Attributes:
|
|
181
|
+
matches: List of match locations with scores.
|
|
182
|
+
best_match_score: Score of best match.
|
|
183
|
+
total_matches: Total number of matches found.
|
|
184
|
+
pattern_variations: Common pattern variations found.
|
|
185
|
+
"""
|
|
186
|
+
|
|
187
|
+
matches: list[dict[str, Any]]
|
|
188
|
+
best_match_score: float
|
|
189
|
+
total_matches: int
|
|
190
|
+
pattern_variations: list[tuple[tuple[int, ...], int]]
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _convert_trace_to_digital_bits(
|
|
194
|
+
trace: WaveformTrace,
|
|
195
|
+
) -> tuple[NDArray[np.int_], NDArray[np.intp], float]:
|
|
196
|
+
"""Convert analog trace to digital bit sequence.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
trace: Input waveform trace.
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
Tuple of (digital_signal, edges, estimated_bit_period).
|
|
203
|
+
"""
|
|
204
|
+
data = trace.data
|
|
205
|
+
v_min = np.percentile(data, 5)
|
|
206
|
+
v_max = np.percentile(data, 95)
|
|
207
|
+
threshold = (v_min + v_max) / 2
|
|
208
|
+
digital = (data > threshold).astype(int)
|
|
209
|
+
edges = np.where(np.diff(digital) != 0)[0]
|
|
210
|
+
|
|
211
|
+
if len(edges) < 2:
|
|
212
|
+
return digital, edges, 0.0
|
|
213
|
+
|
|
214
|
+
gaps = np.diff(edges)
|
|
215
|
+
estimated_bit_period = float(np.min(gaps))
|
|
216
|
+
return digital, edges, estimated_bit_period
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _sample_bits_from_digital(
|
|
220
|
+
digital: NDArray[np.int_], edges: NDArray[np.intp], estimated_bit_period: float
|
|
221
|
+
) -> NDArray[np.int_]:
|
|
222
|
+
"""Sample bits from digital signal at estimated bit period.
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
digital: Digital signal array.
|
|
226
|
+
edges: Edge indices.
|
|
227
|
+
estimated_bit_period: Estimated bit period in samples.
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
Array of sampled bits.
|
|
231
|
+
"""
|
|
232
|
+
bits_list = []
|
|
233
|
+
sample_pos = edges[0] + estimated_bit_period / 2
|
|
234
|
+
|
|
235
|
+
while sample_pos < len(digital):
|
|
236
|
+
idx = int(sample_pos)
|
|
237
|
+
if idx < len(digital):
|
|
238
|
+
bits_list.append(digital[idx])
|
|
239
|
+
sample_pos += estimated_bit_period
|
|
240
|
+
|
|
241
|
+
return np.array(bits_list)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _search_pattern_with_errors(
|
|
245
|
+
bits: NDArray[np.int_],
|
|
246
|
+
pattern: tuple[int, ...],
|
|
247
|
+
max_errors: int,
|
|
248
|
+
error_weight: float,
|
|
249
|
+
edges: NDArray[np.intp],
|
|
250
|
+
estimated_bit_period: float,
|
|
251
|
+
) -> tuple[list[dict[str, Any]], dict[tuple[int, ...], int]]:
|
|
252
|
+
"""Search for pattern matches allowing errors.
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
bits: Sampled bit sequence.
|
|
256
|
+
pattern: Pattern to search for.
|
|
257
|
+
max_errors: Maximum allowed errors.
|
|
258
|
+
error_weight: Weight reduction per error.
|
|
259
|
+
edges: Edge indices for position calculation.
|
|
260
|
+
estimated_bit_period: Bit period for position calculation.
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
Tuple of (matches, variations).
|
|
264
|
+
"""
|
|
265
|
+
pattern_len = len(pattern)
|
|
266
|
+
matches = []
|
|
267
|
+
variations: dict[tuple[int, ...], int] = {}
|
|
268
|
+
|
|
269
|
+
for i in range(len(bits) - pattern_len + 1):
|
|
270
|
+
window = tuple(bits[i : i + pattern_len])
|
|
271
|
+
errors = sum(1 for a, b in zip(window, pattern, strict=False) if a != b)
|
|
272
|
+
|
|
273
|
+
if errors <= max_errors:
|
|
274
|
+
score = 1.0 - errors * error_weight
|
|
275
|
+
matches.append(
|
|
276
|
+
{
|
|
277
|
+
"position": i,
|
|
278
|
+
"sample_position": int(edges[0] + i * estimated_bit_period),
|
|
279
|
+
"errors": errors,
|
|
280
|
+
"score": score,
|
|
281
|
+
"actual_pattern": window,
|
|
282
|
+
}
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
if window != pattern:
|
|
286
|
+
variations[window] = variations.get(window, 0) + 1
|
|
287
|
+
|
|
288
|
+
return matches, variations
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def fuzzy_pattern_match(
|
|
292
|
+
trace: WaveformTrace,
|
|
293
|
+
pattern: list[int] | tuple[int, ...],
|
|
294
|
+
*,
|
|
295
|
+
max_errors: int = 1,
|
|
296
|
+
error_weight: float = 0.5,
|
|
297
|
+
) -> FuzzyPatternResult:
|
|
298
|
+
"""Match pattern with allowed bit errors.
|
|
299
|
+
|
|
300
|
+
Finds pattern occurrences allowing for bit errors, useful for
|
|
301
|
+
noisy signals or partial matches.
|
|
302
|
+
|
|
303
|
+
Args:
|
|
304
|
+
trace: Signal trace to search.
|
|
305
|
+
pattern: Bit pattern to find (list of 0s and 1s).
|
|
306
|
+
max_errors: Maximum allowed bit errors.
|
|
307
|
+
error_weight: Weight reduction per error.
|
|
308
|
+
|
|
309
|
+
Returns:
|
|
310
|
+
FuzzyPatternResult with match locations.
|
|
311
|
+
|
|
312
|
+
Example:
|
|
313
|
+
>>> result = fuzzy_pattern_match(trace, [0, 1, 0, 1, 0, 1], max_errors=1)
|
|
314
|
+
>>> print(f"Found {result.total_matches} matches")
|
|
315
|
+
>>> for match in result.matches[:5]:
|
|
316
|
+
... print(f" Position {match['position']}: score {match['score']:.2f}")
|
|
317
|
+
|
|
318
|
+
References:
|
|
319
|
+
FUZZY-002: Fuzzy Pattern Matching
|
|
320
|
+
"""
|
|
321
|
+
pattern = tuple(pattern)
|
|
322
|
+
pattern_len = len(pattern)
|
|
323
|
+
|
|
324
|
+
# Setup: handle empty pattern
|
|
325
|
+
if pattern_len == 0:
|
|
326
|
+
return FuzzyPatternResult(
|
|
327
|
+
matches=[], best_match_score=0.0, total_matches=0, pattern_variations=[]
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
# Processing: convert to digital and sample bits
|
|
331
|
+
digital, edges, estimated_bit_period = _convert_trace_to_digital_bits(trace)
|
|
332
|
+
|
|
333
|
+
if len(edges) < 2:
|
|
334
|
+
return FuzzyPatternResult(
|
|
335
|
+
matches=[], best_match_score=0.0, total_matches=0, pattern_variations=[]
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
bits = _sample_bits_from_digital(digital, edges, estimated_bit_period)
|
|
339
|
+
|
|
340
|
+
# Result building: search for pattern matches
|
|
341
|
+
matches, variations = _search_pattern_with_errors(
|
|
342
|
+
bits, pattern, max_errors, error_weight, edges, estimated_bit_period
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
matches.sort(key=lambda x: x["score"], reverse=True)
|
|
346
|
+
best_score: float = float(matches[0]["score"]) if matches else 0.0
|
|
347
|
+
variation_list = sorted(variations.items(), key=lambda x: x[1], reverse=True)
|
|
348
|
+
|
|
349
|
+
return FuzzyPatternResult(
|
|
350
|
+
matches=matches,
|
|
351
|
+
best_match_score=best_score,
|
|
352
|
+
total_matches=len(matches),
|
|
353
|
+
pattern_variations=variation_list[:10],
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
@dataclass
|
|
358
|
+
class FuzzyProtocolResult:
|
|
359
|
+
"""Result of fuzzy protocol detection.
|
|
360
|
+
|
|
361
|
+
Attributes:
|
|
362
|
+
detected_protocol: Most likely protocol.
|
|
363
|
+
confidence: Detection confidence.
|
|
364
|
+
alternatives: Alternative protocol candidates.
|
|
365
|
+
timing_score: Score based on timing match.
|
|
366
|
+
pattern_score: Score based on pattern match.
|
|
367
|
+
recommendations: Suggestions for improving detection.
|
|
368
|
+
"""
|
|
369
|
+
|
|
370
|
+
detected_protocol: str
|
|
371
|
+
confidence: float
|
|
372
|
+
alternatives: list[tuple[str, float]]
|
|
373
|
+
timing_score: float
|
|
374
|
+
pattern_score: float
|
|
375
|
+
recommendations: list[str]
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
# Protocol signatures for fuzzy matching
|
|
379
|
+
PROTOCOL_SIGNATURES = {
|
|
380
|
+
"UART": {
|
|
381
|
+
"start_bit": 0,
|
|
382
|
+
"stop_bits": 1,
|
|
383
|
+
"frame_size": [8, 9, 10, 11], # With start/stop
|
|
384
|
+
"typical_rates": [9600, 19200, 38400, 57600, 115200],
|
|
385
|
+
},
|
|
386
|
+
"I2C": {
|
|
387
|
+
"start_pattern": [1, 0], # SDA falls while SCL high
|
|
388
|
+
"stop_pattern": [0, 1], # SDA rises while SCL high
|
|
389
|
+
"ack_bit": 0,
|
|
390
|
+
"typical_rates": [100e3, 400e3, 1e6, 3.4e6],
|
|
391
|
+
},
|
|
392
|
+
"SPI": {
|
|
393
|
+
"idle_clock": [0, 1], # CPOL options
|
|
394
|
+
"clock_phase": [0, 1], # CPHA options
|
|
395
|
+
"frame_size": [8, 16],
|
|
396
|
+
"typical_rates": [1e6, 5e6, 10e6, 20e6, 40e6],
|
|
397
|
+
},
|
|
398
|
+
"CAN": {
|
|
399
|
+
"start_of_frame": 0,
|
|
400
|
+
"frame_patterns": ["standard", "extended"],
|
|
401
|
+
"typical_rates": [125e3, 250e3, 500e3, 1e6],
|
|
402
|
+
},
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def fuzzy_protocol_detect(
|
|
407
|
+
trace: WaveformTrace,
|
|
408
|
+
*,
|
|
409
|
+
candidates: list[str] | None = None,
|
|
410
|
+
timing_tolerance: float = 0.15,
|
|
411
|
+
pattern_tolerance: int = 2,
|
|
412
|
+
) -> FuzzyProtocolResult:
|
|
413
|
+
"""Detect protocol with fuzzy matching.
|
|
414
|
+
|
|
415
|
+
Uses timing tolerance and pattern flexibility to identify
|
|
416
|
+
protocols even with non-ideal signals.
|
|
417
|
+
|
|
418
|
+
Args:
|
|
419
|
+
trace: Signal trace to analyze.
|
|
420
|
+
candidates: List of protocols to consider (None = all).
|
|
421
|
+
timing_tolerance: Timing tolerance as fraction.
|
|
422
|
+
pattern_tolerance: Maximum pattern bit errors.
|
|
423
|
+
|
|
424
|
+
Returns:
|
|
425
|
+
FuzzyProtocolResult with detection results.
|
|
426
|
+
|
|
427
|
+
Example:
|
|
428
|
+
>>> result = fuzzy_protocol_detect(trace)
|
|
429
|
+
>>> print(f"Detected: {result.detected_protocol}")
|
|
430
|
+
>>> print(f"Confidence: {result.confidence:.1%}")
|
|
431
|
+
|
|
432
|
+
References:
|
|
433
|
+
FUZZY-003: Fuzzy Protocol Detection
|
|
434
|
+
"""
|
|
435
|
+
digital, edges, sample_rate = _prepare_fuzzy_detection_data(trace)
|
|
436
|
+
|
|
437
|
+
if len(edges) < 4:
|
|
438
|
+
return _create_unknown_result("Insufficient edges for protocol detection")
|
|
439
|
+
|
|
440
|
+
estimated_bitrate = _estimate_bitrate(edges, sample_rate)
|
|
441
|
+
candidates_list = candidates if candidates else list(PROTOCOL_SIGNATURES.keys())
|
|
442
|
+
scores = _score_all_protocols(
|
|
443
|
+
digital,
|
|
444
|
+
edges,
|
|
445
|
+
estimated_bitrate,
|
|
446
|
+
sample_rate,
|
|
447
|
+
candidates_list,
|
|
448
|
+
timing_tolerance,
|
|
449
|
+
pattern_tolerance,
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
if not scores:
|
|
453
|
+
return _create_unknown_result("No matching protocols found")
|
|
454
|
+
|
|
455
|
+
return _build_detection_result(scores)
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def _prepare_fuzzy_detection_data(
|
|
459
|
+
trace: WaveformTrace,
|
|
460
|
+
) -> tuple[NDArray[np.bool_], NDArray[np.int_], float]:
|
|
461
|
+
"""Prepare signal data for fuzzy protocol detection.
|
|
462
|
+
|
|
463
|
+
Args:
|
|
464
|
+
trace: Input waveform trace.
|
|
465
|
+
|
|
466
|
+
Returns:
|
|
467
|
+
Tuple of (digital_signal, edge_indices, sample_rate).
|
|
468
|
+
"""
|
|
469
|
+
data = trace.data
|
|
470
|
+
sample_rate = trace.metadata.sample_rate
|
|
471
|
+
v_min = np.percentile(data, 5)
|
|
472
|
+
v_max = np.percentile(data, 95)
|
|
473
|
+
threshold = (v_min + v_max) / 2
|
|
474
|
+
digital = data > threshold
|
|
475
|
+
edges = np.where(np.diff(digital.astype(int)) != 0)[0]
|
|
476
|
+
return digital, edges, sample_rate
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def _estimate_bitrate(edges: NDArray[np.int_], sample_rate: float) -> float:
|
|
480
|
+
"""Estimate signal bitrate from edge spacing.
|
|
481
|
+
|
|
482
|
+
Args:
|
|
483
|
+
edges: Array of edge sample indices.
|
|
484
|
+
sample_rate: Sample rate in Hz.
|
|
485
|
+
|
|
486
|
+
Returns:
|
|
487
|
+
Estimated bitrate in bps.
|
|
488
|
+
"""
|
|
489
|
+
intervals = np.diff(edges)
|
|
490
|
+
median_interval = np.median(intervals)
|
|
491
|
+
return float(sample_rate / median_interval)
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
def _score_all_protocols(
|
|
495
|
+
digital: NDArray[np.bool_],
|
|
496
|
+
edges: NDArray[np.int_],
|
|
497
|
+
estimated_bitrate: float,
|
|
498
|
+
sample_rate: float,
|
|
499
|
+
candidates: list[str],
|
|
500
|
+
timing_tolerance: float,
|
|
501
|
+
pattern_tolerance: int,
|
|
502
|
+
) -> dict[str, dict[str, float]]:
|
|
503
|
+
"""Score each candidate protocol against signal characteristics.
|
|
504
|
+
|
|
505
|
+
Args:
|
|
506
|
+
digital: Digital signal array.
|
|
507
|
+
edges: Edge sample indices.
|
|
508
|
+
estimated_bitrate: Estimated bitrate in bps.
|
|
509
|
+
sample_rate: Sample rate in Hz.
|
|
510
|
+
candidates: List of protocol names to test.
|
|
511
|
+
timing_tolerance: Timing tolerance as fraction.
|
|
512
|
+
pattern_tolerance: Maximum pattern bit errors.
|
|
513
|
+
|
|
514
|
+
Returns:
|
|
515
|
+
Dict mapping protocol names to score dicts with timing/pattern/total scores.
|
|
516
|
+
"""
|
|
517
|
+
scores: dict[str, dict[str, float]] = {}
|
|
518
|
+
intervals = np.diff(edges)
|
|
519
|
+
median_interval = np.median(intervals)
|
|
520
|
+
|
|
521
|
+
for protocol in candidates:
|
|
522
|
+
if protocol not in PROTOCOL_SIGNATURES:
|
|
523
|
+
continue
|
|
524
|
+
|
|
525
|
+
sig = PROTOCOL_SIGNATURES[protocol]
|
|
526
|
+
timing_score = _score_protocol_timing(sig, estimated_bitrate, timing_tolerance)
|
|
527
|
+
pattern_score = _score_protocol_patterns(
|
|
528
|
+
sig, digital, edges, intervals, median_interval, pattern_tolerance
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
scores[protocol] = {
|
|
532
|
+
"timing": timing_score,
|
|
533
|
+
"pattern": pattern_score,
|
|
534
|
+
"total": timing_score * 0.5 + pattern_score * 0.5,
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
return scores
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
def _score_protocol_timing(
|
|
541
|
+
sig: dict[str, Any],
|
|
542
|
+
estimated_bitrate: float,
|
|
543
|
+
timing_tolerance: float,
|
|
544
|
+
) -> float:
|
|
545
|
+
"""Score protocol timing match.
|
|
546
|
+
|
|
547
|
+
Args:
|
|
548
|
+
sig: Protocol signature dictionary.
|
|
549
|
+
estimated_bitrate: Estimated bitrate in bps.
|
|
550
|
+
timing_tolerance: Timing tolerance as fraction.
|
|
551
|
+
|
|
552
|
+
Returns:
|
|
553
|
+
Timing score (0.0 to 1.0).
|
|
554
|
+
"""
|
|
555
|
+
timing_score = 0.0
|
|
556
|
+
|
|
557
|
+
if "typical_rates" in sig:
|
|
558
|
+
rates = sig["typical_rates"]
|
|
559
|
+
if hasattr(rates, "__iter__"):
|
|
560
|
+
for rate in rates:
|
|
561
|
+
if isinstance(rate, int | float):
|
|
562
|
+
ratio = estimated_bitrate / rate
|
|
563
|
+
if (1 - timing_tolerance) <= ratio <= (1 + timing_tolerance):
|
|
564
|
+
timing_score = max(timing_score, 1 - abs(1 - ratio) / timing_tolerance)
|
|
565
|
+
|
|
566
|
+
return timing_score
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
def _score_protocol_patterns(
|
|
570
|
+
sig: dict[str, Any],
|
|
571
|
+
digital: NDArray[np.bool_],
|
|
572
|
+
edges: NDArray[np.int_],
|
|
573
|
+
intervals: NDArray[np.float64],
|
|
574
|
+
median_interval: float,
|
|
575
|
+
pattern_tolerance: int,
|
|
576
|
+
) -> float:
|
|
577
|
+
"""Score protocol pattern match.
|
|
578
|
+
|
|
579
|
+
Args:
|
|
580
|
+
sig: Protocol signature dictionary.
|
|
581
|
+
digital: Digital signal array.
|
|
582
|
+
edges: Edge sample indices.
|
|
583
|
+
intervals: Inter-edge intervals.
|
|
584
|
+
median_interval: Median interval between edges.
|
|
585
|
+
pattern_tolerance: Maximum pattern bit errors.
|
|
586
|
+
|
|
587
|
+
Returns:
|
|
588
|
+
Pattern score (0.0 to 1.0).
|
|
589
|
+
"""
|
|
590
|
+
pattern_score = 0.0
|
|
591
|
+
|
|
592
|
+
if "start_pattern" in sig:
|
|
593
|
+
pattern_score = max(
|
|
594
|
+
pattern_score,
|
|
595
|
+
_check_start_pattern(sig, digital, edges, median_interval, pattern_tolerance),
|
|
596
|
+
)
|
|
597
|
+
|
|
598
|
+
if "frame_size" in sig:
|
|
599
|
+
pattern_score = max(pattern_score, _check_frame_size(sig, intervals, median_interval))
|
|
600
|
+
|
|
601
|
+
return pattern_score
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
def _check_start_pattern(
|
|
605
|
+
sig: dict[str, Any],
|
|
606
|
+
digital: NDArray[np.bool_],
|
|
607
|
+
edges: NDArray[np.int_],
|
|
608
|
+
median_interval: float,
|
|
609
|
+
pattern_tolerance: int,
|
|
610
|
+
) -> float:
|
|
611
|
+
"""Check start pattern match.
|
|
612
|
+
|
|
613
|
+
Args:
|
|
614
|
+
sig: Protocol signature dictionary.
|
|
615
|
+
digital: Digital signal array.
|
|
616
|
+
edges: Edge sample indices.
|
|
617
|
+
median_interval: Median interval between edges.
|
|
618
|
+
pattern_tolerance: Maximum pattern bit errors.
|
|
619
|
+
|
|
620
|
+
Returns:
|
|
621
|
+
Start pattern score (0.0 to 1.0).
|
|
622
|
+
"""
|
|
623
|
+
bits = []
|
|
624
|
+
pos = edges[0] + median_interval / 2
|
|
625
|
+
for _ in range(4):
|
|
626
|
+
if pos < len(digital):
|
|
627
|
+
bits.append(int(digital[int(pos)]))
|
|
628
|
+
pos += median_interval
|
|
629
|
+
|
|
630
|
+
expected = sig["start_pattern"]
|
|
631
|
+
if len(bits) >= len(expected):
|
|
632
|
+
errors = sum(1 for a, b in zip(bits[: len(expected)], expected, strict=False) if a != b)
|
|
633
|
+
if errors <= pattern_tolerance:
|
|
634
|
+
return 1 - errors * 0.3
|
|
635
|
+
|
|
636
|
+
return 0.0
|
|
637
|
+
|
|
638
|
+
|
|
639
|
+
def _check_frame_size(
|
|
640
|
+
sig: dict[str, Any],
|
|
641
|
+
intervals: NDArray[np.float64],
|
|
642
|
+
median_interval: float,
|
|
643
|
+
) -> float:
|
|
644
|
+
"""Check frame size match.
|
|
645
|
+
|
|
646
|
+
Args:
|
|
647
|
+
sig: Protocol signature dictionary.
|
|
648
|
+
intervals: Inter-edge intervals.
|
|
649
|
+
median_interval: Median interval between edges.
|
|
650
|
+
|
|
651
|
+
Returns:
|
|
652
|
+
Frame size score (0.0 to 1.0).
|
|
653
|
+
"""
|
|
654
|
+
gap_threshold = median_interval * 2
|
|
655
|
+
long_gaps = intervals[intervals > gap_threshold]
|
|
656
|
+
if len(long_gaps) > 0:
|
|
657
|
+
frame_samples = np.median(long_gaps)
|
|
658
|
+
frame_bits = frame_samples / median_interval
|
|
659
|
+
for valid_size in sig["frame_size"]:
|
|
660
|
+
if abs(frame_bits - valid_size) < 1.5:
|
|
661
|
+
return 0.7
|
|
662
|
+
return 0.0
|
|
663
|
+
|
|
664
|
+
|
|
665
|
+
def _create_unknown_result(recommendation: str) -> FuzzyProtocolResult:
|
|
666
|
+
"""Create result for unknown protocol detection.
|
|
667
|
+
|
|
668
|
+
Args:
|
|
669
|
+
recommendation: Recommendation message explaining why unknown.
|
|
670
|
+
|
|
671
|
+
Returns:
|
|
672
|
+
FuzzyProtocolResult indicating unknown protocol.
|
|
673
|
+
"""
|
|
674
|
+
return FuzzyProtocolResult(
|
|
675
|
+
detected_protocol="Unknown",
|
|
676
|
+
confidence=0.0,
|
|
677
|
+
alternatives=[],
|
|
678
|
+
timing_score=0.0,
|
|
679
|
+
pattern_score=0.0,
|
|
680
|
+
recommendations=[recommendation],
|
|
681
|
+
)
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
def _build_detection_result(scores: dict[str, dict[str, float]]) -> FuzzyProtocolResult:
|
|
685
|
+
"""Build final detection result from scores.
|
|
686
|
+
|
|
687
|
+
Args:
|
|
688
|
+
scores: Dict mapping protocol names to score dicts.
|
|
689
|
+
|
|
690
|
+
Returns:
|
|
691
|
+
FuzzyProtocolResult with best match and alternatives.
|
|
692
|
+
"""
|
|
693
|
+
sorted_protocols = sorted(scores.items(), key=lambda x: x[1]["total"], reverse=True)
|
|
694
|
+
best_protocol, best_scores = sorted_protocols[0]
|
|
695
|
+
confidence = best_scores["total"]
|
|
696
|
+
alternatives = [(p, s["total"]) for p, s in sorted_protocols[1:4] if s["total"] > 0.2]
|
|
697
|
+
recommendations = _generate_recommendations(best_scores, confidence, alternatives)
|
|
698
|
+
|
|
699
|
+
return FuzzyProtocolResult(
|
|
700
|
+
detected_protocol=best_protocol,
|
|
701
|
+
confidence=confidence,
|
|
702
|
+
alternatives=alternatives,
|
|
703
|
+
timing_score=best_scores["timing"],
|
|
704
|
+
pattern_score=best_scores["pattern"],
|
|
705
|
+
recommendations=recommendations,
|
|
706
|
+
)
|
|
707
|
+
|
|
708
|
+
|
|
709
|
+
def _generate_recommendations(
|
|
710
|
+
best_scores: dict[str, float],
|
|
711
|
+
confidence: float,
|
|
712
|
+
alternatives: list[tuple[str, float]],
|
|
713
|
+
) -> list[str]:
|
|
714
|
+
"""Generate recommendations based on detection results.
|
|
715
|
+
|
|
716
|
+
Args:
|
|
717
|
+
best_scores: Score dict for best matching protocol.
|
|
718
|
+
confidence: Overall confidence score.
|
|
719
|
+
alternatives: List of alternative protocol candidates.
|
|
720
|
+
|
|
721
|
+
Returns:
|
|
722
|
+
List of recommendation strings.
|
|
723
|
+
"""
|
|
724
|
+
recommendations = []
|
|
725
|
+
|
|
726
|
+
if confidence < 0.5:
|
|
727
|
+
recommendations.append("Low confidence - verify with protocol-specific decoder")
|
|
728
|
+
if best_scores["timing"] > best_scores["pattern"]:
|
|
729
|
+
recommendations.append("Timing matched better than patterns - check signal quality")
|
|
730
|
+
if best_scores["pattern"] > best_scores["timing"]:
|
|
731
|
+
recommendations.append("Patterns matched but timing off - check clock accuracy")
|
|
732
|
+
if not alternatives:
|
|
733
|
+
recommendations.append("No alternative protocols detected")
|
|
734
|
+
|
|
735
|
+
return recommendations
|
|
736
|
+
|
|
737
|
+
|
|
738
|
+
__all__ = [
|
|
739
|
+
"PROTOCOL_SIGNATURES",
|
|
740
|
+
"FuzzyPatternResult",
|
|
741
|
+
"FuzzyProtocolResult",
|
|
742
|
+
"FuzzyTimingResult",
|
|
743
|
+
"fuzzy_pattern_match",
|
|
744
|
+
"fuzzy_protocol_detect",
|
|
745
|
+
"fuzzy_timing_match",
|
|
746
|
+
]
|