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,297 @@
1
+ """Power efficiency calculations for Oscura.
2
+
3
+ Provides efficiency calculations for power converters and systems.
4
+
5
+
6
+ Example:
7
+ >>> from oscura.analyzers.power.efficiency import efficiency
8
+ >>> eta = efficiency(v_in, i_in, v_out, i_out)
9
+ >>> print(f"Efficiency: {eta*100:.1f}%")
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from typing import TYPE_CHECKING
15
+
16
+ import numpy as np
17
+
18
+ from oscura.analyzers.power.basic import average_power, instantaneous_power
19
+
20
+ if TYPE_CHECKING:
21
+ from numpy.typing import NDArray
22
+
23
+ from oscura.core.types import WaveformTrace
24
+
25
+
26
+ def efficiency(
27
+ v_in: WaveformTrace,
28
+ i_in: WaveformTrace,
29
+ v_out: WaveformTrace,
30
+ i_out: WaveformTrace,
31
+ ) -> float:
32
+ """Calculate power conversion efficiency.
33
+
34
+ eta = P_out / P_in * 100%
35
+
36
+ Args:
37
+ v_in: Input voltage trace.
38
+ i_in: Input current trace.
39
+ v_out: Output voltage trace.
40
+ i_out: Output current trace.
41
+
42
+ Returns:
43
+ Efficiency as a ratio (0 to 1).
44
+
45
+ Example:
46
+ >>> eta = efficiency(v_in, i_in, v_out, i_out)
47
+ >>> print(f"Efficiency: {eta*100:.1f}%")
48
+ """
49
+ p_in = average_power(voltage=v_in, current=i_in)
50
+ p_out = average_power(voltage=v_out, current=i_out)
51
+
52
+ if p_in <= 0:
53
+ return 0.0
54
+
55
+ return p_out / p_in
56
+
57
+
58
+ def power_conversion_efficiency(
59
+ p_in: float,
60
+ p_out: float,
61
+ ) -> float:
62
+ """Calculate efficiency from power values.
63
+
64
+ Args:
65
+ p_in: Input power in Watts.
66
+ p_out: Output power in Watts.
67
+
68
+ Returns:
69
+ Efficiency as a ratio (0 to 1).
70
+
71
+ Example:
72
+ >>> eta = power_conversion_efficiency(p_in=100, p_out=90)
73
+ >>> print(f"Efficiency: {eta*100:.1f}%")
74
+ """
75
+ if p_in <= 0:
76
+ return 0.0
77
+ return p_out / p_in
78
+
79
+
80
+ def multi_output_efficiency(
81
+ v_in: WaveformTrace,
82
+ i_in: WaveformTrace,
83
+ outputs: list[tuple[WaveformTrace, WaveformTrace]],
84
+ ) -> dict[str, float]:
85
+ """Calculate efficiency for multi-output power supply.
86
+
87
+ Args:
88
+ v_in: Input voltage trace.
89
+ i_in: Input current trace.
90
+ outputs: List of (v_out, i_out) trace tuples for each output.
91
+
92
+ Returns:
93
+ Dictionary with:
94
+ - total_efficiency: Overall efficiency
95
+ - output_N_efficiency: Per-output efficiency (contribution)
96
+ - output_N_power: Per-output power
97
+ - total_output_power: Sum of all output powers
98
+ - input_power: Input power
99
+ - losses: Power losses (P_in - P_out_total)
100
+
101
+ Example:
102
+ >>> outputs = [(v1, i1), (v2, i2), (v3, i3)]
103
+ >>> result = multi_output_efficiency(v_in, i_in, outputs)
104
+ >>> print(f"Total efficiency: {result['total_efficiency']*100:.1f}%")
105
+ """
106
+ p_in = average_power(voltage=v_in, current=i_in)
107
+
108
+ result = {
109
+ "input_power": p_in,
110
+ }
111
+
112
+ total_output = 0.0
113
+ for idx, (v_out, i_out) in enumerate(outputs):
114
+ p_out = average_power(voltage=v_out, current=i_out)
115
+ result[f"output_{idx + 1}_power"] = p_out
116
+ result[f"output_{idx + 1}_efficiency"] = p_out / p_in if p_in > 0 else 0.0
117
+ total_output += p_out
118
+
119
+ result["total_output_power"] = total_output
120
+ result["total_efficiency"] = total_output / p_in if p_in > 0 else 0.0
121
+ result["losses"] = p_in - total_output
122
+
123
+ return result
124
+
125
+
126
+ def efficiency_vs_load(
127
+ v_in: WaveformTrace,
128
+ i_in: WaveformTrace,
129
+ v_out: WaveformTrace,
130
+ i_out: WaveformTrace,
131
+ *,
132
+ n_points: int = 100,
133
+ ) -> dict[str, NDArray[np.float64]]:
134
+ """Calculate efficiency across the load range.
135
+
136
+ Segments the waveforms and calculates efficiency at each load level.
137
+
138
+ Args:
139
+ v_in: Input voltage trace.
140
+ i_in: Input current trace.
141
+ v_out: Output voltage trace.
142
+ i_out: Output current trace.
143
+ n_points: Number of load points to evaluate.
144
+
145
+ Returns:
146
+ Dictionary with:
147
+ - load_percent: Load levels as percentage of max
148
+ - efficiency: Efficiency at each load level
149
+ - output_power: Output power at each load level
150
+ - input_power: Input power at each load level
151
+
152
+ Example:
153
+ >>> result = efficiency_vs_load(v_in, i_in, v_out, i_out)
154
+ >>> plt.plot(result['load_percent'], result['efficiency'] * 100)
155
+ """
156
+ # Calculate instantaneous power
157
+ p_out_trace = instantaneous_power(v_out, i_out)
158
+ p_in_trace = instantaneous_power(v_in, i_in)
159
+
160
+ p_out_data = p_out_trace.data
161
+ p_in_data = p_in_trace.data[: len(p_out_data)]
162
+
163
+ # Sort by output power to get load curve
164
+ sort_idx = np.argsort(p_out_data)
165
+ p_out_sorted = p_out_data[sort_idx]
166
+ p_in_sorted = p_in_data[sort_idx]
167
+
168
+ # Divide into bins
169
+ bin_size = len(p_out_sorted) // n_points
170
+ bin_size = max(bin_size, 1)
171
+
172
+ load_pct = []
173
+ efficiency_vals = []
174
+ p_out_vals = []
175
+ p_in_vals = []
176
+
177
+ max_p_out = np.max(p_out_data)
178
+
179
+ for i in range(0, len(p_out_sorted), bin_size):
180
+ bin_p_out = np.mean(p_out_sorted[i : i + bin_size])
181
+ bin_p_in = np.mean(p_in_sorted[i : i + bin_size])
182
+
183
+ load_pct.append(bin_p_out / max_p_out * 100 if max_p_out > 0 else 0)
184
+ p_out_vals.append(bin_p_out)
185
+ p_in_vals.append(bin_p_in)
186
+ efficiency_vals.append(bin_p_out / bin_p_in if bin_p_in > 0 else 0)
187
+
188
+ return {
189
+ "load_percent": np.array(load_pct),
190
+ "efficiency": np.array(efficiency_vals),
191
+ "output_power": np.array(p_out_vals),
192
+ "input_power": np.array(p_in_vals),
193
+ }
194
+
195
+
196
+ def loss_breakdown(
197
+ v_in: WaveformTrace,
198
+ i_in: WaveformTrace,
199
+ v_out: WaveformTrace,
200
+ i_out: WaveformTrace,
201
+ *,
202
+ switching_loss: float = 0.0,
203
+ conduction_loss: float = 0.0,
204
+ magnetic_loss: float = 0.0,
205
+ gate_drive_loss: float = 0.0,
206
+ ) -> dict[str, float]:
207
+ """Break down power losses by category.
208
+
209
+ Args:
210
+ v_in: Input voltage trace.
211
+ i_in: Input current trace.
212
+ v_out: Output voltage trace.
213
+ i_out: Output current trace.
214
+ switching_loss: Known switching losses in Watts.
215
+ conduction_loss: Known conduction losses in Watts.
216
+ magnetic_loss: Known magnetic (core/copper) losses in Watts.
217
+ gate_drive_loss: Known gate drive losses in Watts.
218
+
219
+ Returns:
220
+ Dictionary with loss breakdown.
221
+
222
+ Example:
223
+ >>> result = loss_breakdown(v_in, i_in, v_out, i_out,
224
+ ... switching_loss=2.0, conduction_loss=1.5, magnetic_loss=0.5)
225
+ >>> print(f"Other losses: {result['other_loss']:.2f} W")
226
+ """
227
+ p_in = average_power(voltage=v_in, current=i_in)
228
+ p_out = average_power(voltage=v_out, current=i_out)
229
+ total_loss = p_in - p_out
230
+
231
+ known_losses = switching_loss + conduction_loss + magnetic_loss + gate_drive_loss
232
+ other_loss = total_loss - known_losses
233
+
234
+ eta = p_out / p_in if p_in > 0 else 0.0
235
+
236
+ return {
237
+ "input_power": p_in,
238
+ "output_power": p_out,
239
+ "efficiency": eta,
240
+ "total_loss": total_loss,
241
+ "switching_loss": switching_loss,
242
+ "conduction_loss": conduction_loss,
243
+ "magnetic_loss": magnetic_loss,
244
+ "gate_drive_loss": gate_drive_loss,
245
+ "other_loss": max(0, other_loss), # Clamp to non-negative
246
+ "switching_loss_percent": switching_loss / total_loss * 100 if total_loss > 0 else 0,
247
+ "conduction_loss_percent": conduction_loss / total_loss * 100 if total_loss > 0 else 0,
248
+ "magnetic_loss_percent": magnetic_loss / total_loss * 100 if total_loss > 0 else 0,
249
+ }
250
+
251
+
252
+ def thermal_efficiency(
253
+ p_in: float,
254
+ p_out: float,
255
+ ambient_temp: float,
256
+ case_temp: float,
257
+ thermal_resistance: float,
258
+ ) -> dict[str, float]:
259
+ """Calculate thermal-related efficiency metrics.
260
+
261
+ Args:
262
+ p_in: Input power in Watts.
263
+ p_out: Output power in Watts.
264
+ ambient_temp: Ambient temperature in Celsius.
265
+ case_temp: Case/junction temperature in Celsius.
266
+ thermal_resistance: Thermal resistance (Rth_j-a) in C/W.
267
+
268
+ Returns:
269
+ Dictionary with thermal analysis.
270
+
271
+ Example:
272
+ >>> result = thermal_efficiency(100, 90, 25, 65, 2.5)
273
+ >>> print(f"Estimated losses: {result['estimated_losses']:.1f} W")
274
+ """
275
+ losses = p_in - p_out
276
+ eta = p_out / p_in if p_in > 0 else 0.0
277
+
278
+ # Estimate losses from thermal measurement
279
+ thermal_losses = (case_temp - ambient_temp) / thermal_resistance
280
+
281
+ return {
282
+ "efficiency": eta,
283
+ "electrical_losses": losses,
284
+ "thermal_estimated_losses": thermal_losses,
285
+ "temperature_rise": case_temp - ambient_temp,
286
+ "loss_discrepancy": abs(losses - thermal_losses),
287
+ }
288
+
289
+
290
+ __all__ = [
291
+ "efficiency",
292
+ "efficiency_vs_load",
293
+ "loss_breakdown",
294
+ "multi_output_efficiency",
295
+ "power_conversion_efficiency",
296
+ "thermal_efficiency",
297
+ ]
@@ -0,0 +1,356 @@
1
+ """Ripple measurement for Oscura.
2
+
3
+ Provides AC ripple analysis for DC power supply outputs.
4
+
5
+
6
+ Example:
7
+ >>> from oscura.analyzers.power.ripple import ripple, ripple_statistics
8
+ >>> r_pp, r_rms = ripple(dc_output_trace)
9
+ >>> print(f"Ripple: {r_pp*1e3:.2f} mV pp, {r_rms*1e3:.2f} mV rms")
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import numpy as np
15
+ from scipy import signal
16
+
17
+ from oscura.core.exceptions import AnalysisError
18
+ from oscura.core.types import WaveformTrace
19
+
20
+
21
+ def ripple(
22
+ trace: WaveformTrace,
23
+ *,
24
+ dc_coupling: bool = False,
25
+ ) -> tuple[float, float]:
26
+ """Measure AC ripple on a DC signal.
27
+
28
+ Isolates the AC component from the DC offset and measures
29
+ peak-to-peak and RMS ripple.
30
+
31
+ Args:
32
+ trace: DC voltage/current waveform with AC ripple.
33
+ dc_coupling: If True, include DC component in measurement.
34
+ If False (default), remove DC for pure AC ripple.
35
+
36
+ Returns:
37
+ Tuple of (ripple_pp, ripple_rms) in signal units.
38
+
39
+ Example:
40
+ >>> vpp, vrms = ripple(output_voltage)
41
+ >>> print(f"Ripple: {vpp*1e3:.2f} mV pp, {vrms*1e3:.2f} mV rms")
42
+
43
+ References:
44
+ IEC 61000-4-7 (power quality)
45
+ """
46
+ data = trace.data
47
+
48
+ if dc_coupling:
49
+ ac_component = data
50
+ else:
51
+ # Remove DC (mean)
52
+ ac_component = data - np.mean(data)
53
+
54
+ ripple_pp = float(np.max(ac_component) - np.min(ac_component))
55
+ ripple_rms = float(np.sqrt(np.mean(ac_component**2)))
56
+
57
+ return ripple_pp, ripple_rms
58
+
59
+
60
+ def ripple_percentage(
61
+ trace: WaveformTrace,
62
+ ) -> tuple[float, float]:
63
+ """Measure ripple as percentage of DC level.
64
+
65
+ Args:
66
+ trace: DC voltage/current waveform with AC ripple.
67
+
68
+ Returns:
69
+ Tuple of (ripple_pp_percent, ripple_rms_percent).
70
+
71
+ Example:
72
+ >>> pp_pct, rms_pct = ripple_percentage(output_voltage)
73
+ >>> print(f"Ripple: {pp_pct:.2f}% pp, {rms_pct:.2f}% rms")
74
+ """
75
+ dc_level = float(np.mean(trace.data))
76
+
77
+ if dc_level == 0:
78
+ return np.nan, np.nan
79
+
80
+ r_pp, r_rms = ripple(trace)
81
+
82
+ return (r_pp / dc_level * 100, r_rms / dc_level * 100)
83
+
84
+
85
+ def ripple_frequency(
86
+ trace: WaveformTrace,
87
+ *,
88
+ min_frequency: float | None = None,
89
+ max_frequency: float | None = None,
90
+ ) -> float:
91
+ """Find dominant ripple frequency.
92
+
93
+ Args:
94
+ trace: DC voltage waveform with AC ripple.
95
+ min_frequency: Minimum frequency to consider (Hz).
96
+ max_frequency: Maximum frequency to consider (Hz).
97
+
98
+ Returns:
99
+ Dominant ripple frequency in Hz.
100
+
101
+ Example:
102
+ >>> f_ripple = ripple_frequency(output_voltage)
103
+ >>> print(f"Ripple frequency: {f_ripple/1e3:.2f} kHz")
104
+ """
105
+ data = trace.data
106
+ sample_rate = trace.metadata.sample_rate
107
+
108
+ # Remove DC
109
+ ac_data = data - np.mean(data)
110
+
111
+ # FFT
112
+ n = len(ac_data)
113
+ fft_result = np.abs(np.fft.rfft(ac_data))
114
+ freqs = np.fft.rfftfreq(n, 1 / sample_rate)
115
+
116
+ # Apply frequency limits
117
+ freq_mask = np.ones(len(freqs), dtype=bool)
118
+ if min_frequency is not None:
119
+ freq_mask &= freqs >= min_frequency
120
+ if max_frequency is not None:
121
+ freq_mask &= freqs <= max_frequency
122
+
123
+ # Exclude DC
124
+ freq_mask[0] = False
125
+
126
+ if not np.any(freq_mask):
127
+ return 0.0
128
+
129
+ # Find peak
130
+ masked_fft = fft_result.copy()
131
+ masked_fft[~freq_mask] = 0
132
+ peak_idx = np.argmax(masked_fft)
133
+
134
+ return float(freqs[peak_idx])
135
+
136
+
137
+ def ripple_harmonics(
138
+ trace: WaveformTrace,
139
+ fundamental_freq: float | None = None,
140
+ n_harmonics: int = 10,
141
+ ) -> dict[int, float]:
142
+ """Analyze ripple harmonics.
143
+
144
+ Args:
145
+ trace: DC voltage waveform with AC ripple.
146
+ fundamental_freq: Fundamental ripple frequency. If None, auto-detect.
147
+ n_harmonics: Number of harmonics to analyze.
148
+
149
+ Returns:
150
+ Dictionary mapping harmonic number to amplitude.
151
+
152
+ Example:
153
+ >>> harmonics = ripple_harmonics(output_voltage)
154
+ >>> for h, amp in harmonics.items():
155
+ ... print(f"H{h}: {amp*1e3:.2f} mV")
156
+ """
157
+ data = trace.data
158
+ sample_rate = trace.metadata.sample_rate
159
+
160
+ # Remove DC
161
+ ac_data = data - np.mean(data)
162
+
163
+ # Find fundamental if not provided
164
+ if fundamental_freq is None:
165
+ fundamental_freq = ripple_frequency(trace)
166
+
167
+ if fundamental_freq <= 0:
168
+ return {}
169
+
170
+ # FFT
171
+ n = len(ac_data)
172
+ fft_result = np.abs(np.fft.rfft(ac_data)) * 2 / n # Scale for amplitude
173
+ freqs = np.fft.rfftfreq(n, 1 / sample_rate)
174
+
175
+ harmonics = {}
176
+ for h in range(1, n_harmonics + 1):
177
+ target_freq = h * fundamental_freq
178
+ # Find closest bin
179
+ idx = np.argmin(np.abs(freqs - target_freq))
180
+ if idx < len(fft_result):
181
+ harmonics[h] = float(fft_result[idx])
182
+
183
+ return harmonics
184
+
185
+
186
+ def ripple_statistics(
187
+ trace: WaveformTrace,
188
+ ) -> dict[str, float]:
189
+ """Calculate comprehensive ripple statistics.
190
+
191
+ Args:
192
+ trace: DC voltage waveform with AC ripple.
193
+
194
+ Returns:
195
+ Dictionary with:
196
+ - dc_level: DC (mean) level
197
+ - ripple_pp: Peak-to-peak ripple
198
+ - ripple_rms: RMS ripple
199
+ - ripple_pp_percent: Peak-to-peak as % of DC
200
+ - ripple_rms_percent: RMS as % of DC
201
+ - ripple_frequency: Dominant ripple frequency
202
+ - crest_factor: Ripple peak / ripple RMS
203
+
204
+ Example:
205
+ >>> stats = ripple_statistics(output_voltage)
206
+ >>> print(f"DC: {stats['dc_level']:.2f} V")
207
+ >>> print(f"Ripple: {stats['ripple_pp']*1e3:.2f} mV pp")
208
+ """
209
+ data = trace.data
210
+ dc_level = float(np.mean(data))
211
+ ac_data = data - dc_level
212
+
213
+ r_pp = float(np.max(ac_data) - np.min(ac_data))
214
+ r_rms = float(np.sqrt(np.mean(ac_data**2)))
215
+ r_peak = float(np.max(np.abs(ac_data)))
216
+
217
+ crest_factor = r_peak / r_rms if r_rms > 0 else 0.0
218
+
219
+ if dc_level != 0:
220
+ r_pp_pct = r_pp / dc_level * 100
221
+ r_rms_pct = r_rms / dc_level * 100
222
+ else:
223
+ r_pp_pct = np.nan
224
+ r_rms_pct = np.nan
225
+
226
+ return {
227
+ "dc_level": dc_level,
228
+ "ripple_pp": r_pp,
229
+ "ripple_rms": r_rms,
230
+ "ripple_pp_percent": r_pp_pct,
231
+ "ripple_rms_percent": r_rms_pct,
232
+ "ripple_frequency": ripple_frequency(trace),
233
+ "crest_factor": crest_factor,
234
+ }
235
+
236
+
237
+ def extract_ripple(
238
+ trace: WaveformTrace,
239
+ *,
240
+ high_pass_freq: float | None = None,
241
+ filter_order: int = 4,
242
+ ) -> WaveformTrace:
243
+ """Extract AC ripple component from DC signal.
244
+
245
+ Args:
246
+ trace: DC voltage waveform with AC ripple.
247
+ high_pass_freq: High-pass filter cutoff. If None, uses simple DC removal.
248
+ filter_order: Order of the Butterworth high-pass filter (default: 4).
249
+ Higher orders give sharper cutoff but more phase distortion.
250
+
251
+ Returns:
252
+ Waveform trace containing only the AC ripple component.
253
+
254
+ Raises:
255
+ AnalysisError: If high_pass_freq exceeds Nyquist frequency
256
+
257
+ Example:
258
+ >>> ac_ripple = extract_ripple(output_voltage)
259
+ >>> # Now analyze or plot just the ripple
260
+ >>> # With custom filter order for sharper cutoff:
261
+ >>> ac_ripple = extract_ripple(output_voltage, high_pass_freq=10, filter_order=6)
262
+ """
263
+ data = trace.data
264
+ sample_rate = trace.metadata.sample_rate
265
+
266
+ if high_pass_freq is not None:
267
+ # Use high-pass filter
268
+ nyquist = sample_rate / 2
269
+ if high_pass_freq >= nyquist:
270
+ raise AnalysisError(
271
+ f"High-pass frequency {high_pass_freq} Hz must be less than "
272
+ f"Nyquist frequency {nyquist} Hz"
273
+ )
274
+
275
+ sos = signal.butter(filter_order, high_pass_freq / nyquist, btype="high", output="sos")
276
+ ac_data = signal.sosfiltfilt(sos, data)
277
+ else:
278
+ # Simple DC removal
279
+ ac_data = data - np.mean(data)
280
+
281
+ return WaveformTrace(
282
+ data=ac_data.astype(np.float64),
283
+ metadata=trace.metadata,
284
+ )
285
+
286
+
287
+ def ripple_envelope(
288
+ trace: WaveformTrace,
289
+ *,
290
+ method: str = "hilbert",
291
+ peak_window_size: int | None = None,
292
+ ) -> WaveformTrace:
293
+ """Extract ripple envelope (for amplitude modulation analysis).
294
+
295
+ Args:
296
+ trace: DC voltage waveform with AC ripple.
297
+ method: Envelope detection method ("hilbert" or "peak").
298
+ peak_window_size: Window size for peak envelope detection (samples).
299
+ If None, defaults to a size that covers approximately one ripple period.
300
+ Only used when method="peak".
301
+
302
+ Returns:
303
+ Waveform trace containing the ripple envelope.
304
+
305
+ Raises:
306
+ AnalysisError: If unknown envelope method specified
307
+
308
+ Example:
309
+ >>> envelope = ripple_envelope(output_voltage)
310
+ >>> # Analyze envelope for beat frequencies, etc.
311
+ >>> # With custom peak window size:
312
+ >>> envelope = ripple_envelope(output_voltage, method="peak", peak_window_size=200)
313
+ """
314
+ # First extract AC component
315
+ ac_trace = extract_ripple(trace)
316
+ ac_data = ac_trace.data
317
+
318
+ if method == "hilbert":
319
+ analytic_signal = signal.hilbert(ac_data)
320
+ envelope = np.abs(analytic_signal)
321
+ elif method == "peak":
322
+ # Simple peak detection
323
+ from scipy.ndimage import maximum_filter1d
324
+
325
+ # Determine window size
326
+ if peak_window_size is None:
327
+ # Default: scale to signal frequency if possible
328
+ # Try to detect ripple frequency and use ~1 period
329
+ ripple_freq = ripple_frequency(trace)
330
+ sample_rate = trace.metadata.sample_rate
331
+ if ripple_freq > 0:
332
+ # Use approximately one period of the ripple
333
+ peak_window_size = max(10, int(sample_rate / ripple_freq))
334
+ else:
335
+ # Fallback: use 1% of signal length, min 10, max 1000
336
+ peak_window_size = max(10, min(1000, len(ac_data) // 100))
337
+
338
+ envelope = maximum_filter1d(np.abs(ac_data), size=peak_window_size)
339
+ else:
340
+ raise AnalysisError(f"Unknown envelope method: {method}")
341
+
342
+ return WaveformTrace(
343
+ data=envelope.astype(np.float64),
344
+ metadata=trace.metadata,
345
+ )
346
+
347
+
348
+ __all__ = [
349
+ "extract_ripple",
350
+ "ripple",
351
+ "ripple_envelope",
352
+ "ripple_frequency",
353
+ "ripple_harmonics",
354
+ "ripple_percentage",
355
+ "ripple_statistics",
356
+ ]