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