oscura 0.5.1__py3-none-any.whl → 0.7.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/edges.py +325 -65
- 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/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 +17 -102
- 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/{schemas → core/schemas}/device_mapping.json +2 -8
- oscura/{schemas → core/schemas}/packet_format.json +4 -24
- oscura/{schemas → core/schemas}/protocol_definition.json +2 -12
- 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 -8
- oscura/export/kaitai_struct.py +513 -0
- oscura/export/scapy_layer.py +801 -0
- 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/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 +183 -67
- 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/tss.py +456 -0
- oscura/loaders/vcd.py +215 -117
- oscura/loaders/wav.py +155 -68
- oscura/reporting/__init__.py +9 -0
- 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/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/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 +1 -1
- 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.7.0.dist-info/METADATA +661 -0
- oscura-0.7.0.dist-info/RECORD +591 -0
- 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/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 -291
- 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/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/workflow/__init__.py +0 -13
- oscura-0.5.1.dist-info/METADATA +0 -583
- oscura-0.5.1.dist-info/RECORD +0 -481
- /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/{schemas → core/schemas}/bus_configuration.json +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.1.dist-info → oscura-0.7.0.dist-info}/WHEEL +0 -0
- {oscura-0.5.1.dist-info → oscura-0.7.0.dist-info}/entry_points.txt +0 -0
- {oscura-0.5.1.dist-info → oscura-0.7.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,659 @@
|
|
|
1
|
+
"""Intelligent signal classification pipeline for automatic signal type identification.
|
|
2
|
+
|
|
3
|
+
This module provides multi-method signal classification to automatically identify
|
|
4
|
+
signal types from waveforms without manual configuration.
|
|
5
|
+
|
|
6
|
+
Key capabilities:
|
|
7
|
+
- Classify signals: digital, analog, PWM, UART, SPI, I2C, CAN
|
|
8
|
+
- Multi-method classification: statistical, frequency domain, pattern recognition
|
|
9
|
+
- Confidence scoring for each classification
|
|
10
|
+
- Batch classification with parallel processing support
|
|
11
|
+
- Extensible architecture for adding new classifiers
|
|
12
|
+
|
|
13
|
+
Classification methods:
|
|
14
|
+
- Statistical features: mean, variance, duty cycle, edge density
|
|
15
|
+
- Frequency domain: FFT analysis, dominant frequencies, spectral characteristics
|
|
16
|
+
- Time domain patterns: Protocol-specific signature detection
|
|
17
|
+
- Rule-based classification: Feature threshold matching
|
|
18
|
+
|
|
19
|
+
Typical workflow:
|
|
20
|
+
1. Extract features from signal (statistical, frequency, pattern)
|
|
21
|
+
2. Apply classification rules based on feature values
|
|
22
|
+
3. Return best match with confidence score and alternatives
|
|
23
|
+
|
|
24
|
+
Example:
|
|
25
|
+
>>> from oscura.analyzers.classification import SignalClassifier
|
|
26
|
+
>>> classifier = SignalClassifier()
|
|
27
|
+
>>> result = classifier.classify(signal, sample_rate=1e6)
|
|
28
|
+
>>> print(f"{result.signal_type}: {result.confidence:.2f}")
|
|
29
|
+
uart: 0.94
|
|
30
|
+
>>> print(f"Features: {result.features}")
|
|
31
|
+
Features: {'duty_cycle': 0.52, 'edge_density': 0.042, ...}
|
|
32
|
+
|
|
33
|
+
References:
|
|
34
|
+
IEEE 181-2011: Transitional Waveform Definitions
|
|
35
|
+
DISC-001: Automatic Signal Characterization
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
from __future__ import annotations
|
|
39
|
+
|
|
40
|
+
from dataclasses import dataclass, field
|
|
41
|
+
from typing import TYPE_CHECKING, Any, ClassVar, Literal
|
|
42
|
+
|
|
43
|
+
import numpy as np
|
|
44
|
+
|
|
45
|
+
if TYPE_CHECKING:
|
|
46
|
+
from numpy.typing import NDArray
|
|
47
|
+
|
|
48
|
+
SignalType = Literal["digital", "analog", "pwm", "uart", "spi", "i2c", "can", "unknown"]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass
|
|
52
|
+
class ClassificationResult:
|
|
53
|
+
"""Result of signal classification.
|
|
54
|
+
|
|
55
|
+
Contains detected signal type, confidence score, features used for classification,
|
|
56
|
+
and alternative matches for ambiguous cases.
|
|
57
|
+
|
|
58
|
+
Attributes:
|
|
59
|
+
signal_type: Detected signal type (digital, analog, uart, spi, etc.)
|
|
60
|
+
confidence: Confidence score (0.0-1.0), higher is more confident
|
|
61
|
+
features: Dictionary of extracted features used for classification
|
|
62
|
+
secondary_matches: Alternative classifications with confidence scores
|
|
63
|
+
reasoning: Human-readable explanation of classification decision
|
|
64
|
+
|
|
65
|
+
Example:
|
|
66
|
+
>>> result = classifier.classify(signal, sample_rate=1e6)
|
|
67
|
+
>>> if result.confidence >= 0.8:
|
|
68
|
+
... print(f"High confidence: {result.signal_type}")
|
|
69
|
+
>>> for alt_type, alt_conf in result.secondary_matches:
|
|
70
|
+
... print(f"Alternative: {alt_type} ({alt_conf:.2f})")
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
signal_type: SignalType
|
|
74
|
+
confidence: float
|
|
75
|
+
features: dict[str, float]
|
|
76
|
+
secondary_matches: list[tuple[SignalType, float]] = field(default_factory=list)
|
|
77
|
+
reasoning: str = ""
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass
|
|
81
|
+
class ClassifierRule:
|
|
82
|
+
"""Rule-based classification criteria.
|
|
83
|
+
|
|
84
|
+
Defines feature thresholds for identifying a specific signal type.
|
|
85
|
+
|
|
86
|
+
Attributes:
|
|
87
|
+
signal_type: Signal type this rule identifies
|
|
88
|
+
conditions: Dict mapping feature name to (min, max) thresholds
|
|
89
|
+
weight: Importance weight for this rule (default 1.0)
|
|
90
|
+
required_features: Features that must be present and within range
|
|
91
|
+
|
|
92
|
+
Example:
|
|
93
|
+
>>> rule = ClassifierRule(
|
|
94
|
+
... signal_type="digital",
|
|
95
|
+
... conditions={"variance": (0.2, 1.0), "edge_count": (100, float('inf'))},
|
|
96
|
+
... weight=1.0
|
|
97
|
+
... )
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
signal_type: SignalType
|
|
101
|
+
conditions: dict[str, tuple[float, float]]
|
|
102
|
+
weight: float = 1.0
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class SignalClassifier:
|
|
106
|
+
"""Multi-method signal classifier with extensible architecture.
|
|
107
|
+
|
|
108
|
+
Combines statistical analysis, frequency domain features, and pattern
|
|
109
|
+
recognition to automatically identify signal types from waveforms.
|
|
110
|
+
|
|
111
|
+
Classification Methods:
|
|
112
|
+
- statistical: Mean, variance, duty cycle, edge statistics
|
|
113
|
+
- frequency: FFT-based frequency analysis, spectral characteristics
|
|
114
|
+
- pattern: Protocol-specific pattern detection (UART, SPI, etc.)
|
|
115
|
+
|
|
116
|
+
Attributes:
|
|
117
|
+
methods: List of classification methods to use
|
|
118
|
+
threshold: Minimum confidence to report classification (default 0.5)
|
|
119
|
+
|
|
120
|
+
Example:
|
|
121
|
+
>>> # Default classifier uses all methods
|
|
122
|
+
>>> classifier = SignalClassifier()
|
|
123
|
+
>>> result = classifier.classify(signal, sample_rate=1e6)
|
|
124
|
+
>>>
|
|
125
|
+
>>> # Custom classifier with specific methods
|
|
126
|
+
>>> classifier = SignalClassifier(methods=["statistical", "frequency"])
|
|
127
|
+
>>> result = classifier.classify(signal, sample_rate=1e6, threshold=0.7)
|
|
128
|
+
"""
|
|
129
|
+
|
|
130
|
+
# Classification rules for different signal types
|
|
131
|
+
RULES: ClassVar[list[ClassifierRule]] = [
|
|
132
|
+
# Digital signal: bimodal distribution, many edges
|
|
133
|
+
ClassifierRule(
|
|
134
|
+
"digital",
|
|
135
|
+
{"variance": (0.15, 1.0), "edge_density": (0.01, 1.0)},
|
|
136
|
+
weight=1.0,
|
|
137
|
+
),
|
|
138
|
+
# Analog signal: continuous values, low edge count
|
|
139
|
+
ClassifierRule(
|
|
140
|
+
"analog",
|
|
141
|
+
{"variance": (0.01, 0.5), "edge_density": (0.0, 0.02)},
|
|
142
|
+
weight=1.0,
|
|
143
|
+
),
|
|
144
|
+
# PWM signal: regular duty cycle, periodic
|
|
145
|
+
ClassifierRule(
|
|
146
|
+
"pwm",
|
|
147
|
+
{"duty_cycle": (0.1, 0.9), "periodicity": (0.6, 1.0), "edge_density": (0.01, 0.5)},
|
|
148
|
+
weight=1.2,
|
|
149
|
+
),
|
|
150
|
+
# UART: specific bit timing patterns, moderate edge density
|
|
151
|
+
ClassifierRule(
|
|
152
|
+
"uart",
|
|
153
|
+
{"uart_score": (0.6, 1.0), "edge_density": (0.01, 0.2)},
|
|
154
|
+
weight=1.3,
|
|
155
|
+
),
|
|
156
|
+
# SPI: clock + data patterns, high edge density
|
|
157
|
+
ClassifierRule(
|
|
158
|
+
"spi",
|
|
159
|
+
{"spi_score": (0.6, 1.0), "edge_density": (0.1, 1.0)},
|
|
160
|
+
weight=1.2,
|
|
161
|
+
),
|
|
162
|
+
# I2C: ACK patterns, clock stretching
|
|
163
|
+
ClassifierRule(
|
|
164
|
+
"i2c",
|
|
165
|
+
{"i2c_score": (0.5, 1.0)},
|
|
166
|
+
weight=1.1,
|
|
167
|
+
),
|
|
168
|
+
# CAN: specific encoding
|
|
169
|
+
ClassifierRule(
|
|
170
|
+
"can",
|
|
171
|
+
{"can_score": (0.6, 1.0)},
|
|
172
|
+
weight=1.1,
|
|
173
|
+
),
|
|
174
|
+
]
|
|
175
|
+
|
|
176
|
+
def __init__(self, methods: list[str] | None = None) -> None:
|
|
177
|
+
"""Initialize signal classifier.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
methods: Classification methods to use. Default uses all methods.
|
|
181
|
+
Available: "statistical", "frequency", "pattern"
|
|
182
|
+
|
|
183
|
+
Raises:
|
|
184
|
+
ValueError: If unknown method is specified
|
|
185
|
+
"""
|
|
186
|
+
available_methods = {"statistical", "frequency", "pattern"}
|
|
187
|
+
if methods is None:
|
|
188
|
+
self.methods = ["statistical", "frequency", "pattern"]
|
|
189
|
+
else:
|
|
190
|
+
# Validate methods
|
|
191
|
+
unknown = set(methods) - available_methods
|
|
192
|
+
if unknown:
|
|
193
|
+
raise ValueError(f"Unknown methods: {unknown}. Available: {available_methods}")
|
|
194
|
+
self.methods = methods
|
|
195
|
+
|
|
196
|
+
def classify(
|
|
197
|
+
self,
|
|
198
|
+
signal: NDArray[np.floating[Any]],
|
|
199
|
+
sample_rate: float,
|
|
200
|
+
threshold: float = 0.5,
|
|
201
|
+
) -> ClassificationResult:
|
|
202
|
+
"""Classify single signal.
|
|
203
|
+
|
|
204
|
+
Extracts features using configured methods and applies classification
|
|
205
|
+
rules to determine signal type.
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
signal: Signal data array (voltage samples)
|
|
209
|
+
sample_rate: Sample rate in Hz
|
|
210
|
+
threshold: Minimum confidence for primary classification (0.0-1.0)
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
ClassificationResult with signal type and confidence
|
|
214
|
+
|
|
215
|
+
Raises:
|
|
216
|
+
ValueError: If signal is empty or sample_rate is invalid
|
|
217
|
+
|
|
218
|
+
Example:
|
|
219
|
+
>>> result = classifier.classify(signal, sample_rate=1e6)
|
|
220
|
+
>>> print(f"Type: {result.signal_type}, Confidence: {result.confidence:.2f}")
|
|
221
|
+
Type: uart, Confidence: 0.94
|
|
222
|
+
"""
|
|
223
|
+
if len(signal) == 0:
|
|
224
|
+
raise ValueError("Cannot classify empty signal")
|
|
225
|
+
if sample_rate <= 0:
|
|
226
|
+
raise ValueError(f"sample_rate must be positive, got {sample_rate}")
|
|
227
|
+
if not 0.0 <= threshold <= 1.0:
|
|
228
|
+
raise ValueError(f"threshold must be in [0, 1], got {threshold}")
|
|
229
|
+
|
|
230
|
+
# Extract features using configured methods
|
|
231
|
+
features: dict[str, float] = {}
|
|
232
|
+
|
|
233
|
+
if "statistical" in self.methods:
|
|
234
|
+
features.update(self._extract_statistical_features(signal))
|
|
235
|
+
|
|
236
|
+
if "frequency" in self.methods:
|
|
237
|
+
features.update(self._extract_frequency_features(signal, sample_rate))
|
|
238
|
+
|
|
239
|
+
if "pattern" in self.methods:
|
|
240
|
+
features.update(self._detect_digital_patterns(signal, sample_rate))
|
|
241
|
+
|
|
242
|
+
# Classify from features
|
|
243
|
+
signal_type, confidence, alternatives = self._classify_from_features(features, threshold)
|
|
244
|
+
|
|
245
|
+
# Generate reasoning
|
|
246
|
+
reasoning = self._generate_reasoning(signal_type, features)
|
|
247
|
+
|
|
248
|
+
return ClassificationResult(
|
|
249
|
+
signal_type=signal_type,
|
|
250
|
+
confidence=confidence,
|
|
251
|
+
features=features,
|
|
252
|
+
secondary_matches=alternatives,
|
|
253
|
+
reasoning=reasoning,
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
def classify_batch(
|
|
257
|
+
self,
|
|
258
|
+
signals: list[NDArray[np.floating[Any]]],
|
|
259
|
+
sample_rate: float,
|
|
260
|
+
threshold: float = 0.5,
|
|
261
|
+
) -> list[ClassificationResult]:
|
|
262
|
+
"""Classify multiple signals.
|
|
263
|
+
|
|
264
|
+
Classifies signals sequentially. For large batches, consider using
|
|
265
|
+
multiprocessing for parallel processing.
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
signals: List of signal arrays to classify
|
|
269
|
+
sample_rate: Sample rate in Hz (same for all signals)
|
|
270
|
+
threshold: Minimum confidence for primary classification
|
|
271
|
+
|
|
272
|
+
Returns:
|
|
273
|
+
List of ClassificationResult objects
|
|
274
|
+
|
|
275
|
+
Raises:
|
|
276
|
+
ValueError: If signals list is empty
|
|
277
|
+
|
|
278
|
+
Example:
|
|
279
|
+
>>> results = classifier.classify_batch(signals, sample_rate=1e6)
|
|
280
|
+
>>> for i, result in enumerate(results):
|
|
281
|
+
... print(f"Signal {i}: {result.signal_type}")
|
|
282
|
+
"""
|
|
283
|
+
if not signals:
|
|
284
|
+
raise ValueError("Cannot classify empty signal list")
|
|
285
|
+
|
|
286
|
+
return [self.classify(signal, sample_rate, threshold) for signal in signals]
|
|
287
|
+
|
|
288
|
+
def _extract_statistical_features(
|
|
289
|
+
self,
|
|
290
|
+
signal: NDArray[np.floating[Any]],
|
|
291
|
+
) -> dict[str, float]:
|
|
292
|
+
"""Extract statistical features from signal.
|
|
293
|
+
|
|
294
|
+
Features:
|
|
295
|
+
- mean: Average voltage level
|
|
296
|
+
- variance: Signal variance (normalized)
|
|
297
|
+
- min/max: Voltage range
|
|
298
|
+
- duty_cycle: Fraction of time signal is high (for digital)
|
|
299
|
+
- edge_count: Number of transitions
|
|
300
|
+
- edge_density: Edges per sample
|
|
301
|
+
|
|
302
|
+
Args:
|
|
303
|
+
signal: Signal data array
|
|
304
|
+
|
|
305
|
+
Returns:
|
|
306
|
+
Dictionary of statistical features
|
|
307
|
+
|
|
308
|
+
Example:
|
|
309
|
+
>>> features = classifier._extract_statistical_features(signal)
|
|
310
|
+
>>> print(features['duty_cycle'])
|
|
311
|
+
0.52
|
|
312
|
+
"""
|
|
313
|
+
mean = float(np.mean(signal))
|
|
314
|
+
variance = float(np.var(signal))
|
|
315
|
+
min_val = float(np.min(signal))
|
|
316
|
+
max_val = float(np.max(signal))
|
|
317
|
+
voltage_swing = max_val - min_val
|
|
318
|
+
|
|
319
|
+
# Normalize variance by voltage swing to make it scale-independent
|
|
320
|
+
normalized_variance = variance / (voltage_swing**2 + 1e-10)
|
|
321
|
+
|
|
322
|
+
# Digital features: threshold at midpoint
|
|
323
|
+
threshold = (max_val + min_val) / 2
|
|
324
|
+
digital = (signal > threshold).astype(int)
|
|
325
|
+
edges = np.diff(digital)
|
|
326
|
+
edge_count = int(np.count_nonzero(edges))
|
|
327
|
+
edge_density = edge_count / len(signal) if len(signal) > 0 else 0.0
|
|
328
|
+
|
|
329
|
+
# Duty cycle (fraction of time high)
|
|
330
|
+
duty_cycle = float(np.mean(digital))
|
|
331
|
+
|
|
332
|
+
return {
|
|
333
|
+
"mean": mean,
|
|
334
|
+
"variance": normalized_variance,
|
|
335
|
+
"min": min_val,
|
|
336
|
+
"max": max_val,
|
|
337
|
+
"voltage_swing": voltage_swing,
|
|
338
|
+
"duty_cycle": duty_cycle,
|
|
339
|
+
"edge_count": edge_count,
|
|
340
|
+
"edge_density": edge_density,
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
def _extract_frequency_features(
|
|
344
|
+
self,
|
|
345
|
+
signal: NDArray[np.floating[Any]],
|
|
346
|
+
sample_rate: float,
|
|
347
|
+
) -> dict[str, float]:
|
|
348
|
+
"""Extract frequency domain features via FFT.
|
|
349
|
+
|
|
350
|
+
Features:
|
|
351
|
+
- dominant_frequency: Frequency with highest power (Hz)
|
|
352
|
+
- bandwidth: Frequency range with >10% of peak power (Hz)
|
|
353
|
+
- spectral_centroid: Center of mass of spectrum (Hz)
|
|
354
|
+
- spectral_flatness: Ratio of geometric to arithmetic mean (0-1)
|
|
355
|
+
|
|
356
|
+
Args:
|
|
357
|
+
signal: Signal data array
|
|
358
|
+
sample_rate: Sample rate in Hz
|
|
359
|
+
|
|
360
|
+
Returns:
|
|
361
|
+
Dictionary of frequency domain features
|
|
362
|
+
|
|
363
|
+
Example:
|
|
364
|
+
>>> features = classifier._extract_frequency_features(signal, 1e6)
|
|
365
|
+
>>> print(features['dominant_frequency'])
|
|
366
|
+
115200.0
|
|
367
|
+
"""
|
|
368
|
+
if len(signal) < 2:
|
|
369
|
+
return {
|
|
370
|
+
"dominant_frequency": 0.0,
|
|
371
|
+
"bandwidth": 0.0,
|
|
372
|
+
"spectral_centroid": 0.0,
|
|
373
|
+
"spectral_flatness": 0.0,
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
# Compute FFT
|
|
377
|
+
fft = np.fft.rfft(signal)
|
|
378
|
+
freqs = np.fft.rfftfreq(len(signal), 1.0 / sample_rate)
|
|
379
|
+
magnitude = np.abs(fft)
|
|
380
|
+
|
|
381
|
+
# Dominant frequency (skip DC component)
|
|
382
|
+
if len(magnitude) > 1:
|
|
383
|
+
dominant_idx = np.argmax(magnitude[1:]) + 1
|
|
384
|
+
dominant_frequency = float(freqs[dominant_idx])
|
|
385
|
+
else:
|
|
386
|
+
dominant_frequency = 0.0
|
|
387
|
+
|
|
388
|
+
# Bandwidth (frequencies with >10% of max power)
|
|
389
|
+
max_power = np.max(magnitude)
|
|
390
|
+
if max_power > 0:
|
|
391
|
+
threshold_power = 0.1 * max_power
|
|
392
|
+
active_freqs = freqs[magnitude > threshold_power]
|
|
393
|
+
bandwidth = float(active_freqs[-1] - active_freqs[0]) if len(active_freqs) > 1 else 0.0
|
|
394
|
+
else:
|
|
395
|
+
bandwidth = 0.0
|
|
396
|
+
|
|
397
|
+
# Spectral centroid (center of mass)
|
|
398
|
+
if np.sum(magnitude) > 0:
|
|
399
|
+
spectral_centroid = float(np.sum(freqs * magnitude) / np.sum(magnitude))
|
|
400
|
+
else:
|
|
401
|
+
spectral_centroid = 0.0
|
|
402
|
+
|
|
403
|
+
# Spectral flatness (measure of how noise-like the spectrum is)
|
|
404
|
+
# Geometric mean / arithmetic mean
|
|
405
|
+
# 0 = tonal (single frequency), 1 = noise-like (flat spectrum)
|
|
406
|
+
if len(magnitude) > 0 and np.all(magnitude > 0):
|
|
407
|
+
geometric_mean = np.exp(np.mean(np.log(magnitude + 1e-10)))
|
|
408
|
+
arithmetic_mean = np.mean(magnitude)
|
|
409
|
+
spectral_flatness = float(geometric_mean / (arithmetic_mean + 1e-10))
|
|
410
|
+
else:
|
|
411
|
+
spectral_flatness = 0.0
|
|
412
|
+
|
|
413
|
+
return {
|
|
414
|
+
"dominant_frequency": dominant_frequency,
|
|
415
|
+
"bandwidth": bandwidth,
|
|
416
|
+
"spectral_centroid": spectral_centroid,
|
|
417
|
+
"spectral_flatness": spectral_flatness,
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
def _detect_digital_patterns(
|
|
421
|
+
self,
|
|
422
|
+
signal: NDArray[np.floating[Any]],
|
|
423
|
+
sample_rate: float,
|
|
424
|
+
) -> dict[str, float]:
|
|
425
|
+
"""Detect protocol-specific patterns in signal.
|
|
426
|
+
|
|
427
|
+
Computes scores for:
|
|
428
|
+
- uart_score: UART bit timing alignment
|
|
429
|
+
- spi_score: SPI clock consistency
|
|
430
|
+
- i2c_score: I2C pattern characteristics
|
|
431
|
+
- can_score: CAN encoding characteristics
|
|
432
|
+
- periodicity: Signal periodicity measure
|
|
433
|
+
|
|
434
|
+
Args:
|
|
435
|
+
signal: Signal data array
|
|
436
|
+
sample_rate: Sample rate in Hz
|
|
437
|
+
|
|
438
|
+
Returns:
|
|
439
|
+
Dictionary of pattern detection scores (0.0-1.0)
|
|
440
|
+
|
|
441
|
+
Example:
|
|
442
|
+
>>> patterns = classifier._detect_digital_patterns(signal, 1e6)
|
|
443
|
+
>>> print(patterns['uart_score'])
|
|
444
|
+
0.85
|
|
445
|
+
"""
|
|
446
|
+
# Threshold signal to digital
|
|
447
|
+
threshold = (np.max(signal) + np.min(signal)) / 2
|
|
448
|
+
digital = (signal > threshold).astype(int)
|
|
449
|
+
edges = np.diff(digital)
|
|
450
|
+
edge_indices = np.where(np.abs(edges) > 0)[0]
|
|
451
|
+
|
|
452
|
+
if len(edge_indices) < 3:
|
|
453
|
+
return {
|
|
454
|
+
"uart_score": 0.0,
|
|
455
|
+
"spi_score": 0.0,
|
|
456
|
+
"i2c_score": 0.0,
|
|
457
|
+
"can_score": 0.0,
|
|
458
|
+
"periodicity": 0.0,
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
# Edge intervals
|
|
462
|
+
edge_intervals = np.diff(edge_indices)
|
|
463
|
+
if len(edge_intervals) == 0:
|
|
464
|
+
return {
|
|
465
|
+
"uart_score": 0.0,
|
|
466
|
+
"spi_score": 0.0,
|
|
467
|
+
"i2c_score": 0.0,
|
|
468
|
+
"can_score": 0.0,
|
|
469
|
+
"periodicity": 0.0,
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
# Periodicity score (coefficient of variation)
|
|
473
|
+
mean_interval = np.mean(edge_intervals)
|
|
474
|
+
std_interval = np.std(edge_intervals)
|
|
475
|
+
periodicity = 1.0 - min(1.0, std_interval / (mean_interval + 1e-10))
|
|
476
|
+
|
|
477
|
+
# UART score: check alignment with common baud rates
|
|
478
|
+
uart_score = self._compute_uart_score(edge_intervals, sample_rate)
|
|
479
|
+
|
|
480
|
+
# SPI score: high edge density + consistent timing
|
|
481
|
+
edge_density = len(edge_indices) / len(signal)
|
|
482
|
+
consistency = 1.0 - min(1.0, std_interval / (mean_interval + 1e-10))
|
|
483
|
+
spi_score = min(1.0, edge_density * 10 * consistency)
|
|
484
|
+
|
|
485
|
+
# I2C score: lower edge density than SPI, some irregularity (clock stretching)
|
|
486
|
+
# I2C typically has burst patterns with pauses
|
|
487
|
+
i2c_score = 0.5 if 0.05 < edge_density < 0.3 and periodicity < 0.9 else 0.0
|
|
488
|
+
|
|
489
|
+
# CAN score: similar to digital but with specific encoding patterns
|
|
490
|
+
# CAN uses bit stuffing - look for irregularity in bit timing
|
|
491
|
+
can_score = 0.5 if 0.7 < periodicity < 0.95 and edge_density > 0.1 else 0.0
|
|
492
|
+
|
|
493
|
+
return {
|
|
494
|
+
"uart_score": uart_score,
|
|
495
|
+
"spi_score": spi_score,
|
|
496
|
+
"i2c_score": i2c_score,
|
|
497
|
+
"can_score": can_score,
|
|
498
|
+
"periodicity": periodicity,
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
def _compute_uart_score(
|
|
502
|
+
self,
|
|
503
|
+
edge_intervals: NDArray[np.integer[Any]],
|
|
504
|
+
sample_rate: float,
|
|
505
|
+
) -> float:
|
|
506
|
+
"""Compute UART likelihood score based on baud rate alignment.
|
|
507
|
+
|
|
508
|
+
Checks if edge intervals align with common UART baud rates.
|
|
509
|
+
|
|
510
|
+
Args:
|
|
511
|
+
edge_intervals: Array of sample counts between edges
|
|
512
|
+
sample_rate: Sample rate in Hz
|
|
513
|
+
|
|
514
|
+
Returns:
|
|
515
|
+
UART score (0.0-1.0)
|
|
516
|
+
"""
|
|
517
|
+
common_bauds = [9600, 19200, 38400, 57600, 115200, 230400, 460800, 921600]
|
|
518
|
+
baud_scores = []
|
|
519
|
+
|
|
520
|
+
for baud in common_bauds:
|
|
521
|
+
bit_period_samples = sample_rate / baud
|
|
522
|
+
# Count edges that align with this baud rate (within 20% tolerance)
|
|
523
|
+
tolerance = 0.2
|
|
524
|
+
aligned = np.sum(
|
|
525
|
+
np.abs(edge_intervals % bit_period_samples) < bit_period_samples * tolerance
|
|
526
|
+
)
|
|
527
|
+
score = aligned / len(edge_intervals) if len(edge_intervals) > 0 else 0.0
|
|
528
|
+
baud_scores.append(score)
|
|
529
|
+
|
|
530
|
+
return float(max(baud_scores)) if baud_scores else 0.0
|
|
531
|
+
|
|
532
|
+
def _classify_from_features(
|
|
533
|
+
self,
|
|
534
|
+
features: dict[str, float],
|
|
535
|
+
threshold: float,
|
|
536
|
+
) -> tuple[SignalType, float, list[tuple[SignalType, float]]]:
|
|
537
|
+
"""Make classification decision from extracted features.
|
|
538
|
+
|
|
539
|
+
Applies classification rules and selects best match.
|
|
540
|
+
|
|
541
|
+
Args:
|
|
542
|
+
features: Dictionary of extracted features
|
|
543
|
+
threshold: Minimum confidence for primary classification
|
|
544
|
+
|
|
545
|
+
Returns:
|
|
546
|
+
Tuple of (signal_type, confidence, alternatives)
|
|
547
|
+
alternatives is list of (type, confidence) for secondary matches
|
|
548
|
+
"""
|
|
549
|
+
# Evaluate all rules
|
|
550
|
+
scores: dict[SignalType, float] = {}
|
|
551
|
+
|
|
552
|
+
for rule in self.RULES:
|
|
553
|
+
score = self._evaluate_rule(rule, features)
|
|
554
|
+
scores[rule.signal_type] = score
|
|
555
|
+
|
|
556
|
+
# Sort by score
|
|
557
|
+
sorted_scores = sorted(scores.items(), key=lambda x: x[1], reverse=True)
|
|
558
|
+
|
|
559
|
+
# Best match
|
|
560
|
+
best_type, best_score = sorted_scores[0]
|
|
561
|
+
|
|
562
|
+
# If score is below threshold, mark as unknown
|
|
563
|
+
if best_score < threshold:
|
|
564
|
+
return "unknown", best_score, [(best_type, best_score)]
|
|
565
|
+
|
|
566
|
+
# Collect alternatives (other types with score >= threshold * 0.6)
|
|
567
|
+
alt_threshold = threshold * 0.6
|
|
568
|
+
alternatives = [
|
|
569
|
+
(sig_type, score) for sig_type, score in sorted_scores[1:] if score >= alt_threshold
|
|
570
|
+
]
|
|
571
|
+
|
|
572
|
+
return best_type, best_score, alternatives
|
|
573
|
+
|
|
574
|
+
def _evaluate_rule(
|
|
575
|
+
self,
|
|
576
|
+
rule: ClassifierRule,
|
|
577
|
+
features: dict[str, float],
|
|
578
|
+
) -> float:
|
|
579
|
+
"""Evaluate classification rule against features.
|
|
580
|
+
|
|
581
|
+
Args:
|
|
582
|
+
rule: Classification rule to evaluate
|
|
583
|
+
features: Extracted features
|
|
584
|
+
|
|
585
|
+
Returns:
|
|
586
|
+
Match score (0.0-1.0), weighted by rule weight
|
|
587
|
+
"""
|
|
588
|
+
matches = 0
|
|
589
|
+
total = len(rule.conditions)
|
|
590
|
+
|
|
591
|
+
for feature_name, (min_val, max_val) in rule.conditions.items():
|
|
592
|
+
if feature_name in features:
|
|
593
|
+
value = features[feature_name]
|
|
594
|
+
if min_val <= value <= max_val:
|
|
595
|
+
matches += 1
|
|
596
|
+
|
|
597
|
+
# Base score is fraction of conditions met
|
|
598
|
+
base_score = matches / total if total > 0 else 0.0
|
|
599
|
+
|
|
600
|
+
# Apply rule weight
|
|
601
|
+
weighted_score = base_score * rule.weight
|
|
602
|
+
|
|
603
|
+
return min(1.0, weighted_score)
|
|
604
|
+
|
|
605
|
+
def _generate_reasoning(
|
|
606
|
+
self,
|
|
607
|
+
signal_type: SignalType,
|
|
608
|
+
features: dict[str, float],
|
|
609
|
+
) -> str:
|
|
610
|
+
"""Generate human-readable explanation of classification.
|
|
611
|
+
|
|
612
|
+
Args:
|
|
613
|
+
signal_type: Classified signal type
|
|
614
|
+
features: Features used for classification
|
|
615
|
+
|
|
616
|
+
Returns:
|
|
617
|
+
Reasoning string
|
|
618
|
+
"""
|
|
619
|
+
if signal_type == "digital":
|
|
620
|
+
return (
|
|
621
|
+
f"Digital signal detected: high variance ({features.get('variance', 0):.2f}), "
|
|
622
|
+
f"edge density {features.get('edge_density', 0):.3f}"
|
|
623
|
+
)
|
|
624
|
+
elif signal_type == "analog":
|
|
625
|
+
return (
|
|
626
|
+
f"Analog signal detected: low edge density ({features.get('edge_density', 0):.3f}), "
|
|
627
|
+
f"continuous values"
|
|
628
|
+
)
|
|
629
|
+
elif signal_type == "pwm":
|
|
630
|
+
return (
|
|
631
|
+
f"PWM signal detected: periodic pattern (periodicity {features.get('periodicity', 0):.2f}), "
|
|
632
|
+
f"duty cycle {features.get('duty_cycle', 0):.2f}"
|
|
633
|
+
)
|
|
634
|
+
elif signal_type == "uart":
|
|
635
|
+
return (
|
|
636
|
+
f"UART signal detected: baud rate alignment score {features.get('uart_score', 0):.2f}, "
|
|
637
|
+
f"edge density {features.get('edge_density', 0):.3f}"
|
|
638
|
+
)
|
|
639
|
+
elif signal_type == "spi":
|
|
640
|
+
return (
|
|
641
|
+
f"SPI signal detected: high edge density ({features.get('edge_density', 0):.3f}), "
|
|
642
|
+
f"consistent timing"
|
|
643
|
+
)
|
|
644
|
+
elif signal_type == "i2c":
|
|
645
|
+
return f"I2C signal detected: characteristic patterns (score {features.get('i2c_score', 0):.2f})"
|
|
646
|
+
elif signal_type == "can":
|
|
647
|
+
return (
|
|
648
|
+
f"CAN signal detected: encoding patterns (score {features.get('can_score', 0):.2f})"
|
|
649
|
+
)
|
|
650
|
+
else:
|
|
651
|
+
return "Signal type unclear: low confidence in all classifications"
|
|
652
|
+
|
|
653
|
+
|
|
654
|
+
__all__ = [
|
|
655
|
+
"ClassificationResult",
|
|
656
|
+
"ClassifierRule",
|
|
657
|
+
"SignalClassifier",
|
|
658
|
+
"SignalType",
|
|
659
|
+
]
|