oscura 0.0.1__py3-none-any.whl → 0.1.1__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.1.dist-info/METADATA +300 -0
  460. oscura-0.1.1.dist-info/RECORD +463 -0
  461. oscura-0.1.1.dist-info/entry_points.txt +2 -0
  462. {oscura-0.0.1.dist-info → oscura-0.1.1.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.1.dist-info}/WHEEL +0 -0
@@ -0,0 +1,502 @@
1
+ """Multi-Trace Workflow Support.
2
+
3
+ Provides workflows for processing and analyzing multiple traces together.
4
+ """
5
+
6
+ import concurrent.futures
7
+ from collections.abc import Iterator
8
+ from dataclasses import dataclass, field
9
+ from glob import glob as glob_func
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ import numpy as np
14
+
15
+ from oscura.core.exceptions import OscuraError
16
+ from oscura.core.progress import create_progress_tracker
17
+
18
+
19
+ class AlignmentMethod:
20
+ """Alignment method constants."""
21
+
22
+ TRIGGER = "trigger"
23
+ TIME_SYNC = "time"
24
+ CROSS_CORRELATION = "correlation"
25
+ MANUAL = "manual"
26
+
27
+
28
+ @dataclass
29
+ class TraceStatistics:
30
+ """Statistics for a measurement across traces.
31
+
32
+ Attributes:
33
+ mean: Mean value
34
+ std: Standard deviation
35
+ min: Minimum value
36
+ max: Maximum value
37
+ median: Median value
38
+ count: Number of traces
39
+ """
40
+
41
+ mean: float
42
+ std: float
43
+ min: float
44
+ max: float
45
+ median: float
46
+ count: int
47
+
48
+
49
+ @dataclass
50
+ class MultiTraceResults:
51
+ """Results from multi-trace workflow.
52
+
53
+ Attributes:
54
+ trace_ids: List of trace identifiers
55
+ measurements: Dict mapping trace_id -> measurement results
56
+ statistics: Dict mapping measurement_name -> TraceStatistics
57
+ metadata: Additional workflow metadata
58
+ """
59
+
60
+ trace_ids: list[str] = field(default_factory=list)
61
+ measurements: dict[str, dict[str, Any]] = field(default_factory=dict)
62
+ statistics: dict[str, TraceStatistics] = field(default_factory=dict)
63
+ metadata: dict[str, Any] = field(default_factory=dict)
64
+
65
+
66
+ class MultiTraceWorkflow:
67
+ """Workflow manager for multi-trace processing.
68
+
69
+ Provides methods to load, align, process, and analyze multiple traces
70
+ with memory-efficient streaming and optional parallelization.
71
+ """
72
+
73
+ def __init__(
74
+ self,
75
+ pattern: str | None = None,
76
+ traces: list[Any] | None = None,
77
+ lazy: bool = False,
78
+ ):
79
+ """Initialize multi-trace workflow.
80
+
81
+ Args:
82
+ pattern: Glob pattern for trace files (e.g., "*.csv")
83
+ traces: Pre-loaded trace objects
84
+ lazy: If True, load traces on demand
85
+
86
+ Raises:
87
+ OscuraError: If neither pattern nor traces provided
88
+ """
89
+ self.pattern = pattern
90
+ self._traces = traces or []
91
+ self._lazy = lazy
92
+ self._file_paths: list[Path] = []
93
+ self._aligned = False
94
+ self._alignment_offset: dict[str, int] = {}
95
+ self.results = MultiTraceResults()
96
+
97
+ # Discover files if pattern provided
98
+ if pattern:
99
+ self._discover_files()
100
+ elif not traces:
101
+ raise OscuraError("Must provide either pattern or traces")
102
+
103
+ def _discover_files(self) -> None:
104
+ """Discover trace files matching pattern."""
105
+ if not self.pattern:
106
+ return
107
+
108
+ paths = glob_func(self.pattern) # noqa: PTH207
109
+ if not paths:
110
+ raise OscuraError(f"No files match pattern: {self.pattern}")
111
+
112
+ self._file_paths = [Path(p) for p in sorted(paths)]
113
+ self.results.trace_ids = [p.name for p in self._file_paths]
114
+
115
+ def _load_trace(self, path: Path) -> Any:
116
+ """Load a single trace file.
117
+
118
+ Args:
119
+ path: Path to trace file
120
+
121
+ Returns:
122
+ Loaded trace object
123
+
124
+ Raises:
125
+ OscuraError: If trace cannot be loaded
126
+ """
127
+ # Determine loader based on extension
128
+ ext = path.suffix.lower()
129
+
130
+ try:
131
+ if ext == ".csv":
132
+ from oscura.loaders.csv import ( # type: ignore[import-not-found]
133
+ load_csv, # type: ignore[import-not-found]
134
+ )
135
+
136
+ return load_csv(str(path))
137
+ elif ext == ".bin":
138
+ from oscura.loaders.binary import ( # type: ignore[import-not-found]
139
+ load_binary, # type: ignore[import-not-found]
140
+ )
141
+
142
+ return load_binary(str(path))
143
+ elif ext in (".h5", ".hdf5"):
144
+ from oscura.loaders.hdf5 import ( # type: ignore[import-not-found]
145
+ load_hdf5, # type: ignore[import-not-found]
146
+ )
147
+
148
+ return load_hdf5(str(path))
149
+ else:
150
+ raise OscuraError(f"Unsupported format: {ext}")
151
+
152
+ except ImportError as e:
153
+ raise OscuraError(f"Loader not available for {ext}: {e}") # noqa: B904
154
+
155
+ def _iter_traces(self, lazy: bool = False) -> Iterator[tuple[str, Any]]:
156
+ """Iterate over traces.
157
+
158
+ Args:
159
+ lazy: If True, load on demand; if False, load all first
160
+
161
+ Yields:
162
+ Tuple of (trace_id, trace)
163
+ """
164
+ # Use pre-loaded traces if available
165
+ if self._traces:
166
+ for i, trace in enumerate(self._traces):
167
+ trace_id = (
168
+ self.results.trace_ids[i] if i < len(self.results.trace_ids) else f"trace_{i}"
169
+ )
170
+ yield trace_id, trace
171
+ return
172
+
173
+ # Load from files
174
+ for path in self._file_paths:
175
+ trace_id = path.name
176
+ if lazy or self._lazy:
177
+ # Load on demand
178
+ trace = self._load_trace(path)
179
+ else:
180
+ # Would load all at once (not implemented here)
181
+ trace = self._load_trace(path)
182
+ yield trace_id, trace
183
+
184
+ def align(
185
+ self,
186
+ method: str = AlignmentMethod.TRIGGER,
187
+ channel: int = 0,
188
+ threshold: float | None = None,
189
+ **kwargs: Any,
190
+ ) -> None:
191
+ """Align traces using specified method.
192
+
193
+ Args:
194
+ method: Alignment method ('trigger', 'time', 'correlation', 'manual')
195
+ channel: Channel to use for alignment (for multi-channel traces)
196
+ threshold: Trigger threshold (for trigger alignment)
197
+ **kwargs: Additional method-specific parameters
198
+
199
+ Raises:
200
+ OscuraError: If alignment fails
201
+ """
202
+ if method == AlignmentMethod.TRIGGER:
203
+ self._align_by_trigger(channel, threshold, **kwargs)
204
+ elif method == AlignmentMethod.TIME_SYNC:
205
+ self._align_by_time(**kwargs)
206
+ elif method == AlignmentMethod.CROSS_CORRELATION:
207
+ self._align_by_correlation(channel, **kwargs)
208
+ elif method == AlignmentMethod.MANUAL:
209
+ self._align_manual(**kwargs)
210
+ else:
211
+ raise OscuraError(f"Unknown alignment method: {method}")
212
+
213
+ self._aligned = True
214
+
215
+ def _align_by_trigger(
216
+ self,
217
+ channel: int,
218
+ threshold: float | None,
219
+ **kwargs: Any,
220
+ ) -> None:
221
+ """Align traces by trigger point.
222
+
223
+ Args:
224
+ channel: Channel index
225
+ threshold: Trigger threshold
226
+ **kwargs: Additional parameters
227
+ """
228
+ # Find trigger point in each trace
229
+ for trace_id, trace in self._iter_traces(lazy=True):
230
+ # Find first crossing of threshold
231
+ if hasattr(trace, "data"):
232
+ data = trace.data
233
+ if threshold is None:
234
+ # Auto threshold: 50% of max
235
+ threshold = 0.5 * (np.max(data) + np.min(data))
236
+
237
+ # Find first rising edge
238
+ above = data > threshold
239
+ edges = np.diff(above.astype(int))
240
+ rising = np.where(edges > 0)[0]
241
+
242
+ if len(rising) > 0:
243
+ self._alignment_offset[trace_id] = int(rising[0])
244
+ else:
245
+ self._alignment_offset[trace_id] = 0
246
+ else:
247
+ self._alignment_offset[trace_id] = 0
248
+
249
+ def _align_by_time(self, **kwargs: Any) -> None:
250
+ """Align traces by timestamp.
251
+
252
+ Args:
253
+ **kwargs: Additional parameters
254
+ """
255
+ # Align based on trace timestamps
256
+ # Placeholder implementation
257
+ for trace_id, _trace in self._iter_traces(lazy=True):
258
+ self._alignment_offset[trace_id] = 0
259
+
260
+ def _align_by_correlation(self, channel: int, **kwargs: Any) -> None:
261
+ """Align traces by cross-correlation.
262
+
263
+ Args:
264
+ channel: Channel index
265
+ **kwargs: Additional parameters
266
+ """
267
+ # Use cross-correlation to find alignment
268
+ # Placeholder implementation
269
+ for trace_id, _trace in self._iter_traces(lazy=True):
270
+ self._alignment_offset[trace_id] = 0
271
+
272
+ def _align_manual(self, **kwargs: Any) -> None:
273
+ """Manual alignment with specified offsets.
274
+
275
+ Args:
276
+ **kwargs: Must include 'offsets' dict mapping trace_id -> offset
277
+
278
+ Raises:
279
+ OscuraError: If offsets parameter not provided.
280
+ """
281
+ offsets = kwargs.get("offsets", {})
282
+ if not offsets:
283
+ raise OscuraError("Manual alignment requires 'offsets' parameter")
284
+
285
+ self._alignment_offset.update(offsets)
286
+
287
+ def measure(
288
+ self, *measurements: str, parallel: bool = False, max_workers: int | None = None
289
+ ) -> None:
290
+ """Measure properties across all traces.
291
+
292
+ Args:
293
+ *measurements: Measurement names (rise_time, fall_time, etc.)
294
+ parallel: If True, process traces in parallel
295
+ max_workers: Maximum parallel workers (None = CPU count)
296
+
297
+ Raises:
298
+ OscuraError: If measurement fails
299
+ """
300
+ if not measurements:
301
+ raise OscuraError("At least one measurement required")
302
+
303
+ if parallel:
304
+ self._measure_parallel(measurements, max_workers)
305
+ else:
306
+ self._measure_sequential(measurements)
307
+
308
+ def _measure_sequential(self, measurements: tuple[str, ...]) -> None:
309
+ """Measure sequentially."""
310
+ # Progress tracking
311
+ progress = create_progress_tracker( # type: ignore[call-arg]
312
+ total=len(self.results.trace_ids),
313
+ description="Measuring traces",
314
+ )
315
+
316
+ for trace_id, trace in self._iter_traces(lazy=True):
317
+ results = {}
318
+ for meas_name in measurements:
319
+ try:
320
+ # Call measurement function
321
+ # Placeholder - would call actual measurement
322
+ results[meas_name] = self._perform_measurement(trace, meas_name)
323
+ except Exception as e:
324
+ results[meas_name] = None
325
+ print(f"Warning: {meas_name} failed for {trace_id}: {e}")
326
+
327
+ self.results.measurements[trace_id] = results
328
+ progress.update(1)
329
+
330
+ def _measure_parallel(self, measurements: tuple[str, ...], max_workers: int | None) -> None:
331
+ """Measure in parallel."""
332
+ with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
333
+ futures = {}
334
+
335
+ for trace_id, trace in self._iter_traces(lazy=False):
336
+ future = executor.submit(self._measure_trace, trace, measurements)
337
+ futures[future] = trace_id
338
+
339
+ for future in concurrent.futures.as_completed(futures):
340
+ trace_id = futures[future]
341
+ try:
342
+ results = future.result()
343
+ self.results.measurements[trace_id] = results
344
+ except Exception as e:
345
+ print(f"Error measuring {trace_id}: {e}")
346
+
347
+ def _measure_trace(self, trace: Any, measurements: tuple[str, ...]) -> dict[str, Any]:
348
+ """Measure a single trace.
349
+
350
+ Args:
351
+ trace: Trace object
352
+ measurements: Measurement names
353
+
354
+ Returns:
355
+ Dict mapping measurement_name -> value
356
+ """
357
+ results = {}
358
+ for meas_name in measurements:
359
+ try:
360
+ results[meas_name] = self._perform_measurement(trace, meas_name)
361
+ except Exception:
362
+ results[meas_name] = None
363
+ return results
364
+
365
+ def _perform_measurement(self, trace: Any, measurement: str) -> Any:
366
+ """Perform a single measurement.
367
+
368
+ Args:
369
+ trace: Trace object
370
+ measurement: Measurement name
371
+
372
+ Raises:
373
+ OscuraError: If measurement not available
374
+ """
375
+ # Placeholder - would call actual measurement functions
376
+ # from oscura.analyzers.measurements
377
+ raise OscuraError(
378
+ f"Measurement '{measurement}' not yet implemented in multi-trace workflow"
379
+ )
380
+
381
+ def aggregate(self) -> MultiTraceResults:
382
+ """Compute aggregate statistics across traces.
383
+
384
+ Returns:
385
+ Results with statistics
386
+
387
+ Raises:
388
+ OscuraError: If no measurements available
389
+ """
390
+ if not self.results.measurements:
391
+ raise OscuraError("No measurements available. Call measure() first.")
392
+
393
+ # Compute statistics for each measurement type
394
+ all_measurements = set() # type: ignore[var-annotated]
395
+ for trace_results in self.results.measurements.values():
396
+ all_measurements.update(trace_results.keys())
397
+
398
+ for meas_name in all_measurements:
399
+ values = []
400
+ for trace_results in self.results.measurements.values():
401
+ val = trace_results.get(meas_name)
402
+ if val is not None and not (isinstance(val, float) and np.isnan(val)):
403
+ values.append(float(val))
404
+
405
+ if values:
406
+ self.results.statistics[meas_name] = TraceStatistics(
407
+ mean=float(np.mean(values)),
408
+ std=float(np.std(values)),
409
+ min=float(np.min(values)),
410
+ max=float(np.max(values)),
411
+ median=float(np.median(values)),
412
+ count=len(values),
413
+ )
414
+
415
+ return self.results
416
+
417
+ def export_report(self, filename: str, format: str = "pdf") -> None:
418
+ """Export combined report.
419
+
420
+ Args:
421
+ filename: Output filename
422
+ format: Report format ('pdf', 'html', 'json')
423
+
424
+ Raises:
425
+ OscuraError: If export fails
426
+ """
427
+ if format == "json":
428
+ self._export_json(filename)
429
+ elif format == "pdf":
430
+ self._export_pdf(filename)
431
+ elif format == "html":
432
+ self._export_html(filename)
433
+ else:
434
+ raise OscuraError(f"Unsupported report format: {format}")
435
+
436
+ def _export_json(self, filename: str) -> None:
437
+ """Export results to JSON."""
438
+ import json
439
+
440
+ data = {
441
+ "trace_ids": self.results.trace_ids,
442
+ "measurements": self.results.measurements,
443
+ "statistics": {
444
+ name: {
445
+ "mean": stats.mean,
446
+ "std": stats.std,
447
+ "min": stats.min,
448
+ "max": stats.max,
449
+ "median": stats.median,
450
+ "count": stats.count,
451
+ }
452
+ for name, stats in self.results.statistics.items()
453
+ },
454
+ "metadata": self.results.metadata,
455
+ }
456
+
457
+ with open(filename, "w") as f:
458
+ json.dump(data, f, indent=2)
459
+
460
+ def _export_pdf(self, filename: str) -> None:
461
+ """Export results to PDF.
462
+
463
+ Args:
464
+ filename: Output filename
465
+
466
+ Raises:
467
+ OscuraError: PDF export not yet implemented
468
+ """
469
+ raise OscuraError("PDF export not yet implemented")
470
+
471
+ def _export_html(self, filename: str) -> None:
472
+ """Export results to HTML.
473
+
474
+ Args:
475
+ filename: Output filename
476
+
477
+ Raises:
478
+ OscuraError: HTML export not yet implemented
479
+ """
480
+ raise OscuraError("HTML export not yet implemented")
481
+
482
+
483
+ def load_all(pattern: str, lazy: bool = True) -> list[Any]:
484
+ """Load all traces matching pattern.
485
+
486
+ Args:
487
+ pattern: Glob pattern
488
+ lazy: If True, return lazy-loading proxy objects
489
+
490
+ Returns:
491
+ List of trace objects
492
+
493
+ Raises:
494
+ OscuraError: If no traces found
495
+ """
496
+ paths = glob_func(pattern) # noqa: PTH207
497
+ if not paths:
498
+ raise OscuraError(f"No files match pattern: {pattern}")
499
+
500
+ # For now, just return file paths
501
+ # Would implement lazy loading proxy
502
+ return [Path(p) for p in paths]
@@ -0,0 +1,178 @@
1
+ """Power analysis workflow.
2
+
3
+ This module implements comprehensive power consumption analysis from
4
+ voltage and current traces.
5
+
6
+
7
+ Example:
8
+ >>> import oscura as osc
9
+ >>> voltage = osc.load('vdd.wfm')
10
+ >>> current = osc.load('idd.wfm')
11
+ >>> result = osc.power_analysis(voltage, current)
12
+ >>> print(f"Average Power: {result['average_power']*1e3:.2f} mW")
13
+
14
+ References:
15
+ IEC 61000: Electromagnetic compatibility
16
+ IEEE 1241-2010: ADC terminology and test methods
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ from typing import TYPE_CHECKING, Any
22
+
23
+ import numpy as np
24
+
25
+ from oscura.core.exceptions import AnalysisError
26
+
27
+ if TYPE_CHECKING:
28
+ from oscura.core.types import WaveformTrace
29
+
30
+
31
+ def power_analysis(
32
+ voltage: WaveformTrace,
33
+ current: WaveformTrace,
34
+ *,
35
+ input_voltage: WaveformTrace | None = None,
36
+ input_current: WaveformTrace | None = None,
37
+ report: str | None = None,
38
+ ) -> dict[str, Any]:
39
+ """Comprehensive power consumption analysis.
40
+
41
+ Analyzes power consumption from voltage and current measurements:
42
+ - Instantaneous power calculation
43
+ - Average, RMS, and peak power
44
+ - Energy consumption
45
+ - Efficiency (if input power provided)
46
+ - Power profile generation
47
+
48
+ Args:
49
+ voltage: Output voltage trace.
50
+ current: Output current trace.
51
+ input_voltage: Optional input voltage for efficiency calculation.
52
+ input_current: Optional input current for efficiency calculation.
53
+ report: Optional path to save HTML report.
54
+
55
+ Returns:
56
+ Dictionary containing:
57
+ - power_trace: WaveformTrace of instantaneous power P(t)
58
+ - average_power: Mean power in watts
59
+ - output_power_avg: Average output power (same as average_power)
60
+ - output_power_rms: RMS output power in watts
61
+ - peak_power: Maximum power in watts
62
+ - min_power: Minimum power in watts
63
+ - energy: Total energy in joules
64
+ - duration: Measurement duration in seconds
65
+ - efficiency: Efficiency percentage (if input provided)
66
+ - power_loss: Power loss in watts (if input provided)
67
+ - input_power_avg: Average input power (if input provided)
68
+
69
+ Raises:
70
+ AnalysisError: If traces have incompatible sample rates or lengths.
71
+
72
+ Example:
73
+ >>> voltage = osc.load('vout.wfm')
74
+ >>> current = osc.load('iout.wfm')
75
+ >>> result = osc.power_analysis(voltage, current)
76
+ >>> print(f"Average: {result['average_power']*1e3:.2f} mW")
77
+ >>> print(f"Peak: {result['peak_power']*1e3:.2f} mW")
78
+ >>> print(f"Energy: {result['energy']*1e6:.2f} µJ")
79
+
80
+ References:
81
+ IEC 61000-4-7: Harmonics and interharmonics measurement
82
+ IEEE 1459-2010: Definitions for measurement of electric power
83
+ """
84
+ # Import power analysis functions
85
+ from oscura.analyzers.power.basic import (
86
+ instantaneous_power,
87
+ power_statistics,
88
+ )
89
+
90
+ # Validate traces
91
+ if voltage.metadata.sample_rate != current.metadata.sample_rate:
92
+ # Would need interpolation in real implementation
93
+ raise AnalysisError(
94
+ "Voltage and current traces must have same sample rate. "
95
+ f"Got {voltage.metadata.sample_rate} and {current.metadata.sample_rate}"
96
+ )
97
+
98
+ # Calculate instantaneous power
99
+ power_trace = instantaneous_power(voltage, current)
100
+
101
+ # Calculate power statistics
102
+ stats = power_statistics(power_trace)
103
+
104
+ # Build result with output power
105
+ result = {
106
+ "power_trace": power_trace,
107
+ "average_power": stats["average"],
108
+ "output_power_avg": stats["average"],
109
+ "output_power_rms": stats["rms"],
110
+ "peak_power": stats["peak"],
111
+ "min_power": stats.get("min", np.min(power_trace.data)),
112
+ "energy": stats["energy"],
113
+ "duration": stats["duration"],
114
+ }
115
+
116
+ # Calculate efficiency if input provided
117
+ if input_voltage is not None and input_current is not None:
118
+ input_power_trace = instantaneous_power(input_voltage, input_current)
119
+ input_stats = power_statistics(input_power_trace)
120
+
121
+ input_power_avg = input_stats["average"]
122
+ output_power_avg = stats["average"]
123
+
124
+ if input_power_avg > 0:
125
+ efficiency = (output_power_avg / input_power_avg) * 100.0
126
+ power_loss = input_power_avg - output_power_avg
127
+ else:
128
+ efficiency = 0.0
129
+ power_loss = 0.0
130
+
131
+ result["efficiency"] = efficiency
132
+ result["power_loss"] = power_loss
133
+ result["input_power_avg"] = input_power_avg
134
+
135
+ # Generate report if requested
136
+ if report is not None:
137
+ _generate_power_report(result, report)
138
+
139
+ return result
140
+
141
+
142
+ def _generate_power_report(result: dict[str, Any], output_path: str) -> None:
143
+ """Generate HTML report for power analysis.
144
+
145
+ Args:
146
+ result: Power analysis result dictionary.
147
+ output_path: Path to save HTML report.
148
+ """
149
+ html = f"""
150
+ <html>
151
+ <head><title>Power Analysis Report</title></head>
152
+ <body>
153
+ <h1>Power Analysis Report</h1>
154
+ <h2>Power Statistics</h2>
155
+ <table>
156
+ <tr><th>Parameter</th><th>Value</th><th>Units</th></tr>
157
+ <tr><td>Average Power</td><td>{result["average_power"] * 1e3:.3f}</td><td>mW</td></tr>
158
+ <tr><td>RMS Power</td><td>{result["output_power_rms"] * 1e3:.3f}</td><td>mW</td></tr>
159
+ <tr><td>Peak Power</td><td>{result["peak_power"] * 1e3:.3f}</td><td>mW</td></tr>
160
+ <tr><td>Total Energy</td><td>{result["energy"] * 1e6:.3f}</td><td>µJ</td></tr>
161
+ <tr><td>Duration</td><td>{result["duration"] * 1e3:.3f}</td><td>ms</td></tr>
162
+ """
163
+ if "efficiency" in result:
164
+ html += f"""
165
+ <tr><td>Efficiency</td><td>{result["efficiency"]:.1f}</td><td>%</td></tr>
166
+ <tr><td>Input Power</td><td>{result["input_power_avg"] * 1e3:.3f}</td><td>mW</td></tr>
167
+ <tr><td>Power Loss</td><td>{result["power_loss"] * 1e3:.3f}</td><td>mW</td></tr>
168
+ """
169
+ html += """
170
+ </table>
171
+ </body>
172
+ </html>
173
+ """
174
+ with open(output_path, "w") as f:
175
+ f.write(html)
176
+
177
+
178
+ __all__ = ["power_analysis"]