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
@@ -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, "•")