oscura 0.0.1__py3-none-any.whl → 0.1.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 (465) hide show
  1. oscura/__init__.py +813 -8
  2. oscura/__main__.py +392 -0
  3. oscura/analyzers/__init__.py +37 -0
  4. oscura/analyzers/digital/__init__.py +177 -0
  5. oscura/analyzers/digital/bus.py +691 -0
  6. oscura/analyzers/digital/clock.py +805 -0
  7. oscura/analyzers/digital/correlation.py +720 -0
  8. oscura/analyzers/digital/edges.py +632 -0
  9. oscura/analyzers/digital/extraction.py +413 -0
  10. oscura/analyzers/digital/quality.py +878 -0
  11. oscura/analyzers/digital/signal_quality.py +877 -0
  12. oscura/analyzers/digital/thresholds.py +708 -0
  13. oscura/analyzers/digital/timing.py +1104 -0
  14. oscura/analyzers/eye/__init__.py +46 -0
  15. oscura/analyzers/eye/diagram.py +434 -0
  16. oscura/analyzers/eye/metrics.py +555 -0
  17. oscura/analyzers/jitter/__init__.py +83 -0
  18. oscura/analyzers/jitter/ber.py +333 -0
  19. oscura/analyzers/jitter/decomposition.py +759 -0
  20. oscura/analyzers/jitter/measurements.py +413 -0
  21. oscura/analyzers/jitter/spectrum.py +220 -0
  22. oscura/analyzers/measurements.py +40 -0
  23. oscura/analyzers/packet/__init__.py +171 -0
  24. oscura/analyzers/packet/daq.py +1077 -0
  25. oscura/analyzers/packet/metrics.py +437 -0
  26. oscura/analyzers/packet/parser.py +327 -0
  27. oscura/analyzers/packet/payload.py +2156 -0
  28. oscura/analyzers/packet/payload_analysis.py +1312 -0
  29. oscura/analyzers/packet/payload_extraction.py +236 -0
  30. oscura/analyzers/packet/payload_patterns.py +670 -0
  31. oscura/analyzers/packet/stream.py +359 -0
  32. oscura/analyzers/patterns/__init__.py +266 -0
  33. oscura/analyzers/patterns/clustering.py +1036 -0
  34. oscura/analyzers/patterns/discovery.py +539 -0
  35. oscura/analyzers/patterns/learning.py +797 -0
  36. oscura/analyzers/patterns/matching.py +1091 -0
  37. oscura/analyzers/patterns/periodic.py +650 -0
  38. oscura/analyzers/patterns/sequences.py +767 -0
  39. oscura/analyzers/power/__init__.py +116 -0
  40. oscura/analyzers/power/ac_power.py +391 -0
  41. oscura/analyzers/power/basic.py +383 -0
  42. oscura/analyzers/power/conduction.py +314 -0
  43. oscura/analyzers/power/efficiency.py +297 -0
  44. oscura/analyzers/power/ripple.py +356 -0
  45. oscura/analyzers/power/soa.py +372 -0
  46. oscura/analyzers/power/switching.py +479 -0
  47. oscura/analyzers/protocol/__init__.py +150 -0
  48. oscura/analyzers/protocols/__init__.py +150 -0
  49. oscura/analyzers/protocols/base.py +500 -0
  50. oscura/analyzers/protocols/can.py +620 -0
  51. oscura/analyzers/protocols/can_fd.py +448 -0
  52. oscura/analyzers/protocols/flexray.py +405 -0
  53. oscura/analyzers/protocols/hdlc.py +399 -0
  54. oscura/analyzers/protocols/i2c.py +368 -0
  55. oscura/analyzers/protocols/i2s.py +296 -0
  56. oscura/analyzers/protocols/jtag.py +393 -0
  57. oscura/analyzers/protocols/lin.py +445 -0
  58. oscura/analyzers/protocols/manchester.py +333 -0
  59. oscura/analyzers/protocols/onewire.py +501 -0
  60. oscura/analyzers/protocols/spi.py +334 -0
  61. oscura/analyzers/protocols/swd.py +325 -0
  62. oscura/analyzers/protocols/uart.py +393 -0
  63. oscura/analyzers/protocols/usb.py +495 -0
  64. oscura/analyzers/signal_integrity/__init__.py +63 -0
  65. oscura/analyzers/signal_integrity/embedding.py +294 -0
  66. oscura/analyzers/signal_integrity/equalization.py +370 -0
  67. oscura/analyzers/signal_integrity/sparams.py +484 -0
  68. oscura/analyzers/spectral/__init__.py +53 -0
  69. oscura/analyzers/spectral/chunked.py +273 -0
  70. oscura/analyzers/spectral/chunked_fft.py +571 -0
  71. oscura/analyzers/spectral/chunked_wavelet.py +391 -0
  72. oscura/analyzers/spectral/fft.py +92 -0
  73. oscura/analyzers/statistical/__init__.py +250 -0
  74. oscura/analyzers/statistical/checksum.py +923 -0
  75. oscura/analyzers/statistical/chunked_corr.py +228 -0
  76. oscura/analyzers/statistical/classification.py +778 -0
  77. oscura/analyzers/statistical/entropy.py +1113 -0
  78. oscura/analyzers/statistical/ngrams.py +614 -0
  79. oscura/analyzers/statistics/__init__.py +119 -0
  80. oscura/analyzers/statistics/advanced.py +885 -0
  81. oscura/analyzers/statistics/basic.py +263 -0
  82. oscura/analyzers/statistics/correlation.py +630 -0
  83. oscura/analyzers/statistics/distribution.py +298 -0
  84. oscura/analyzers/statistics/outliers.py +463 -0
  85. oscura/analyzers/statistics/streaming.py +93 -0
  86. oscura/analyzers/statistics/trend.py +520 -0
  87. oscura/analyzers/validation.py +598 -0
  88. oscura/analyzers/waveform/__init__.py +36 -0
  89. oscura/analyzers/waveform/measurements.py +943 -0
  90. oscura/analyzers/waveform/measurements_with_uncertainty.py +371 -0
  91. oscura/analyzers/waveform/spectral.py +1689 -0
  92. oscura/analyzers/waveform/wavelets.py +298 -0
  93. oscura/api/__init__.py +62 -0
  94. oscura/api/dsl.py +538 -0
  95. oscura/api/fluent.py +571 -0
  96. oscura/api/operators.py +498 -0
  97. oscura/api/optimization.py +392 -0
  98. oscura/api/profiling.py +396 -0
  99. oscura/automotive/__init__.py +73 -0
  100. oscura/automotive/can/__init__.py +52 -0
  101. oscura/automotive/can/analysis.py +356 -0
  102. oscura/automotive/can/checksum.py +250 -0
  103. oscura/automotive/can/correlation.py +212 -0
  104. oscura/automotive/can/discovery.py +355 -0
  105. oscura/automotive/can/message_wrapper.py +375 -0
  106. oscura/automotive/can/models.py +385 -0
  107. oscura/automotive/can/patterns.py +381 -0
  108. oscura/automotive/can/session.py +452 -0
  109. oscura/automotive/can/state_machine.py +300 -0
  110. oscura/automotive/can/stimulus_response.py +461 -0
  111. oscura/automotive/dbc/__init__.py +15 -0
  112. oscura/automotive/dbc/generator.py +156 -0
  113. oscura/automotive/dbc/parser.py +146 -0
  114. oscura/automotive/dtc/__init__.py +30 -0
  115. oscura/automotive/dtc/database.py +3036 -0
  116. oscura/automotive/j1939/__init__.py +14 -0
  117. oscura/automotive/j1939/decoder.py +745 -0
  118. oscura/automotive/loaders/__init__.py +35 -0
  119. oscura/automotive/loaders/asc.py +98 -0
  120. oscura/automotive/loaders/blf.py +77 -0
  121. oscura/automotive/loaders/csv_can.py +136 -0
  122. oscura/automotive/loaders/dispatcher.py +136 -0
  123. oscura/automotive/loaders/mdf.py +331 -0
  124. oscura/automotive/loaders/pcap.py +132 -0
  125. oscura/automotive/obd/__init__.py +14 -0
  126. oscura/automotive/obd/decoder.py +707 -0
  127. oscura/automotive/uds/__init__.py +48 -0
  128. oscura/automotive/uds/decoder.py +265 -0
  129. oscura/automotive/uds/models.py +64 -0
  130. oscura/automotive/visualization.py +369 -0
  131. oscura/batch/__init__.py +55 -0
  132. oscura/batch/advanced.py +627 -0
  133. oscura/batch/aggregate.py +300 -0
  134. oscura/batch/analyze.py +139 -0
  135. oscura/batch/logging.py +487 -0
  136. oscura/batch/metrics.py +556 -0
  137. oscura/builders/__init__.py +41 -0
  138. oscura/builders/signal_builder.py +1131 -0
  139. oscura/cli/__init__.py +14 -0
  140. oscura/cli/batch.py +339 -0
  141. oscura/cli/characterize.py +273 -0
  142. oscura/cli/compare.py +775 -0
  143. oscura/cli/decode.py +551 -0
  144. oscura/cli/main.py +247 -0
  145. oscura/cli/shell.py +350 -0
  146. oscura/comparison/__init__.py +66 -0
  147. oscura/comparison/compare.py +397 -0
  148. oscura/comparison/golden.py +487 -0
  149. oscura/comparison/limits.py +391 -0
  150. oscura/comparison/mask.py +434 -0
  151. oscura/comparison/trace_diff.py +30 -0
  152. oscura/comparison/visualization.py +481 -0
  153. oscura/compliance/__init__.py +70 -0
  154. oscura/compliance/advanced.py +756 -0
  155. oscura/compliance/masks.py +363 -0
  156. oscura/compliance/reporting.py +483 -0
  157. oscura/compliance/testing.py +298 -0
  158. oscura/component/__init__.py +38 -0
  159. oscura/component/impedance.py +365 -0
  160. oscura/component/reactive.py +598 -0
  161. oscura/component/transmission_line.py +312 -0
  162. oscura/config/__init__.py +191 -0
  163. oscura/config/defaults.py +254 -0
  164. oscura/config/loader.py +348 -0
  165. oscura/config/memory.py +271 -0
  166. oscura/config/migration.py +458 -0
  167. oscura/config/pipeline.py +1077 -0
  168. oscura/config/preferences.py +530 -0
  169. oscura/config/protocol.py +875 -0
  170. oscura/config/schema.py +713 -0
  171. oscura/config/settings.py +420 -0
  172. oscura/config/thresholds.py +599 -0
  173. oscura/convenience.py +457 -0
  174. oscura/core/__init__.py +299 -0
  175. oscura/core/audit.py +457 -0
  176. oscura/core/backend_selector.py +405 -0
  177. oscura/core/cache.py +590 -0
  178. oscura/core/cancellation.py +439 -0
  179. oscura/core/confidence.py +225 -0
  180. oscura/core/config.py +506 -0
  181. oscura/core/correlation.py +216 -0
  182. oscura/core/cross_domain.py +422 -0
  183. oscura/core/debug.py +301 -0
  184. oscura/core/edge_cases.py +541 -0
  185. oscura/core/exceptions.py +535 -0
  186. oscura/core/gpu_backend.py +523 -0
  187. oscura/core/lazy.py +832 -0
  188. oscura/core/log_query.py +540 -0
  189. oscura/core/logging.py +931 -0
  190. oscura/core/logging_advanced.py +952 -0
  191. oscura/core/memoize.py +171 -0
  192. oscura/core/memory_check.py +274 -0
  193. oscura/core/memory_guard.py +290 -0
  194. oscura/core/memory_limits.py +336 -0
  195. oscura/core/memory_monitor.py +453 -0
  196. oscura/core/memory_progress.py +465 -0
  197. oscura/core/memory_warnings.py +315 -0
  198. oscura/core/numba_backend.py +362 -0
  199. oscura/core/performance.py +352 -0
  200. oscura/core/progress.py +524 -0
  201. oscura/core/provenance.py +358 -0
  202. oscura/core/results.py +331 -0
  203. oscura/core/types.py +504 -0
  204. oscura/core/uncertainty.py +383 -0
  205. oscura/discovery/__init__.py +52 -0
  206. oscura/discovery/anomaly_detector.py +672 -0
  207. oscura/discovery/auto_decoder.py +415 -0
  208. oscura/discovery/comparison.py +497 -0
  209. oscura/discovery/quality_validator.py +528 -0
  210. oscura/discovery/signal_detector.py +769 -0
  211. oscura/dsl/__init__.py +73 -0
  212. oscura/dsl/commands.py +246 -0
  213. oscura/dsl/interpreter.py +455 -0
  214. oscura/dsl/parser.py +689 -0
  215. oscura/dsl/repl.py +172 -0
  216. oscura/exceptions.py +59 -0
  217. oscura/exploratory/__init__.py +111 -0
  218. oscura/exploratory/error_recovery.py +642 -0
  219. oscura/exploratory/fuzzy.py +513 -0
  220. oscura/exploratory/fuzzy_advanced.py +786 -0
  221. oscura/exploratory/legacy.py +831 -0
  222. oscura/exploratory/parse.py +358 -0
  223. oscura/exploratory/recovery.py +275 -0
  224. oscura/exploratory/sync.py +382 -0
  225. oscura/exploratory/unknown.py +707 -0
  226. oscura/export/__init__.py +25 -0
  227. oscura/export/wireshark/README.md +265 -0
  228. oscura/export/wireshark/__init__.py +47 -0
  229. oscura/export/wireshark/generator.py +312 -0
  230. oscura/export/wireshark/lua_builder.py +159 -0
  231. oscura/export/wireshark/templates/dissector.lua.j2 +92 -0
  232. oscura/export/wireshark/type_mapping.py +165 -0
  233. oscura/export/wireshark/validator.py +105 -0
  234. oscura/exporters/__init__.py +94 -0
  235. oscura/exporters/csv.py +303 -0
  236. oscura/exporters/exporters.py +44 -0
  237. oscura/exporters/hdf5.py +219 -0
  238. oscura/exporters/html_export.py +701 -0
  239. oscura/exporters/json_export.py +291 -0
  240. oscura/exporters/markdown_export.py +367 -0
  241. oscura/exporters/matlab_export.py +354 -0
  242. oscura/exporters/npz_export.py +219 -0
  243. oscura/exporters/spice_export.py +210 -0
  244. oscura/extensibility/__init__.py +131 -0
  245. oscura/extensibility/docs.py +752 -0
  246. oscura/extensibility/extensions.py +1125 -0
  247. oscura/extensibility/logging.py +259 -0
  248. oscura/extensibility/measurements.py +485 -0
  249. oscura/extensibility/plugins.py +414 -0
  250. oscura/extensibility/registry.py +346 -0
  251. oscura/extensibility/templates.py +913 -0
  252. oscura/extensibility/validation.py +651 -0
  253. oscura/filtering/__init__.py +89 -0
  254. oscura/filtering/base.py +563 -0
  255. oscura/filtering/convenience.py +564 -0
  256. oscura/filtering/design.py +725 -0
  257. oscura/filtering/filters.py +32 -0
  258. oscura/filtering/introspection.py +605 -0
  259. oscura/guidance/__init__.py +24 -0
  260. oscura/guidance/recommender.py +429 -0
  261. oscura/guidance/wizard.py +518 -0
  262. oscura/inference/__init__.py +251 -0
  263. oscura/inference/active_learning/README.md +153 -0
  264. oscura/inference/active_learning/__init__.py +38 -0
  265. oscura/inference/active_learning/lstar.py +257 -0
  266. oscura/inference/active_learning/observation_table.py +230 -0
  267. oscura/inference/active_learning/oracle.py +78 -0
  268. oscura/inference/active_learning/teachers/__init__.py +15 -0
  269. oscura/inference/active_learning/teachers/simulator.py +192 -0
  270. oscura/inference/adaptive_tuning.py +453 -0
  271. oscura/inference/alignment.py +653 -0
  272. oscura/inference/bayesian.py +943 -0
  273. oscura/inference/binary.py +1016 -0
  274. oscura/inference/crc_reverse.py +711 -0
  275. oscura/inference/logic.py +288 -0
  276. oscura/inference/message_format.py +1305 -0
  277. oscura/inference/protocol.py +417 -0
  278. oscura/inference/protocol_dsl.py +1084 -0
  279. oscura/inference/protocol_library.py +1230 -0
  280. oscura/inference/sequences.py +809 -0
  281. oscura/inference/signal_intelligence.py +1509 -0
  282. oscura/inference/spectral.py +215 -0
  283. oscura/inference/state_machine.py +634 -0
  284. oscura/inference/stream.py +918 -0
  285. oscura/integrations/__init__.py +59 -0
  286. oscura/integrations/llm.py +1827 -0
  287. oscura/jupyter/__init__.py +32 -0
  288. oscura/jupyter/display.py +268 -0
  289. oscura/jupyter/magic.py +334 -0
  290. oscura/loaders/__init__.py +526 -0
  291. oscura/loaders/binary.py +69 -0
  292. oscura/loaders/configurable.py +1255 -0
  293. oscura/loaders/csv.py +26 -0
  294. oscura/loaders/csv_loader.py +473 -0
  295. oscura/loaders/hdf5.py +9 -0
  296. oscura/loaders/hdf5_loader.py +510 -0
  297. oscura/loaders/lazy.py +370 -0
  298. oscura/loaders/mmap_loader.py +583 -0
  299. oscura/loaders/numpy_loader.py +436 -0
  300. oscura/loaders/pcap.py +432 -0
  301. oscura/loaders/preprocessing.py +368 -0
  302. oscura/loaders/rigol.py +287 -0
  303. oscura/loaders/sigrok.py +321 -0
  304. oscura/loaders/tdms.py +367 -0
  305. oscura/loaders/tektronix.py +711 -0
  306. oscura/loaders/validation.py +584 -0
  307. oscura/loaders/vcd.py +464 -0
  308. oscura/loaders/wav.py +233 -0
  309. oscura/math/__init__.py +45 -0
  310. oscura/math/arithmetic.py +824 -0
  311. oscura/math/interpolation.py +413 -0
  312. oscura/onboarding/__init__.py +39 -0
  313. oscura/onboarding/help.py +498 -0
  314. oscura/onboarding/tutorials.py +405 -0
  315. oscura/onboarding/wizard.py +466 -0
  316. oscura/optimization/__init__.py +19 -0
  317. oscura/optimization/parallel.py +440 -0
  318. oscura/optimization/search.py +532 -0
  319. oscura/pipeline/__init__.py +43 -0
  320. oscura/pipeline/base.py +338 -0
  321. oscura/pipeline/composition.py +242 -0
  322. oscura/pipeline/parallel.py +448 -0
  323. oscura/pipeline/pipeline.py +375 -0
  324. oscura/pipeline/reverse_engineering.py +1119 -0
  325. oscura/plugins/__init__.py +122 -0
  326. oscura/plugins/base.py +272 -0
  327. oscura/plugins/cli.py +497 -0
  328. oscura/plugins/discovery.py +411 -0
  329. oscura/plugins/isolation.py +418 -0
  330. oscura/plugins/lifecycle.py +959 -0
  331. oscura/plugins/manager.py +493 -0
  332. oscura/plugins/registry.py +421 -0
  333. oscura/plugins/versioning.py +372 -0
  334. oscura/py.typed +0 -0
  335. oscura/quality/__init__.py +65 -0
  336. oscura/quality/ensemble.py +740 -0
  337. oscura/quality/explainer.py +338 -0
  338. oscura/quality/scoring.py +616 -0
  339. oscura/quality/warnings.py +456 -0
  340. oscura/reporting/__init__.py +248 -0
  341. oscura/reporting/advanced.py +1234 -0
  342. oscura/reporting/analyze.py +448 -0
  343. oscura/reporting/argument_preparer.py +596 -0
  344. oscura/reporting/auto_report.py +507 -0
  345. oscura/reporting/batch.py +615 -0
  346. oscura/reporting/chart_selection.py +223 -0
  347. oscura/reporting/comparison.py +330 -0
  348. oscura/reporting/config.py +615 -0
  349. oscura/reporting/content/__init__.py +39 -0
  350. oscura/reporting/content/executive.py +127 -0
  351. oscura/reporting/content/filtering.py +191 -0
  352. oscura/reporting/content/minimal.py +257 -0
  353. oscura/reporting/content/verbosity.py +162 -0
  354. oscura/reporting/core.py +508 -0
  355. oscura/reporting/core_formats/__init__.py +17 -0
  356. oscura/reporting/core_formats/multi_format.py +210 -0
  357. oscura/reporting/engine.py +836 -0
  358. oscura/reporting/export.py +366 -0
  359. oscura/reporting/formatting/__init__.py +129 -0
  360. oscura/reporting/formatting/emphasis.py +81 -0
  361. oscura/reporting/formatting/numbers.py +403 -0
  362. oscura/reporting/formatting/standards.py +55 -0
  363. oscura/reporting/formatting.py +466 -0
  364. oscura/reporting/html.py +578 -0
  365. oscura/reporting/index.py +590 -0
  366. oscura/reporting/multichannel.py +296 -0
  367. oscura/reporting/output.py +379 -0
  368. oscura/reporting/pdf.py +373 -0
  369. oscura/reporting/plots.py +731 -0
  370. oscura/reporting/pptx_export.py +360 -0
  371. oscura/reporting/renderers/__init__.py +11 -0
  372. oscura/reporting/renderers/pdf.py +94 -0
  373. oscura/reporting/sections.py +471 -0
  374. oscura/reporting/standards.py +680 -0
  375. oscura/reporting/summary_generator.py +368 -0
  376. oscura/reporting/tables.py +397 -0
  377. oscura/reporting/template_system.py +724 -0
  378. oscura/reporting/templates/__init__.py +15 -0
  379. oscura/reporting/templates/definition.py +205 -0
  380. oscura/reporting/templates/index.html +649 -0
  381. oscura/reporting/templates/index.md +173 -0
  382. oscura/schemas/__init__.py +158 -0
  383. oscura/schemas/bus_configuration.json +322 -0
  384. oscura/schemas/device_mapping.json +182 -0
  385. oscura/schemas/packet_format.json +418 -0
  386. oscura/schemas/protocol_definition.json +363 -0
  387. oscura/search/__init__.py +16 -0
  388. oscura/search/anomaly.py +292 -0
  389. oscura/search/context.py +149 -0
  390. oscura/search/pattern.py +160 -0
  391. oscura/session/__init__.py +34 -0
  392. oscura/session/annotations.py +289 -0
  393. oscura/session/history.py +313 -0
  394. oscura/session/session.py +445 -0
  395. oscura/streaming/__init__.py +43 -0
  396. oscura/streaming/chunked.py +611 -0
  397. oscura/streaming/progressive.py +393 -0
  398. oscura/streaming/realtime.py +622 -0
  399. oscura/testing/__init__.py +54 -0
  400. oscura/testing/synthetic.py +808 -0
  401. oscura/triggering/__init__.py +68 -0
  402. oscura/triggering/base.py +229 -0
  403. oscura/triggering/edge.py +353 -0
  404. oscura/triggering/pattern.py +344 -0
  405. oscura/triggering/pulse.py +581 -0
  406. oscura/triggering/window.py +453 -0
  407. oscura/ui/__init__.py +48 -0
  408. oscura/ui/formatters.py +526 -0
  409. oscura/ui/progressive_display.py +340 -0
  410. oscura/utils/__init__.py +99 -0
  411. oscura/utils/autodetect.py +338 -0
  412. oscura/utils/buffer.py +389 -0
  413. oscura/utils/lazy.py +407 -0
  414. oscura/utils/lazy_imports.py +147 -0
  415. oscura/utils/memory.py +836 -0
  416. oscura/utils/memory_advanced.py +1326 -0
  417. oscura/utils/memory_extensions.py +465 -0
  418. oscura/utils/progressive.py +352 -0
  419. oscura/utils/windowing.py +362 -0
  420. oscura/visualization/__init__.py +321 -0
  421. oscura/visualization/accessibility.py +526 -0
  422. oscura/visualization/annotations.py +374 -0
  423. oscura/visualization/axis_scaling.py +305 -0
  424. oscura/visualization/colors.py +453 -0
  425. oscura/visualization/digital.py +337 -0
  426. oscura/visualization/eye.py +420 -0
  427. oscura/visualization/histogram.py +281 -0
  428. oscura/visualization/interactive.py +858 -0
  429. oscura/visualization/jitter.py +702 -0
  430. oscura/visualization/keyboard.py +394 -0
  431. oscura/visualization/layout.py +365 -0
  432. oscura/visualization/optimization.py +1028 -0
  433. oscura/visualization/palettes.py +446 -0
  434. oscura/visualization/plot.py +92 -0
  435. oscura/visualization/power.py +290 -0
  436. oscura/visualization/power_extended.py +626 -0
  437. oscura/visualization/presets.py +467 -0
  438. oscura/visualization/protocols.py +932 -0
  439. oscura/visualization/render.py +207 -0
  440. oscura/visualization/rendering.py +444 -0
  441. oscura/visualization/reverse_engineering.py +791 -0
  442. oscura/visualization/signal_integrity.py +808 -0
  443. oscura/visualization/specialized.py +553 -0
  444. oscura/visualization/spectral.py +811 -0
  445. oscura/visualization/styles.py +381 -0
  446. oscura/visualization/thumbnails.py +311 -0
  447. oscura/visualization/time_axis.py +351 -0
  448. oscura/visualization/waveform.py +367 -0
  449. oscura/workflow/__init__.py +13 -0
  450. oscura/workflow/dag.py +377 -0
  451. oscura/workflows/__init__.py +58 -0
  452. oscura/workflows/compliance.py +280 -0
  453. oscura/workflows/digital.py +272 -0
  454. oscura/workflows/multi_trace.py +502 -0
  455. oscura/workflows/power.py +178 -0
  456. oscura/workflows/protocol.py +492 -0
  457. oscura/workflows/reverse_engineering.py +639 -0
  458. oscura/workflows/signal_integrity.py +227 -0
  459. oscura-0.1.0.dist-info/METADATA +300 -0
  460. oscura-0.1.0.dist-info/RECORD +463 -0
  461. oscura-0.1.0.dist-info/entry_points.txt +2 -0
  462. {oscura-0.0.1.dist-info → oscura-0.1.0.dist-info}/licenses/LICENSE +1 -1
  463. oscura-0.0.1.dist-info/METADATA +0 -63
  464. oscura-0.0.1.dist-info/RECORD +0 -5
  465. {oscura-0.0.1.dist-info → oscura-0.1.0.dist-info}/WHEEL +0 -0
@@ -0,0 +1,626 @@
1
+ """Extended Power Analysis Visualization Functions.
2
+
3
+ This module provides visualization functions for power conversion analysis
4
+ including efficiency curves, ripple analysis, loss breakdown, and
5
+ multi-channel power waveforms.
6
+
7
+ Example:
8
+ >>> from oscura.visualization.power_extended import (
9
+ ... plot_efficiency_curve, plot_ripple_waveform, plot_loss_breakdown
10
+ ... )
11
+ >>> fig = plot_efficiency_curve(load_currents, efficiencies)
12
+ >>> fig = plot_ripple_waveform(voltage_trace, ripple_trace)
13
+
14
+ References:
15
+ - Power supply measurement best practices
16
+ - DC-DC converter efficiency testing
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ from collections.abc import Callable
22
+ from pathlib import Path
23
+ from typing import TYPE_CHECKING, Any, cast
24
+
25
+ import numpy as np
26
+
27
+ try:
28
+ import matplotlib.pyplot as plt
29
+
30
+ HAS_MATPLOTLIB = True
31
+ except ImportError:
32
+ HAS_MATPLOTLIB = False
33
+
34
+ if TYPE_CHECKING:
35
+ from matplotlib.axes import Axes
36
+ from matplotlib.figure import Figure
37
+ from numpy.typing import NDArray
38
+
39
+
40
+ __all__ = [
41
+ "plot_efficiency_curve",
42
+ "plot_loss_breakdown",
43
+ "plot_power_waveforms",
44
+ "plot_ripple_waveform",
45
+ ]
46
+
47
+
48
+ def plot_efficiency_curve(
49
+ load_values: NDArray[np.floating[Any]],
50
+ efficiency_values: NDArray[np.floating[Any]],
51
+ *,
52
+ v_in_values: list[float] | None = None,
53
+ efficiency_sets: list[NDArray[np.floating[Any]]] | None = None,
54
+ ax: Axes | None = None,
55
+ figsize: tuple[float, float] = (10, 6),
56
+ title: str | None = None,
57
+ load_unit: str = "A",
58
+ target_efficiency: float | None = None,
59
+ show_peak: bool = True,
60
+ show: bool = True,
61
+ save_path: str | Path | None = None,
62
+ ) -> Figure:
63
+ """Plot efficiency vs load curve for power converters.
64
+
65
+ Creates an efficiency plot showing converter efficiency as a function
66
+ of load current or power, with optional multiple input voltage curves.
67
+
68
+ Args:
69
+ load_values: Load current or power array.
70
+ efficiency_values: Efficiency values (0-100 or 0-1).
71
+ v_in_values: List of input voltages for multi-curve plot.
72
+ efficiency_sets: List of efficiency arrays for each v_in.
73
+ ax: Matplotlib axes.
74
+ figsize: Figure size.
75
+ title: Plot title.
76
+ load_unit: Load axis unit ("A", "W", "%").
77
+ target_efficiency: Target efficiency line.
78
+ show_peak: Annotate peak efficiency point.
79
+ show: Display plot.
80
+ save_path: Save path.
81
+
82
+ Returns:
83
+ Matplotlib Figure object.
84
+
85
+ Example:
86
+ >>> load = np.linspace(0.1, 5, 50) # 0.1A to 5A
87
+ >>> eff = 90 - 5 * np.exp(-load) # Example efficiency curve
88
+ >>> fig = plot_efficiency_curve(load, eff, target_efficiency=85)
89
+ """
90
+ if not HAS_MATPLOTLIB:
91
+ raise ImportError("matplotlib is required for visualization")
92
+
93
+ if ax is None:
94
+ fig, ax = plt.subplots(figsize=figsize)
95
+ else:
96
+ fig_temp = ax.get_figure()
97
+ if fig_temp is None:
98
+ raise ValueError("Axes must have an associated figure")
99
+ fig = cast("Figure", fig_temp)
100
+
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"]
109
+
110
+ 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)
119
+ else:
120
+ # Single curve
121
+ ax.plot(
122
+ load_values, efficiency_values, "-", linewidth=2.5, color="#3498DB", label="Efficiency"
123
+ )
124
+
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="--",
147
+ linewidth=1.5,
148
+ label=f"Target: {target_efficiency}%",
149
+ )
150
+
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",
158
+ )
159
+
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")
167
+
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")
172
+
173
+ fig.tight_layout()
174
+
175
+ if save_path is not None:
176
+ fig.savefig(save_path, dpi=300, bbox_inches="tight")
177
+
178
+ if show:
179
+ plt.show()
180
+
181
+ return fig
182
+
183
+
184
+ def plot_power_waveforms(
185
+ time: NDArray[np.floating[Any]],
186
+ *,
187
+ v_in: NDArray[np.floating[Any]] | None = None,
188
+ i_in: NDArray[np.floating[Any]] | None = None,
189
+ v_out: NDArray[np.floating[Any]] | None = None,
190
+ i_out: NDArray[np.floating[Any]] | None = None,
191
+ figsize: tuple[float, float] = (12, 10),
192
+ title: str | None = None,
193
+ time_unit: str = "auto",
194
+ show_power: bool = True,
195
+ show: bool = True,
196
+ save_path: str | Path | None = None,
197
+ ) -> Figure:
198
+ """Plot multi-channel power waveforms with optional power calculation.
199
+
200
+ Creates a multi-panel plot showing input/output voltage and current
201
+ waveforms with optional instantaneous power overlay.
202
+
203
+ Args:
204
+ time: Time array in seconds.
205
+ v_in: Input voltage waveform.
206
+ i_in: Input current waveform.
207
+ v_out: Output voltage waveform.
208
+ i_out: Output current waveform.
209
+ figsize: Figure size.
210
+ title: Plot title.
211
+ time_unit: Time axis unit.
212
+ show_power: Calculate and show instantaneous power.
213
+ show: Display plot.
214
+ save_path: Save path.
215
+
216
+ Returns:
217
+ Matplotlib Figure object.
218
+ """
219
+ if not HAS_MATPLOTLIB:
220
+ raise ImportError("matplotlib is required for visualization")
221
+
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")
232
+
233
+ fig, axes = plt.subplots(n_plots, 1, figsize=figsize, sharex=True)
234
+ if n_plots == 1:
235
+ axes = [axes]
236
+
237
+ # Time unit conversion
238
+ if time_unit == "auto":
239
+ max_time = np.max(time)
240
+ if max_time < 1e-6:
241
+ time_unit = "us"
242
+ time_mult = 1e6
243
+ elif max_time < 1e-3:
244
+ time_unit = "ms"
245
+ time_mult = 1e3
246
+ else:
247
+ time_unit = "s"
248
+ time_mult = 1.0
249
+ else:
250
+ time_mult = {"s": 1, "ms": 1e3, "us": 1e6, "ns": 1e9}.get(time_unit, 1.0)
251
+
252
+ time_scaled = time * time_mult
253
+
254
+ ax_idx = 0
255
+
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
272
+
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
289
+
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
+
322
+ if title:
323
+ fig.suptitle(title, fontsize=14, fontweight="bold")
324
+ else:
325
+ fig.suptitle("Power Converter Waveforms", fontsize=14, fontweight="bold")
326
+
327
+ fig.tight_layout()
328
+
329
+ if save_path is not None:
330
+ fig.savefig(save_path, dpi=300, bbox_inches="tight")
331
+
332
+ if show:
333
+ plt.show()
334
+
335
+ return fig
336
+
337
+
338
+ def plot_ripple_waveform(
339
+ time: NDArray[np.floating[Any]],
340
+ voltage: NDArray[np.floating[Any]],
341
+ *,
342
+ ax: Axes | None = None,
343
+ figsize: tuple[float, float] = (12, 8),
344
+ title: str | None = None,
345
+ time_unit: str = "auto",
346
+ show_dc: bool = True,
347
+ show_ac: bool = True,
348
+ show_spectrum: bool = True,
349
+ sample_rate: float | None = None,
350
+ show: bool = True,
351
+ save_path: str | Path | None = None,
352
+ ) -> Figure:
353
+ """Plot ripple waveform with DC, AC, and spectral analysis.
354
+
355
+ Creates a multi-panel view showing DC-coupled waveform, AC-coupled
356
+ ripple, and optionally the ripple frequency spectrum.
357
+
358
+ Args:
359
+ time: Time array in seconds.
360
+ voltage: Voltage waveform.
361
+ ax: Matplotlib axes (creates multi-panel if None).
362
+ figsize: Figure size.
363
+ title: Plot title.
364
+ time_unit: Time axis unit.
365
+ show_dc: Show DC-coupled waveform.
366
+ show_ac: Show AC-coupled ripple.
367
+ show_spectrum: Show ripple spectrum.
368
+ sample_rate: Sample rate for FFT (required if show_spectrum=True).
369
+ show: Display plot.
370
+ save_path: Save path.
371
+
372
+ Returns:
373
+ Matplotlib Figure object.
374
+ """
375
+ if not HAS_MATPLOTLIB:
376
+ raise ImportError("matplotlib is required for visualization")
377
+
378
+ n_plots = sum([show_dc, show_ac, show_spectrum])
379
+ if n_plots == 0:
380
+ raise ValueError("At least one display option must be True")
381
+
382
+ fig, axes = plt.subplots(n_plots, 1, figsize=figsize)
383
+ if n_plots == 1:
384
+ axes = [axes]
385
+
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
+
401
+ time_scaled = time * time_mult
402
+
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)
408
+
409
+ ax_idx = 0
410
+
411
+ # DC-coupled view
412
+ 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)
422
+ ax_idx += 1
423
+
424
+ # AC-coupled (ripple only) view
425
+ 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)
448
+ ax_idx += 1
449
+
450
+ # Spectrum view
451
+ 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)
484
+ else:
485
+ axes[-1].set_xlabel(f"Time ({time_unit})", fontsize=11)
486
+
487
+ if title:
488
+ fig.suptitle(title, fontsize=14, fontweight="bold")
489
+ else:
490
+ fig.suptitle("Ripple Analysis", fontsize=14, fontweight="bold")
491
+
492
+ fig.tight_layout()
493
+
494
+ if save_path is not None:
495
+ fig.savefig(save_path, dpi=300, bbox_inches="tight")
496
+
497
+ if show:
498
+ plt.show()
499
+
500
+ return fig
501
+
502
+
503
+ def plot_loss_breakdown(
504
+ loss_values: dict[str, float],
505
+ *,
506
+ ax: Axes | None = None,
507
+ figsize: tuple[float, float] = (10, 8),
508
+ title: str | None = None,
509
+ show_watts: bool = True,
510
+ show: bool = True,
511
+ save_path: str | Path | None = None,
512
+ ) -> Figure:
513
+ """Plot power loss breakdown as pie chart.
514
+
515
+ Creates a pie chart showing the contribution of each loss mechanism
516
+ (switching, conduction, magnetic, etc.) to total power dissipation.
517
+
518
+ Args:
519
+ loss_values: Dictionary mapping loss type to value in Watts.
520
+ ax: Matplotlib axes.
521
+ figsize: Figure size.
522
+ title: Plot title.
523
+ show_watts: Show watt values on slices.
524
+ show: Display plot.
525
+ save_path: Save path.
526
+
527
+ Returns:
528
+ Matplotlib Figure object.
529
+
530
+ Example:
531
+ >>> losses = {
532
+ ... "Switching": 0.5,
533
+ ... "Conduction": 0.3,
534
+ ... "Magnetic": 0.15,
535
+ ... "Gate Drive": 0.05
536
+ ... }
537
+ >>> fig = plot_loss_breakdown(losses)
538
+ """
539
+ if not HAS_MATPLOTLIB:
540
+ raise ImportError("matplotlib is required for visualization")
541
+
542
+ if ax is None:
543
+ fig, ax = plt.subplots(figsize=figsize)
544
+ else:
545
+ fig_temp = ax.get_figure()
546
+ if fig_temp is None:
547
+ raise ValueError("Axes must have an associated figure")
548
+ fig = cast("Figure", fig_temp)
549
+
550
+ labels = list(loss_values.keys())
551
+ values = list(loss_values.values())
552
+ total_loss = sum(values)
553
+
554
+ # Color palette
555
+ colors = [
556
+ "#3498DB",
557
+ "#E74C3C",
558
+ "#27AE60",
559
+ "#9B59B6",
560
+ "#F39C12",
561
+ "#1ABC9C",
562
+ "#E67E22",
563
+ "#95A5A6",
564
+ ]
565
+
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)
617
+
618
+ fig.tight_layout()
619
+
620
+ if save_path is not None:
621
+ fig.savefig(save_path, dpi=300, bbox_inches="tight")
622
+
623
+ if show:
624
+ plt.show()
625
+
626
+ return fig