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
@@ -15,7 +15,7 @@ References:
15
15
 
16
16
  from __future__ import annotations
17
17
 
18
- from typing import TYPE_CHECKING
18
+ from typing import TYPE_CHECKING, Any
19
19
 
20
20
  import numpy as np
21
21
 
@@ -83,10 +83,25 @@ def render_thumbnail(
83
83
  if not HAS_MATPLOTLIB:
84
84
  raise ImportError("matplotlib is required for visualization")
85
85
 
86
- # Default sample rate if not provided
87
- if sample_rate is None:
88
- sample_rate = 1.0
86
+ sample_rate = sample_rate if sample_rate is not None else 1.0
87
+ _validate_thumbnail_params(signal, sample_rate, max_samples)
88
+ size = _compute_thumbnail_size(size, width, height)
89
89
 
90
+ with plt.rc_context(_get_fast_rendering_config()):
91
+ fig, ax = _create_thumbnail_figure(size, dpi)
92
+ decimated_signal = _decimate_uniform(signal, max_samples)
93
+ total_time = len(signal) / sample_rate
94
+ time_scaled, time_unit = _prepare_time_axis(decimated_signal, total_time, time_unit)
95
+ _plot_thumbnail_signal(ax, time_scaled, decimated_signal, time_unit, title)
96
+ fig.tight_layout(pad=0.5)
97
+
98
+ return fig
99
+
100
+
101
+ def _validate_thumbnail_params(
102
+ signal: NDArray[np.float64], sample_rate: float, max_samples: int
103
+ ) -> None:
104
+ """Validate thumbnail rendering parameters."""
90
105
  if len(signal) == 0:
91
106
  raise ValueError("Signal cannot be empty")
92
107
  if sample_rate <= 0:
@@ -94,70 +109,75 @@ def render_thumbnail(
94
109
  if max_samples < 10:
95
110
  raise ValueError("max_samples must be >= 10")
96
111
 
97
- # Handle width/height as alternative to size
112
+
113
+ def _compute_thumbnail_size(
114
+ size: tuple[int, int], width: int | None, height: int | None
115
+ ) -> tuple[int, int]:
116
+ """Compute thumbnail size from width/height or size tuple."""
98
117
  if width is not None:
99
118
  h = height if height is not None else int(width * 0.75)
100
- size = (width, h)
101
- elif height is not None:
102
- size = (int(height * 4 / 3), height)
103
-
104
- # Configure matplotlib for fast rendering (no anti-aliasing, etc.)
105
- with plt.rc_context(
106
- {
107
- "path.simplify": True,
108
- "path.simplify_threshold": 1.0,
109
- "agg.path.chunksize": 1000,
110
- "lines.antialiased": False,
111
- "patch.antialiased": False,
112
- "text.antialiased": False,
113
- }
114
- ):
115
- # Calculate figure size in inches
116
- width_inches = size[0] / dpi
117
- height_inches = size[1] / dpi
118
-
119
- # Create figure with no fancy features
120
- fig, ax = plt.subplots(figsize=(width_inches, height_inches), dpi=dpi)
121
-
122
- # Decimate signal to max_samples
123
- decimated_signal = _decimate_uniform(signal, max_samples)
124
-
125
- # Create time vector for decimated signal
126
- total_time = len(signal) / sample_rate
127
- time = np.linspace(0, total_time, len(decimated_signal))
128
-
129
- # Auto-select time unit
130
- if time_unit == "auto":
131
- if total_time < 1e-6:
132
- time_unit = "ns"
133
- elif total_time < 1e-3:
134
- time_unit = "us"
135
- elif total_time < 1:
136
- time_unit = "ms"
137
- else:
138
- time_unit = "s"
139
-
140
- time_multipliers = {"s": 1.0, "ms": 1e3, "us": 1e6, "ns": 1e9}
141
- multiplier = time_multipliers.get(time_unit, 1.0)
142
- time_scaled = time * multiplier
143
-
144
- # Plot with simplified style
145
- ax.plot(time_scaled, decimated_signal, "b-", linewidth=0.5, antialiased=False)
146
-
147
- # Minimal labels (no grid, no fancy formatting)
148
- ax.set_xlabel(f"Time ({time_unit})", fontsize=8)
149
- ax.set_ylabel("Amplitude", fontsize=8)
150
-
151
- if title:
152
- ax.set_title(title, fontsize=9)
153
-
154
- # Reduce tick label size
155
- ax.tick_params(labelsize=7)
156
-
157
- # Tight layout to maximize plot area
158
- fig.tight_layout(pad=0.5)
159
-
160
- return fig
119
+ return (width, h)
120
+ if height is not None:
121
+ return (int(height * 4 / 3), height)
122
+ return size
123
+
124
+
125
+ def _get_fast_rendering_config() -> dict[str, bool | float]:
126
+ """Get matplotlib configuration for fast rendering."""
127
+ return {
128
+ "path.simplify": True,
129
+ "path.simplify_threshold": 1.0,
130
+ "agg.path.chunksize": 1000,
131
+ "lines.antialiased": False,
132
+ "patch.antialiased": False,
133
+ "text.antialiased": False,
134
+ }
135
+
136
+
137
+ def _create_thumbnail_figure(size: tuple[int, int], dpi: int) -> tuple[Figure, Any]:
138
+ """Create matplotlib figure for thumbnail."""
139
+ width_inches = size[0] / dpi
140
+ height_inches = size[1] / dpi
141
+ return plt.subplots(figsize=(width_inches, height_inches), dpi=dpi)
142
+
143
+
144
+ def _prepare_time_axis(
145
+ decimated_signal: NDArray[np.float64], total_time: float, time_unit: str
146
+ ) -> tuple[NDArray[np.float64], str]:
147
+ """Prepare time axis with auto unit selection."""
148
+ time = np.linspace(0, total_time, len(decimated_signal))
149
+ if time_unit == "auto":
150
+ time_unit = _auto_select_time_unit(total_time)
151
+ time_multipliers = {"s": 1.0, "ms": 1e3, "us": 1e6, "ns": 1e9}
152
+ multiplier = time_multipliers.get(time_unit, 1.0)
153
+ return time * multiplier, time_unit
154
+
155
+
156
+ def _auto_select_time_unit(total_time: float) -> str:
157
+ """Auto-select appropriate time unit based on signal duration."""
158
+ if total_time < 1e-6:
159
+ return "ns"
160
+ if total_time < 1e-3:
161
+ return "us"
162
+ if total_time < 1:
163
+ return "ms"
164
+ return "s"
165
+
166
+
167
+ def _plot_thumbnail_signal(
168
+ ax: Any,
169
+ time_scaled: NDArray[np.float64],
170
+ decimated_signal: NDArray[np.float64],
171
+ time_unit: str,
172
+ title: str | None,
173
+ ) -> None:
174
+ """Plot signal on thumbnail axes."""
175
+ ax.plot(time_scaled, decimated_signal, "b-", linewidth=0.5, antialiased=False)
176
+ ax.set_xlabel(f"Time ({time_unit})", fontsize=8)
177
+ ax.set_ylabel("Amplitude", fontsize=8)
178
+ if title:
179
+ ax.set_title(title, fontsize=9)
180
+ ax.tick_params(labelsize=7)
161
181
 
162
182
 
163
183
  def _decimate_uniform(signal: NDArray[np.float64], target_samples: int) -> NDArray[np.float64]:
@@ -220,89 +240,98 @@ def render_thumbnail_multichannel(
220
240
  References:
221
241
  VIS-018: Thumbnail Mode
222
242
  """
243
+ _validate_multichannel_params(signals, sample_rate)
244
+ n_channels = len(signals)
245
+ names = channel_names if channel_names is not None else _default_channel_names(n_channels)
246
+ time_unit_resolved, multiplier = _resolve_time_unit(signals[0], sample_rate, time_unit)
247
+
248
+ with plt.rc_context(_get_fast_rendering_config()):
249
+ fig, axes = _create_multichannel_figure(n_channels, size, dpi)
250
+ _plot_multichannel_signals(
251
+ axes, signals, names, sample_rate, max_samples, multiplier, time_unit_resolved
252
+ )
253
+ fig.tight_layout(pad=0.3)
254
+
255
+ return fig
256
+
257
+
258
+ def _validate_multichannel_params(signals: list[NDArray[np.float64]], sample_rate: float) -> None:
259
+ """Validate multichannel thumbnail parameters."""
223
260
  if not HAS_MATPLOTLIB:
224
261
  raise ImportError("matplotlib is required for visualization")
225
-
226
262
  if len(signals) == 0:
227
263
  raise ValueError("Must provide at least one signal")
228
264
  if sample_rate <= 0:
229
265
  raise ValueError("Sample rate must be positive")
230
266
 
267
+
268
+ def _default_channel_names(n_channels: int) -> list[str]:
269
+ """Generate default channel names."""
270
+ return [f"CH{i + 1}" for i in range(n_channels)]
271
+
272
+
273
+ def _resolve_time_unit(
274
+ first_signal: NDArray[np.float64], sample_rate: float, time_unit: str
275
+ ) -> tuple[str, float]:
276
+ """Resolve time unit and multiplier."""
277
+ if len(first_signal) > 0 and time_unit == "auto":
278
+ total_time = len(first_signal) / sample_rate
279
+ time_unit = _auto_select_time_unit(total_time)
280
+ elif time_unit == "auto":
281
+ time_unit = "s"
282
+
283
+ time_multipliers = {"s": 1.0, "ms": 1e3, "us": 1e6, "ns": 1e9}
284
+ multiplier = time_multipliers.get(time_unit, 1.0)
285
+ return time_unit, multiplier
286
+
287
+
288
+ def _create_multichannel_figure(
289
+ n_channels: int, size: tuple[int, int], dpi: int
290
+ ) -> tuple[Figure, Any]:
291
+ """Create matplotlib figure for multichannel display."""
292
+ width_inches = size[0] / dpi
293
+ height_inches = size[1] / dpi
294
+
295
+ fig, axes = plt.subplots(
296
+ n_channels,
297
+ 1,
298
+ figsize=(width_inches, height_inches),
299
+ dpi=dpi,
300
+ sharex=True,
301
+ )
302
+
303
+ if n_channels == 1:
304
+ axes = [axes]
305
+
306
+ return fig, axes
307
+
308
+
309
+ def _plot_multichannel_signals(
310
+ axes: Any,
311
+ signals: list[NDArray[np.float64]],
312
+ names: list[str],
313
+ sample_rate: float,
314
+ max_samples: int,
315
+ multiplier: float,
316
+ time_unit: str,
317
+ ) -> None:
318
+ """Plot all channels on their respective axes."""
231
319
  n_channels = len(signals)
232
320
 
233
- if channel_names is None:
234
- channel_names = [f"CH{i + 1}" for i in range(n_channels)]
235
-
236
- # Configure matplotlib for fast rendering
237
- with plt.rc_context(
238
- {
239
- "path.simplify": True,
240
- "path.simplify_threshold": 1.0,
241
- "agg.path.chunksize": 1000,
242
- "lines.antialiased": False,
243
- "patch.antialiased": False,
244
- "text.antialiased": False,
245
- }
246
- ):
247
- # Calculate figure size
248
- width_inches = size[0] / dpi
249
- height_inches = size[1] / dpi
250
-
251
- fig, axes = plt.subplots(
252
- n_channels,
253
- 1,
254
- figsize=(width_inches, height_inches),
255
- dpi=dpi,
256
- sharex=True,
257
- )
321
+ for i, (sig, name, ax) in enumerate(zip(signals, names, axes, strict=False)):
322
+ if len(sig) == 0:
323
+ continue
258
324
 
259
- if n_channels == 1:
260
- axes = [axes]
261
-
262
- # Get time unit from first signal
263
- if len(signals[0]) > 0:
264
- total_time = len(signals[0]) / sample_rate
265
- if time_unit == "auto":
266
- if total_time < 1e-6:
267
- time_unit = "ns"
268
- elif total_time < 1e-3:
269
- time_unit = "us"
270
- elif total_time < 1:
271
- time_unit = "ms"
272
- else:
273
- time_unit = "s"
274
- else:
275
- time_unit = "s"
276
-
277
- time_multipliers = {"s": 1.0, "ms": 1e3, "us": 1e6, "ns": 1e9}
278
- multiplier = time_multipliers.get(time_unit, 1.0)
279
-
280
- # Plot each channel
281
- for i, (sig, name, ax) in enumerate(zip(signals, channel_names, axes, strict=False)):
282
- if len(sig) == 0:
283
- continue
284
-
285
- # Decimate signal
286
- decimated = _decimate_uniform(sig, max_samples)
287
-
288
- # Time vector
289
- total_time = len(sig) / sample_rate
290
- time = np.linspace(0, total_time, len(decimated)) * multiplier
291
-
292
- # Plot
293
- ax.plot(time, decimated, "b-", linewidth=0.5, antialiased=False)
294
-
295
- # Channel label
296
- ax.set_ylabel(name, fontsize=7, rotation=0, ha="right", va="center")
297
- ax.tick_params(labelsize=6)
298
-
299
- # Only x-label on bottom
300
- if i == n_channels - 1:
301
- ax.set_xlabel(f"Time ({time_unit})", fontsize=8)
325
+ decimated = _decimate_uniform(sig, max_samples)
326
+ total_time = len(sig) / sample_rate
327
+ time = np.linspace(0, total_time, len(decimated)) * multiplier
302
328
 
303
- fig.tight_layout(pad=0.3)
329
+ ax.plot(time, decimated, "b-", linewidth=0.5, antialiased=False)
330
+ ax.set_ylabel(name, fontsize=7, rotation=0, ha="right", va="center")
331
+ ax.tick_params(labelsize=6)
304
332
 
305
- return fig
333
+ if i == n_channels - 1:
334
+ ax.set_xlabel(f"Time ({time_unit})", fontsize=8)
306
335
 
307
336
 
308
337
  __all__ = [
@@ -92,15 +92,58 @@ def plot_waveform(
92
92
  if not HAS_MATPLOTLIB:
93
93
  raise ImportError("matplotlib is required for visualization")
94
94
 
95
+ # Setup figure and axes
96
+ fig, ax = _setup_waveform_figure(ax, figsize)
97
+
98
+ # Prepare time axis
99
+ time_unit_final, time_info = _prepare_time_axis(trace, time_unit)
100
+ time_scaled, _ = time_info
101
+
102
+ # Plot waveform
103
+ _plot_waveform_data(ax, time_scaled, trace.data, color, label)
104
+
105
+ # Apply styling and formatting
106
+ _apply_waveform_formatting(
107
+ ax,
108
+ time_range,
109
+ time_unit_final,
110
+ time_info,
111
+ xlabel,
112
+ ylabel,
113
+ title,
114
+ trace.metadata.channel_name,
115
+ show_grid,
116
+ label,
117
+ )
118
+
119
+ # Add measurements if provided
120
+ if show_measurements:
121
+ _add_measurement_annotations(ax, trace, show_measurements, time_unit_final, time_info[1])
122
+
123
+ fig.tight_layout()
124
+
125
+ # Save and show
126
+ _save_and_show_figure(fig, save_path, show)
127
+
128
+ return fig
129
+
130
+
131
+ def _setup_waveform_figure(ax: Axes | None, figsize: tuple[float, float]) -> tuple[Figure, Axes]:
132
+ """Setup figure and axes for waveform plot."""
95
133
  if ax is None:
96
134
  fig, ax = plt.subplots(figsize=figsize)
97
- else:
98
- fig_temp = ax.get_figure()
99
- if fig_temp is None:
100
- raise ValueError("Axes must have an associated figure")
101
- fig = cast("Figure", fig_temp)
135
+ return fig, ax
102
136
 
103
- # Calculate time axis
137
+ fig_temp = ax.get_figure()
138
+ if fig_temp is None:
139
+ raise ValueError("Axes must have an associated figure")
140
+ return cast("Figure", fig_temp), ax
141
+
142
+
143
+ def _prepare_time_axis(
144
+ trace: WaveformTrace, time_unit: str
145
+ ) -> tuple[str, tuple[NDArray[np.float64], float]]:
146
+ """Prepare time axis with appropriate unit and scaling."""
104
147
  time = trace.time_vector
105
148
 
106
149
  # Auto-select time unit
@@ -119,8 +162,34 @@ def plot_waveform(
119
162
  multiplier = time_multipliers.get(time_unit, 1.0)
120
163
  time_scaled = time * multiplier
121
164
 
122
- # Plot waveform
123
- ax.plot(time_scaled, trace.data, color=color, label=label, linewidth=0.8)
165
+ return time_unit, (time_scaled, multiplier)
166
+
167
+
168
+ def _plot_waveform_data(
169
+ ax: Axes,
170
+ time_scaled: NDArray[np.float64],
171
+ data: NDArray[np.float64],
172
+ color: str,
173
+ label: str | None,
174
+ ) -> None:
175
+ """Plot waveform data on axes."""
176
+ ax.plot(time_scaled, data, color=color, label=label, linewidth=0.8)
177
+
178
+
179
+ def _apply_waveform_formatting(
180
+ ax: Axes,
181
+ time_range: tuple[float, float] | None,
182
+ time_unit: str,
183
+ time_info: tuple[NDArray[np.float64], float],
184
+ xlabel: str,
185
+ ylabel: str,
186
+ title: str | None,
187
+ channel_name: str | None,
188
+ show_grid: bool,
189
+ label: str | None,
190
+ ) -> None:
191
+ """Apply formatting to waveform plot."""
192
+ _, multiplier = time_info
124
193
 
125
194
  # Apply time range if specified
126
195
  if time_range is not None:
@@ -130,33 +199,28 @@ def plot_waveform(
130
199
  ax.set_xlabel(f"{xlabel} ({time_unit})")
131
200
  ax.set_ylabel(ylabel)
132
201
 
202
+ # Title
133
203
  if title:
134
204
  ax.set_title(title)
135
- elif trace.metadata.channel_name:
136
- ax.set_title(f"Waveform - {trace.metadata.channel_name}")
205
+ elif channel_name:
206
+ ax.set_title(f"Waveform - {channel_name}")
137
207
 
208
+ # Grid and legend
138
209
  if show_grid:
139
210
  ax.grid(True, alpha=0.3)
140
211
 
141
212
  if label:
142
213
  ax.legend()
143
214
 
144
- # Add measurement annotations
145
- if show_measurements:
146
- _add_measurement_annotations(ax, trace, show_measurements, time_unit, multiplier)
147
215
 
148
- fig.tight_layout()
149
-
150
- # Save if path provided
216
+ def _save_and_show_figure(fig: Figure, save_path: str | None, show: bool) -> None:
217
+ """Save and/or display the figure."""
151
218
  if save_path is not None:
152
219
  fig.savefig(save_path, dpi=300, bbox_inches="tight")
153
220
 
154
- # Show if requested
155
221
  if show:
156
222
  plt.show()
157
223
 
158
- return fig
159
-
160
224
 
161
225
  def plot_multi_channel(
162
226
  traces: list[WaveformTrace | DigitalTrace],
@@ -193,74 +257,97 @@ def plot_multi_channel(
193
257
  >>> fig = plot_multi_channel([ch1, ch2, ch3], names=["CLK", "DATA", "CS"])
194
258
  >>> plt.show()
195
259
  """
196
- # Handle share_x alias
197
- if share_x is not None:
198
- shared_x = share_x
199
260
  if not HAS_MATPLOTLIB:
200
261
  raise ImportError("matplotlib is required for visualization")
201
262
 
263
+ shared_x = share_x if share_x is not None else shared_x
202
264
  n_channels = len(traces)
265
+ names = names or [f"CH{i + 1}" for i in range(n_channels)]
266
+ figsize = figsize or (10, 2 * n_channels)
203
267
 
204
- if names is None:
205
- names = [f"CH{i + 1}" for i in range(n_channels)]
268
+ fig, axes = plt.subplots(n_channels, 1, figsize=figsize, sharex=shared_x)
269
+ axes = [axes] if n_channels == 1 else axes
206
270
 
207
- if figsize is None:
208
- figsize = (10, 2 * n_channels)
271
+ time_unit, multiplier = _determine_time_unit_and_multiplier(time_unit, traces)
209
272
 
210
- fig, axes = plt.subplots(
211
- n_channels,
212
- 1,
213
- figsize=figsize,
214
- sharex=shared_x,
215
- )
273
+ _plot_channels(traces, names, axes, colors, time_unit, multiplier, show_grid, n_channels)
216
274
 
217
- if n_channels == 1:
218
- axes = [axes]
275
+ if title:
276
+ fig.suptitle(title)
277
+
278
+ fig.tight_layout()
279
+ return fig
219
280
 
220
- # Auto-select time unit from first trace
281
+
282
+ def _determine_time_unit_and_multiplier(
283
+ time_unit: str, traces: list[WaveformTrace | DigitalTrace]
284
+ ) -> tuple[str, float]:
285
+ """Determine time unit and multiplier for plotting."""
221
286
  if time_unit == "auto" and len(traces) > 0:
222
287
  ref_trace = traces[0]
223
288
  duration = len(ref_trace.data) * ref_trace.metadata.time_base
224
- if duration < 1e-6:
225
- time_unit = "ns"
226
- elif duration < 1e-3:
227
- time_unit = "us"
228
- elif duration < 1:
229
- time_unit = "ms"
230
- else:
231
- time_unit = "s"
289
+ time_unit = _select_time_unit_from_duration(duration)
232
290
 
233
291
  time_multipliers = {"s": 1.0, "ms": 1e3, "us": 1e6, "ns": 1e9}
234
292
  multiplier = time_multipliers.get(time_unit, 1.0)
235
293
 
236
- for i, (trace, name, ax) in enumerate(zip(traces, names, axes, strict=False)):
237
- time = trace.time_vector * multiplier
238
- color = colors[i] if colors is not None and i < len(colors) else f"C{i}"
294
+ return time_unit, multiplier
239
295
 
240
- if isinstance(trace, WaveformTrace):
241
- ax.plot(time, trace.data, color=color, linewidth=0.8)
242
- ax.set_ylabel("V")
243
- else:
244
- # Digital trace - step plot
245
- ax.step(time, trace.data.astype(int), color=color, where="post", linewidth=1.0)
246
- ax.set_ylim(-0.1, 1.1)
247
- ax.set_yticks([0, 1])
248
- ax.set_yticklabels(["L", "H"])
249
296
 
250
- ax.set_ylabel(name, rotation=0, ha="right", va="center")
297
+ def _select_time_unit_from_duration(duration: float) -> str:
298
+ """Select appropriate time unit based on duration."""
299
+ if duration < 1e-6:
300
+ return "ns"
301
+ if duration < 1e-3:
302
+ return "us"
303
+ if duration < 1:
304
+ return "ms"
305
+ return "s"
251
306
 
252
- if show_grid:
253
- ax.grid(True, alpha=0.3)
254
307
 
255
- # Only show x-label on bottom plot
308
+ def _plot_channels(
309
+ traces: list[WaveformTrace | DigitalTrace],
310
+ names: list[str],
311
+ axes: list[Any],
312
+ colors: list[str] | None,
313
+ time_unit: str,
314
+ multiplier: float,
315
+ show_grid: bool,
316
+ n_channels: int,
317
+ ) -> None:
318
+ """Plot each channel on its subplot."""
319
+ for i, (trace, name, ax) in enumerate(zip(traces, names, axes, strict=False)):
320
+ time = trace.time_vector * multiplier
321
+ color = colors[i] if colors and i < len(colors) else f"C{i}"
322
+
323
+ _plot_single_channel(ax, trace, time, color, name, show_grid)
324
+
256
325
  if i == n_channels - 1:
257
326
  ax.set_xlabel(f"Time ({time_unit})")
258
327
 
259
- if title:
260
- fig.suptitle(title)
261
328
 
262
- fig.tight_layout()
263
- return fig
329
+ def _plot_single_channel(
330
+ ax: Any,
331
+ trace: WaveformTrace | DigitalTrace,
332
+ time: Any,
333
+ color: str,
334
+ name: str,
335
+ show_grid: bool,
336
+ ) -> None:
337
+ """Plot a single channel (analog or digital)."""
338
+ if isinstance(trace, WaveformTrace):
339
+ ax.plot(time, trace.data, color=color, linewidth=0.8)
340
+ ax.set_ylabel("V")
341
+ else:
342
+ ax.step(time, trace.data.astype(int), color=color, where="post", linewidth=1.0)
343
+ ax.set_ylim(-0.1, 1.1)
344
+ ax.set_yticks([0, 1])
345
+ ax.set_yticklabels(["L", "H"])
346
+
347
+ ax.set_ylabel(name, rotation=0, ha="right", va="center")
348
+
349
+ if show_grid:
350
+ ax.grid(True, alpha=0.3)
264
351
 
265
352
 
266
353
  def plot_xy(
@@ -16,6 +16,7 @@ Example:
16
16
  >>> stats = osc.workflows.load_all(["trace1.wfm", "trace2.wfm"])
17
17
  """
18
18
 
19
+ from oscura.workflows.complete_re import CompleteREResult, full_protocol_re
19
20
  from oscura.workflows.compliance import emc_compliance_test
20
21
  from oscura.workflows.digital import characterize_buffer
21
22
  from oscura.workflows.multi_trace import (
@@ -40,6 +41,7 @@ __all__ = [
40
41
  # Multi-trace
41
42
  "AlignmentMethod",
42
43
  # Reverse engineering
44
+ "CompleteREResult",
43
45
  "FieldSpec",
44
46
  "InferredFrame",
45
47
  "MultiTraceResults",
@@ -51,6 +53,7 @@ __all__ = [
51
53
  "characterize_buffer",
52
54
  "debug_protocol",
53
55
  "emc_compliance_test",
56
+ "full_protocol_re",
54
57
  "load_all",
55
58
  "power_analysis",
56
59
  "reverse_engineer_signal",
@@ -5,23 +5,23 @@ This module enables efficient batch analysis of multiple signal files
5
5
  with parallel execution support and comprehensive result aggregation.
6
6
  """
7
7
 
8
- from oscura.batch.advanced import (
8
+ from oscura.workflows.batch.advanced import (
9
9
  AdvancedBatchProcessor,
10
10
  BatchCheckpoint,
11
11
  BatchConfig,
12
12
  FileResult,
13
13
  resume_batch,
14
14
  )
15
- from oscura.batch.aggregate import aggregate_results
16
- from oscura.batch.analyze import batch_analyze
17
- from oscura.batch.logging import (
15
+ from oscura.workflows.batch.aggregate import aggregate_results
16
+ from oscura.workflows.batch.analyze import batch_analyze
17
+ from oscura.workflows.batch.logging import (
18
18
  BatchLogger,
19
19
  BatchSummary,
20
20
  FileLogEntry,
21
21
  FileLogger,
22
22
  aggregate_batch_logs,
23
23
  )
24
- from oscura.batch.metrics import (
24
+ from oscura.workflows.batch.metrics import (
25
25
  BatchMetrics,
26
26
  BatchMetricsSummary,
27
27
  ErrorBreakdown,