oscura 0.5.0__py3-none-any.whl → 0.6.0__py3-none-any.whl

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