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,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
+ }