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
@@ -45,6 +45,142 @@ __all__ = [
45
45
  ]
46
46
 
47
47
 
48
+ def _normalize_efficiency_values(
49
+ efficiency_values: NDArray[np.floating[Any]],
50
+ efficiency_sets: list[NDArray[np.floating[Any]]] | None,
51
+ ) -> tuple[NDArray[np.floating[Any]], list[NDArray[np.floating[Any]]] | None]:
52
+ """Normalize efficiency values to percentage (0-100).
53
+
54
+ Args:
55
+ efficiency_values: Efficiency values (0-100 or 0-1).
56
+ efficiency_sets: List of efficiency arrays or None.
57
+
58
+ Returns:
59
+ Tuple of (normalized_efficiency, normalized_sets).
60
+ """
61
+ if np.max(efficiency_values) <= 1.0:
62
+ efficiency_values = efficiency_values * 100
63
+ if efficiency_sets is not None:
64
+ efficiency_sets = [e * 100 for e in efficiency_sets]
65
+
66
+ return efficiency_values, efficiency_sets
67
+
68
+
69
+ def _plot_multi_efficiency_curves(
70
+ ax: Axes,
71
+ load_values: NDArray[np.floating[Any]],
72
+ v_in_values: list[float],
73
+ efficiency_sets: list[NDArray[np.floating[Any]]],
74
+ show_peak: bool,
75
+ ) -> None:
76
+ """Plot multiple efficiency curves for different input voltages.
77
+
78
+ Args:
79
+ ax: Matplotlib axes to plot on.
80
+ load_values: Load current or power array.
81
+ v_in_values: List of input voltages.
82
+ efficiency_sets: List of efficiency arrays.
83
+ show_peak: Show peak efficiency markers.
84
+ """
85
+ colors = ["#3498DB", "#E74C3C", "#27AE60", "#9B59B6", "#F39C12"]
86
+
87
+ for i, (v_in, eff) in enumerate(zip(v_in_values, efficiency_sets, strict=False)):
88
+ color = colors[i % len(colors)]
89
+ ax.plot(load_values, eff, "-", linewidth=2, color=color, label=f"Vin = {v_in}V")
90
+
91
+ if show_peak:
92
+ peak_idx = np.argmax(eff)
93
+ ax.plot(load_values[peak_idx], eff[peak_idx], "o", color=color, markersize=8)
94
+
95
+
96
+ def _plot_single_efficiency_curve(
97
+ ax: Axes,
98
+ load_values: NDArray[np.floating[Any]],
99
+ efficiency_values: NDArray[np.floating[Any]],
100
+ load_unit: str,
101
+ show_peak: bool,
102
+ ) -> None:
103
+ """Plot single efficiency curve with peak annotation.
104
+
105
+ Args:
106
+ ax: Matplotlib axes to plot on.
107
+ load_values: Load current or power array.
108
+ efficiency_values: Efficiency values in %.
109
+ load_unit: Load axis unit.
110
+ show_peak: Show peak efficiency annotation.
111
+ """
112
+ ax.plot(load_values, efficiency_values, "-", linewidth=2.5, color="#3498DB", label="Efficiency")
113
+
114
+ if show_peak:
115
+ peak_idx = np.argmax(efficiency_values)
116
+ peak_load = load_values[peak_idx]
117
+ peak_eff = efficiency_values[peak_idx]
118
+ ax.plot(peak_load, peak_eff, "o", color="#E74C3C", markersize=10, zorder=5)
119
+ ax.annotate(
120
+ f"Peak: {peak_eff:.1f}%\n@ {peak_load:.2f} {load_unit}",
121
+ xy=(peak_load, peak_eff),
122
+ xytext=(15, -15),
123
+ textcoords="offset points",
124
+ fontsize=9,
125
+ ha="left",
126
+ bbox={"boxstyle": "round,pad=0.3", "facecolor": "white", "alpha": 0.9},
127
+ arrowprops={"arrowstyle": "->", "connectionstyle": "arc3,rad=0.2"},
128
+ )
129
+
130
+
131
+ def _format_efficiency_plot(
132
+ ax: Axes,
133
+ load_values: NDArray[np.floating[Any]],
134
+ efficiency_values: NDArray[np.floating[Any]],
135
+ efficiency_sets: list[NDArray[np.floating[Any]]] | None,
136
+ load_unit: str,
137
+ target_efficiency: float | None,
138
+ title: str | None,
139
+ ) -> None:
140
+ """Format efficiency plot axes and labels.
141
+
142
+ Args:
143
+ ax: Matplotlib axes to format.
144
+ load_values: Load current or power array.
145
+ efficiency_values: Efficiency values in %.
146
+ efficiency_sets: List of efficiency arrays or None.
147
+ load_unit: Load axis unit.
148
+ target_efficiency: Target efficiency line.
149
+ title: Plot title.
150
+ """
151
+ # Target efficiency line
152
+ if target_efficiency is not None:
153
+ ax.axhline(
154
+ target_efficiency,
155
+ color="#E74C3C",
156
+ linestyle="--",
157
+ linewidth=1.5,
158
+ label=f"Target: {target_efficiency}%",
159
+ )
160
+
161
+ # Fill area under curve
162
+ ax.fill_between(
163
+ load_values,
164
+ 0,
165
+ efficiency_values if efficiency_sets is None else efficiency_sets[0],
166
+ alpha=0.1,
167
+ color="#3498DB",
168
+ )
169
+
170
+ # Labels and formatting
171
+ ax.set_xlabel(f"Load ({load_unit})", fontsize=11)
172
+ ax.set_ylabel("Efficiency (%)", fontsize=11)
173
+ ax.set_ylim(0, 100)
174
+ ax.set_xlim(load_values[0], load_values[-1])
175
+ ax.grid(True, alpha=0.3)
176
+ ax.legend(loc="best")
177
+
178
+ if title:
179
+ ax.set_title(title, fontsize=12, fontweight="bold")
180
+ else:
181
+ ax.set_title("Converter Efficiency vs Load", fontsize=12, fontweight="bold")
182
+
183
+
48
184
  def plot_efficiency_curve(
49
185
  load_values: NDArray[np.floating[Any]],
50
186
  efficiency_values: NDArray[np.floating[Any]],
@@ -90,6 +226,7 @@ def plot_efficiency_curve(
90
226
  if not HAS_MATPLOTLIB:
91
227
  raise ImportError("matplotlib is required for visualization")
92
228
 
229
+ # Figure/axes creation
93
230
  if ax is None:
94
231
  fig, ax = plt.subplots(figsize=figsize)
95
232
  else:
@@ -98,78 +235,246 @@ def plot_efficiency_curve(
98
235
  raise ValueError("Axes must have an associated figure")
99
236
  fig = cast("Figure", fig_temp)
100
237
 
101
- # Normalize efficiency to percentage if needed
102
- if np.max(efficiency_values) <= 1.0:
103
- efficiency_values = efficiency_values * 100
104
- if efficiency_sets is not None:
105
- efficiency_sets = [e * 100 for e in efficiency_sets]
106
-
107
- # Color palette for multiple curves
108
- colors = ["#3498DB", "#E74C3C", "#27AE60", "#9B59B6", "#F39C12"]
238
+ # Data preparation/validation
239
+ efficiency_values, efficiency_sets = _normalize_efficiency_values(
240
+ efficiency_values, efficiency_sets
241
+ )
109
242
 
243
+ # Plotting/rendering
110
244
  if v_in_values is not None and efficiency_sets is not None:
111
- # Multiple input voltage curves
112
- for i, (v_in, eff) in enumerate(zip(v_in_values, efficiency_sets, strict=False)):
113
- color = colors[i % len(colors)]
114
- ax.plot(load_values, eff, "-", linewidth=2, color=color, label=f"Vin = {v_in}V")
115
-
116
- if show_peak:
117
- peak_idx = np.argmax(eff)
118
- ax.plot(load_values[peak_idx], eff[peak_idx], "o", color=color, markersize=8)
245
+ _plot_multi_efficiency_curves(ax, load_values, v_in_values, efficiency_sets, show_peak)
246
+ else:
247
+ _plot_single_efficiency_curve(ax, load_values, efficiency_values, load_unit, show_peak)
248
+
249
+ # Annotation/labeling and layout/formatting
250
+ _format_efficiency_plot(
251
+ ax, load_values, efficiency_values, efficiency_sets, load_unit, target_efficiency, title
252
+ )
253
+
254
+ fig.tight_layout()
255
+
256
+ if save_path is not None:
257
+ fig.savefig(save_path, dpi=300, bbox_inches="tight")
258
+
259
+ if show:
260
+ plt.show()
261
+
262
+ return fig
263
+
264
+
265
+ def _determine_time_scale(time: NDArray[np.floating[Any]], time_unit: str) -> tuple[str, float]:
266
+ """Determine time axis scale and multiplier.
267
+
268
+ Args:
269
+ time: Time array in seconds.
270
+ time_unit: Requested time unit ("auto" or specific).
271
+
272
+ Returns:
273
+ Tuple of (unit_name, multiplier).
274
+ """
275
+ if time_unit != "auto":
276
+ time_mult = {"s": 1, "ms": 1e3, "us": 1e6, "ns": 1e9}.get(time_unit, 1.0)
277
+ return time_unit, time_mult
278
+
279
+ max_time = np.max(time)
280
+ if max_time < 1e-6:
281
+ return "us", 1e6
282
+ elif max_time < 1e-3:
283
+ return "ms", 1e3
119
284
  else:
120
- # Single curve
285
+ return "s", 1.0
286
+
287
+
288
+ def _plot_voltage_current_panel(
289
+ ax: Axes,
290
+ time_scaled: NDArray[np.floating[Any]],
291
+ voltage: NDArray[np.floating[Any]],
292
+ current: NDArray[np.floating[Any]] | None,
293
+ v_label: str,
294
+ i_label: str,
295
+ v_color: str,
296
+ i_color: str,
297
+ panel_title: str,
298
+ ) -> None:
299
+ """Plot voltage and current on dual-axis panel.
300
+
301
+ Args:
302
+ ax: Matplotlib axes for voltage.
303
+ time_scaled: Scaled time array.
304
+ voltage: Voltage waveform.
305
+ current: Current waveform (optional).
306
+ v_label: Voltage axis label.
307
+ i_label: Current axis label.
308
+ v_color: Voltage plot color.
309
+ i_color: Current plot color.
310
+ panel_title: Panel title.
311
+ """
312
+ ax.plot(time_scaled, voltage, v_color, linewidth=1.5)
313
+ ax.set_ylabel(v_label, color=v_color, fontsize=10)
314
+ ax.tick_params(axis="y", labelcolor=v_color)
315
+ ax.grid(True, alpha=0.3)
316
+
317
+ if current is not None:
318
+ ax2 = ax.twinx()
319
+ ax2.plot(time_scaled, current, i_color, linewidth=1.5)
320
+ ax2.set_ylabel(i_label, color=i_color, fontsize=10)
321
+ ax2.tick_params(axis="y", labelcolor=i_color)
322
+
323
+ ax.set_title(panel_title, fontsize=10, fontweight="bold", loc="left")
324
+
325
+
326
+ def _plot_power_panel(
327
+ ax: Axes,
328
+ time_scaled: NDArray[np.floating[Any]],
329
+ v_in: NDArray[np.floating[Any]] | None,
330
+ i_in: NDArray[np.floating[Any]] | None,
331
+ v_out: NDArray[np.floating[Any]] | None,
332
+ i_out: NDArray[np.floating[Any]] | None,
333
+ ) -> None:
334
+ """Plot instantaneous power panel.
335
+
336
+ Args:
337
+ ax: Matplotlib axes.
338
+ time_scaled: Scaled time array.
339
+ v_in: Input voltage (optional).
340
+ i_in: Input current (optional).
341
+ v_out: Output voltage (optional).
342
+ i_out: Output current (optional).
343
+ """
344
+ if v_in is not None and i_in is not None:
345
+ p_in = v_in * i_in
121
346
  ax.plot(
122
- load_values, efficiency_values, "-", linewidth=2.5, color="#3498DB", label="Efficiency"
347
+ time_scaled,
348
+ p_in,
349
+ "#3498DB",
350
+ linewidth=1.5,
351
+ label=f"P_in (avg: {np.mean(p_in):.2f}W)",
123
352
  )
124
353
 
125
- if show_peak:
126
- peak_idx = np.argmax(efficiency_values)
127
- peak_load = load_values[peak_idx]
128
- peak_eff = efficiency_values[peak_idx]
129
- ax.plot(peak_load, peak_eff, "o", color="#E74C3C", markersize=10, zorder=5)
130
- ax.annotate(
131
- f"Peak: {peak_eff:.1f}%\n@ {peak_load:.2f} {load_unit}",
132
- xy=(peak_load, peak_eff),
133
- xytext=(15, -15),
134
- textcoords="offset points",
135
- fontsize=9,
136
- ha="left",
137
- bbox={"boxstyle": "round,pad=0.3", "facecolor": "white", "alpha": 0.9},
138
- arrowprops={"arrowstyle": "->", "connectionstyle": "arc3,rad=0.2"},
139
- )
140
-
141
- # Target efficiency line
142
- if target_efficiency is not None:
143
- ax.axhline(
144
- target_efficiency,
145
- color="#E74C3C",
146
- linestyle="--",
354
+ if v_out is not None and i_out is not None:
355
+ p_out = v_out * i_out
356
+ ax.plot(
357
+ time_scaled,
358
+ p_out,
359
+ "#27AE60",
147
360
  linewidth=1.5,
148
- label=f"Target: {target_efficiency}%",
361
+ label=f"P_out (avg: {np.mean(p_out):.2f}W)",
149
362
  )
150
363
 
151
- # Fill area under curve
152
- ax.fill_between(
153
- load_values,
154
- 0,
155
- efficiency_values if efficiency_sets is None else efficiency_sets[0],
156
- alpha=0.1,
157
- color="#3498DB",
364
+ ax.set_ylabel("Power (W)", fontsize=10)
365
+ ax.set_title("Instantaneous Power", fontsize=10, fontweight="bold", loc="left")
366
+ ax.legend(loc="upper right", fontsize=9)
367
+ ax.grid(True, alpha=0.3)
368
+
369
+
370
+ def _setup_power_waveform_figure(
371
+ figsize: tuple[float, float],
372
+ v_in: NDArray[np.floating[Any]] | None,
373
+ v_out: NDArray[np.floating[Any]] | None,
374
+ show_power: bool,
375
+ ) -> tuple[Figure, list[Axes]]:
376
+ """Setup figure and axes for power waveform plot.
377
+
378
+ Args:
379
+ figsize: Figure size.
380
+ v_in: Input voltage (optional).
381
+ v_out: Output voltage (optional).
382
+ show_power: Show power panel.
383
+
384
+ Returns:
385
+ Tuple of (figure, axes_list).
386
+ """
387
+ n_plots = sum(
388
+ [
389
+ v_in is not None,
390
+ v_out is not None,
391
+ show_power and (v_in is not None or v_out is not None),
392
+ ]
158
393
  )
394
+ if n_plots == 0:
395
+ raise ValueError("At least one voltage waveform must be provided")
159
396
 
160
- # Labels
161
- ax.set_xlabel(f"Load ({load_unit})", fontsize=11)
162
- ax.set_ylabel("Efficiency (%)", fontsize=11)
163
- ax.set_ylim(0, 100)
164
- ax.set_xlim(load_values[0], load_values[-1])
165
- ax.grid(True, alpha=0.3)
166
- ax.legend(loc="best")
397
+ fig, axes = plt.subplots(n_plots, 1, figsize=figsize, sharex=True)
398
+ if n_plots == 1:
399
+ axes = [axes]
167
400
 
168
- if title:
169
- ax.set_title(title, fontsize=12, fontweight="bold")
170
- else:
171
- ax.set_title("Converter Efficiency vs Load", fontsize=12, fontweight="bold")
401
+ return fig, axes
402
+
403
+
404
+ def _plot_power_waveform_panels(
405
+ axes: list[Axes],
406
+ time_scaled: NDArray[np.floating[Any]],
407
+ v_in: NDArray[np.floating[Any]] | None,
408
+ i_in: NDArray[np.floating[Any]] | None,
409
+ v_out: NDArray[np.floating[Any]] | None,
410
+ i_out: NDArray[np.floating[Any]] | None,
411
+ show_power: bool,
412
+ ) -> None:
413
+ """Plot all voltage/current panels.
414
+
415
+ Args:
416
+ axes: List of axes to plot on.
417
+ time_scaled: Scaled time array.
418
+ v_in: Input voltage (optional).
419
+ i_in: Input current (optional).
420
+ v_out: Output voltage (optional).
421
+ i_out: Output current (optional).
422
+ show_power: Show power panel.
423
+ """
424
+ ax_idx = 0
172
425
 
426
+ if v_in is not None:
427
+ _plot_voltage_current_panel(
428
+ axes[ax_idx],
429
+ time_scaled,
430
+ v_in,
431
+ i_in,
432
+ "V_in (V)",
433
+ "I_in (A)",
434
+ "#3498DB",
435
+ "#E74C3C",
436
+ "Input",
437
+ )
438
+ ax_idx += 1
439
+
440
+ if v_out is not None:
441
+ _plot_voltage_current_panel(
442
+ axes[ax_idx],
443
+ time_scaled,
444
+ v_out,
445
+ i_out,
446
+ "V_out (V)",
447
+ "I_out (A)",
448
+ "#27AE60",
449
+ "#9B59B6",
450
+ "Output",
451
+ )
452
+ ax_idx += 1
453
+
454
+ if show_power:
455
+ _plot_power_panel(axes[ax_idx], time_scaled, v_in, i_in, v_out, i_out)
456
+
457
+
458
+ def _finalize_power_waveform_plot(
459
+ fig: Figure,
460
+ axes: list[Axes],
461
+ time_unit: str,
462
+ title: str | None,
463
+ save_path: str | Path | None,
464
+ show: bool,
465
+ ) -> None:
466
+ """Finalize power waveform plot formatting and save.
467
+
468
+ Args:
469
+ fig: Matplotlib figure.
470
+ axes: List of axes.
471
+ time_unit: Time axis unit.
472
+ title: Plot title.
473
+ save_path: Save path.
474
+ show: Display plot.
475
+ """
476
+ axes[-1].set_xlabel(f"Time ({time_unit})", fontsize=11)
477
+ fig.suptitle(title if title else "Power Converter Waveforms", fontsize=14, fontweight="bold")
173
478
  fig.tight_layout()
174
479
 
175
480
  if save_path is not None:
@@ -178,8 +483,6 @@ def plot_efficiency_curve(
178
483
  if show:
179
484
  plt.show()
180
485
 
181
- return fig
182
-
183
486
 
184
487
  def plot_power_waveforms(
185
488
  time: NDArray[np.floating[Any]],
@@ -219,120 +522,178 @@ def plot_power_waveforms(
219
522
  if not HAS_MATPLOTLIB:
220
523
  raise ImportError("matplotlib is required for visualization")
221
524
 
222
- # Determine number of subplots needed
223
- n_plots = sum(
224
- [
225
- v_in is not None,
226
- v_out is not None,
227
- show_power and (v_in is not None or v_out is not None),
228
- ]
229
- )
230
- if n_plots == 0:
231
- raise ValueError("At least one voltage waveform must be provided")
525
+ # Setup: determine layout and prepare axes
526
+ fig, axes = _setup_power_waveform_figure(figsize, v_in, v_out, show_power)
527
+ time_unit, time_mult = _determine_time_scale(time, time_unit)
528
+ time_scaled = time * time_mult
529
+
530
+ # Processing: plot data panels
531
+ _plot_power_waveform_panels(axes, time_scaled, v_in, i_in, v_out, i_out, show_power)
532
+
533
+ # Formatting: finalize and save
534
+ _finalize_power_waveform_plot(fig, axes, time_unit, title, save_path, show)
535
+
536
+ return fig
232
537
 
233
- fig, axes = plt.subplots(n_plots, 1, figsize=figsize, sharex=True)
234
- if n_plots == 1:
235
- axes = [axes]
236
538
 
237
- # Time unit conversion
539
+ def _determine_time_unit_and_multiplier(
540
+ time: NDArray[np.floating[Any]], time_unit: str
541
+ ) -> tuple[str, float]:
542
+ """Determine time unit and multiplier for time axis scaling.
543
+
544
+ Args:
545
+ time: Time array in seconds.
546
+ time_unit: Requested time unit ("auto" or specific unit).
547
+
548
+ Returns:
549
+ Tuple of (time_unit, time_multiplier).
550
+ """
238
551
  if time_unit == "auto":
239
552
  max_time = np.max(time)
240
553
  if max_time < 1e-6:
241
- time_unit = "us"
242
- time_mult = 1e6
554
+ return "us", 1e6
243
555
  elif max_time < 1e-3:
244
- time_unit = "ms"
245
- time_mult = 1e3
556
+ return "ms", 1e3
246
557
  else:
247
- time_unit = "s"
248
- time_mult = 1.0
558
+ return "s", 1.0
249
559
  else:
250
560
  time_mult = {"s": 1, "ms": 1e3, "us": 1e6, "ns": 1e9}.get(time_unit, 1.0)
561
+ return time_unit, time_mult
251
562
 
252
- time_scaled = time * time_mult
253
563
 
254
- ax_idx = 0
564
+ def _calculate_ripple_metrics(
565
+ voltage: NDArray[np.floating[Any]],
566
+ ) -> tuple[float, NDArray[np.floating[Any]], float, float]:
567
+ """Calculate DC level and AC ripple metrics.
255
568
 
256
- # Input voltage/current panel
257
- if v_in is not None:
258
- ax = axes[ax_idx]
259
- ax.plot(time_scaled, v_in, "#3498DB", linewidth=1.5, label="V_in")
260
- ax.set_ylabel("V_in (V)", color="#3498DB", fontsize=10)
261
- ax.tick_params(axis="y", labelcolor="#3498DB")
262
- ax.grid(True, alpha=0.3)
263
-
264
- if i_in is not None:
265
- ax2 = ax.twinx()
266
- ax2.plot(time_scaled, i_in, "#E74C3C", linewidth=1.5, label="I_in")
267
- ax2.set_ylabel("I_in (A)", color="#E74C3C", fontsize=10)
268
- ax2.tick_params(axis="y", labelcolor="#E74C3C")
269
-
270
- ax.set_title("Input", fontsize=10, fontweight="bold", loc="left")
271
- ax_idx += 1
569
+ Args:
570
+ voltage: Voltage waveform array.
272
571
 
273
- # Output voltage/current panel
274
- if v_out is not None:
275
- ax = axes[ax_idx]
276
- ax.plot(time_scaled, v_out, "#27AE60", linewidth=1.5, label="V_out")
277
- ax.set_ylabel("V_out (V)", color="#27AE60", fontsize=10)
278
- ax.tick_params(axis="y", labelcolor="#27AE60")
279
- ax.grid(True, alpha=0.3)
280
-
281
- if i_out is not None:
282
- ax2 = ax.twinx()
283
- ax2.plot(time_scaled, i_out, "#9B59B6", linewidth=1.5, label="I_out")
284
- ax2.set_ylabel("I_out (A)", color="#9B59B6", fontsize=10)
285
- ax2.tick_params(axis="y", labelcolor="#9B59B6")
286
-
287
- ax.set_title("Output", fontsize=10, fontweight="bold", loc="left")
288
- ax_idx += 1
572
+ Returns:
573
+ Tuple of (dc_level, ac_ripple, ripple_pp, ripple_rms).
574
+ """
575
+ dc_level = float(np.mean(voltage))
576
+ ac_ripple = voltage - dc_level
577
+ ripple_pp = float(np.ptp(ac_ripple))
578
+ ripple_rms = float(np.std(ac_ripple))
579
+ return dc_level, ac_ripple, ripple_pp, ripple_rms
289
580
 
290
- # Power panel
291
- if show_power:
292
- ax = axes[ax_idx]
293
-
294
- if v_in is not None and i_in is not None:
295
- p_in = v_in * i_in
296
- ax.plot(
297
- time_scaled,
298
- p_in,
299
- "#3498DB",
300
- linewidth=1.5,
301
- label=f"P_in (avg: {np.mean(p_in):.2f}W)",
302
- )
303
-
304
- if v_out is not None and i_out is not None:
305
- p_out = v_out * i_out
306
- ax.plot(
307
- time_scaled,
308
- p_out,
309
- "#27AE60",
310
- linewidth=1.5,
311
- label=f"P_out (avg: {np.mean(p_out):.2f}W)",
312
- )
313
-
314
- ax.set_ylabel("Power (W)", fontsize=10)
315
- ax.set_title("Instantaneous Power", fontsize=10, fontweight="bold", loc="left")
316
- ax.legend(loc="upper right", fontsize=9)
317
- ax.grid(True, alpha=0.3)
318
-
319
- # X-axis label on bottom
320
- axes[-1].set_xlabel(f"Time ({time_unit})", fontsize=11)
321
581
 
322
- if title:
323
- fig.suptitle(title, fontsize=14, fontweight="bold")
324
- else:
325
- fig.suptitle("Power Converter Waveforms", fontsize=14, fontweight="bold")
582
+ def _plot_dc_coupled_waveform(
583
+ ax: Axes,
584
+ time_scaled: NDArray[np.floating[Any]],
585
+ voltage: NDArray[np.floating[Any]],
586
+ dc_level: float,
587
+ ) -> None:
588
+ """Plot DC-coupled waveform with DC level indicator.
326
589
 
327
- fig.tight_layout()
590
+ Args:
591
+ ax: Matplotlib axes to plot on.
592
+ time_scaled: Scaled time array.
593
+ voltage: Voltage waveform.
594
+ dc_level: DC level value.
595
+ """
596
+ ax.plot(time_scaled, voltage, "#3498DB", linewidth=1)
597
+ ax.axhline(
598
+ dc_level, color="#E74C3C", linestyle="--", linewidth=1.5, label=f"DC: {dc_level:.3f}V"
599
+ )
600
+ ax.set_ylabel("Voltage (V)", fontsize=10)
601
+ ax.set_title("DC-Coupled Waveform", fontsize=10, fontweight="bold", loc="left")
602
+ ax.legend(loc="upper right", fontsize=9)
603
+ ax.grid(True, alpha=0.3)
328
604
 
329
- if save_path is not None:
330
- fig.savefig(save_path, dpi=300, bbox_inches="tight")
331
605
 
332
- if show:
333
- plt.show()
606
+ def _plot_ac_ripple_waveform(
607
+ ax: Axes,
608
+ time_scaled: NDArray[np.floating[Any]],
609
+ ac_ripple: NDArray[np.floating[Any]],
610
+ ripple_pp: float,
611
+ ripple_rms: float,
612
+ ) -> None:
613
+ """Plot AC-coupled ripple waveform with peak-to-peak annotation.
614
+
615
+ Args:
616
+ ax: Matplotlib axes to plot on.
617
+ time_scaled: Scaled time array.
618
+ ac_ripple: AC ripple waveform.
619
+ ripple_pp: Peak-to-peak ripple voltage.
620
+ ripple_rms: RMS ripple voltage.
621
+ """
622
+ ax.plot(time_scaled, ac_ripple * 1e3, "#27AE60", linewidth=1) # Convert to mV
623
+ ax.axhline(0, color="gray", linestyle="-", linewidth=0.5)
624
+
625
+ # Mark peak-to-peak
626
+ max_idx = int(np.argmax(ac_ripple))
627
+ min_idx = int(np.argmin(ac_ripple))
628
+ ax.annotate(
629
+ "",
630
+ xy=(time_scaled[max_idx], ac_ripple[max_idx] * 1e3),
631
+ xytext=(time_scaled[min_idx], ac_ripple[min_idx] * 1e3),
632
+ arrowprops={"arrowstyle": "<->", "color": "#E74C3C", "lw": 1.5},
633
+ )
634
+
635
+ ax.set_ylabel("Ripple (mV)", fontsize=10)
636
+ ax.set_title(
637
+ f"AC Ripple (pk-pk: {ripple_pp * 1e3:.2f}mV, RMS: {ripple_rms * 1e3:.2f}mV)",
638
+ fontsize=10,
639
+ fontweight="bold",
640
+ loc="left",
641
+ )
642
+ ax.grid(True, alpha=0.3)
334
643
 
335
- return fig
644
+
645
+ def _plot_ripple_spectrum(
646
+ ax: Axes,
647
+ ac_ripple: NDArray[np.floating[Any]],
648
+ sample_rate: float,
649
+ ) -> None:
650
+ """Plot ripple frequency spectrum.
651
+
652
+ Args:
653
+ ax: Matplotlib axes to plot on.
654
+ ac_ripple: AC ripple waveform.
655
+ sample_rate: Sample rate in Hz.
656
+ """
657
+ n_fft = len(ac_ripple)
658
+ freq = np.fft.rfftfreq(n_fft, 1 / sample_rate)
659
+ fft_mag = np.abs(np.fft.rfft(ac_ripple)) / n_fft * 2
660
+ fft_db = 20 * np.log10(fft_mag + 1e-12)
661
+
662
+ # Find dominant ripple frequency
663
+ peak_idx = int(np.argmax(fft_mag[1:])) + 1 # Skip DC
664
+ peak_freq = freq[peak_idx]
665
+
666
+ # Plot in kHz
667
+ freq_khz = freq / 1e3
668
+ ax.plot(freq_khz, fft_db, "#9B59B6", linewidth=1)
669
+ ax.plot(
670
+ freq_khz[peak_idx],
671
+ fft_db[peak_idx],
672
+ "ro",
673
+ markersize=8,
674
+ label=f"Peak: {peak_freq / 1e3:.1f}kHz",
675
+ )
676
+
677
+ ax.set_ylabel("Magnitude (dB)", fontsize=10)
678
+ ax.set_xlabel("Frequency (kHz)", fontsize=10)
679
+ ax.set_title("Ripple Spectrum", fontsize=10, fontweight="bold", loc="left")
680
+ ax.set_xlim(0, min(freq_khz[-1], sample_rate / 2e3))
681
+ ax.legend(loc="upper right", fontsize=9)
682
+ ax.grid(True, alpha=0.3)
683
+
684
+
685
+ def _estimate_sample_rate(time: NDArray[np.floating[Any]]) -> float:
686
+ """Estimate sample rate from time array.
687
+
688
+ Args:
689
+ time: Time array in seconds.
690
+
691
+ Returns:
692
+ Estimated sample rate in Hz.
693
+ """
694
+ if len(time) > 1:
695
+ return float(1 / (time[1] - time[0]))
696
+ return 1e6 # Default 1 MHz
336
697
 
337
698
 
338
699
  def plot_ripple_waveform(
@@ -361,16 +722,21 @@ def plot_ripple_waveform(
361
722
  ax: Matplotlib axes (creates multi-panel if None).
362
723
  figsize: Figure size.
363
724
  title: Plot title.
364
- time_unit: Time axis unit.
725
+ time_unit: Time axis unit ("auto", "s", "ms", "us", "ns").
365
726
  show_dc: Show DC-coupled waveform.
366
727
  show_ac: Show AC-coupled ripple.
367
728
  show_spectrum: Show ripple spectrum.
368
- sample_rate: Sample rate for FFT (required if show_spectrum=True).
729
+ sample_rate: Sample rate for FFT (estimated if None).
369
730
  show: Display plot.
370
731
  save_path: Save path.
371
732
 
372
733
  Returns:
373
734
  Matplotlib Figure object.
735
+
736
+ Example:
737
+ >>> time = np.linspace(0, 1e-3, 1000) # 1ms capture
738
+ >>> voltage = 5.0 + 0.01 * np.sin(2 * np.pi * 100e3 * time) # 5V + 10mV ripple
739
+ >>> fig = plot_ripple_waveform(time, voltage, show_spectrum=True)
374
740
  """
375
741
  if not HAS_MATPLOTLIB:
376
742
  raise ImportError("matplotlib is required for visualization")
@@ -383,107 +749,33 @@ def plot_ripple_waveform(
383
749
  if n_plots == 1:
384
750
  axes = [axes]
385
751
 
386
- # Time unit conversion
387
- if time_unit == "auto":
388
- max_time = np.max(time)
389
- if max_time < 1e-6:
390
- time_unit = "us"
391
- time_mult = 1e6
392
- elif max_time < 1e-3:
393
- time_unit = "ms"
394
- time_mult = 1e3
395
- else:
396
- time_unit = "s"
397
- time_mult = 1.0
398
- else:
399
- time_mult = {"s": 1, "ms": 1e3, "us": 1e6, "ns": 1e9}.get(time_unit, 1.0)
400
-
752
+ # Determine time scaling
753
+ time_unit, time_mult = _determine_time_unit_and_multiplier(time, time_unit)
401
754
  time_scaled = time * time_mult
402
755
 
403
- # Calculate DC level and ripple
404
- dc_level = np.mean(voltage)
405
- ac_ripple = voltage - dc_level
406
- ripple_pp = np.ptp(ac_ripple)
407
- ripple_rms = np.std(ac_ripple)
756
+ # Calculate ripple metrics
757
+ dc_level, ac_ripple, ripple_pp, ripple_rms = _calculate_ripple_metrics(voltage)
408
758
 
409
759
  ax_idx = 0
410
760
 
411
- # DC-coupled view
761
+ # Plot DC-coupled waveform
412
762
  if show_dc:
413
- ax = axes[ax_idx]
414
- ax.plot(time_scaled, voltage, "#3498DB", linewidth=1)
415
- ax.axhline(
416
- dc_level, color="#E74C3C", linestyle="--", linewidth=1.5, label=f"DC: {dc_level:.3f}V"
417
- )
418
- ax.set_ylabel("Voltage (V)", fontsize=10)
419
- ax.set_title("DC-Coupled Waveform", fontsize=10, fontweight="bold", loc="left")
420
- ax.legend(loc="upper right", fontsize=9)
421
- ax.grid(True, alpha=0.3)
763
+ _plot_dc_coupled_waveform(axes[ax_idx], time_scaled, voltage, dc_level)
422
764
  ax_idx += 1
423
765
 
424
- # AC-coupled (ripple only) view
766
+ # Plot AC-coupled ripple
425
767
  if show_ac:
426
- ax = axes[ax_idx]
427
- ax.plot(time_scaled, ac_ripple * 1e3, "#27AE60", linewidth=1) # Convert to mV
428
- ax.axhline(0, color="gray", linestyle="-", linewidth=0.5)
429
-
430
- # Mark peak-to-peak
431
- max_idx = np.argmax(ac_ripple)
432
- min_idx = np.argmin(ac_ripple)
433
- ax.annotate(
434
- "",
435
- xy=(time_scaled[max_idx], ac_ripple[max_idx] * 1e3),
436
- xytext=(time_scaled[min_idx], ac_ripple[min_idx] * 1e3),
437
- arrowprops={"arrowstyle": "<->", "color": "#E74C3C", "lw": 1.5},
438
- )
439
-
440
- ax.set_ylabel("Ripple (mV)", fontsize=10)
441
- ax.set_title(
442
- f"AC Ripple (pk-pk: {ripple_pp * 1e3:.2f}mV, RMS: {ripple_rms * 1e3:.2f}mV)",
443
- fontsize=10,
444
- fontweight="bold",
445
- loc="left",
446
- )
447
- ax.grid(True, alpha=0.3)
768
+ _plot_ac_ripple_waveform(axes[ax_idx], time_scaled, ac_ripple, ripple_pp, ripple_rms)
448
769
  ax_idx += 1
449
770
 
450
- # Spectrum view
771
+ # Plot ripple spectrum
451
772
  if show_spectrum:
452
- ax = axes[ax_idx]
453
-
454
- if sample_rate is None:
455
- # Estimate from time array
456
- sample_rate = 1 / (time[1] - time[0]) if len(time) > 1 else 1e6
457
-
458
- n_fft = len(voltage)
459
- freq = np.fft.rfftfreq(n_fft, 1 / sample_rate)
460
- fft_mag = np.abs(np.fft.rfft(ac_ripple)) / n_fft * 2
461
- fft_db = 20 * np.log10(fft_mag + 1e-12)
462
-
463
- # Find dominant ripple frequency
464
- peak_idx = np.argmax(fft_mag[1:]) + 1 # Skip DC
465
- peak_freq = freq[peak_idx]
466
-
467
- # Plot in kHz
468
- freq_khz = freq / 1e3
469
- ax.plot(freq_khz, fft_db, "#9B59B6", linewidth=1)
470
- ax.plot(
471
- freq_khz[peak_idx],
472
- fft_db[peak_idx],
473
- "ro",
474
- markersize=8,
475
- label=f"Peak: {peak_freq / 1e3:.1f}kHz",
476
- )
477
-
478
- ax.set_ylabel("Magnitude (dB)", fontsize=10)
479
- ax.set_xlabel("Frequency (kHz)", fontsize=10)
480
- ax.set_title("Ripple Spectrum", fontsize=10, fontweight="bold", loc="left")
481
- ax.set_xlim(0, min(freq_khz[-1], sample_rate / 2e3))
482
- ax.legend(loc="upper right", fontsize=9)
483
- ax.grid(True, alpha=0.3)
773
+ sr = sample_rate if sample_rate is not None else _estimate_sample_rate(time)
774
+ _plot_ripple_spectrum(axes[ax_idx], ac_ripple, sr)
484
775
  else:
485
776
  axes[-1].set_xlabel(f"Time ({time_unit})", fontsize=11)
486
777
 
778
+ # Finalize figure
487
779
  if title:
488
780
  fig.suptitle(title, fontsize=14, fontweight="bold")
489
781
  else:
@@ -500,6 +792,90 @@ def plot_ripple_waveform(
500
792
  return fig
501
793
 
502
794
 
795
+ def _create_loss_autopct_formatter(
796
+ show_watts: bool, total_loss: float
797
+ ) -> str | Callable[[float], str]:
798
+ """Create autopct formatter for pie chart labels.
799
+
800
+ Args:
801
+ show_watts: Whether to show watt values.
802
+ total_loss: Total loss in watts.
803
+
804
+ Returns:
805
+ Format string or callable for autopct.
806
+ """
807
+ if show_watts:
808
+
809
+ def autopct_func(pct: float) -> str:
810
+ watts = pct / 100 * total_loss
811
+ return f"{pct:.1f}%\n({watts * 1e3:.1f}mW)"
812
+
813
+ return autopct_func
814
+ return "%1.1f%%"
815
+
816
+
817
+ def _create_loss_pie_chart(
818
+ ax: Axes,
819
+ labels: list[str],
820
+ values: list[float],
821
+ colors: list[str],
822
+ autopct_val: str | Callable[[float], str],
823
+ ) -> tuple[Any, ...]:
824
+ """Create pie chart with loss breakdown.
825
+
826
+ Args:
827
+ ax: Matplotlib axes.
828
+ labels: Loss type labels.
829
+ values: Loss values.
830
+ colors: Color palette.
831
+ autopct_val: Autopct formatter.
832
+
833
+ Returns:
834
+ Pie chart result tuple.
835
+ """
836
+ return ax.pie(
837
+ values,
838
+ labels=labels,
839
+ autopct=autopct_val,
840
+ colors=colors[: len(labels)],
841
+ startangle=90,
842
+ explode=[0.02] * len(labels),
843
+ shadow=True,
844
+ )
845
+
846
+
847
+ def _format_loss_pie_chart(
848
+ ax: Axes, pie_result: tuple[Any, ...], total_loss: float, title: str | None
849
+ ) -> None:
850
+ """Format pie chart styling and annotations.
851
+
852
+ Args:
853
+ ax: Matplotlib axes.
854
+ pie_result: Result from ax.pie.
855
+ total_loss: Total loss value.
856
+ title: Chart title.
857
+ """
858
+ # Style autotexts if available
859
+ if len(pie_result) >= 3:
860
+ autotexts = pie_result[2]
861
+ for autotext in autotexts:
862
+ autotext.set_fontsize(9)
863
+ autotext.set_fontweight("bold")
864
+
865
+ # Add total loss annotation
866
+ ax.text(
867
+ 0,
868
+ -1.3,
869
+ f"Total Loss: {total_loss * 1e3:.1f}mW ({total_loss:.3f}W)",
870
+ ha="center",
871
+ fontsize=11,
872
+ fontweight="bold",
873
+ )
874
+
875
+ ax.set_aspect("equal")
876
+ ax.set_title(title if title else "Power Loss Breakdown", fontsize=12, fontweight="bold", pad=20)
877
+
878
+
503
879
  def plot_loss_breakdown(
504
880
  loss_values: dict[str, float],
505
881
  *,
@@ -539,6 +915,7 @@ def plot_loss_breakdown(
539
915
  if not HAS_MATPLOTLIB:
540
916
  raise ImportError("matplotlib is required for visualization")
541
917
 
918
+ # Setup: create figure and extract data
542
919
  if ax is None:
543
920
  fig, ax = plt.subplots(figsize=figsize)
544
921
  else:
@@ -550,8 +927,6 @@ def plot_loss_breakdown(
550
927
  labels = list(loss_values.keys())
551
928
  values = list(loss_values.values())
552
929
  total_loss = sum(values)
553
-
554
- # Color palette
555
930
  colors = [
556
931
  "#3498DB",
557
932
  "#E74C3C",
@@ -563,58 +938,12 @@ def plot_loss_breakdown(
563
938
  "#95A5A6",
564
939
  ]
565
940
 
566
- # Format labels with percentages and watts
567
- autopct_val: str | Callable[[float], str]
568
- if show_watts:
569
-
570
- def autopct_func(pct: float) -> str:
571
- watts = pct / 100 * total_loss
572
- return f"{pct:.1f}%\n({watts * 1e3:.1f}mW)"
573
-
574
- autopct_val = autopct_func
575
- else:
576
- autopct_val = "%1.1f%%"
577
-
578
- pie_result = ax.pie(
579
- values,
580
- labels=labels,
581
- autopct=autopct_val, # type: ignore[arg-type]
582
- colors=colors[: len(labels)],
583
- startangle=90,
584
- explode=[0.02] * len(labels),
585
- shadow=True,
586
- )
587
- # ax.pie returns (wedges, texts, autotexts) when autopct is provided
588
- # Unpack with length check for type safety
589
- if len(pie_result) >= 3:
590
- _wedges = pie_result[0]
591
- _texts = pie_result[1]
592
- autotexts = pie_result[2]
593
- else:
594
- autotexts = []
595
-
596
- # Style autotexts
597
- for autotext in autotexts:
598
- autotext.set_fontsize(9)
599
- autotext.set_fontweight("bold")
600
-
601
- # Add total loss annotation
602
- ax.text(
603
- 0,
604
- -1.3,
605
- f"Total Loss: {total_loss * 1e3:.1f}mW ({total_loss:.3f}W)",
606
- ha="center",
607
- fontsize=11,
608
- fontweight="bold",
609
- )
610
-
611
- ax.set_aspect("equal")
612
-
613
- if title:
614
- ax.set_title(title, fontsize=12, fontweight="bold", pad=20)
615
- else:
616
- ax.set_title("Power Loss Breakdown", fontsize=12, fontweight="bold", pad=20)
941
+ # Processing: create pie chart
942
+ autopct_val = _create_loss_autopct_formatter(show_watts, total_loss)
943
+ pie_result = _create_loss_pie_chart(ax, labels, values, colors, autopct_val)
617
944
 
945
+ # Result building: format and finalize
946
+ _format_loss_pie_chart(ax, pie_result, total_loss, title)
618
947
  fig.tight_layout()
619
948
 
620
949
  if save_path is not None: