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