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
@@ -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,