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
@@ -11,10 +11,410 @@ from pathlib import Path
11
11
 
12
12
  import matplotlib.pyplot as plt
13
13
  import numpy as np
14
+ from matplotlib.axes import Axes
14
15
  from matplotlib.figure import Figure
15
16
  from numpy.typing import NDArray
16
17
 
17
18
 
19
+ def _normalize_power_channels(
20
+ power: NDArray[np.float64] | dict[str, NDArray[np.float64]],
21
+ ) -> tuple[dict[str, NDArray[np.float64]], bool]:
22
+ """Normalize power input into channels dictionary.
23
+
24
+ Args:
25
+ power: Single array or dict of arrays.
26
+
27
+ Returns:
28
+ Tuple of (channels dict, is_multi boolean).
29
+
30
+ Example:
31
+ >>> channels, is_multi = _normalize_power_channels(np.array([1, 2, 3]))
32
+ >>> channels
33
+ {'Power': array([1, 2, 3])}
34
+ """
35
+ if isinstance(power, dict):
36
+ return power, True
37
+ return {"Power": np.asarray(power, dtype=np.float64)}, False
38
+
39
+
40
+ def _validate_and_create_time_array(
41
+ time_array: NDArray[np.float64] | None,
42
+ sample_rate: float | None,
43
+ trace_length: int,
44
+ ) -> NDArray[np.float64]:
45
+ """Validate inputs and create time array.
46
+
47
+ Args:
48
+ time_array: Optional explicit time array.
49
+ sample_rate: Optional sample rate in Hz.
50
+ trace_length: Length of power trace.
51
+
52
+ Returns:
53
+ Validated time array.
54
+
55
+ Raises:
56
+ ValueError: If neither time_array nor sample_rate provided.
57
+ ValueError: If time_array length doesn't match trace.
58
+
59
+ Example:
60
+ >>> time = _validate_and_create_time_array(None, 1000.0, 100)
61
+ >>> len(time)
62
+ 100
63
+ """
64
+ if time_array is None and sample_rate is None:
65
+ raise ValueError("Either time_array or sample_rate must be provided")
66
+
67
+ if time_array is None:
68
+ if sample_rate is None:
69
+ raise ValueError("sample_rate is required when time_array is not provided")
70
+ return np.arange(trace_length) / sample_rate
71
+
72
+ time_array_validated = np.asarray(time_array, dtype=np.float64)
73
+ if len(time_array_validated) != trace_length:
74
+ raise ValueError(
75
+ f"time_array length {len(time_array_validated)} doesn't match "
76
+ f"power trace length {trace_length}"
77
+ )
78
+ return time_array_validated
79
+
80
+
81
+ def _compute_time_scale(time_array: NDArray[np.float64]) -> tuple[NDArray[np.float64], str]:
82
+ """Compute time scaling factor and units.
83
+
84
+ Args:
85
+ time_array: Time array in seconds.
86
+
87
+ Returns:
88
+ Tuple of (scaled time array, unit string).
89
+
90
+ Example:
91
+ >>> time = np.array([0, 1e-6, 2e-6])
92
+ >>> scaled, unit = _compute_time_scale(time)
93
+ >>> unit
94
+ 'µs'
95
+ """
96
+ time_max = time_array[-1]
97
+
98
+ if time_max < 1e-6:
99
+ return time_array * 1e9, "ns"
100
+ if time_max < 1e-3:
101
+ return time_array * 1e6, "µs"
102
+ if time_max < 1:
103
+ return time_array * 1e3, "ms"
104
+ return time_array, "s"
105
+
106
+
107
+ def _create_figure_layout(
108
+ is_multi: bool,
109
+ layout: str,
110
+ n_channels: int,
111
+ show_energy: bool,
112
+ figsize: tuple[float, float],
113
+ ) -> tuple[Figure, list[Axes]]:
114
+ """Create figure and axes layout.
115
+
116
+ Args:
117
+ is_multi: Multiple channels flag.
118
+ layout: 'stacked' or 'overlay'.
119
+ n_channels: Number of channels.
120
+ show_energy: Show energy plot flag.
121
+ figsize: Figure size.
122
+
123
+ Returns:
124
+ Tuple of (figure, axes list).
125
+
126
+ Example:
127
+ >>> fig, axes = _create_figure_layout(True, 'stacked', 2, True, (12, 6))
128
+ >>> len(axes)
129
+ 3
130
+ """
131
+ if is_multi and layout == "stacked":
132
+ n_plots = n_channels + (1 if show_energy else 0)
133
+ fig, axes_obj = plt.subplots(n_plots, 1, figsize=figsize, sharex=True)
134
+ if n_plots == 1:
135
+ return fig, [axes_obj]
136
+ return fig, list(axes_obj)
137
+
138
+ fig, ax_power = plt.subplots(figsize=figsize)
139
+ return fig, [ax_power]
140
+
141
+
142
+ def _plot_stacked_channels(
143
+ axes: list[Axes],
144
+ channels: dict[str, NDArray[np.float64]],
145
+ time_scaled: NDArray[np.float64],
146
+ time_unit: str,
147
+ statistics: dict[str, float] | None,
148
+ show_average: bool,
149
+ show_peak: bool,
150
+ show_energy: bool,
151
+ sample_rate: float | None,
152
+ ) -> None:
153
+ """Plot channels in stacked layout.
154
+
155
+ Args:
156
+ axes: List of axes objects.
157
+ channels: Channel data dictionary.
158
+ time_scaled: Scaled time array.
159
+ time_unit: Time unit string.
160
+ statistics: Optional statistics dictionary.
161
+ show_average: Show average line flag.
162
+ show_peak: Show peak marker flag.
163
+ show_energy: Show energy plot flag.
164
+ sample_rate: Sample rate in Hz.
165
+
166
+ Example:
167
+ >>> _plot_stacked_channels(axes, channels, time, 'ms', None, True, True, True, 1e6)
168
+ """
169
+ for idx, (name, trace) in enumerate(channels.items()):
170
+ ax = axes[idx]
171
+ ax.plot(time_scaled, trace * 1e3, linewidth=0.8, label=name)
172
+
173
+ # Compute or use statistics
174
+ if statistics is None or name not in statistics:
175
+ avg = np.mean(trace)
176
+ peak = np.max(trace)
177
+ else:
178
+ avg = statistics[name]["average"] # type: ignore[index]
179
+ peak = statistics[name]["peak"] # type: ignore[index]
180
+
181
+ # Annotations
182
+ if show_average:
183
+ ax.axhline(
184
+ float(avg * 1e3),
185
+ color="r",
186
+ linestyle="--",
187
+ linewidth=1,
188
+ alpha=0.7,
189
+ label=f"Avg: {avg * 1e3:.2f} mW",
190
+ )
191
+
192
+ if show_peak:
193
+ peak_idx = np.argmax(trace)
194
+ ax.plot(
195
+ time_scaled[peak_idx],
196
+ peak * 1e3,
197
+ "rv",
198
+ markersize=8,
199
+ label=f"Peak: {peak * 1e3:.2f} mW",
200
+ )
201
+
202
+ ax.set_ylabel(f"{name}\n(mW)")
203
+ ax.legend(loc="upper right", fontsize=8)
204
+ ax.grid(True, alpha=0.3)
205
+
206
+ # Energy accumulation plot
207
+ if show_energy:
208
+ ax_energy = axes[-1]
209
+ for name, trace in channels.items():
210
+ if sample_rate is not None:
211
+ energy = np.cumsum(trace) / sample_rate * 1e6 # µJ
212
+ ax_energy.plot(time_scaled, energy, linewidth=0.8, label=name)
213
+
214
+ ax_energy.set_ylabel("Cumulative\nEnergy (µJ)")
215
+ ax_energy.set_xlabel(f"Time ({time_unit})")
216
+ ax_energy.legend(loc="upper left", fontsize=8)
217
+ ax_energy.grid(True, alpha=0.3)
218
+
219
+
220
+ def _plot_overlay_channels(
221
+ ax: Axes,
222
+ channels: dict[str, NDArray[np.float64]],
223
+ time_scaled: NDArray[np.float64],
224
+ time_unit: str,
225
+ statistics: dict[str, float] | None,
226
+ show_average: bool,
227
+ show_peak: bool,
228
+ show_energy: bool,
229
+ sample_rate: float | None,
230
+ ) -> None:
231
+ """Plot channels in overlay layout.
232
+
233
+ Args:
234
+ ax: Axes object.
235
+ channels: Channel data dictionary.
236
+ time_scaled: Scaled time array.
237
+ time_unit: Time unit string.
238
+ statistics: Optional statistics dictionary.
239
+ show_average: Show average line flag.
240
+ show_peak: Show peak marker flag.
241
+ show_energy: Show energy plot flag.
242
+ sample_rate: Sample rate in Hz.
243
+
244
+ Example:
245
+ >>> _plot_overlay_channels(ax, channels, time, 'ms', None, True, True, True, 1e6)
246
+ """
247
+ for name, trace in channels.items():
248
+ ax.plot(time_scaled, trace * 1e3, linewidth=0.8, label=name)
249
+
250
+ # Statistics for first channel (or combined if overlay)
251
+ first_trace = next(iter(channels.values()))
252
+ if statistics is None:
253
+ avg_val = float(np.mean(first_trace))
254
+ peak_val = float(np.max(first_trace))
255
+ total_energy_val: float | None = (
256
+ float(np.sum(first_trace) / sample_rate) if sample_rate else None
257
+ )
258
+ else:
259
+ avg_val = float(statistics.get("average", float(np.mean(first_trace))))
260
+ peak_val = float(statistics.get("peak", float(np.max(first_trace))))
261
+ total_energy_val = statistics.get("energy", None)
262
+
263
+ # Annotations
264
+ if show_average:
265
+ ax.axhline(
266
+ avg_val * 1e3,
267
+ color="r",
268
+ linestyle="--",
269
+ linewidth=1,
270
+ alpha=0.7,
271
+ label=f"Avg: {avg_val * 1e3:.2f} mW",
272
+ )
273
+
274
+ if show_peak:
275
+ peak_idx = np.argmax(first_trace)
276
+ ax.plot(
277
+ time_scaled[peak_idx],
278
+ peak_val * 1e3,
279
+ "rv",
280
+ markersize=8,
281
+ label=f"Peak: {peak_val * 1e3:.2f} mW",
282
+ )
283
+
284
+ ax.set_ylabel("Power (mW)")
285
+ ax.set_xlabel(f"Time ({time_unit})")
286
+ ax.legend(loc="upper right")
287
+ ax.grid(True, alpha=0.3)
288
+
289
+ # Energy overlay on secondary y-axis
290
+ if show_energy and sample_rate is not None:
291
+ ax2 = ax.twinx()
292
+ energy = np.cumsum(first_trace) / sample_rate * 1e6 # µJ
293
+ ax2.plot(time_scaled, energy, "g--", linewidth=1.5, alpha=0.6)
294
+ ax2.set_ylabel("Cumulative Energy (µJ)", color="g")
295
+ ax2.tick_params(axis="y", labelcolor="g")
296
+
297
+ if total_energy_val is not None:
298
+ ax2.text(
299
+ 0.98,
300
+ 0.98,
301
+ f"Total: {total_energy_val * 1e6:.2f} µJ",
302
+ transform=ax.transAxes,
303
+ ha="right",
304
+ va="top",
305
+ bbox={"boxstyle": "round", "facecolor": "wheat", "alpha": 0.5},
306
+ )
307
+
308
+
309
+ def _prepare_power_plot_data(
310
+ power: NDArray[np.float64] | dict[str, NDArray[np.float64]],
311
+ time_array: NDArray[np.float64] | None,
312
+ sample_rate: float | None,
313
+ ) -> tuple[dict[str, NDArray[np.float64]], bool, NDArray[np.float64], str]:
314
+ """Prepare data for power plot.
315
+
316
+ Args:
317
+ power: Power data (array or dict).
318
+ time_array: Optional time array.
319
+ sample_rate: Optional sample rate.
320
+
321
+ Returns:
322
+ Tuple of (channels, is_multi, time_scaled, time_unit).
323
+ """
324
+ channels, is_multi = _normalize_power_channels(power)
325
+ time_array_validated = _validate_and_create_time_array(
326
+ time_array, sample_rate, len(next(iter(channels.values())))
327
+ )
328
+ time_scaled, time_unit = _compute_time_scale(time_array_validated)
329
+ return channels, is_multi, time_scaled, time_unit
330
+
331
+
332
+ def _render_power_plots(
333
+ axes: list[Axes],
334
+ channels: dict[str, NDArray[np.float64]],
335
+ time_scaled: NDArray[np.float64],
336
+ time_unit: str,
337
+ is_multi: bool,
338
+ layout: str,
339
+ statistics: dict[str, float] | None,
340
+ show_average: bool,
341
+ show_peak: bool,
342
+ show_energy: bool,
343
+ sample_rate: float | None,
344
+ ) -> None:
345
+ """Render power plots based on layout.
346
+
347
+ Args:
348
+ axes: List of axes.
349
+ channels: Channel data.
350
+ time_scaled: Scaled time array.
351
+ time_unit: Time unit string.
352
+ is_multi: Multiple channels flag.
353
+ layout: Layout type.
354
+ statistics: Optional statistics.
355
+ show_average: Show average line.
356
+ show_peak: Show peak marker.
357
+ show_energy: Show energy plot.
358
+ sample_rate: Sample rate.
359
+ """
360
+ if is_multi and layout == "stacked":
361
+ _plot_stacked_channels(
362
+ axes,
363
+ channels,
364
+ time_scaled,
365
+ time_unit,
366
+ statistics,
367
+ show_average,
368
+ show_peak,
369
+ show_energy,
370
+ sample_rate,
371
+ )
372
+ else:
373
+ _plot_overlay_channels(
374
+ axes[0],
375
+ channels,
376
+ time_scaled,
377
+ time_unit,
378
+ statistics,
379
+ show_average,
380
+ show_peak,
381
+ show_energy,
382
+ sample_rate,
383
+ )
384
+
385
+
386
+ def _finalize_plot(
387
+ fig: Figure,
388
+ title: str | None,
389
+ is_multi: bool,
390
+ save_path: str | Path | None,
391
+ show: bool,
392
+ ) -> None:
393
+ """Finalize plot with title, layout, save, and display.
394
+
395
+ Args:
396
+ fig: Figure object.
397
+ title: Plot title.
398
+ is_multi: Multiple channels flag.
399
+ save_path: Optional save path.
400
+ show: Display flag.
401
+
402
+ Example:
403
+ >>> _finalize_plot(fig, "Power Profile", False, None, True)
404
+ """
405
+ if title is None:
406
+ title = "Power Profile" + (" (Multi-Channel)" if is_multi else "")
407
+ fig.suptitle(title, fontsize=14, fontweight="bold")
408
+
409
+ plt.tight_layout()
410
+
411
+ if save_path is not None:
412
+ fig.savefig(save_path, dpi=150, bbox_inches="tight")
413
+
414
+ if show:
415
+ plt.show()
416
+
417
+
18
418
  def plot_power_profile(
19
419
  power: NDArray[np.float64] | dict[str, NDArray[np.float64]],
20
420
  *,
@@ -32,7 +432,7 @@ def plot_power_profile(
32
432
  ) -> Figure:
33
433
  """Generate power profile plot with annotations.
34
434
 
35
- : Time-domain power visualization with average/peak markers
435
+ Time-domain power visualization with average/peak markers
36
436
  and optional energy accumulation overlay. Supports multi-channel stacked view.
37
437
 
38
438
  Args:
@@ -62,36 +462,19 @@ def plot_power_profile(
62
462
  ValueError: If time_array length doesn't match power trace
63
463
 
64
464
  Examples:
65
- >>> # Simple power profile plot
66
465
  >>> import numpy as np
67
- >>> power = np.random.rand(1000) * 0.5 + 0.3 # 300-800 mW
68
- >>> fig = plot_power_profile(
69
- ... power,
70
- ... sample_rate=1e6,
71
- ... title="Device Power Consumption"
72
- ... )
73
-
74
- >>> # With pre-computed statistics
466
+ >>> power = np.random.rand(1000) * 0.5 + 0.3
467
+ >>> fig = plot_power_profile(power, sample_rate=1e6, title="Power")
468
+
75
469
  >>> from oscura.analyzers.power import power_statistics
76
470
  >>> stats = power_statistics(power, sample_rate=1e6)
77
- >>> fig = plot_power_profile(
78
- ... power,
79
- ... sample_rate=1e6,
80
- ... statistics=stats,
81
- ... show_energy=True
82
- ... )
83
-
84
- >>> # Multi-channel stacked view
471
+ >>> fig = plot_power_profile(power, sample_rate=1e6, statistics=stats)
472
+
85
473
  >>> power_channels = {
86
474
  ... 'VDD_CORE': np.random.rand(1000) * 0.5,
87
475
  ... 'VDD_IO': np.random.rand(1000) * 0.3,
88
- ... 'VDD_ANALOG': np.random.rand(1000) * 0.2,
89
476
  ... }
90
- >>> fig = plot_power_profile(
91
- ... power_channels,
92
- ... sample_rate=1e6,
93
- ... multi_channel_layout='stacked'
94
- ... )
477
+ >>> fig = plot_power_profile(power_channels, sample_rate=1e6)
95
478
 
96
479
  Notes:
97
480
  - Energy accumulation computed via cumulative sum
@@ -102,191 +485,24 @@ def plot_power_profile(
102
485
  References:
103
486
  PWR-004: Power Profile Visualization
104
487
  """
105
- # Handle multi-channel input
106
- if isinstance(power, dict):
107
- channels = power
108
- is_multi = True
109
- else:
110
- channels = {"Power": np.asarray(power, dtype=np.float64)}
111
- is_multi = False
112
-
113
- # Validate inputs
114
- if time_array is None and sample_rate is None:
115
- raise ValueError("Either time_array or sample_rate must be provided")
116
-
117
- # Generate time array
118
- first_trace = next(iter(channels.values()))
119
- if time_array is None:
120
- if sample_rate is None:
121
- raise ValueError("sample_rate is required when time_array is not provided")
122
- time_array = np.arange(len(first_trace)) / sample_rate
123
- else:
124
- time_array = np.asarray(time_array, dtype=np.float64)
125
- if len(time_array) != len(first_trace):
126
- raise ValueError(
127
- f"time_array length {len(time_array)} doesn't match "
128
- f"power trace length {len(first_trace)}"
129
- )
130
-
131
- # Determine time scale and units
132
- time_max = time_array[-1]
133
- if time_max < 1e-6:
134
- time_scale = 1e9
135
- time_unit = "ns"
136
- elif time_max < 1e-3:
137
- time_scale = 1e6
138
- time_unit = "µs"
139
- elif time_max < 1:
140
- time_scale = 1e3
141
- time_unit = "ms"
142
- else:
143
- time_scale = 1
144
- time_unit = "s"
145
-
146
- time_scaled = time_array * time_scale
147
-
148
- # Create figure
149
- if is_multi and multi_channel_layout == "stacked":
150
- n_channels = len(channels)
151
- n_plots = n_channels + (1 if show_energy else 0)
152
- fig, axes = plt.subplots(n_plots, 1, figsize=figsize, sharex=True)
153
- if n_plots == 1:
154
- axes = [axes]
155
- else:
156
- fig, ax_power = plt.subplots(figsize=figsize)
157
- axes = [ax_power]
158
-
159
- # Plot each channel
160
- if is_multi and multi_channel_layout == "stacked":
161
- # Stacked layout - one subplot per channel
162
- for idx, (name, trace) in enumerate(channels.items()):
163
- ax = axes[idx]
164
- ax.plot(time_scaled, trace * 1e3, linewidth=0.8, label=name)
165
-
166
- # Compute or use statistics
167
- if statistics is None or name not in statistics:
168
- avg = np.mean(trace)
169
- peak = np.max(trace)
170
- else:
171
- avg = statistics[name]["average"] # type: ignore[index]
172
- peak = statistics[name]["peak"] # type: ignore[index]
173
-
174
- # Annotations
175
- if show_average:
176
- ax.axhline(
177
- avg * 1e3,
178
- color="r",
179
- linestyle="--",
180
- linewidth=1,
181
- alpha=0.7,
182
- label=f"Avg: {avg * 1e3:.2f} mW",
183
- )
184
-
185
- if show_peak:
186
- peak_idx = np.argmax(trace)
187
- ax.plot(
188
- time_scaled[peak_idx],
189
- peak * 1e3,
190
- "rv",
191
- markersize=8,
192
- label=f"Peak: {peak * 1e3:.2f} mW",
193
- )
194
-
195
- ax.set_ylabel(f"{name}\n(mW)")
196
- ax.legend(loc="upper right", fontsize=8)
197
- ax.grid(True, alpha=0.3)
198
-
199
- # Energy accumulation plot
200
- if show_energy:
201
- ax_energy = axes[-1]
202
- for name, trace in channels.items():
203
- if sample_rate is not None:
204
- energy = np.cumsum(trace) / sample_rate * 1e6 # µJ
205
- ax_energy.plot(time_scaled, energy, linewidth=0.8, label=name)
206
-
207
- ax_energy.set_ylabel("Cumulative\nEnergy (µJ)")
208
- ax_energy.set_xlabel(f"Time ({time_unit})")
209
- ax_energy.legend(loc="upper left", fontsize=8)
210
- ax_energy.grid(True, alpha=0.3)
211
-
212
- else:
213
- # Overlay layout or single channel
214
- ax = axes[0]
215
-
216
- for name, trace in channels.items():
217
- ax.plot(time_scaled, trace * 1e3, linewidth=0.8, label=name)
218
-
219
- # Statistics for first channel (or combined if overlay)
220
- first_trace = next(iter(channels.values()))
221
- if statistics is None:
222
- avg_val = float(np.mean(first_trace))
223
- peak_val = float(np.max(first_trace))
224
- total_energy_val: float | None = (
225
- float(np.sum(first_trace) / sample_rate) if sample_rate else None
226
- )
227
- else:
228
- avg_val = float(statistics.get("average", float(np.mean(first_trace))))
229
- peak_val = float(statistics.get("peak", float(np.max(first_trace))))
230
- total_energy_val = statistics.get("energy", None)
231
-
232
- # Annotations
233
- if show_average:
234
- ax.axhline(
235
- avg_val * 1e3,
236
- color="r",
237
- linestyle="--",
238
- linewidth=1,
239
- alpha=0.7,
240
- label=f"Avg: {avg_val * 1e3:.2f} mW",
241
- )
242
-
243
- if show_peak:
244
- peak_idx = np.argmax(first_trace)
245
- ax.plot(
246
- time_scaled[peak_idx],
247
- peak_val * 1e3,
248
- "rv",
249
- markersize=8,
250
- label=f"Peak: {peak_val * 1e3:.2f} mW",
251
- )
252
-
253
- ax.set_ylabel("Power (mW)")
254
- ax.set_xlabel(f"Time ({time_unit})")
255
- ax.legend(loc="upper right")
256
- ax.grid(True, alpha=0.3)
257
-
258
- # Energy overlay on secondary y-axis
259
- if show_energy and sample_rate is not None:
260
- ax2 = ax.twinx()
261
- energy = np.cumsum(first_trace) / sample_rate * 1e6 # µJ
262
- ax2.plot(time_scaled, energy, "g--", linewidth=1.5, alpha=0.6)
263
- ax2.set_ylabel("Cumulative Energy (µJ)", color="g")
264
- ax2.tick_params(axis="y", labelcolor="g")
265
-
266
- if total_energy_val is not None:
267
- ax2.text(
268
- 0.98,
269
- 0.98,
270
- f"Total: {total_energy_val * 1e6:.2f} µJ",
271
- transform=ax.transAxes,
272
- ha="right",
273
- va="top",
274
- bbox={"boxstyle": "round", "facecolor": "wheat", "alpha": 0.5},
275
- )
276
-
277
- # Set title
278
- if title is None:
279
- title = "Power Profile" + (" (Multi-Channel)" if is_multi else "")
280
- fig.suptitle(title, fontsize=14, fontweight="bold")
281
-
282
- plt.tight_layout()
283
-
284
- # Save if requested
285
- if save_path is not None:
286
- fig.savefig(save_path, dpi=150, bbox_inches="tight")
287
-
288
- # Show if requested
289
- if show:
290
- plt.show()
291
-
488
+ channels, is_multi, time_scaled, time_unit = _prepare_power_plot_data(
489
+ power, time_array, sample_rate
490
+ )
491
+ fig, axes = _create_figure_layout(
492
+ is_multi, multi_channel_layout, len(channels), show_energy, figsize
493
+ )
494
+ _render_power_plots(
495
+ axes,
496
+ channels,
497
+ time_scaled,
498
+ time_unit,
499
+ is_multi,
500
+ multi_channel_layout,
501
+ statistics,
502
+ show_average,
503
+ show_peak,
504
+ show_energy,
505
+ sample_rate,
506
+ )
507
+ _finalize_plot(fig, title, is_multi, save_path, show)
292
508
  return fig