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.
Files changed (497) hide show
  1. oscura/__init__.py +169 -167
  2. oscura/analyzers/__init__.py +3 -0
  3. oscura/analyzers/classification.py +659 -0
  4. oscura/analyzers/digital/edges.py +325 -65
  5. oscura/analyzers/digital/quality.py +293 -166
  6. oscura/analyzers/digital/timing.py +260 -115
  7. oscura/analyzers/digital/timing_numba.py +334 -0
  8. oscura/analyzers/entropy.py +605 -0
  9. oscura/analyzers/eye/diagram.py +176 -109
  10. oscura/analyzers/eye/metrics.py +5 -5
  11. oscura/analyzers/jitter/__init__.py +6 -4
  12. oscura/analyzers/jitter/ber.py +52 -52
  13. oscura/analyzers/jitter/classification.py +156 -0
  14. oscura/analyzers/jitter/decomposition.py +163 -113
  15. oscura/analyzers/jitter/spectrum.py +80 -64
  16. oscura/analyzers/ml/__init__.py +39 -0
  17. oscura/analyzers/ml/features.py +600 -0
  18. oscura/analyzers/ml/signal_classifier.py +604 -0
  19. oscura/analyzers/packet/daq.py +246 -158
  20. oscura/analyzers/packet/parser.py +12 -1
  21. oscura/analyzers/packet/payload.py +50 -2110
  22. oscura/analyzers/packet/payload_analysis.py +361 -181
  23. oscura/analyzers/packet/payload_patterns.py +133 -70
  24. oscura/analyzers/packet/stream.py +84 -23
  25. oscura/analyzers/patterns/__init__.py +26 -5
  26. oscura/analyzers/patterns/anomaly_detection.py +908 -0
  27. oscura/analyzers/patterns/clustering.py +169 -108
  28. oscura/analyzers/patterns/clustering_optimized.py +227 -0
  29. oscura/analyzers/patterns/discovery.py +1 -1
  30. oscura/analyzers/patterns/matching.py +581 -197
  31. oscura/analyzers/patterns/pattern_mining.py +778 -0
  32. oscura/analyzers/patterns/periodic.py +121 -38
  33. oscura/analyzers/patterns/sequences.py +175 -78
  34. oscura/analyzers/power/conduction.py +1 -1
  35. oscura/analyzers/power/soa.py +6 -6
  36. oscura/analyzers/power/switching.py +250 -110
  37. oscura/analyzers/protocol/__init__.py +17 -1
  38. oscura/analyzers/protocols/base.py +6 -6
  39. oscura/analyzers/protocols/ble/__init__.py +38 -0
  40. oscura/analyzers/protocols/ble/analyzer.py +809 -0
  41. oscura/analyzers/protocols/ble/uuids.py +288 -0
  42. oscura/analyzers/protocols/can.py +257 -127
  43. oscura/analyzers/protocols/can_fd.py +107 -80
  44. oscura/analyzers/protocols/flexray.py +139 -80
  45. oscura/analyzers/protocols/hdlc.py +93 -58
  46. oscura/analyzers/protocols/i2c.py +247 -106
  47. oscura/analyzers/protocols/i2s.py +138 -86
  48. oscura/analyzers/protocols/industrial/__init__.py +40 -0
  49. oscura/analyzers/protocols/industrial/bacnet/__init__.py +33 -0
  50. oscura/analyzers/protocols/industrial/bacnet/analyzer.py +708 -0
  51. oscura/analyzers/protocols/industrial/bacnet/encoding.py +412 -0
  52. oscura/analyzers/protocols/industrial/bacnet/services.py +622 -0
  53. oscura/analyzers/protocols/industrial/ethercat/__init__.py +30 -0
  54. oscura/analyzers/protocols/industrial/ethercat/analyzer.py +474 -0
  55. oscura/analyzers/protocols/industrial/ethercat/mailbox.py +339 -0
  56. oscura/analyzers/protocols/industrial/ethercat/topology.py +166 -0
  57. oscura/analyzers/protocols/industrial/modbus/__init__.py +31 -0
  58. oscura/analyzers/protocols/industrial/modbus/analyzer.py +525 -0
  59. oscura/analyzers/protocols/industrial/modbus/crc.py +79 -0
  60. oscura/analyzers/protocols/industrial/modbus/functions.py +436 -0
  61. oscura/analyzers/protocols/industrial/opcua/__init__.py +21 -0
  62. oscura/analyzers/protocols/industrial/opcua/analyzer.py +552 -0
  63. oscura/analyzers/protocols/industrial/opcua/datatypes.py +446 -0
  64. oscura/analyzers/protocols/industrial/opcua/services.py +264 -0
  65. oscura/analyzers/protocols/industrial/profinet/__init__.py +23 -0
  66. oscura/analyzers/protocols/industrial/profinet/analyzer.py +441 -0
  67. oscura/analyzers/protocols/industrial/profinet/dcp.py +263 -0
  68. oscura/analyzers/protocols/industrial/profinet/ptcp.py +200 -0
  69. oscura/analyzers/protocols/jtag.py +180 -98
  70. oscura/analyzers/protocols/lin.py +219 -114
  71. oscura/analyzers/protocols/manchester.py +4 -4
  72. oscura/analyzers/protocols/onewire.py +253 -149
  73. oscura/analyzers/protocols/parallel_bus/__init__.py +20 -0
  74. oscura/analyzers/protocols/parallel_bus/centronics.py +92 -0
  75. oscura/analyzers/protocols/parallel_bus/gpib.py +137 -0
  76. oscura/analyzers/protocols/spi.py +192 -95
  77. oscura/analyzers/protocols/swd.py +321 -167
  78. oscura/analyzers/protocols/uart.py +267 -125
  79. oscura/analyzers/protocols/usb.py +235 -131
  80. oscura/analyzers/side_channel/power.py +17 -12
  81. oscura/analyzers/signal/__init__.py +15 -0
  82. oscura/analyzers/signal/timing_analysis.py +1086 -0
  83. oscura/analyzers/signal_integrity/__init__.py +4 -1
  84. oscura/analyzers/signal_integrity/sparams.py +2 -19
  85. oscura/analyzers/spectral/chunked.py +129 -60
  86. oscura/analyzers/spectral/chunked_fft.py +300 -94
  87. oscura/analyzers/spectral/chunked_wavelet.py +100 -80
  88. oscura/analyzers/statistical/checksum.py +376 -217
  89. oscura/analyzers/statistical/classification.py +229 -107
  90. oscura/analyzers/statistical/entropy.py +78 -53
  91. oscura/analyzers/statistics/correlation.py +407 -211
  92. oscura/analyzers/statistics/outliers.py +2 -2
  93. oscura/analyzers/statistics/streaming.py +30 -5
  94. oscura/analyzers/validation.py +216 -101
  95. oscura/analyzers/waveform/measurements.py +9 -0
  96. oscura/analyzers/waveform/measurements_with_uncertainty.py +31 -15
  97. oscura/analyzers/waveform/spectral.py +500 -228
  98. oscura/api/__init__.py +31 -5
  99. oscura/api/dsl/__init__.py +582 -0
  100. oscura/{dsl → api/dsl}/commands.py +43 -76
  101. oscura/{dsl → api/dsl}/interpreter.py +26 -51
  102. oscura/{dsl → api/dsl}/parser.py +107 -77
  103. oscura/{dsl → api/dsl}/repl.py +2 -2
  104. oscura/api/dsl.py +1 -1
  105. oscura/{integrations → api/integrations}/__init__.py +1 -1
  106. oscura/{integrations → api/integrations}/llm.py +201 -102
  107. oscura/api/operators.py +3 -3
  108. oscura/api/optimization.py +144 -30
  109. oscura/api/rest_server.py +921 -0
  110. oscura/api/server/__init__.py +17 -0
  111. oscura/api/server/dashboard.py +850 -0
  112. oscura/api/server/static/README.md +34 -0
  113. oscura/api/server/templates/base.html +181 -0
  114. oscura/api/server/templates/export.html +120 -0
  115. oscura/api/server/templates/home.html +284 -0
  116. oscura/api/server/templates/protocols.html +58 -0
  117. oscura/api/server/templates/reports.html +43 -0
  118. oscura/api/server/templates/session_detail.html +89 -0
  119. oscura/api/server/templates/sessions.html +83 -0
  120. oscura/api/server/templates/waveforms.html +73 -0
  121. oscura/automotive/__init__.py +8 -1
  122. oscura/automotive/can/__init__.py +10 -0
  123. oscura/automotive/can/checksum.py +3 -1
  124. oscura/automotive/can/dbc_generator.py +590 -0
  125. oscura/automotive/can/message_wrapper.py +121 -74
  126. oscura/automotive/can/patterns.py +98 -21
  127. oscura/automotive/can/session.py +292 -56
  128. oscura/automotive/can/state_machine.py +6 -3
  129. oscura/automotive/can/stimulus_response.py +97 -75
  130. oscura/automotive/dbc/__init__.py +10 -2
  131. oscura/automotive/dbc/generator.py +84 -56
  132. oscura/automotive/dbc/parser.py +6 -6
  133. oscura/automotive/dtc/data.json +17 -102
  134. oscura/automotive/dtc/database.py +2 -2
  135. oscura/automotive/flexray/__init__.py +31 -0
  136. oscura/automotive/flexray/analyzer.py +504 -0
  137. oscura/automotive/flexray/crc.py +185 -0
  138. oscura/automotive/flexray/fibex.py +449 -0
  139. oscura/automotive/j1939/__init__.py +45 -8
  140. oscura/automotive/j1939/analyzer.py +605 -0
  141. oscura/automotive/j1939/spns.py +326 -0
  142. oscura/automotive/j1939/transport.py +306 -0
  143. oscura/automotive/lin/__init__.py +47 -0
  144. oscura/automotive/lin/analyzer.py +612 -0
  145. oscura/automotive/loaders/blf.py +13 -2
  146. oscura/automotive/loaders/csv_can.py +143 -72
  147. oscura/automotive/loaders/dispatcher.py +50 -2
  148. oscura/automotive/loaders/mdf.py +86 -45
  149. oscura/automotive/loaders/pcap.py +111 -61
  150. oscura/automotive/uds/__init__.py +4 -0
  151. oscura/automotive/uds/analyzer.py +725 -0
  152. oscura/automotive/uds/decoder.py +140 -58
  153. oscura/automotive/uds/models.py +7 -1
  154. oscura/automotive/visualization.py +1 -1
  155. oscura/cli/analyze.py +348 -0
  156. oscura/cli/batch.py +142 -122
  157. oscura/cli/benchmark.py +275 -0
  158. oscura/cli/characterize.py +137 -82
  159. oscura/cli/compare.py +224 -131
  160. oscura/cli/completion.py +250 -0
  161. oscura/cli/config_cmd.py +361 -0
  162. oscura/cli/decode.py +164 -87
  163. oscura/cli/export.py +286 -0
  164. oscura/cli/main.py +115 -31
  165. oscura/{onboarding → cli/onboarding}/__init__.py +3 -3
  166. oscura/{onboarding → cli/onboarding}/help.py +80 -58
  167. oscura/{onboarding → cli/onboarding}/tutorials.py +97 -72
  168. oscura/{onboarding → cli/onboarding}/wizard.py +55 -36
  169. oscura/cli/progress.py +147 -0
  170. oscura/cli/shell.py +157 -135
  171. oscura/cli/validate_cmd.py +204 -0
  172. oscura/cli/visualize.py +158 -0
  173. oscura/convenience.py +125 -79
  174. oscura/core/__init__.py +4 -2
  175. oscura/core/backend_selector.py +3 -3
  176. oscura/core/cache.py +126 -15
  177. oscura/core/cancellation.py +1 -1
  178. oscura/{config → core/config}/__init__.py +20 -11
  179. oscura/{config → core/config}/defaults.py +1 -1
  180. oscura/{config → core/config}/loader.py +7 -5
  181. oscura/{config → core/config}/memory.py +5 -5
  182. oscura/{config → core/config}/migration.py +1 -1
  183. oscura/{config → core/config}/pipeline.py +99 -23
  184. oscura/{config → core/config}/preferences.py +1 -1
  185. oscura/{config → core/config}/protocol.py +3 -3
  186. oscura/{config → core/config}/schema.py +426 -272
  187. oscura/{config → core/config}/settings.py +1 -1
  188. oscura/{config → core/config}/thresholds.py +195 -153
  189. oscura/core/correlation.py +5 -6
  190. oscura/core/cross_domain.py +0 -2
  191. oscura/core/debug.py +9 -5
  192. oscura/{extensibility → core/extensibility}/docs.py +158 -70
  193. oscura/{extensibility → core/extensibility}/extensions.py +160 -76
  194. oscura/{extensibility → core/extensibility}/logging.py +1 -1
  195. oscura/{extensibility → core/extensibility}/measurements.py +1 -1
  196. oscura/{extensibility → core/extensibility}/plugins.py +1 -1
  197. oscura/{extensibility → core/extensibility}/templates.py +73 -3
  198. oscura/{extensibility → core/extensibility}/validation.py +1 -1
  199. oscura/core/gpu_backend.py +11 -7
  200. oscura/core/log_query.py +101 -11
  201. oscura/core/logging.py +126 -54
  202. oscura/core/logging_advanced.py +5 -5
  203. oscura/core/memory_limits.py +108 -70
  204. oscura/core/memory_monitor.py +2 -2
  205. oscura/core/memory_progress.py +7 -7
  206. oscura/core/memory_warnings.py +1 -1
  207. oscura/core/numba_backend.py +13 -13
  208. oscura/{plugins → core/plugins}/__init__.py +9 -9
  209. oscura/{plugins → core/plugins}/base.py +7 -7
  210. oscura/{plugins → core/plugins}/cli.py +3 -3
  211. oscura/{plugins → core/plugins}/discovery.py +186 -106
  212. oscura/{plugins → core/plugins}/lifecycle.py +1 -1
  213. oscura/{plugins → core/plugins}/manager.py +7 -7
  214. oscura/{plugins → core/plugins}/registry.py +3 -3
  215. oscura/{plugins → core/plugins}/versioning.py +1 -1
  216. oscura/core/progress.py +16 -1
  217. oscura/core/provenance.py +8 -2
  218. oscura/{schemas → core/schemas}/__init__.py +2 -2
  219. oscura/{schemas → core/schemas}/device_mapping.json +2 -8
  220. oscura/{schemas → core/schemas}/packet_format.json +4 -24
  221. oscura/{schemas → core/schemas}/protocol_definition.json +2 -12
  222. oscura/core/types.py +4 -0
  223. oscura/core/uncertainty.py +3 -3
  224. oscura/correlation/__init__.py +52 -0
  225. oscura/correlation/multi_protocol.py +811 -0
  226. oscura/discovery/auto_decoder.py +117 -35
  227. oscura/discovery/comparison.py +191 -86
  228. oscura/discovery/quality_validator.py +155 -68
  229. oscura/discovery/signal_detector.py +196 -79
  230. oscura/export/__init__.py +18 -8
  231. oscura/export/kaitai_struct.py +513 -0
  232. oscura/export/scapy_layer.py +801 -0
  233. oscura/export/wireshark/generator.py +1 -1
  234. oscura/export/wireshark/templates/dissector.lua.j2 +2 -2
  235. oscura/export/wireshark_dissector.py +746 -0
  236. oscura/guidance/wizard.py +207 -111
  237. oscura/hardware/__init__.py +19 -0
  238. oscura/{acquisition → hardware/acquisition}/__init__.py +4 -4
  239. oscura/{acquisition → hardware/acquisition}/file.py +2 -2
  240. oscura/{acquisition → hardware/acquisition}/hardware.py +7 -7
  241. oscura/{acquisition → hardware/acquisition}/saleae.py +15 -12
  242. oscura/{acquisition → hardware/acquisition}/socketcan.py +1 -1
  243. oscura/{acquisition → hardware/acquisition}/streaming.py +2 -2
  244. oscura/{acquisition → hardware/acquisition}/synthetic.py +3 -3
  245. oscura/{acquisition → hardware/acquisition}/visa.py +33 -11
  246. oscura/hardware/firmware/__init__.py +29 -0
  247. oscura/hardware/firmware/pattern_recognition.py +874 -0
  248. oscura/hardware/hal_detector.py +736 -0
  249. oscura/hardware/security/__init__.py +37 -0
  250. oscura/hardware/security/side_channel_detector.py +1126 -0
  251. oscura/inference/__init__.py +4 -0
  252. oscura/inference/active_learning/observation_table.py +4 -1
  253. oscura/inference/alignment.py +216 -123
  254. oscura/inference/bayesian.py +113 -33
  255. oscura/inference/crc_reverse.py +101 -55
  256. oscura/inference/logic.py +6 -2
  257. oscura/inference/message_format.py +342 -183
  258. oscura/inference/protocol.py +95 -44
  259. oscura/inference/protocol_dsl.py +180 -82
  260. oscura/inference/signal_intelligence.py +1439 -706
  261. oscura/inference/spectral.py +99 -57
  262. oscura/inference/state_machine.py +810 -158
  263. oscura/inference/stream.py +270 -110
  264. oscura/iot/__init__.py +34 -0
  265. oscura/iot/coap/__init__.py +32 -0
  266. oscura/iot/coap/analyzer.py +668 -0
  267. oscura/iot/coap/options.py +212 -0
  268. oscura/iot/lorawan/__init__.py +21 -0
  269. oscura/iot/lorawan/crypto.py +206 -0
  270. oscura/iot/lorawan/decoder.py +801 -0
  271. oscura/iot/lorawan/mac_commands.py +341 -0
  272. oscura/iot/mqtt/__init__.py +27 -0
  273. oscura/iot/mqtt/analyzer.py +999 -0
  274. oscura/iot/mqtt/properties.py +315 -0
  275. oscura/iot/zigbee/__init__.py +31 -0
  276. oscura/iot/zigbee/analyzer.py +615 -0
  277. oscura/iot/zigbee/security.py +153 -0
  278. oscura/iot/zigbee/zcl.py +349 -0
  279. oscura/jupyter/display.py +125 -45
  280. oscura/{exploratory → jupyter/exploratory}/__init__.py +8 -8
  281. oscura/{exploratory → jupyter/exploratory}/error_recovery.py +298 -141
  282. oscura/jupyter/exploratory/fuzzy.py +746 -0
  283. oscura/{exploratory → jupyter/exploratory}/fuzzy_advanced.py +258 -100
  284. oscura/{exploratory → jupyter/exploratory}/legacy.py +464 -242
  285. oscura/{exploratory → jupyter/exploratory}/parse.py +167 -145
  286. oscura/{exploratory → jupyter/exploratory}/recovery.py +119 -87
  287. oscura/jupyter/exploratory/sync.py +612 -0
  288. oscura/{exploratory → jupyter/exploratory}/unknown.py +299 -176
  289. oscura/jupyter/magic.py +4 -4
  290. oscura/{ui → jupyter/ui}/__init__.py +2 -2
  291. oscura/{ui → jupyter/ui}/formatters.py +3 -3
  292. oscura/{ui → jupyter/ui}/progressive_display.py +153 -82
  293. oscura/loaders/__init__.py +183 -67
  294. oscura/loaders/binary.py +88 -1
  295. oscura/loaders/chipwhisperer.py +153 -137
  296. oscura/loaders/configurable.py +208 -86
  297. oscura/loaders/csv_loader.py +458 -215
  298. oscura/loaders/hdf5_loader.py +278 -119
  299. oscura/loaders/lazy.py +87 -54
  300. oscura/loaders/mmap_loader.py +1 -1
  301. oscura/loaders/numpy_loader.py +253 -116
  302. oscura/loaders/pcap.py +226 -151
  303. oscura/loaders/rigol.py +110 -49
  304. oscura/loaders/sigrok.py +201 -78
  305. oscura/loaders/tdms.py +81 -58
  306. oscura/loaders/tektronix.py +291 -174
  307. oscura/loaders/touchstone.py +182 -87
  308. oscura/loaders/tss.py +456 -0
  309. oscura/loaders/vcd.py +215 -117
  310. oscura/loaders/wav.py +155 -68
  311. oscura/reporting/__init__.py +9 -0
  312. oscura/reporting/analyze.py +352 -146
  313. oscura/reporting/argument_preparer.py +69 -14
  314. oscura/reporting/auto_report.py +97 -61
  315. oscura/reporting/batch.py +131 -58
  316. oscura/reporting/chart_selection.py +57 -45
  317. oscura/reporting/comparison.py +63 -17
  318. oscura/reporting/content/executive.py +76 -24
  319. oscura/reporting/core_formats/multi_format.py +11 -8
  320. oscura/reporting/engine.py +312 -158
  321. oscura/reporting/enhanced_reports.py +949 -0
  322. oscura/reporting/export.py +86 -43
  323. oscura/reporting/formatting/numbers.py +69 -42
  324. oscura/reporting/html.py +139 -58
  325. oscura/reporting/index.py +137 -65
  326. oscura/reporting/output.py +158 -67
  327. oscura/reporting/pdf.py +67 -102
  328. oscura/reporting/plots.py +191 -112
  329. oscura/reporting/sections.py +88 -47
  330. oscura/reporting/standards.py +104 -61
  331. oscura/reporting/summary_generator.py +75 -55
  332. oscura/reporting/tables.py +138 -54
  333. oscura/reporting/templates/enhanced/protocol_re.html +525 -0
  334. oscura/sessions/__init__.py +14 -23
  335. oscura/sessions/base.py +3 -3
  336. oscura/sessions/blackbox.py +106 -10
  337. oscura/sessions/generic.py +2 -2
  338. oscura/sessions/legacy.py +783 -0
  339. oscura/side_channel/__init__.py +63 -0
  340. oscura/side_channel/dpa.py +1025 -0
  341. oscura/utils/__init__.py +15 -1
  342. oscura/utils/bitwise.py +118 -0
  343. oscura/{builders → utils/builders}/__init__.py +1 -1
  344. oscura/{comparison → utils/comparison}/__init__.py +6 -6
  345. oscura/{comparison → utils/comparison}/compare.py +202 -101
  346. oscura/{comparison → utils/comparison}/golden.py +83 -63
  347. oscura/{comparison → utils/comparison}/limits.py +313 -89
  348. oscura/{comparison → utils/comparison}/mask.py +151 -45
  349. oscura/{comparison → utils/comparison}/trace_diff.py +1 -1
  350. oscura/{comparison → utils/comparison}/visualization.py +147 -89
  351. oscura/{component → utils/component}/__init__.py +3 -3
  352. oscura/{component → utils/component}/impedance.py +122 -58
  353. oscura/{component → utils/component}/reactive.py +165 -168
  354. oscura/{component → utils/component}/transmission_line.py +3 -3
  355. oscura/{filtering → utils/filtering}/__init__.py +6 -6
  356. oscura/{filtering → utils/filtering}/base.py +1 -1
  357. oscura/{filtering → utils/filtering}/convenience.py +2 -2
  358. oscura/{filtering → utils/filtering}/design.py +169 -93
  359. oscura/{filtering → utils/filtering}/filters.py +2 -2
  360. oscura/{filtering → utils/filtering}/introspection.py +2 -2
  361. oscura/utils/geometry.py +31 -0
  362. oscura/utils/imports.py +184 -0
  363. oscura/utils/lazy.py +1 -1
  364. oscura/{math → utils/math}/__init__.py +2 -2
  365. oscura/{math → utils/math}/arithmetic.py +114 -48
  366. oscura/{math → utils/math}/interpolation.py +139 -106
  367. oscura/utils/memory.py +129 -66
  368. oscura/utils/memory_advanced.py +92 -9
  369. oscura/utils/memory_extensions.py +10 -8
  370. oscura/{optimization → utils/optimization}/__init__.py +1 -1
  371. oscura/{optimization → utils/optimization}/search.py +2 -2
  372. oscura/utils/performance/__init__.py +58 -0
  373. oscura/utils/performance/caching.py +889 -0
  374. oscura/utils/performance/lsh_clustering.py +333 -0
  375. oscura/utils/performance/memory_optimizer.py +699 -0
  376. oscura/utils/performance/optimizations.py +675 -0
  377. oscura/utils/performance/parallel.py +654 -0
  378. oscura/utils/performance/profiling.py +661 -0
  379. oscura/{pipeline → utils/pipeline}/base.py +1 -1
  380. oscura/{pipeline → utils/pipeline}/composition.py +1 -1
  381. oscura/{pipeline → utils/pipeline}/parallel.py +3 -2
  382. oscura/{pipeline → utils/pipeline}/pipeline.py +1 -1
  383. oscura/{pipeline → utils/pipeline}/reverse_engineering.py +412 -221
  384. oscura/{search → utils/search}/__init__.py +3 -3
  385. oscura/{search → utils/search}/anomaly.py +188 -58
  386. oscura/utils/search/context.py +294 -0
  387. oscura/{search → utils/search}/pattern.py +138 -10
  388. oscura/utils/serial.py +51 -0
  389. oscura/utils/storage/__init__.py +61 -0
  390. oscura/utils/storage/database.py +1166 -0
  391. oscura/{streaming → utils/streaming}/chunked.py +302 -143
  392. oscura/{streaming → utils/streaming}/progressive.py +1 -1
  393. oscura/{streaming → utils/streaming}/realtime.py +3 -2
  394. oscura/{triggering → utils/triggering}/__init__.py +6 -6
  395. oscura/{triggering → utils/triggering}/base.py +6 -6
  396. oscura/{triggering → utils/triggering}/edge.py +2 -2
  397. oscura/{triggering → utils/triggering}/pattern.py +2 -2
  398. oscura/{triggering → utils/triggering}/pulse.py +115 -74
  399. oscura/{triggering → utils/triggering}/window.py +2 -2
  400. oscura/utils/validation.py +32 -0
  401. oscura/validation/__init__.py +121 -0
  402. oscura/{compliance → validation/compliance}/__init__.py +5 -5
  403. oscura/{compliance → validation/compliance}/advanced.py +5 -5
  404. oscura/{compliance → validation/compliance}/masks.py +1 -1
  405. oscura/{compliance → validation/compliance}/reporting.py +127 -53
  406. oscura/{compliance → validation/compliance}/testing.py +114 -52
  407. oscura/validation/compliance_tests.py +915 -0
  408. oscura/validation/fuzzer.py +990 -0
  409. oscura/validation/grammar_tests.py +596 -0
  410. oscura/validation/grammar_validator.py +904 -0
  411. oscura/validation/hil_testing.py +977 -0
  412. oscura/{quality → validation/quality}/__init__.py +4 -4
  413. oscura/{quality → validation/quality}/ensemble.py +251 -171
  414. oscura/{quality → validation/quality}/explainer.py +3 -3
  415. oscura/{quality → validation/quality}/scoring.py +1 -1
  416. oscura/{quality → validation/quality}/warnings.py +4 -4
  417. oscura/validation/regression_suite.py +808 -0
  418. oscura/validation/replay.py +788 -0
  419. oscura/{testing → validation/testing}/__init__.py +2 -2
  420. oscura/{testing → validation/testing}/synthetic.py +5 -5
  421. oscura/visualization/__init__.py +9 -0
  422. oscura/visualization/accessibility.py +1 -1
  423. oscura/visualization/annotations.py +64 -67
  424. oscura/visualization/colors.py +7 -7
  425. oscura/visualization/digital.py +180 -81
  426. oscura/visualization/eye.py +236 -85
  427. oscura/visualization/interactive.py +320 -143
  428. oscura/visualization/jitter.py +587 -247
  429. oscura/visualization/layout.py +169 -134
  430. oscura/visualization/optimization.py +103 -52
  431. oscura/visualization/palettes.py +1 -1
  432. oscura/visualization/power.py +427 -211
  433. oscura/visualization/power_extended.py +626 -297
  434. oscura/visualization/presets.py +2 -0
  435. oscura/visualization/protocols.py +495 -181
  436. oscura/visualization/render.py +79 -63
  437. oscura/visualization/reverse_engineering.py +171 -124
  438. oscura/visualization/signal_integrity.py +460 -279
  439. oscura/visualization/specialized.py +190 -100
  440. oscura/visualization/spectral.py +670 -255
  441. oscura/visualization/thumbnails.py +166 -137
  442. oscura/visualization/waveform.py +150 -63
  443. oscura/workflows/__init__.py +3 -0
  444. oscura/{batch → workflows/batch}/__init__.py +5 -5
  445. oscura/{batch → workflows/batch}/advanced.py +150 -75
  446. oscura/workflows/batch/aggregate.py +531 -0
  447. oscura/workflows/batch/analyze.py +236 -0
  448. oscura/{batch → workflows/batch}/logging.py +2 -2
  449. oscura/{batch → workflows/batch}/metrics.py +1 -1
  450. oscura/workflows/complete_re.py +1144 -0
  451. oscura/workflows/compliance.py +44 -54
  452. oscura/workflows/digital.py +197 -51
  453. oscura/workflows/legacy/__init__.py +12 -0
  454. oscura/{workflow → workflows/legacy}/dag.py +4 -1
  455. oscura/workflows/multi_trace.py +9 -9
  456. oscura/workflows/power.py +42 -62
  457. oscura/workflows/protocol.py +82 -49
  458. oscura/workflows/reverse_engineering.py +351 -150
  459. oscura/workflows/signal_integrity.py +157 -82
  460. oscura-0.7.0.dist-info/METADATA +661 -0
  461. oscura-0.7.0.dist-info/RECORD +591 -0
  462. oscura/batch/aggregate.py +0 -300
  463. oscura/batch/analyze.py +0 -139
  464. oscura/dsl/__init__.py +0 -73
  465. oscura/exceptions.py +0 -59
  466. oscura/exploratory/fuzzy.py +0 -513
  467. oscura/exploratory/sync.py +0 -384
  468. oscura/exporters/__init__.py +0 -94
  469. oscura/exporters/csv.py +0 -303
  470. oscura/exporters/exporters.py +0 -44
  471. oscura/exporters/hdf5.py +0 -217
  472. oscura/exporters/html_export.py +0 -701
  473. oscura/exporters/json_export.py +0 -291
  474. oscura/exporters/markdown_export.py +0 -367
  475. oscura/exporters/matlab_export.py +0 -354
  476. oscura/exporters/npz_export.py +0 -219
  477. oscura/exporters/spice_export.py +0 -210
  478. oscura/search/context.py +0 -149
  479. oscura/session/__init__.py +0 -34
  480. oscura/session/annotations.py +0 -289
  481. oscura/session/history.py +0 -313
  482. oscura/session/session.py +0 -520
  483. oscura/workflow/__init__.py +0 -13
  484. oscura-0.5.1.dist-info/METADATA +0 -583
  485. oscura-0.5.1.dist-info/RECORD +0 -481
  486. /oscura/core/{config.py → config/legacy.py} +0 -0
  487. /oscura/{extensibility → core/extensibility}/__init__.py +0 -0
  488. /oscura/{extensibility → core/extensibility}/registry.py +0 -0
  489. /oscura/{plugins → core/plugins}/isolation.py +0 -0
  490. /oscura/{schemas → core/schemas}/bus_configuration.json +0 -0
  491. /oscura/{builders → utils/builders}/signal_builder.py +0 -0
  492. /oscura/{optimization → utils/optimization}/parallel.py +0 -0
  493. /oscura/{pipeline → utils/pipeline}/__init__.py +0 -0
  494. /oscura/{streaming → utils/streaming}/__init__.py +0 -0
  495. {oscura-0.5.1.dist-info → oscura-0.7.0.dist-info}/WHEEL +0 -0
  496. {oscura-0.5.1.dist-info → oscura-0.7.0.dist-info}/entry_points.txt +0 -0
  497. {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
+ ]