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,39 @@
1
+ """Machine learning-based signal analysis module.
2
+
3
+ This module provides ML-based signal classification and analysis tools:
4
+ - Automatic protocol detection using supervised learning
5
+ - Feature extraction (statistical, spectral, temporal, entropy)
6
+ - Multiple ML algorithms (Random Forest, SVM, Neural Networks)
7
+ - Model persistence and incremental learning
8
+
9
+ Supported signal types:
10
+ - Digital protocols: UART, SPI, I2C, CAN, Manchester, NRZ, RZ
11
+ - Analog signals: PWM, AM, FM, analog baseband
12
+ - General classifications: digital, analog, mixed-signal
13
+
14
+ Example:
15
+ >>> from oscura.analyzers.ml import MLSignalClassifier, TrainingDataset
16
+ >>> classifier = MLSignalClassifier(algorithm="random_forest")
17
+ >>> dataset = TrainingDataset(
18
+ ... signals=[uart_signal, spi_signal],
19
+ ... labels=["uart", "spi"],
20
+ ... sample_rates=[1e6, 1e6]
21
+ ... )
22
+ >>> metrics = classifier.train(dataset)
23
+ >>> result = classifier.predict(unknown_signal, sample_rate=1e6)
24
+ >>> print(f"Detected: {result.signal_type} ({result.confidence:.2%})")
25
+ """
26
+
27
+ from oscura.analyzers.ml.features import FeatureExtractor
28
+ from oscura.analyzers.ml.signal_classifier import (
29
+ MLClassificationResult,
30
+ MLSignalClassifier,
31
+ TrainingDataset,
32
+ )
33
+
34
+ __all__ = [
35
+ "FeatureExtractor",
36
+ "MLClassificationResult",
37
+ "MLSignalClassifier",
38
+ "TrainingDataset",
39
+ ]
@@ -0,0 +1,600 @@
1
+ """Feature extraction utilities for ML-based signal classification.
2
+
3
+ This module provides comprehensive feature extraction for signal classification:
4
+ - Statistical features (mean, variance, skewness, kurtosis)
5
+ - Spectral features (FFT-based frequency domain analysis)
6
+ - Temporal features (autocorrelation, zero-crossings, peaks)
7
+ - Entropy features (Shannon entropy, permutation entropy)
8
+ - Shape features (rise/fall time, duty cycle for digital signals)
9
+
10
+ Features are extracted in a standardized format suitable for ML algorithms.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import logging
16
+ from typing import TYPE_CHECKING, Any
17
+
18
+ import numpy as np
19
+ from scipy import signal as sp_signal
20
+ from scipy import stats
21
+
22
+ if TYPE_CHECKING:
23
+ from numpy.typing import NDArray
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+
28
+ class FeatureExtractor:
29
+ """Extract comprehensive features from signals for ML classification.
30
+
31
+ This class provides a unified interface for extracting multiple feature types
32
+ from time-domain signals. Features are returned as dictionaries with consistent
33
+ naming for use in machine learning pipelines.
34
+
35
+ Example:
36
+ >>> extractor = FeatureExtractor()
37
+ >>> features = extractor.extract_all(signal, sample_rate=1e6)
38
+ >>> print(f"Extracted {len(features)} features")
39
+ >>> print(f"Dominant frequency: {features['dominant_frequency']:.1f} Hz")
40
+ """
41
+
42
+ def extract_all(self, data: NDArray[np.floating[Any]], sample_rate: float) -> dict[str, float]:
43
+ """Extract all feature types from signal.
44
+
45
+ Args:
46
+ data: Input signal as 1D numpy array.
47
+ sample_rate: Sampling rate in Hz.
48
+
49
+ Returns:
50
+ Dictionary of feature name to value mappings. Contains 40+ features
51
+ spanning statistical, spectral, temporal, entropy, and shape categories.
52
+
53
+ Example:
54
+ >>> signal = np.sin(2 * np.pi * 1000 * np.linspace(0, 1, 10000))
55
+ >>> features = extractor.extract_all(signal, 10000)
56
+ >>> features['dominant_frequency'] # Should be ~1000 Hz
57
+ 1000.0
58
+ """
59
+ features: dict[str, float] = {}
60
+
61
+ # Extract each feature category
62
+ features.update(self.extract_statistical(data))
63
+ features.update(self.extract_spectral(data, sample_rate))
64
+ features.update(self.extract_temporal(data))
65
+ features.update(self.extract_entropy(data))
66
+ features.update(self.extract_shape(data, sample_rate))
67
+
68
+ return features
69
+
70
+ def extract_statistical(self, data: NDArray[np.floating[Any]]) -> dict[str, float]:
71
+ """Extract statistical features from signal.
72
+
73
+ Computes basic statistical moments and distribution properties:
74
+ - Central tendency: mean, median
75
+ - Dispersion: std, variance, range, IQR
76
+ - Shape: skewness, kurtosis
77
+
78
+ Args:
79
+ data: Input signal as 1D numpy array.
80
+
81
+ Returns:
82
+ Dictionary with 9 statistical features.
83
+
84
+ Example:
85
+ >>> gaussian = np.random.normal(0, 1, 10000)
86
+ >>> features = extractor.extract_statistical(gaussian)
87
+ >>> abs(features['mean']) < 0.1 # Near zero
88
+ True
89
+ >>> 0.9 < features['std'] < 1.1 # Near 1
90
+ True
91
+ """
92
+ import warnings
93
+
94
+ # For constant signals, skew/kurtosis cause precision warnings
95
+ with warnings.catch_warnings():
96
+ warnings.filterwarnings("ignore", category=RuntimeWarning)
97
+ skewness = float(stats.skew(data))
98
+ kurtosis = float(stats.kurtosis(data))
99
+
100
+ # Handle NaN from constant signals
101
+ if np.isnan(skewness):
102
+ skewness = 0.0
103
+ if np.isnan(kurtosis):
104
+ kurtosis = 0.0
105
+
106
+ return {
107
+ "mean": float(np.mean(data)),
108
+ "median": float(np.median(data)),
109
+ "std": float(np.std(data)),
110
+ "variance": float(np.var(data)),
111
+ "min": float(np.min(data)),
112
+ "max": float(np.max(data)),
113
+ "range": float(np.ptp(data)),
114
+ "skewness": skewness,
115
+ "kurtosis": kurtosis,
116
+ }
117
+
118
+ def extract_spectral(
119
+ self, data: NDArray[np.floating[Any]], sample_rate: float
120
+ ) -> dict[str, float]:
121
+ """Extract spectral features via FFT analysis.
122
+
123
+ Analyzes frequency domain properties:
124
+ - Dominant frequency (peak in spectrum)
125
+ - Spectral centroid (center of mass of spectrum)
126
+ - Bandwidth (frequency range containing most energy)
127
+ - Spectral energy (total power)
128
+ - Spectral flatness (measure of tone vs noise)
129
+ - Spectral rolloff (95th percentile frequency)
130
+
131
+ Args:
132
+ data: Input signal as 1D numpy array.
133
+ sample_rate: Sampling rate in Hz.
134
+
135
+ Returns:
136
+ Dictionary with 8 spectral features.
137
+
138
+ Example:
139
+ >>> # 1 kHz sine wave
140
+ >>> t = np.linspace(0, 1, 10000)
141
+ >>> signal = np.sin(2 * np.pi * 1000 * t)
142
+ >>> features = extractor.extract_spectral(signal, 10000)
143
+ >>> 950 < features['dominant_frequency'] < 1050
144
+ True
145
+ """
146
+ # Compute FFT (use real FFT for efficiency)
147
+ fft = np.fft.rfft(data)
148
+ freqs = np.fft.rfftfreq(len(data), 1.0 / sample_rate)
149
+ magnitude = np.abs(fft)
150
+ power = magnitude**2
151
+
152
+ # Avoid division by zero
153
+ magnitude_safe = magnitude + 1e-10
154
+ power_safe = power + 1e-10
155
+
156
+ # Dominant frequency (peak in spectrum)
157
+ if len(magnitude) > 1:
158
+ dominant_idx = np.argmax(magnitude[1:]) + 1 # Skip DC component
159
+ dominant_freq = float(freqs[dominant_idx])
160
+ else:
161
+ dominant_freq = 0.0
162
+
163
+ # Spectral centroid (center of mass)
164
+ spectral_centroid = float(np.sum(freqs * magnitude) / np.sum(magnitude_safe))
165
+
166
+ # Bandwidth (frequencies containing >10% of max power)
167
+ threshold = 0.1 * np.max(power)
168
+ bandwidth_freqs = freqs[power > threshold]
169
+ bandwidth = (
170
+ float(bandwidth_freqs[-1] - bandwidth_freqs[0]) if len(bandwidth_freqs) > 1 else 0.0
171
+ )
172
+
173
+ # Spectral energy
174
+ spectral_energy = float(np.sum(power))
175
+
176
+ # Spectral flatness (geometric mean / arithmetic mean)
177
+ geometric_mean = float(np.exp(np.mean(np.log(magnitude_safe))))
178
+ arithmetic_mean = float(np.mean(magnitude))
179
+ spectral_flatness = geometric_mean / arithmetic_mean if arithmetic_mean > 0 else 0.0
180
+
181
+ # Spectral rolloff (95th percentile frequency)
182
+ cumsum = np.cumsum(power)
183
+ total_energy = cumsum[-1]
184
+ rolloff_threshold = 0.95 * total_energy
185
+ rolloff_idx = np.where(cumsum >= rolloff_threshold)[0]
186
+ spectral_rolloff = float(freqs[rolloff_idx[0]]) if len(rolloff_idx) > 0 else 0.0
187
+
188
+ # Number of spectral peaks
189
+ peaks, _ = sp_signal.find_peaks(magnitude, height=0.1 * np.max(magnitude))
190
+ num_spectral_peaks = float(len(peaks))
191
+
192
+ # Spectral spread (standard deviation around centroid)
193
+ spectral_spread = float(
194
+ np.sqrt(np.sum(((freqs - spectral_centroid) ** 2) * power) / np.sum(power_safe))
195
+ )
196
+
197
+ return {
198
+ "dominant_frequency": dominant_freq,
199
+ "spectral_centroid": spectral_centroid,
200
+ "bandwidth": bandwidth,
201
+ "spectral_energy": spectral_energy,
202
+ "spectral_flatness": spectral_flatness,
203
+ "spectral_rolloff": spectral_rolloff,
204
+ "num_spectral_peaks": num_spectral_peaks,
205
+ "spectral_spread": spectral_spread,
206
+ }
207
+
208
+ def extract_temporal(self, data: NDArray[np.floating[Any]]) -> dict[str, float]:
209
+ """Extract temporal domain features.
210
+
211
+ Analyzes time-domain signal properties:
212
+ - Zero-crossings (transitions through mean)
213
+ - Autocorrelation (self-similarity measure)
214
+ - Peak count (number of local maxima)
215
+ - Peak prominence (average peak height)
216
+ - Energy (sum of squared values)
217
+ - RMS (root mean square)
218
+
219
+ Args:
220
+ data: Input signal as 1D numpy array.
221
+
222
+ Returns:
223
+ Dictionary with 8 temporal features.
224
+
225
+ Example:
226
+ >>> # Square wave has many zero crossings
227
+ >>> square = np.sign(np.sin(2 * np.pi * 10 * np.linspace(0, 1, 1000)))
228
+ >>> features = extractor.extract_temporal(square)
229
+ >>> features['zero_crossing_rate'] > 0.01
230
+ True
231
+ """
232
+ # Zero-crossings (normalized by length)
233
+ mean_centered = data - np.mean(data)
234
+ zero_crossings = np.sum(np.diff(np.sign(mean_centered)) != 0)
235
+ zero_crossing_rate = float(zero_crossings) / len(data)
236
+
237
+ # Autocorrelation at lag 1
238
+ if len(data) > 1:
239
+ autocorr = float(np.corrcoef(data[:-1], data[1:])[0, 1])
240
+ # Handle NaN from constant signals
241
+ autocorr = 0.0 if np.isnan(autocorr) else autocorr
242
+ else:
243
+ autocorr = 0.0
244
+
245
+ # Peak detection
246
+ peaks, properties = sp_signal.find_peaks(data, prominence=0.1 * np.ptp(data))
247
+ peak_count = float(len(peaks))
248
+ peak_prominence = float(np.mean(properties["prominences"])) if len(peaks) > 0 else 0.0
249
+
250
+ # Energy and RMS
251
+ energy = float(np.sum(data**2))
252
+ rms = float(np.sqrt(np.mean(data**2)))
253
+
254
+ # Signal to noise ratio estimate (robust)
255
+ # Use median absolute deviation for noise estimate
256
+ mad = float(np.median(np.abs(data - np.median(data))))
257
+ signal_power = float(np.mean(data**2))
258
+ noise_power = (1.4826 * mad) ** 2 # Convert MAD to std estimate
259
+ snr_estimate = 10 * np.log10(signal_power / noise_power) if noise_power > 0 else 0.0
260
+
261
+ # Crest factor (peak to RMS ratio)
262
+ crest_factor = float(np.max(np.abs(data)) / rms) if rms > 0 else 0.0
263
+
264
+ return {
265
+ "zero_crossing_rate": zero_crossing_rate,
266
+ "autocorrelation": autocorr,
267
+ "peak_count": peak_count,
268
+ "peak_prominence": peak_prominence,
269
+ "energy": energy,
270
+ "rms": rms,
271
+ "snr_estimate": snr_estimate,
272
+ "crest_factor": crest_factor,
273
+ }
274
+
275
+ def extract_entropy(self, data: NDArray[np.floating[Any]]) -> dict[str, float]:
276
+ """Extract entropy-based features.
277
+
278
+ Computes information-theoretic measures:
279
+ - Shannon entropy (information content)
280
+ - Approximate entropy (regularity measure)
281
+ - Sample entropy (complexity measure, similar to ApEn)
282
+
283
+ Args:
284
+ data: Input signal as 1D numpy array.
285
+
286
+ Returns:
287
+ Dictionary with 3 entropy features.
288
+
289
+ Example:
290
+ >>> # Random signal has high entropy
291
+ >>> random = np.random.randn(1000)
292
+ >>> features = extractor.extract_entropy(random)
293
+ >>> features['shannon_entropy'] > 5.0
294
+ True
295
+ """
296
+ # Shannon entropy (discretize signal into bins)
297
+ # Normalize to 0-255 range for byte-like entropy
298
+ data_normalized = ((data - np.min(data)) / (np.ptp(data) + 1e-10) * 255).astype(np.uint8)
299
+ _, counts = np.unique(data_normalized, return_counts=True)
300
+ probabilities = counts / len(data_normalized)
301
+ shannon_entropy = float(-np.sum(probabilities * np.log2(probabilities + 1e-10)))
302
+
303
+ # Approximate entropy (ApEn)
304
+ # Measures regularity - low for regular signals, high for complex
305
+ approx_entropy = self._approximate_entropy(data, m=2, r=0.2 * np.std(data))
306
+
307
+ # Sample entropy (SampEn) - improved version of ApEn
308
+ sample_entropy = self._sample_entropy(data, m=2, r=0.2 * np.std(data))
309
+
310
+ return {
311
+ "shannon_entropy": shannon_entropy,
312
+ "approximate_entropy": approx_entropy,
313
+ "sample_entropy": sample_entropy,
314
+ }
315
+
316
+ def _approximate_entropy(self, data: NDArray[np.floating[Any]], m: int, r: float) -> float:
317
+ """Calculate approximate entropy (ApEn).
318
+
319
+ Args:
320
+ data: Input signal.
321
+ m: Embedding dimension.
322
+ r: Tolerance (fraction of std).
323
+
324
+ Returns:
325
+ Approximate entropy value.
326
+ """
327
+ n = len(data)
328
+
329
+ # For very long signals, downsample to avoid O(n^2) cost
330
+ # Use aggressive downsampling to keep performance reasonable
331
+ # Approximate entropy is statistical and works well on smaller samples
332
+ if n > 200:
333
+ downsample_factor = max(n // 100, 1) # Target ~100 samples max
334
+ data = data[::downsample_factor]
335
+ n = len(data)
336
+
337
+ def _phi(m: int) -> float:
338
+ if n - m + 1 <= 0:
339
+ return 0.0
340
+
341
+ patterns = np.array([data[i : i + m] for i in range(n - m + 1)])
342
+ counts = np.zeros(len(patterns))
343
+
344
+ for i, pattern in enumerate(patterns):
345
+ # Count patterns within tolerance r
346
+ distances = np.max(np.abs(patterns - pattern), axis=1)
347
+ counts[i] = np.sum(distances <= r)
348
+
349
+ # Avoid log(0)
350
+ counts = np.maximum(counts, 1)
351
+ phi_value = np.sum(np.log(counts / (n - m + 1))) / (n - m + 1)
352
+ return float(phi_value)
353
+
354
+ try:
355
+ return float(_phi(m) - _phi(m + 1))
356
+ except (ValueError, RuntimeWarning):
357
+ return 0.0
358
+
359
+ def _sample_entropy(self, data: NDArray[np.floating[Any]], m: int, r: float) -> float:
360
+ """Calculate sample entropy (SampEn).
361
+
362
+ Args:
363
+ data: Input signal.
364
+ m: Embedding dimension.
365
+ r: Tolerance (fraction of std).
366
+
367
+ Returns:
368
+ Sample entropy value.
369
+ """
370
+ n = len(data)
371
+
372
+ # For very long signals, downsample to avoid O(n^2) cost
373
+ # Use aggressive downsampling to keep performance reasonable
374
+ # Sample entropy is statistical and works well on smaller samples
375
+ if n > 200:
376
+ downsample_factor = max(n // 100, 1) # Target ~100 samples max
377
+ data = data[::downsample_factor]
378
+ n = len(data)
379
+
380
+ def _count_matches(m: int) -> tuple[int, int]:
381
+ if n - m <= 0:
382
+ return 0, 0
383
+
384
+ patterns = np.array([data[i : i + m] for i in range(n - m)])
385
+ count_a = 0
386
+ count_b = 0
387
+
388
+ for i in range(len(patterns)):
389
+ # Don't count self-matches
390
+ for j in range(i + 1, len(patterns)):
391
+ dist = np.max(np.abs(patterns[i] - patterns[j]))
392
+ if dist <= r:
393
+ count_b += 1
394
+ if m > 1:
395
+ # Check if extended pattern also matches
396
+ if i + m < n and j + m < n:
397
+ if abs(data[i + m] - data[j + m]) <= r:
398
+ count_a += 1
399
+
400
+ return count_a, count_b
401
+
402
+ try:
403
+ count_a, count_b = _count_matches(m)
404
+ if count_a == 0 or count_b == 0:
405
+ return 0.0
406
+ return float(-np.log(count_a / count_b))
407
+ except (ValueError, RuntimeWarning):
408
+ return 0.0
409
+
410
+ def _normalize_signal(self, data: NDArray[np.floating[Any]]) -> NDArray[np.floating[Any]]:
411
+ """Normalize signal to 0-1 range.
412
+
413
+ Args:
414
+ data: Input signal
415
+
416
+ Returns:
417
+ Normalized signal
418
+ """
419
+ normalized: NDArray[np.floating[Any]] = (data - np.min(data)) / (np.ptp(data) + 1e-10)
420
+ return normalized
421
+
422
+ def _find_edges(
423
+ self, data_normalized: NDArray[np.floating[Any]]
424
+ ) -> tuple[NDArray[np.intp], NDArray[np.intp]]:
425
+ """Find rising and falling edges.
426
+
427
+ Args:
428
+ data_normalized: Normalized signal data
429
+
430
+ Returns:
431
+ Tuple of (rising edges, falling edges)
432
+ """
433
+ rising_edges = np.where((data_normalized[:-1] < 0.5) & (data_normalized[1:] >= 0.5))[0]
434
+ falling_edges = np.where((data_normalized[:-1] >= 0.5) & (data_normalized[1:] < 0.5))[0]
435
+ return rising_edges, falling_edges
436
+
437
+ def _calculate_rise_times(
438
+ self,
439
+ rising_edges: NDArray[np.intp],
440
+ data_normalized: NDArray[np.floating[Any]],
441
+ threshold_low: float,
442
+ threshold_high: float,
443
+ ) -> list[int]:
444
+ """Calculate rise times for all rising edges.
445
+
446
+ Args:
447
+ rising_edges: Array of rising edge indices
448
+ data_normalized: Normalized signal data
449
+ threshold_low: Low threshold (10%)
450
+ threshold_high: High threshold (90%)
451
+
452
+ Returns:
453
+ List of rise times in samples
454
+ """
455
+ rise_times = []
456
+ for edge in rising_edges:
457
+ # Look backward for 10% crossing
458
+ start_idx = edge
459
+ for i in range(max(0, edge - 100), edge):
460
+ if data_normalized[i] < threshold_low:
461
+ start_idx = i
462
+ break
463
+ # Look forward for 90% crossing
464
+ end_idx = edge
465
+ for i in range(edge, min(len(data_normalized), edge + 100)):
466
+ if data_normalized[i] > threshold_high:
467
+ end_idx = i
468
+ break
469
+ if end_idx > start_idx:
470
+ rise_times.append(end_idx - start_idx)
471
+ return rise_times
472
+
473
+ def _calculate_fall_times(
474
+ self,
475
+ falling_edges: NDArray[np.intp],
476
+ data_normalized: NDArray[np.floating[Any]],
477
+ threshold_low: float,
478
+ threshold_high: float,
479
+ ) -> list[int]:
480
+ """Calculate fall times for all falling edges.
481
+
482
+ Args:
483
+ falling_edges: Array of falling edge indices
484
+ data_normalized: Normalized signal data
485
+ threshold_low: Low threshold (10%)
486
+ threshold_high: High threshold (90%)
487
+
488
+ Returns:
489
+ List of fall times in samples
490
+ """
491
+ fall_times = []
492
+ for edge in falling_edges:
493
+ # Look backward for 90% crossing
494
+ start_idx = edge
495
+ for i in range(max(0, edge - 100), edge):
496
+ if data_normalized[i] > threshold_high:
497
+ start_idx = i
498
+ break
499
+ # Look forward for 10% crossing
500
+ end_idx = edge
501
+ for i in range(edge, min(len(data_normalized), edge + 100)):
502
+ if data_normalized[i] < threshold_low:
503
+ end_idx = i
504
+ break
505
+ if end_idx > start_idx:
506
+ fall_times.append(end_idx - start_idx)
507
+ return fall_times
508
+
509
+ def _calculate_pulse_widths(
510
+ self, rising_edges: NDArray[np.intp], falling_edges: NDArray[np.intp]
511
+ ) -> list[int]:
512
+ """Calculate pulse widths from edges.
513
+
514
+ Args:
515
+ rising_edges: Array of rising edge indices
516
+ falling_edges: Array of falling edge indices
517
+
518
+ Returns:
519
+ List of pulse widths in samples
520
+ """
521
+ pulse_widths = []
522
+ for rising in rising_edges:
523
+ # Find next falling edge
524
+ next_falling = falling_edges[falling_edges > rising]
525
+ if len(next_falling) > 0:
526
+ pulse_widths.append(int(next_falling[0] - rising))
527
+ return pulse_widths
528
+
529
+ def _calculate_form_factor(self, data: NDArray[np.floating[Any]]) -> float:
530
+ """Calculate form factor (RMS / mean).
531
+
532
+ Args:
533
+ data: Input signal
534
+
535
+ Returns:
536
+ Form factor
537
+ """
538
+ rms = float(np.sqrt(np.mean(data**2)))
539
+ mean_abs = float(np.mean(np.abs(data)))
540
+ return rms / mean_abs if mean_abs > 0 else 0.0
541
+
542
+ def extract_shape(
543
+ self, data: NDArray[np.floating[Any]], sample_rate: float
544
+ ) -> dict[str, float]:
545
+ """Extract shape-related features for digital signals.
546
+
547
+ Analyzes waveform shape properties:
548
+ - Rise time (10% to 90% transition)
549
+ - Fall time (90% to 10% transition)
550
+ - Duty cycle (high time / period for digital)
551
+ - Pulse width (average high duration)
552
+ - Form factor (RMS / mean)
553
+
554
+ Args:
555
+ data: Input signal as 1D numpy array.
556
+ sample_rate: Sampling rate in Hz.
557
+
558
+ Returns:
559
+ Dictionary with 5 shape features.
560
+
561
+ Example:
562
+ >>> # 50% duty cycle square wave
563
+ >>> square = np.tile([1, 1, 1, 1, 1, 0, 0, 0, 0, 0], 100)
564
+ >>> features = extractor.extract_shape(square, 1000)
565
+ >>> 0.4 < features['duty_cycle'] < 0.6
566
+ True
567
+ """
568
+ # Normalize and extract basic features
569
+ data_normalized = self._normalize_signal(data)
570
+ duty_cycle = float(np.mean(data_normalized > 0.5))
571
+
572
+ # Find edges
573
+ rising_edges, falling_edges = self._find_edges(data_normalized)
574
+
575
+ # Calculate timing features
576
+ threshold_low = 0.1
577
+ threshold_high = 0.9
578
+
579
+ rise_times = self._calculate_rise_times(
580
+ rising_edges, data_normalized, threshold_low, threshold_high
581
+ )
582
+ rise_time = float(np.mean(rise_times)) / sample_rate if rise_times else 0.0
583
+
584
+ fall_times = self._calculate_fall_times(
585
+ falling_edges, data_normalized, threshold_low, threshold_high
586
+ )
587
+ fall_time = float(np.mean(fall_times)) / sample_rate if fall_times else 0.0
588
+
589
+ pulse_widths = self._calculate_pulse_widths(rising_edges, falling_edges)
590
+ pulse_width = float(np.mean(pulse_widths)) / sample_rate if pulse_widths else 0.0
591
+
592
+ form_factor = self._calculate_form_factor(data)
593
+
594
+ return {
595
+ "duty_cycle": duty_cycle,
596
+ "rise_time": rise_time,
597
+ "fall_time": fall_time,
598
+ "pulse_width": pulse_width,
599
+ "form_factor": form_factor,
600
+ }