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,368 @@
1
+ """Idle and padding detection and removal.
2
+
3
+ This module provides functions to detect and optionally remove idle regions,
4
+ padding, and non-data samples from loaded binary captures.
5
+
6
+
7
+ Example:
8
+ >>> from oscura.loaders.preprocessing import detect_idle_regions, trim_idle
9
+ >>> regions = detect_idle_regions(trace, pattern='zeros', min_duration=100)
10
+ >>> print(f"Found {len(regions)} idle regions")
11
+ >>> trimmed_trace = trim_idle(trace, trim_start=True, trim_end=True)
12
+ >>> print(f"Trimmed {len(trace.data) - len(trimmed_trace.data)} samples")
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import logging
18
+ from dataclasses import dataclass
19
+ from typing import TYPE_CHECKING
20
+
21
+ import numpy as np
22
+
23
+ from oscura.core.types import DigitalTrace, TraceMetadata
24
+
25
+ if TYPE_CHECKING:
26
+ from numpy.typing import NDArray
27
+
28
+ # Logger for debug output
29
+ logger = logging.getLogger(__name__)
30
+
31
+
32
+ @dataclass
33
+ class IdleRegion:
34
+ """Idle region in a trace.
35
+
36
+
37
+
38
+ Attributes:
39
+ start: Start sample index.
40
+ end: End sample index (exclusive).
41
+ pattern: Detected idle pattern.
42
+ duration_samples: Duration in samples.
43
+ """
44
+
45
+ start: int
46
+ end: int
47
+ pattern: str
48
+ duration_samples: int
49
+
50
+ @property
51
+ def length(self) -> int:
52
+ """Get region length in samples.
53
+
54
+ Returns:
55
+ Number of samples in region.
56
+ """
57
+ return self.end - self.start
58
+
59
+ def get_duration_seconds(self, sample_rate: float) -> float:
60
+ """Get region duration in seconds.
61
+
62
+ Args:
63
+ sample_rate: Sample rate in Hz.
64
+
65
+ Returns:
66
+ Duration in seconds.
67
+ """
68
+ return self.length / sample_rate
69
+
70
+
71
+ @dataclass
72
+ class IdleStatistics:
73
+ """Statistics about idle regions in a trace.
74
+
75
+
76
+
77
+ Attributes:
78
+ total_samples: Total number of samples in trace.
79
+ idle_samples: Total number of idle samples.
80
+ active_samples: Total number of active samples.
81
+ idle_regions: List of idle regions.
82
+ dominant_pattern: Most common idle pattern.
83
+ """
84
+
85
+ total_samples: int
86
+ idle_samples: int
87
+ active_samples: int
88
+ idle_regions: list[IdleRegion]
89
+ dominant_pattern: str
90
+
91
+ @property
92
+ def idle_fraction(self) -> float:
93
+ """Fraction of trace that is idle.
94
+
95
+ Returns:
96
+ Idle fraction (0.0 to 1.0).
97
+ """
98
+ if self.total_samples == 0:
99
+ return 0.0
100
+ return self.idle_samples / self.total_samples
101
+
102
+ @property
103
+ def active_fraction(self) -> float:
104
+ """Fraction of trace that is active.
105
+
106
+ Returns:
107
+ Active fraction (0.0 to 1.0).
108
+ """
109
+ return 1.0 - self.idle_fraction
110
+
111
+
112
+ def detect_idle_regions(
113
+ trace: DigitalTrace,
114
+ pattern: str = "auto",
115
+ min_duration: int = 100,
116
+ ) -> list[IdleRegion]:
117
+ """Detect idle regions in a digital trace.
118
+
119
+
120
+
121
+ Identifies regions where the signal is idle (constant pattern) for
122
+ a minimum duration. Supports auto-detection and explicit patterns.
123
+
124
+ Args:
125
+ trace: Digital trace to analyze.
126
+ pattern: Idle pattern to detect ("auto", "zeros", "ones", or byte value).
127
+ min_duration: Minimum duration in samples to consider as idle.
128
+
129
+ Returns:
130
+ List of detected idle regions.
131
+
132
+ Example:
133
+ >>> regions = detect_idle_regions(trace, pattern='zeros', min_duration=100)
134
+ >>> for region in regions:
135
+ ... print(f"Idle from {region.start} to {region.end}")
136
+ """
137
+ data = trace.data
138
+
139
+ if len(data) < min_duration:
140
+ return []
141
+
142
+ idle_regions: list[IdleRegion] = []
143
+
144
+ if pattern == "auto":
145
+ # Auto-detect pattern from start/end of trace
146
+ pattern = _auto_detect_pattern(data)
147
+ logger.debug("Auto-detected idle pattern: %s", pattern)
148
+
149
+ # Detect idle runs
150
+ if pattern == "zeros":
151
+ idle_mask = ~data # Invert: True where data is False (zero)
152
+ elif pattern == "ones":
153
+ idle_mask = data # True where data is True (one)
154
+ else:
155
+ # For specific byte values, would need multi-bit comparison
156
+ # For now, default to zeros
157
+ logger.warning("Pattern '%s' not fully supported, using zeros", pattern)
158
+ idle_mask = ~data
159
+
160
+ # Find runs of idle samples
161
+ # Pad mask to detect transitions at boundaries
162
+ padded = np.concatenate(([False], idle_mask, [False]))
163
+ transitions = np.diff(padded.astype(np.int8))
164
+
165
+ # Rising edges (start of idle region)
166
+ starts = np.where(transitions == 1)[0]
167
+ # Falling edges (end of idle region)
168
+ ends = np.where(transitions == -1)[0]
169
+
170
+ # Filter by minimum duration
171
+ for start, end in zip(starts, ends, strict=False):
172
+ duration = end - start
173
+ if duration >= min_duration:
174
+ idle_regions.append(
175
+ IdleRegion(
176
+ start=int(start),
177
+ end=int(end),
178
+ pattern=pattern,
179
+ duration_samples=int(duration),
180
+ )
181
+ )
182
+
183
+ logger.info(
184
+ "Detected %d idle regions (pattern: %s, min_duration: %d)",
185
+ len(idle_regions),
186
+ pattern,
187
+ min_duration,
188
+ )
189
+
190
+ return idle_regions
191
+
192
+
193
+ def _auto_detect_pattern(data: NDArray[np.bool_]) -> str:
194
+ """Auto-detect idle pattern from trace data.
195
+
196
+ Looks at the start and end of the trace to determine the
197
+ most likely idle pattern.
198
+
199
+ Args:
200
+ data: Boolean trace data.
201
+
202
+ Returns:
203
+ Detected pattern ("zeros", "ones", or "unknown").
204
+ """
205
+ if len(data) == 0:
206
+ return "zeros"
207
+
208
+ # Check first and last 100 samples (or 10% of trace, whichever is smaller)
209
+ check_len = min(100, len(data) // 10, len(data))
210
+
211
+ if check_len == 0:
212
+ return "zeros"
213
+
214
+ start_samples = data[:check_len]
215
+ end_samples = data[-check_len:]
216
+
217
+ # Count zeros in start/end regions
218
+ start_zeros = np.sum(~start_samples)
219
+ end_zeros = np.sum(~end_samples)
220
+
221
+ # If majority are zeros, pattern is zeros
222
+ if start_zeros > check_len // 2 or end_zeros > check_len // 2:
223
+ return "zeros"
224
+
225
+ # If majority are ones, pattern is ones
226
+ if start_zeros < check_len // 4 and end_zeros < check_len // 4:
227
+ return "ones"
228
+
229
+ # Default to zeros
230
+ return "zeros"
231
+
232
+
233
+ def trim_idle(
234
+ trace: DigitalTrace,
235
+ trim_start: bool = True,
236
+ trim_end: bool = True,
237
+ pattern: str = "auto",
238
+ min_duration: int = 100,
239
+ ) -> DigitalTrace:
240
+ """Trim idle regions from trace.
241
+
242
+
243
+
244
+ Removes idle regions from the start and/or end of a trace.
245
+
246
+ Args:
247
+ trace: Digital trace to trim.
248
+ trim_start: Remove idle from start of trace.
249
+ trim_end: Remove idle from end of trace.
250
+ pattern: Idle pattern to detect ("auto", "zeros", "ones").
251
+ min_duration: Minimum idle duration to trim.
252
+
253
+ Returns:
254
+ New DigitalTrace with idle regions removed.
255
+
256
+ Example:
257
+ >>> trimmed = trim_idle(trace, trim_start=True, trim_end=True)
258
+ >>> print(f"Removed {len(trace.data) - len(trimmed.data)} idle samples")
259
+ """
260
+ if len(trace.data) == 0:
261
+ return trace
262
+
263
+ # Detect idle regions
264
+ idle_regions = detect_idle_regions(trace, pattern=pattern, min_duration=min_duration)
265
+
266
+ if not idle_regions:
267
+ return trace
268
+
269
+ # Find start and end trim points
270
+ start_idx = 0
271
+ end_idx = len(trace.data)
272
+
273
+ if trim_start and idle_regions:
274
+ # Check if first region starts at beginning
275
+ first_region = idle_regions[0]
276
+ if first_region.start == 0:
277
+ start_idx = first_region.end
278
+ logger.info("Trimming %d idle samples from start", first_region.length)
279
+
280
+ if trim_end and idle_regions:
281
+ # Check if last region ends at end
282
+ last_region = idle_regions[-1]
283
+ if last_region.end == len(trace.data):
284
+ end_idx = last_region.start
285
+ logger.info("Trimming %d idle samples from end", last_region.length)
286
+
287
+ # Create trimmed trace
288
+ if start_idx > 0 or end_idx < len(trace.data):
289
+ trimmed_data = trace.data[start_idx:end_idx]
290
+
291
+ # Preserve metadata
292
+ new_metadata = TraceMetadata(
293
+ sample_rate=trace.metadata.sample_rate,
294
+ vertical_scale=trace.metadata.vertical_scale,
295
+ vertical_offset=trace.metadata.vertical_offset,
296
+ acquisition_time=trace.metadata.acquisition_time,
297
+ trigger_info=trace.metadata.trigger_info,
298
+ source_file=trace.metadata.source_file,
299
+ channel_name=trace.metadata.channel_name,
300
+ )
301
+
302
+ return DigitalTrace(data=trimmed_data, metadata=new_metadata, edges=None)
303
+
304
+ return trace
305
+
306
+
307
+ def get_idle_statistics(
308
+ trace: DigitalTrace,
309
+ pattern: str = "auto",
310
+ min_duration: int = 100,
311
+ ) -> IdleStatistics:
312
+ """Get statistics about idle regions in trace.
313
+
314
+
315
+
316
+ Computes comprehensive statistics about idle vs. active samples.
317
+
318
+ Args:
319
+ trace: Digital trace to analyze.
320
+ pattern: Idle pattern to detect ("auto", "zeros", "ones").
321
+ min_duration: Minimum idle duration to count.
322
+
323
+ Returns:
324
+ IdleStatistics with analysis results.
325
+
326
+ Example:
327
+ >>> stats = get_idle_statistics(trace)
328
+ >>> print(f"Idle fraction: {stats.idle_fraction:.1%}")
329
+ >>> print(f"Found {len(stats.idle_regions)} idle regions")
330
+ """
331
+ idle_regions = detect_idle_regions(trace, pattern=pattern, min_duration=min_duration)
332
+
333
+ total_samples = len(trace.data)
334
+ idle_samples = sum(region.length for region in idle_regions)
335
+ active_samples = total_samples - idle_samples
336
+
337
+ # Determine dominant pattern
338
+ if idle_regions:
339
+ # Count pattern occurrences
340
+ pattern_counts: dict[str, int] = {}
341
+ for region in idle_regions:
342
+ pattern_counts[region.pattern] = pattern_counts.get(region.pattern, 0) + region.length
343
+
344
+ dominant_pattern = max(pattern_counts, key=pattern_counts.get) # type: ignore[arg-type]
345
+ else:
346
+ dominant_pattern = "none"
347
+
348
+ return IdleStatistics(
349
+ total_samples=total_samples,
350
+ idle_samples=idle_samples,
351
+ active_samples=active_samples,
352
+ idle_regions=idle_regions,
353
+ dominant_pattern=dominant_pattern,
354
+ )
355
+
356
+
357
+ # Type alias for backward compatibility
358
+ IdleStats = IdleStatistics
359
+ """Type alias for IdleStatistics."""
360
+
361
+ __all__ = [
362
+ "IdleRegion",
363
+ "IdleStatistics",
364
+ "IdleStats",
365
+ "detect_idle_regions",
366
+ "get_idle_statistics",
367
+ "trim_idle",
368
+ ]
@@ -0,0 +1,287 @@
1
+ """Rigol WFM file loader.
2
+
3
+ This module provides loading of Rigol oscilloscope .wfm files
4
+ using the RigolWFM library when available.
5
+
6
+
7
+ Example:
8
+ >>> from oscura.loaders.rigol import load_rigol_wfm
9
+ >>> trace = load_rigol_wfm("DS1054Z_001.wfm")
10
+ >>> print(f"Sample rate: {trace.metadata.sample_rate} Hz")
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from pathlib import Path
16
+ from typing import TYPE_CHECKING, Any
17
+
18
+ import numpy as np
19
+
20
+ from oscura.core.exceptions import FormatError, LoaderError
21
+ from oscura.core.types import TraceMetadata, WaveformTrace
22
+
23
+ if TYPE_CHECKING:
24
+ from os import PathLike
25
+
26
+ # Try to import RigolWFM for full Rigol support
27
+ try:
28
+ import RigolWFM.wfm as rigol_wfm # type: ignore[import-not-found, import-untyped]
29
+
30
+ RIGOL_WFM_AVAILABLE = True
31
+ except ImportError:
32
+ RIGOL_WFM_AVAILABLE = False
33
+
34
+
35
+ def load_rigol_wfm(
36
+ path: str | PathLike[str],
37
+ *,
38
+ channel: int = 0,
39
+ ) -> WaveformTrace:
40
+ """Load a Rigol oscilloscope WFM file.
41
+
42
+ Extracts waveform data and metadata from Rigol .wfm files.
43
+ Uses the RigolWFM library when available for full support.
44
+
45
+ Args:
46
+ path: Path to the Rigol .wfm file.
47
+ channel: Channel index for multi-channel files (default: 0).
48
+
49
+ Returns:
50
+ WaveformTrace containing the waveform data and metadata.
51
+
52
+ Raises:
53
+ LoaderError: If the file cannot be loaded or does not exist.
54
+
55
+ Example:
56
+ >>> trace = load_rigol_wfm("DS1054Z_001.wfm")
57
+ >>> print(f"Sample rate: {trace.metadata.sample_rate} Hz")
58
+ >>> print(f"Vertical scale: {trace.metadata.vertical_scale} V/div")
59
+ """
60
+ path = Path(path)
61
+
62
+ if not path.exists():
63
+ raise LoaderError(
64
+ "File not found",
65
+ file_path=str(path),
66
+ )
67
+
68
+ # Try RigolWFM first for full metadata, fall back to basic loader
69
+ if RIGOL_WFM_AVAILABLE:
70
+ try:
71
+ return _load_with_rigolwfm(path, channel=channel)
72
+ except Exception:
73
+ # RigolWFM failed (likely synthetic/malformed file)
74
+ # Force garbage collection to close any leaked file handles
75
+ import gc
76
+
77
+ gc.collect()
78
+ # Try basic loader as fallback
79
+ return _load_basic(path, channel=channel)
80
+ else:
81
+ return _load_basic(path, channel=channel)
82
+
83
+
84
+ def _load_with_rigolwfm(
85
+ path: Path,
86
+ *,
87
+ channel: int = 0,
88
+ ) -> WaveformTrace:
89
+ """Load Rigol WFM using RigolWFM library.
90
+
91
+ Args:
92
+ path: Path to the WFM file.
93
+ channel: Channel index.
94
+
95
+ Returns:
96
+ WaveformTrace with full metadata.
97
+
98
+ Raises:
99
+ FormatError: If no waveform data is found in the file.
100
+ LoaderError: If the file cannot be loaded.
101
+ """
102
+ try:
103
+ # Try to auto-detect model from filename (e.g., DS1054Z)
104
+ model = None
105
+ filename_upper = path.name.upper()
106
+ if "DS1" in filename_upper or "MSO1" in filename_upper or "DHO" in filename_upper:
107
+ if "Z" in filename_upper or "MSO" in filename_upper or "DHO" in filename_upper:
108
+ model = "Z"
109
+ elif "E" in filename_upper:
110
+ model = "E"
111
+
112
+ # Try model detection, fallback to trying both models
113
+ last_error = None
114
+ for try_model in [model] if model else ["Z", "E"]:
115
+ try:
116
+ wfm = rigol_wfm.Wfm.from_file(str(path), model=try_model)
117
+ break
118
+ except Exception as e:
119
+ last_error = e
120
+ continue
121
+ else:
122
+ # None of the models worked
123
+ raise last_error if last_error else RuntimeError("Failed to load WFM file")
124
+
125
+ # Get channel data
126
+ if hasattr(wfm, "channels") and len(wfm.channels) > channel:
127
+ ch = wfm.channels[channel]
128
+ data = np.array(ch.volts, dtype=np.float64)
129
+ sample_rate = wfm.sample_rate if hasattr(wfm, "sample_rate") else 1e6
130
+ vertical_scale = ch.volts_per_div if hasattr(ch, "volts_per_div") else None
131
+ vertical_offset = ch.volt_offset if hasattr(ch, "volt_offset") else None
132
+ channel_name = f"CH{channel + 1}"
133
+ elif hasattr(wfm, "volts"):
134
+ # Single channel format
135
+ data = np.array(wfm.volts, dtype=np.float64)
136
+ sample_rate = wfm.sample_rate if hasattr(wfm, "sample_rate") else 1e6
137
+ vertical_scale = wfm.volts_per_div if hasattr(wfm, "volts_per_div") else None
138
+ vertical_offset = wfm.volt_offset if hasattr(wfm, "volt_offset") else None
139
+ channel_name = "CH1"
140
+ else:
141
+ raise FormatError(
142
+ "No waveform data found in Rigol file",
143
+ file_path=str(path),
144
+ expected="Rigol channel data",
145
+ )
146
+
147
+ # Build metadata
148
+ metadata = TraceMetadata(
149
+ sample_rate=sample_rate,
150
+ vertical_scale=vertical_scale,
151
+ vertical_offset=vertical_offset,
152
+ source_file=str(path),
153
+ channel_name=channel_name,
154
+ trigger_info=_extract_trigger_info(wfm),
155
+ )
156
+
157
+ return WaveformTrace(data=data, metadata=metadata)
158
+
159
+ except Exception as e:
160
+ # Re-raise FormatError as-is for tests that expect it
161
+ # All other exceptions (including kaitaistruct errors) get wrapped
162
+ if isinstance(e, FormatError):
163
+ raise
164
+ # Wrap other exceptions in LoaderError
165
+ # The outer load_rigol_wfm() will catch LoaderError and fall back to basic loader
166
+ raise LoaderError(
167
+ "Failed to load Rigol WFM file with RigolWFM library",
168
+ file_path=str(path),
169
+ details=str(e),
170
+ fix_hint="File may be malformed or incompatible with RigolWFM library.",
171
+ ) from e
172
+
173
+
174
+ def _load_basic(
175
+ path: Path,
176
+ *,
177
+ channel: int = 0,
178
+ ) -> WaveformTrace:
179
+ """Basic Rigol WFM loader without RigolWFM library.
180
+
181
+ This is a simplified loader that reads basic waveform data
182
+ from Rigol WFM files. For full feature support, install RigolWFM.
183
+
184
+ Args:
185
+ path: Path to the WFM file.
186
+ channel: Channel index (ignored in basic mode).
187
+
188
+ Returns:
189
+ WaveformTrace with basic metadata.
190
+
191
+ Raises:
192
+ FormatError: If the file is too small or has no waveform data.
193
+ LoaderError: If the file cannot be read or parsed.
194
+ """
195
+ try:
196
+ with open(path, "rb") as f:
197
+ # Read header
198
+ header = f.read(256)
199
+
200
+ # Basic validation
201
+ if len(header) < 256:
202
+ raise FormatError(
203
+ "File too small to be a valid Rigol WFM",
204
+ file_path=str(path),
205
+ expected="At least 256 bytes header",
206
+ got=f"{len(header)} bytes",
207
+ )
208
+
209
+ # Default values
210
+ sample_rate = 1e6 # Default 1 MSa/s
211
+ vertical_scale = None
212
+ vertical_offset = None
213
+
214
+ # Read waveform data
215
+ f.seek(0, 2)
216
+ file_size = f.tell()
217
+ data_size = file_size - 256
218
+
219
+ if data_size <= 0:
220
+ raise FormatError(
221
+ "No waveform data in file",
222
+ file_path=str(path),
223
+ )
224
+
225
+ f.seek(256)
226
+ raw_data = f.read(data_size)
227
+
228
+ # Rigol typically uses int16 or int8 for samples
229
+ try:
230
+ # Try int16 first (common in Rigol files)
231
+ data = np.frombuffer(raw_data, dtype=np.int16).astype(np.float64)
232
+ data = data / 32768.0 # Normalize to -1 to 1
233
+ except ValueError:
234
+ # Fall back to int8
235
+ data = np.frombuffer(raw_data, dtype=np.int8).astype(np.float64)
236
+ data = data / 128.0 # Normalize to -1 to 1
237
+
238
+ # Build metadata
239
+ metadata = TraceMetadata(
240
+ sample_rate=sample_rate,
241
+ vertical_scale=vertical_scale,
242
+ vertical_offset=vertical_offset,
243
+ source_file=str(path),
244
+ channel_name=f"CH{channel + 1}",
245
+ )
246
+
247
+ return WaveformTrace(data=data, metadata=metadata)
248
+
249
+ except OSError as e:
250
+ raise LoaderError(
251
+ "Failed to read Rigol WFM file",
252
+ file_path=str(path),
253
+ details=str(e),
254
+ ) from e
255
+ except Exception as e:
256
+ if isinstance(e, LoaderError | FormatError):
257
+ raise
258
+ raise LoaderError(
259
+ "Failed to parse Rigol WFM file",
260
+ file_path=str(path),
261
+ details=str(e),
262
+ fix_hint="Install RigolWFM for full Rigol support: pip install RigolWFM",
263
+ ) from e
264
+
265
+
266
+ def _extract_trigger_info(wfm: Any) -> dict[str, Any] | None:
267
+ """Extract trigger information from Rigol waveform object.
268
+
269
+ Args:
270
+ wfm: Rigol waveform object from RigolWFM.
271
+
272
+ Returns:
273
+ Dictionary of trigger settings or None.
274
+ """
275
+ trigger_info: dict[str, Any] = {}
276
+
277
+ if hasattr(wfm, "trigger_level"):
278
+ trigger_info["level"] = wfm.trigger_level
279
+ if hasattr(wfm, "trigger_mode"):
280
+ trigger_info["mode"] = wfm.trigger_mode
281
+ if hasattr(wfm, "trigger_source"):
282
+ trigger_info["source"] = wfm.trigger_source
283
+
284
+ return trigger_info if trigger_info else None
285
+
286
+
287
+ __all__ = ["load_rigol_wfm"]