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
@@ -10,7 +10,7 @@ and protocols, including binary field detection and pattern analysis.
10
10
  - UNKNOWN-005: Reverse Engineering Workflow
11
11
 
12
12
  Example:
13
- >>> from oscura.exploratory.unknown import characterize_unknown_signal
13
+ >>> from oscura.jupyter.exploratory.unknown import characterize_unknown_signal
14
14
  >>> result = characterize_unknown_signal(trace)
15
15
  >>> print(f"Signal type: {result.signal_type}")
16
16
  >>> print(f"Suggested protocols: {result.suggested_protocols}")
@@ -75,35 +75,66 @@ def detect_binary_fields(
75
75
  References:
76
76
  UNKNOWN-001: Binary Field Detection
77
77
  """
78
+ # Convert analog to digital
79
+ digital, sample_rate = _convert_to_digital(trace)
80
+
81
+ # Find edges and estimate bit period
82
+ edges = np.where(np.diff(digital) != 0)[0]
83
+ if len(edges) < 2:
84
+ return _create_empty_field_result()
85
+
86
+ bit_period = np.median(np.diff(edges))
87
+
88
+ # Extract fields from edges
89
+ fields = _extract_binary_fields(
90
+ edges, digital, bit_period, max_gap_ratio, min_field_bits, sample_rate
91
+ )
92
+
93
+ # Calculate bit rate
94
+ bit_rate = sample_rate / bit_period if bit_period > 0 else None
95
+
96
+ # Detect encoding
97
+ encoding = _detect_encoding(digital, edges, bit_period)
98
+
99
+ # Calculate confidence
100
+ confidence = min(1.0, len(fields) / 10.0) * 0.8 + (0.2 if bit_rate else 0)
101
+
102
+ return BinaryFieldResult(
103
+ fields=fields,
104
+ field_count=len(fields),
105
+ bit_rate=bit_rate,
106
+ encoding=encoding,
107
+ confidence=confidence,
108
+ )
109
+
110
+
111
+ def _convert_to_digital(trace: WaveformTrace) -> tuple[NDArray[np.int_], float]:
112
+ """Convert analog signal to digital representation."""
78
113
  data = trace.data
79
114
  sample_rate = trace.metadata.sample_rate
80
-
81
- # Threshold for digital conversion
82
115
  v_min = np.percentile(data, 5)
83
116
  v_max = np.percentile(data, 95)
84
117
  threshold = (v_min + v_max) / 2
85
-
86
- # Convert to digital
87
118
  digital = (data > threshold).astype(int)
119
+ return digital, sample_rate
88
120
 
89
- # Find edges
90
- edges = np.where(np.diff(digital) != 0)[0]
91
121
 
92
- if len(edges) < 2:
93
- return BinaryFieldResult(
94
- fields=[],
95
- field_count=0,
96
- bit_rate=None,
97
- encoding="unknown",
98
- confidence=0.0,
99
- )
122
+ def _create_empty_field_result() -> BinaryFieldResult:
123
+ """Create empty result for insufficient edges."""
124
+ return BinaryFieldResult(
125
+ fields=[], field_count=0, bit_rate=None, encoding="unknown", confidence=0.0
126
+ )
100
127
 
101
- # Estimate bit period from edge spacing
102
- edge_gaps = np.diff(edges)
103
- median_gap = np.median(edge_gaps)
104
- bit_period = median_gap
105
128
 
106
- # Group edges into fields
129
+ def _extract_binary_fields(
130
+ edges: NDArray[np.int_],
131
+ digital: NDArray[np.int_],
132
+ bit_period: float,
133
+ max_gap_ratio: float,
134
+ min_field_bits: int,
135
+ sample_rate: float,
136
+ ) -> list[dict[str, Any]]:
137
+ """Extract binary fields from edges."""
107
138
  fields = []
108
139
  current_field_start = edges[0]
109
140
  current_field_edges = [edges[0]]
@@ -114,26 +145,10 @@ def detect_binary_fields(
114
145
  if gap > max_gap_ratio * bit_period:
115
146
  # End current field
116
147
  if len(current_field_edges) >= min_field_bits:
117
- current_field_edges[-1] - current_field_edges[0]
118
- n_bits = len(current_field_edges) - 1
119
-
120
- # Extract bit pattern
121
- bits = []
122
- for j in range(len(current_field_edges) - 1):
123
- start = current_field_edges[j]
124
- end = current_field_edges[j + 1]
125
- mid = (start + end) // 2
126
- bits.append(digital[mid])
127
-
128
- fields.append(
129
- {
130
- "start_sample": int(current_field_start),
131
- "end_sample": int(current_field_edges[-1]),
132
- "length": n_bits,
133
- "bits": bits,
134
- "timestamp": current_field_start / sample_rate,
135
- }
148
+ field = _create_field_from_edges(
149
+ current_field_start, current_field_edges, digital, sample_rate
136
150
  )
151
+ fields.append(field)
137
152
 
138
153
  # Start new field
139
154
  current_field_start = edges[i]
@@ -143,43 +158,36 @@ def detect_binary_fields(
143
158
 
144
159
  # Handle last field
145
160
  if len(current_field_edges) >= min_field_bits:
146
- current_field_edges[-1] - current_field_edges[0]
147
- n_bits = len(current_field_edges) - 1
148
- bits = []
149
- for j in range(len(current_field_edges) - 1):
150
- start = current_field_edges[j]
151
- end = current_field_edges[j + 1]
152
- mid = (start + end) // 2
153
- bits.append(digital[mid])
154
-
155
- fields.append(
156
- {
157
- "start_sample": int(current_field_start),
158
- "end_sample": int(current_field_edges[-1]),
159
- "length": n_bits,
160
- "bits": bits,
161
- "timestamp": current_field_start / sample_rate,
162
- }
161
+ field = _create_field_from_edges(
162
+ current_field_start, current_field_edges, digital, sample_rate
163
163
  )
164
+ fields.append(field)
164
165
 
165
- # Estimate bit rate
166
- bit_rate = sample_rate / bit_period if bit_period > 0 else None
167
-
168
- # Detect encoding
169
- encoding = _detect_encoding(digital, edges, bit_period)
166
+ return fields
170
167
 
171
- # Calculate confidence
172
- confidence = min(1.0, len(fields) / 10.0) * 0.8
173
- if bit_rate is not None:
174
- confidence += 0.2
175
168
 
176
- return BinaryFieldResult(
177
- fields=fields,
178
- field_count=len(fields),
179
- bit_rate=bit_rate,
180
- encoding=encoding,
181
- confidence=confidence,
182
- )
169
+ def _create_field_from_edges(
170
+ field_start: int,
171
+ field_edges: list[int],
172
+ digital: NDArray[np.int_],
173
+ sample_rate: float,
174
+ ) -> dict[str, Any]:
175
+ """Create field dictionary from edge list."""
176
+ n_bits = len(field_edges) - 1
177
+ bits = []
178
+ for j in range(len(field_edges) - 1):
179
+ start = field_edges[j]
180
+ end = field_edges[j + 1]
181
+ mid = (start + end) // 2
182
+ bits.append(digital[mid])
183
+
184
+ return {
185
+ "start_sample": int(field_start),
186
+ "end_sample": int(field_edges[-1]),
187
+ "length": n_bits,
188
+ "bits": bits,
189
+ "timestamp": field_start / sample_rate,
190
+ }
183
191
 
184
192
 
185
193
  def _detect_encoding(
@@ -253,156 +261,216 @@ class UnknownSignalCharacterization:
253
261
  features: dict[str, Any] = field(default_factory=dict)
254
262
 
255
263
 
256
- def characterize_unknown_signal(
257
- trace: WaveformTrace,
258
- ) -> UnknownSignalCharacterization:
259
- """Comprehensive characterization of unknown signal per UNKNOWN-003.
260
-
261
- Analyzes signal characteristics to determine type, periodicity,
262
- and suggest possible protocols.
264
+ def _handle_short_trace(data: NDArray[np.float64]) -> UnknownSignalCharacterization:
265
+ """Handle edge case of very short traces.
263
266
 
264
267
  Args:
265
- trace: Signal trace to characterize.
268
+ data: Signal data array
266
269
 
267
270
  Returns:
268
- UnknownSignalCharacterization with all extracted features.
271
+ UnknownSignalCharacterization with minimal info
272
+ """
273
+ return UnknownSignalCharacterization(
274
+ signal_type="analog",
275
+ is_periodic=False,
276
+ fundamental_frequency=None,
277
+ dc_offset=float(data[0]) if len(data) > 0 else 0.0,
278
+ amplitude=0.0,
279
+ rise_time=None,
280
+ fall_time=None,
281
+ suggested_protocols=[],
282
+ noise_floor=0.0,
283
+ snr_db=float("inf"),
284
+ features={},
285
+ )
269
286
 
270
- Example:
271
- >>> result = characterize_unknown_signal(trace)
272
- >>> print(f"Signal type: {result.signal_type}")
273
- >>> print(f"Periodic: {result.is_periodic}")
274
- >>> for protocol, confidence in result.suggested_protocols:
275
- ... print(f" {protocol}: {confidence:.1%}")
276
287
 
277
- References:
278
- UNKNOWN-003: Unknown Signal Characterization
288
+ def _compute_basic_statistics(
289
+ data: NDArray[np.float64],
290
+ ) -> tuple[float, float, float, float, float, float]:
291
+ """Compute basic signal statistics.
292
+
293
+ Args:
294
+ data: Signal data array
295
+
296
+ Returns:
297
+ Tuple of (v_min, v_max, v_mean, v_std, dc_offset, amplitude)
279
298
  """
280
- data = trace.data
281
- sample_rate = trace.metadata.sample_rate
299
+ v_min: float = float(np.min(data))
300
+ v_max: float = float(np.max(data))
301
+ v_mean: float = float(np.mean(data))
302
+ v_std: float = float(np.std(data))
303
+ dc_offset: float = v_mean
304
+ amplitude: float = (v_max - v_min) / 2
305
+ return v_min, v_max, v_mean, v_std, dc_offset, amplitude
282
306
 
283
- # Handle edge case of very short traces
284
- if len(data) < 2:
285
- return UnknownSignalCharacterization(
286
- signal_type="analog",
287
- is_periodic=False,
288
- fundamental_frequency=None,
289
- dc_offset=float(data[0]) if len(data) > 0 else 0.0,
290
- amplitude=0.0,
291
- rise_time=None,
292
- fall_time=None,
293
- suggested_protocols=[],
294
- noise_floor=0.0,
295
- snr_db=float("inf"),
296
- features={},
297
- )
298
307
 
299
- # Basic statistics
300
- v_min = np.min(data)
301
- v_max = np.max(data)
302
- v_mean = np.mean(data)
303
- v_std = np.std(data)
308
+ def _find_histogram_peaks(
309
+ data: NDArray[np.float64],
310
+ v_min: float,
311
+ v_max: float,
312
+ ) -> list[tuple[float, float]]:
313
+ """Find peaks in signal histogram for distribution analysis.
304
314
 
305
- dc_offset = v_mean
306
- amplitude = (v_max - v_min) / 2
315
+ Args:
316
+ data: Signal data array
317
+ v_min: Minimum voltage
318
+ v_max: Maximum voltage
307
319
 
308
- # Determine signal type
309
- # Digital signals have bimodal distribution
320
+ Returns:
321
+ List of (peak_position, peak_height) tuples
322
+ """
310
323
  hist, bin_edges = np.histogram(data, bins=50)
311
324
  centers = (bin_edges[:-1] + bin_edges[1:]) / 2
312
325
 
313
- # Find peaks in histogram
314
326
  peaks = []
315
327
  for i in range(1, len(hist) - 1):
316
328
  if hist[i] > hist[i - 1] and hist[i] > hist[i + 1] and hist[i] > 0.1 * np.max(hist):
317
329
  peaks.append((centers[i], hist[i]))
318
330
 
331
+ return peaks
332
+
333
+
334
+ def _classify_signal_type(
335
+ peaks: list[tuple[float, float]],
336
+ v_min: float,
337
+ v_max: float,
338
+ v_std: float,
339
+ amplitude: float,
340
+ ) -> Literal["digital", "analog", "mixed"]:
341
+ """Classify signal type based on histogram peaks.
342
+
343
+ Args:
344
+ peaks: List of histogram peaks
345
+ v_min: Minimum voltage
346
+ v_max: Maximum voltage
347
+ v_std: Standard deviation
348
+ amplitude: Signal amplitude
349
+
350
+ Returns:
351
+ Signal type classification
352
+ """
319
353
  if len(peaks) >= 4:
320
- # Many peaks suggest analog signal (e.g., sine wave with noisy histogram)
321
- signal_type: Literal["digital", "analog", "mixed"] = "analog"
354
+ # Many peaks suggest analog signal
355
+ return "analog"
322
356
  elif len(peaks) == 2 or len(peaks) == 3:
323
- # Two peaks suggest digital (bimodal), but check if they're well-separated
357
+ # Check if peaks are well-separated (digital bimodal)
324
358
  peak_positions = [p[0] for p in peaks]
325
- # Normalize peak positions to 0-1 range
326
359
  normalized_peaks = [(p - v_min) / (v_max - v_min) for p in peak_positions]
327
360
 
328
- # If peaks are well-separated (one < 0.4, one > 0.6), likely digital
329
361
  has_low_peak = any(p < 0.4 for p in normalized_peaks)
330
362
  has_high_peak = any(p > 0.6 for p in normalized_peaks)
331
363
 
332
364
  if has_low_peak and has_high_peak:
333
- signal_type = "digital"
365
+ return "digital"
334
366
  else:
335
- # Peaks not well separated, likely analog
336
- signal_type = "analog"
367
+ return "analog"
337
368
  elif len(peaks) == 1:
338
369
  # Check for modulated signal
339
- signal_type = "mixed" if v_std > 0.2 * amplitude else "analog"
370
+ return "mixed" if v_std > 0.2 * amplitude else "analog"
340
371
  else:
341
- signal_type = "analog"
372
+ return "analog"
373
+
374
+
375
+ def _analyze_periodicity(
376
+ data: NDArray[np.float64],
377
+ sample_rate: float,
378
+ ) -> tuple[bool, float | None, float, float]:
379
+ """Analyze signal periodicity using FFT.
342
380
 
343
- # Check periodicity via FFT
381
+ Args:
382
+ data: Signal data array
383
+ sample_rate: Sample rate in Hz
384
+
385
+ Returns:
386
+ Tuple of (is_periodic, fundamental_frequency, noise_floor, snr_db)
387
+ """
344
388
  from scipy import signal as sp_signal
345
389
 
346
390
  n = len(data)
347
- # Need at least 4 samples for meaningful FFT analysis
348
- if n >= 4:
349
- f, psd = sp_signal.welch(data, fs=sample_rate, nperseg=min(4096, n))
350
-
351
- # Find dominant frequency (excluding DC)
352
- psd_no_dc = psd.copy()
353
- psd_no_dc[0] = 0
354
-
355
- if len(psd_no_dc) > 0 and np.any(psd_no_dc > 0):
356
- peak_idx = np.argmax(psd_no_dc)
357
- mean_psd = np.mean(psd_no_dc[psd_no_dc > 0]) if np.any(psd_no_dc > 0) else 0
358
- fundamental_frequency = f[peak_idx] if psd_no_dc[peak_idx] > 10 * mean_psd else None
359
- else:
360
- fundamental_frequency = None
391
+ if n < 4:
392
+ return False, None, 0.0, 0.0
361
393
 
362
- is_periodic = fundamental_frequency is not None
394
+ f, psd = sp_signal.welch(data, fs=sample_rate, nperseg=min(4096, n))
395
+
396
+ # Find dominant frequency (excluding DC)
397
+ psd_no_dc = psd.copy()
398
+ psd_no_dc[0] = 0
399
+
400
+ if len(psd_no_dc) > 0 and np.any(psd_no_dc > 0):
401
+ peak_idx = np.argmax(psd_no_dc)
402
+ mean_psd = np.mean(psd_no_dc[psd_no_dc > 0]) if np.any(psd_no_dc > 0) else 0
403
+ fundamental_frequency = f[peak_idx] if psd_no_dc[peak_idx] > 10 * mean_psd else None
363
404
  else:
364
405
  fundamental_frequency = None
365
- is_periodic = False
406
+
407
+ is_periodic = fundamental_frequency is not None
366
408
 
367
409
  # Estimate noise floor
368
- if n >= 4:
369
- noise_floor = np.median(np.sort(psd)[: len(psd) // 4]) if len(psd) > 0 else 0.0
370
- signal_power = np.max(psd) - noise_floor if len(psd) > 0 else 0.0
371
- else:
372
- noise_floor = 0.0
373
- signal_power = 0.0
410
+ noise_floor = np.median(np.sort(psd)[: len(psd) // 4]) if len(psd) > 0 else 0.0
411
+ signal_power = np.max(psd) - noise_floor if len(psd) > 0 else 0.0
374
412
  snr_db = 10 * np.log10(signal_power / noise_floor) if noise_floor > 0 else 0
375
413
 
376
- # Estimate rise/fall times for digital signals
377
- rise_time = None
378
- fall_time = None
414
+ return is_periodic, fundamental_frequency, noise_floor, snr_db
379
415
 
380
- if signal_type == "digital":
381
- threshold_low = v_min + 0.1 * (v_max - v_min)
382
- threshold_high = v_min + 0.9 * (v_max - v_min)
383
-
384
- # Find rising edges
385
- rising_times = []
386
- falling_times = []
387
-
388
- for i in range(1, len(data) - 1):
389
- if data[i - 1] < threshold_low and data[i + 1] > threshold_high:
390
- # Rising edge
391
- rising_times.append(1 / sample_rate)
392
- elif data[i - 1] > threshold_high and data[i + 1] < threshold_low:
393
- # Falling edge
394
- falling_times.append(1 / sample_rate)
395
-
396
- if rising_times:
397
- rise_time = float(np.median(rising_times))
398
- if falling_times:
399
- fall_time = float(np.median(falling_times))
400
416
 
401
- # Suggest protocols
402
- suggested_protocols = _suggest_protocols(signal_type, fundamental_frequency, sample_rate, data)
417
+ def _estimate_rise_fall_times(
418
+ data: NDArray[np.float64],
419
+ v_min: float,
420
+ v_max: float,
421
+ sample_rate: float,
422
+ ) -> tuple[float | None, float | None]:
423
+ """Estimate rise and fall times for digital signals.
424
+
425
+ Args:
426
+ data: Signal data array
427
+ v_min: Minimum voltage
428
+ v_max: Maximum voltage
429
+ sample_rate: Sample rate in Hz
430
+
431
+ Returns:
432
+ Tuple of (rise_time, fall_time) in seconds
433
+ """
434
+ threshold_low = v_min + 0.1 * (v_max - v_min)
435
+ threshold_high = v_min + 0.9 * (v_max - v_min)
436
+
437
+ rising_times = []
438
+ falling_times = []
403
439
 
404
- # Collect features
405
- features = {
440
+ for i in range(1, len(data) - 1):
441
+ if data[i - 1] < threshold_low and data[i + 1] > threshold_high:
442
+ rising_times.append(1 / sample_rate)
443
+ elif data[i - 1] > threshold_high and data[i + 1] < threshold_low:
444
+ falling_times.append(1 / sample_rate)
445
+
446
+ rise_time = float(np.median(rising_times)) if rising_times else None
447
+ fall_time = float(np.median(falling_times)) if falling_times else None
448
+
449
+ return rise_time, fall_time
450
+
451
+
452
+ def _build_features_dict(
453
+ v_min: float,
454
+ v_max: float,
455
+ v_mean: float,
456
+ v_std: float,
457
+ data: NDArray[np.float64],
458
+ peaks: list[tuple[float, float]],
459
+ ) -> dict[str, Any]:
460
+ """Build features dictionary for characterization.
461
+
462
+ Args:
463
+ v_min: Minimum voltage
464
+ v_max: Maximum voltage
465
+ v_mean: Mean voltage
466
+ v_std: Standard deviation
467
+ data: Signal data array
468
+ peaks: Histogram peaks
469
+
470
+ Returns:
471
+ Dictionary of extracted features
472
+ """
473
+ return {
406
474
  "v_min": v_min,
407
475
  "v_max": v_max,
408
476
  "v_mean": v_mean,
@@ -412,6 +480,61 @@ def characterize_unknown_signal(
412
480
  "peak_positions": [p[0] for p in peaks],
413
481
  }
414
482
 
483
+
484
+ def characterize_unknown_signal(
485
+ trace: WaveformTrace,
486
+ ) -> UnknownSignalCharacterization:
487
+ """Comprehensive characterization of unknown signal per UNKNOWN-003.
488
+
489
+ Analyzes signal characteristics to determine type, periodicity,
490
+ and suggest possible protocols.
491
+
492
+ Args:
493
+ trace: Signal trace to characterize.
494
+
495
+ Returns:
496
+ UnknownSignalCharacterization with all extracted features.
497
+
498
+ Example:
499
+ >>> result = characterize_unknown_signal(trace)
500
+ >>> print(f"Signal type: {result.signal_type}")
501
+ >>> print(f"Periodic: {result.is_periodic}")
502
+ >>> for protocol, confidence in result.suggested_protocols:
503
+ ... print(f" {protocol}: {confidence:.1%}")
504
+
505
+ References:
506
+ UNKNOWN-003: Unknown Signal Characterization
507
+ """
508
+ data = trace.data
509
+ sample_rate = trace.metadata.sample_rate
510
+
511
+ # Handle edge case of very short traces
512
+ if len(data) < 2:
513
+ return _handle_short_trace(data)
514
+
515
+ # Compute basic statistics
516
+ v_min, v_max, v_mean, v_std, dc_offset, amplitude = _compute_basic_statistics(data)
517
+
518
+ # Find histogram peaks and classify signal type
519
+ peaks = _find_histogram_peaks(data, v_min, v_max)
520
+ signal_type = _classify_signal_type(peaks, v_min, v_max, v_std, amplitude)
521
+
522
+ # Analyze periodicity
523
+ is_periodic, fundamental_frequency, noise_floor, snr_db = _analyze_periodicity(
524
+ data, sample_rate
525
+ )
526
+
527
+ # Estimate rise/fall times for digital signals
528
+ rise_time, fall_time = None, None
529
+ if signal_type == "digital":
530
+ rise_time, fall_time = _estimate_rise_fall_times(data, v_min, v_max, sample_rate)
531
+
532
+ # Suggest protocols
533
+ suggested_protocols = _suggest_protocols(signal_type, fundamental_frequency, sample_rate, data)
534
+
535
+ # Build features dictionary
536
+ features = _build_features_dict(v_min, v_max, v_mean, v_std, data, peaks)
537
+
415
538
  return UnknownSignalCharacterization(
416
539
  signal_type=signal_type,
417
540
  is_periodic=is_periodic,
oscura/jupyter/magic.py CHANGED
@@ -68,7 +68,7 @@ except ImportError:
68
68
  class Magics: # type: ignore[no-redef]
69
69
  """Fallback Magics class when IPython not available."""
70
70
 
71
- def magics_class(cls: type[Any]) -> type[Any]: # type: ignore[no-redef,misc]
71
+ def magics_class(cls: Any) -> Any:
72
72
  """Dummy decorator when IPython not available."""
73
73
  return cls
74
74
 
@@ -82,14 +82,14 @@ except ImportError:
82
82
 
83
83
 
84
84
  @magics_class
85
- class OscuraMagics(Magics): # type: ignore[misc]
85
+ class OscuraMagics(Magics):
86
86
  """IPython magics for Oscura analysis.
87
87
 
88
88
  Provides convenient shortcuts for loading traces, running measurements,
89
89
  and displaying results in Jupyter notebooks.
90
90
  """
91
91
 
92
- @line_magic # type: ignore[misc, untyped-decorator]
92
+ @line_magic # type: ignore[untyped-decorator]
93
93
  def oscura(self, line: str) -> Any:
94
94
  """Oscura line magic for quick operations.
95
95
 
@@ -263,7 +263,7 @@ Example:
263
263
  """
264
264
  print(help_text)
265
265
 
266
- @cell_magic # type: ignore[misc, untyped-decorator]
266
+ @cell_magic # type: ignore[untyped-decorator]
267
267
  def analyze(self, line: str, cell: str) -> Any:
268
268
  """Cell magic for multi-line analysis.
269
269
 
@@ -3,7 +3,7 @@
3
3
  Provides user interface patterns for progressive disclosure and formatting utilities.
4
4
  """
5
5
 
6
- from oscura.ui.formatters import (
6
+ from oscura.jupyter.ui.formatters import (
7
7
  Color,
8
8
  FormattedText,
9
9
  TextAlignment,
@@ -20,7 +20,7 @@ from oscura.ui.formatters import (
20
20
  format_text,
21
21
  truncate,
22
22
  )
23
- from oscura.ui.progressive_display import (
23
+ from oscura.jupyter.ui.progressive_display import (
24
24
  ProgressiveDisplay,
25
25
  ProgressiveOutput,
26
26
  Section,
@@ -6,7 +6,7 @@ including text alignment, truncation, color codes, and structured output.
6
6
  - UI formatting for terminal and web outputs
7
7
 
8
8
  Example:
9
- >>> from oscura.ui.formatters import format_text, truncate, colorize
9
+ >>> from oscura.jupyter.ui.formatters import format_text, truncate, colorize
10
10
  >>> format_text("Status", "active", align="left", width=20)
11
11
  ' Status: active'
12
12
  >>> truncate("Very long text here", max_length=10)
@@ -294,7 +294,7 @@ def format_status(
294
294
  "pass": "✓",
295
295
  "fail": "✗",
296
296
  "warning": "⚠",
297
- "info": "ℹ", # noqa: RUF001 - intentional unicode info symbol
297
+ "info": "ℹ",
298
298
  "pending": "⏳",
299
299
  }
300
300
 
@@ -307,7 +307,7 @@ def format_status(
307
307
  }
308
308
 
309
309
  # Get color with default, cast needed because dict.get default is str
310
- status_color: ColorName = colors.get(status, "white") # type: ignore[assignment]
310
+ status_color: ColorName = colors.get(status, "white")
311
311
 
312
312
  if use_symbols:
313
313
  symbol = symbols.get(status, "•")