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,351 @@
1
+ """Time-aware X-axis formatting and optimization.
2
+
3
+ This module provides intelligent time axis formatting with automatic unit
4
+ selection, relative time offsets, and cursor readout with full precision.
5
+
6
+
7
+ Example:
8
+ >>> from oscura.visualization.time_axis import format_time_axis
9
+ >>> labels = format_time_axis(time_values, unit="auto")
10
+
11
+ References:
12
+ - SI prefixes for time units
13
+ - IEEE publication time axis standards
14
+ - Matplotlib formatter customization
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from typing import TYPE_CHECKING, Literal
20
+
21
+ import numpy as np
22
+
23
+ if TYPE_CHECKING:
24
+ from numpy.typing import NDArray
25
+
26
+ TimeUnit = Literal["s", "ms", "us", "ns", "ps", "auto"]
27
+
28
+
29
+ def select_time_unit(
30
+ time_range: float,
31
+ *,
32
+ prefer_larger: bool = False,
33
+ ) -> TimeUnit:
34
+ """Automatically select appropriate time unit based on range.
35
+
36
+ Args:
37
+ time_range: Time range in seconds.
38
+ prefer_larger: Prefer larger units when ambiguous.
39
+
40
+ Returns:
41
+ Selected time unit ("s", "ms", "us", "ns", "ps").
42
+
43
+ Example:
44
+ >>> select_time_unit(0.001) # 1 ms
45
+ 'ms'
46
+ >>> select_time_unit(1e-6) # 1 us
47
+ 'us'
48
+
49
+ References:
50
+ VIS-014: Adaptive X-Axis Time Window
51
+ """
52
+ if time_range >= 1.0:
53
+ return "s"
54
+ elif time_range >= 1e-3:
55
+ return "ms" if not prefer_larger else "s"
56
+ elif time_range >= 1e-6:
57
+ return "us" if not prefer_larger else "ms"
58
+ elif time_range >= 1e-9:
59
+ return "ns" if not prefer_larger else "us"
60
+ else:
61
+ return "ps" if not prefer_larger else "ns"
62
+
63
+
64
+ def convert_time_values(
65
+ time: NDArray[np.float64],
66
+ unit: TimeUnit,
67
+ ) -> NDArray[np.float64]:
68
+ """Convert time values to specified unit.
69
+
70
+ Args:
71
+ time: Time array in seconds.
72
+ unit: Target time unit.
73
+
74
+ Returns:
75
+ Time array in target unit.
76
+
77
+ Raises:
78
+ ValueError: If unit is invalid.
79
+
80
+ Example:
81
+ >>> time_s = np.array([0.001, 0.002, 0.003])
82
+ >>> time_ms = convert_time_values(time_s, "ms")
83
+ >>> # Returns [1.0, 2.0, 3.0]
84
+
85
+ References:
86
+ VIS-014: Adaptive X-Axis Time Window
87
+ """
88
+ multipliers = {
89
+ "s": 1.0,
90
+ "ms": 1e3,
91
+ "us": 1e6,
92
+ "ns": 1e9,
93
+ "ps": 1e12,
94
+ }
95
+
96
+ if unit == "auto":
97
+ time_range = float(np.ptp(time))
98
+ unit = select_time_unit(time_range)
99
+
100
+ if unit not in multipliers:
101
+ raise ValueError(f"Invalid time unit: {unit}")
102
+
103
+ return time * multipliers[unit]
104
+
105
+
106
+ def format_time_labels(
107
+ time: NDArray[np.float64],
108
+ unit: TimeUnit = "auto",
109
+ *,
110
+ precision: int | None = None,
111
+ scientific_threshold: float = 1e6,
112
+ ) -> list[str]:
113
+ """Format time values as labels with appropriate precision.
114
+
115
+ Args:
116
+ time: Time array in seconds.
117
+ unit: Time unit ("s", "ms", "us", "ns", "ps", "auto").
118
+ precision: Number of decimal places (auto if None).
119
+ scientific_threshold: Use scientific notation above this value.
120
+
121
+ Returns:
122
+ List of formatted time labels.
123
+
124
+ Example:
125
+ >>> time = np.array([0.0, 0.001, 0.002])
126
+ >>> labels = format_time_labels(time, unit="ms")
127
+ >>> # Returns ['0', '1', '2']
128
+
129
+ References:
130
+ VIS-014: Adaptive X-Axis Time Window
131
+ """
132
+ # Convert to target unit
133
+ time_converted = convert_time_values(time, unit)
134
+
135
+ # Auto-select precision based on value range
136
+ if precision is None:
137
+ value_range = np.ptp(time_converted)
138
+ if value_range == 0:
139
+ precision = 1
140
+ else:
141
+ # Use enough precision to show differences
142
+ magnitude = np.log10(value_range)
143
+ precision = max(0, int(np.ceil(2 - magnitude)))
144
+
145
+ # Format labels
146
+ labels = []
147
+ for val in time_converted:
148
+ if abs(val) >= scientific_threshold:
149
+ # Scientific notation
150
+ labels.append(f"{val:.{precision}e}")
151
+ else:
152
+ # Fixed point
153
+ labels.append(f"{val:.{precision}f}".rstrip("0").rstrip("."))
154
+
155
+ return labels
156
+
157
+
158
+ def create_relative_time(
159
+ time: NDArray[np.float64],
160
+ *,
161
+ start_at_zero: bool = True,
162
+ reference_time: float | None = None,
163
+ ) -> NDArray[np.float64]:
164
+ """Create relative time axis starting at zero or reference.
165
+
166
+ Args:
167
+ time: Absolute time array in seconds.
168
+ start_at_zero: Start time axis at t=0.
169
+ reference_time: Reference time (uses first sample if None).
170
+
171
+ Returns:
172
+ Relative time array.
173
+
174
+ Example:
175
+ >>> time_abs = np.array([1000.5, 1000.6, 1000.7])
176
+ >>> time_rel = create_relative_time(time_abs)
177
+ >>> # Returns [0.0, 0.1, 0.2]
178
+
179
+ References:
180
+ VIS-014: Adaptive X-Axis Time Window
181
+ """
182
+ if len(time) == 0:
183
+ return time
184
+
185
+ if reference_time is None:
186
+ reference_time = time[0] if start_at_zero else 0.0
187
+
188
+ return time - reference_time
189
+
190
+
191
+ def calculate_major_ticks(
192
+ time_min: float,
193
+ time_max: float,
194
+ *,
195
+ target_count: int = 7,
196
+ unit: TimeUnit = "auto",
197
+ ) -> NDArray[np.float64]:
198
+ """Calculate major tick positions for time axis.
199
+
200
+ Args:
201
+ time_min: Minimum time value in seconds.
202
+ time_max: Maximum time value in seconds.
203
+ target_count: Target number of major ticks.
204
+ unit: Time unit for tick alignment.
205
+
206
+ Returns:
207
+ Array of major tick positions in seconds.
208
+
209
+ Example:
210
+ >>> ticks = calculate_major_ticks(0, 0.01, target_count=5, unit="ms")
211
+
212
+ References:
213
+ VIS-014: Adaptive X-Axis Time Window
214
+ VIS-019: Grid Auto-Spacing
215
+ """
216
+ time_range = time_max - time_min
217
+
218
+ if time_range <= 0:
219
+ return np.array([time_min])
220
+
221
+ # Select unit if auto
222
+ if unit == "auto":
223
+ unit = select_time_unit(time_range)
224
+
225
+ # Convert to selected unit
226
+ multipliers = {
227
+ "s": 1.0,
228
+ "ms": 1e3,
229
+ "us": 1e6,
230
+ "ns": 1e9,
231
+ "ps": 1e12,
232
+ }
233
+ multiplier = multipliers[unit]
234
+
235
+ time_min_unit = time_min * multiplier
236
+ time_max_unit = time_max * multiplier
237
+ range_unit = time_max_unit - time_min_unit
238
+
239
+ # Calculate rough spacing
240
+ rough_spacing = range_unit / target_count
241
+
242
+ # Round to nice number
243
+ nice_spacing = _round_to_nice_time(rough_spacing)
244
+
245
+ # Generate ticks
246
+ first_tick = np.ceil(time_min_unit / nice_spacing) * nice_spacing
247
+ n_ticks = int((time_max_unit - first_tick) / nice_spacing) + 1
248
+
249
+ ticks_unit = first_tick + np.arange(n_ticks) * nice_spacing
250
+
251
+ # Convert back to seconds
252
+ ticks = ticks_unit / multiplier
253
+
254
+ # Filter to range
255
+ filtered_ticks: NDArray[np.float64] = ticks[(ticks >= time_min) & (ticks <= time_max)]
256
+
257
+ return filtered_ticks
258
+
259
+
260
+ def _round_to_nice_time(value: float) -> float:
261
+ """Round to nice time value (1, 2, 5, 10, 20, 50 × 10^n). # noqa: RUF002
262
+
263
+ Args:
264
+ value: Value to round.
265
+
266
+ Returns:
267
+ Nice rounded value.
268
+ """
269
+ if value <= 0:
270
+ return 1.0
271
+
272
+ exponent = np.floor(np.log10(value))
273
+ mantissa = value / (10**exponent)
274
+
275
+ # Nice fractions for time
276
+ nice_fractions = [1.0, 2.0, 5.0, 10.0]
277
+
278
+ # Find closest
279
+ distances = [abs(f - mantissa) for f in nice_fractions]
280
+ min_idx = np.argmin(distances)
281
+ nice_mantissa = nice_fractions[min_idx]
282
+
283
+ # Handle overflow
284
+ if nice_mantissa >= 10.0:
285
+ nice_mantissa = 1.0
286
+ exponent += 1
287
+
288
+ return nice_mantissa * (10**exponent) # type: ignore[no-any-return]
289
+
290
+
291
+ def format_cursor_readout(
292
+ time_value: float,
293
+ *,
294
+ unit: TimeUnit = "auto",
295
+ full_precision: bool = True,
296
+ ) -> str:
297
+ """Format time value for cursor readout with full precision.
298
+
299
+ Args:
300
+ time_value: Time value in seconds.
301
+ unit: Display unit.
302
+ full_precision: Show full floating-point precision.
303
+
304
+ Returns:
305
+ Formatted time string.
306
+
307
+ Example:
308
+ >>> readout = format_cursor_readout(1.23456789e-6, unit="us")
309
+ >>> # Returns "1.23456789 μs"
310
+
311
+ References:
312
+ VIS-014: Adaptive X-Axis Time Window (cursor readout)
313
+ """
314
+ # Select unit if auto
315
+ if unit == "auto":
316
+ unit = select_time_unit(abs(time_value))
317
+
318
+ # Convert to unit
319
+ time_converted = convert_time_values(np.array([time_value]), unit)[0]
320
+
321
+ # Unit symbols
322
+ unit_symbols = {
323
+ "s": "s",
324
+ "ms": "ms",
325
+ "us": "μs",
326
+ "ns": "ns",
327
+ "ps": "ps",
328
+ }
329
+
330
+ symbol = unit_symbols.get(unit, unit)
331
+
332
+ # Format with appropriate precision
333
+ if full_precision:
334
+ # Maximum useful precision (avoid floating point noise)
335
+ formatted = f"{time_converted:.12g}"
336
+ else:
337
+ # Standard precision
338
+ formatted = f"{time_converted:.6g}"
339
+
340
+ return f"{formatted} {symbol}"
341
+
342
+
343
+ __all__ = [
344
+ "TimeUnit",
345
+ "calculate_major_ticks",
346
+ "convert_time_values",
347
+ "create_relative_time",
348
+ "format_cursor_readout",
349
+ "format_time_labels",
350
+ "select_time_unit",
351
+ ]
@@ -0,0 +1,367 @@
1
+ """Waveform visualization functions.
2
+
3
+ This module provides time-domain waveform and multi-channel plots
4
+ with measurement annotations.
5
+
6
+
7
+ Example:
8
+ >>> from oscura.visualization.waveform import plot_waveform, plot_multi_channel
9
+ >>> plot_waveform(trace)
10
+ >>> plot_multi_channel([ch1, ch2, ch3])
11
+
12
+ References:
13
+ matplotlib best practices for scientific visualization
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from typing import TYPE_CHECKING, Any, cast
19
+
20
+ import numpy as np
21
+
22
+ try:
23
+ import matplotlib.pyplot as plt
24
+
25
+ HAS_MATPLOTLIB = True
26
+ except ImportError:
27
+ HAS_MATPLOTLIB = False
28
+
29
+ from oscura.core.types import DigitalTrace, WaveformTrace
30
+
31
+ if TYPE_CHECKING:
32
+ from matplotlib.axes import Axes
33
+ from matplotlib.figure import Figure
34
+ from numpy.typing import NDArray
35
+
36
+
37
+ def plot_waveform(
38
+ trace: WaveformTrace,
39
+ *,
40
+ ax: Axes | None = None,
41
+ time_unit: str = "auto",
42
+ time_range: tuple[float, float] | None = None,
43
+ show_grid: bool = True,
44
+ color: str = "C0",
45
+ label: str | None = None,
46
+ show_measurements: dict[str, Any] | None = None,
47
+ title: str | None = None,
48
+ xlabel: str = "Time",
49
+ ylabel: str = "Amplitude",
50
+ show: bool = True,
51
+ save_path: str | None = None,
52
+ figsize: tuple[float, float] = (10, 6),
53
+ ) -> Figure:
54
+ """Plot time-domain waveform.
55
+
56
+ Args:
57
+ trace: Waveform trace to plot.
58
+ ax: Matplotlib axes. If None, creates new figure.
59
+ time_unit: Time unit ("s", "ms", "us", "ns", "auto").
60
+ time_range: Optional (start, end) time range in seconds to display.
61
+ show_grid: Show grid lines.
62
+ color: Line color.
63
+ label: Legend label.
64
+ show_measurements: Dictionary of measurements to annotate.
65
+ title: Plot title.
66
+ xlabel: X-axis label (appended with time unit).
67
+ ylabel: Y-axis label.
68
+ show: If True, call plt.show() to display the plot.
69
+ save_path: Path to save the figure. If None, figure is not saved.
70
+ figsize: Figure size (width, height) in inches. Only used if ax is None.
71
+
72
+ Returns:
73
+ Matplotlib Figure object.
74
+
75
+ Raises:
76
+ ImportError: If matplotlib is not installed.
77
+ ValueError: If axes has no associated figure.
78
+
79
+ Example:
80
+ >>> import oscura as tk
81
+ >>> trace = tk.load("signal.wfm")
82
+ >>> fig = tk.plot_waveform(trace, time_unit="us", show=False)
83
+ >>> fig.savefig("waveform.png")
84
+
85
+ >>> # With custom styling
86
+ >>> fig = tk.plot_waveform(trace,
87
+ ... title="Captured Signal",
88
+ ... xlabel="Time",
89
+ ... ylabel="Voltage",
90
+ ... color="blue")
91
+ """
92
+ if not HAS_MATPLOTLIB:
93
+ raise ImportError("matplotlib is required for visualization")
94
+
95
+ if ax is None:
96
+ fig, ax = plt.subplots(figsize=figsize)
97
+ else:
98
+ fig_temp = ax.get_figure()
99
+ if fig_temp is None:
100
+ raise ValueError("Axes must have an associated figure")
101
+ fig = cast("Figure", fig_temp)
102
+
103
+ # Calculate time axis
104
+ time = trace.time_vector
105
+
106
+ # Auto-select time unit
107
+ if time_unit == "auto":
108
+ duration = time[-1] if len(time) > 0 else 0
109
+ if duration < 1e-6:
110
+ time_unit = "ns"
111
+ elif duration < 1e-3:
112
+ time_unit = "us"
113
+ elif duration < 1:
114
+ time_unit = "ms"
115
+ else:
116
+ time_unit = "s"
117
+
118
+ time_multipliers = {"s": 1.0, "ms": 1e3, "us": 1e6, "ns": 1e9}
119
+ multiplier = time_multipliers.get(time_unit, 1.0)
120
+ time_scaled = time * multiplier
121
+
122
+ # Plot waveform
123
+ ax.plot(time_scaled, trace.data, color=color, label=label, linewidth=0.8)
124
+
125
+ # Apply time range if specified
126
+ if time_range is not None:
127
+ ax.set_xlim(time_range[0] * multiplier, time_range[1] * multiplier)
128
+
129
+ # Labels
130
+ ax.set_xlabel(f"{xlabel} ({time_unit})")
131
+ ax.set_ylabel(ylabel)
132
+
133
+ if title:
134
+ ax.set_title(title)
135
+ elif trace.metadata.channel_name:
136
+ ax.set_title(f"Waveform - {trace.metadata.channel_name}")
137
+
138
+ if show_grid:
139
+ ax.grid(True, alpha=0.3)
140
+
141
+ if label:
142
+ ax.legend()
143
+
144
+ # Add measurement annotations
145
+ if show_measurements:
146
+ _add_measurement_annotations(ax, trace, show_measurements, time_unit, multiplier)
147
+
148
+ fig.tight_layout()
149
+
150
+ # Save if path provided
151
+ if save_path is not None:
152
+ fig.savefig(save_path, dpi=300, bbox_inches="tight")
153
+
154
+ # Show if requested
155
+ if show:
156
+ plt.show()
157
+
158
+ return fig
159
+
160
+
161
+ def plot_multi_channel(
162
+ traces: list[WaveformTrace | DigitalTrace],
163
+ *,
164
+ names: list[str] | None = None,
165
+ shared_x: bool = True,
166
+ share_x: bool | None = None,
167
+ colors: list[str] | None = None,
168
+ time_unit: str = "auto",
169
+ show_grid: bool = True,
170
+ figsize: tuple[float, float] | None = None,
171
+ title: str | None = None,
172
+ ) -> Figure:
173
+ """Plot multiple channels in stacked subplots.
174
+
175
+ Args:
176
+ traces: List of traces to plot.
177
+ names: Channel names for labels.
178
+ shared_x: Share x-axis across subplots.
179
+ share_x: Alias for shared_x (for compatibility).
180
+ colors: List of colors for each trace. If None, uses default cycle.
181
+ time_unit: Time unit ("s", "ms", "us", "ns", "auto").
182
+ show_grid: Show grid lines.
183
+ figsize: Figure size (width, height) in inches.
184
+ title: Overall figure title.
185
+
186
+ Returns:
187
+ Matplotlib Figure object.
188
+
189
+ Raises:
190
+ ImportError: If matplotlib is not available.
191
+
192
+ Example:
193
+ >>> fig = plot_multi_channel([ch1, ch2, ch3], names=["CLK", "DATA", "CS"])
194
+ >>> plt.show()
195
+ """
196
+ # Handle share_x alias
197
+ if share_x is not None:
198
+ shared_x = share_x
199
+ if not HAS_MATPLOTLIB:
200
+ raise ImportError("matplotlib is required for visualization")
201
+
202
+ n_channels = len(traces)
203
+
204
+ if names is None:
205
+ names = [f"CH{i + 1}" for i in range(n_channels)]
206
+
207
+ if figsize is None:
208
+ figsize = (10, 2 * n_channels)
209
+
210
+ fig, axes = plt.subplots(
211
+ n_channels,
212
+ 1,
213
+ figsize=figsize,
214
+ sharex=shared_x,
215
+ )
216
+
217
+ if n_channels == 1:
218
+ axes = [axes]
219
+
220
+ # Auto-select time unit from first trace
221
+ if time_unit == "auto" and len(traces) > 0:
222
+ ref_trace = traces[0]
223
+ duration = len(ref_trace.data) * ref_trace.metadata.time_base
224
+ if duration < 1e-6:
225
+ time_unit = "ns"
226
+ elif duration < 1e-3:
227
+ time_unit = "us"
228
+ elif duration < 1:
229
+ time_unit = "ms"
230
+ else:
231
+ time_unit = "s"
232
+
233
+ time_multipliers = {"s": 1.0, "ms": 1e3, "us": 1e6, "ns": 1e9}
234
+ multiplier = time_multipliers.get(time_unit, 1.0)
235
+
236
+ for i, (trace, name, ax) in enumerate(zip(traces, names, axes, strict=False)):
237
+ time = trace.time_vector * multiplier
238
+ color = colors[i] if colors is not None and i < len(colors) else f"C{i}"
239
+
240
+ if isinstance(trace, WaveformTrace):
241
+ ax.plot(time, trace.data, color=color, linewidth=0.8)
242
+ ax.set_ylabel("V")
243
+ else:
244
+ # Digital trace - step plot
245
+ ax.step(time, trace.data.astype(int), color=color, where="post", linewidth=1.0)
246
+ ax.set_ylim(-0.1, 1.1)
247
+ ax.set_yticks([0, 1])
248
+ ax.set_yticklabels(["L", "H"])
249
+
250
+ ax.set_ylabel(name, rotation=0, ha="right", va="center")
251
+
252
+ if show_grid:
253
+ ax.grid(True, alpha=0.3)
254
+
255
+ # Only show x-label on bottom plot
256
+ if i == n_channels - 1:
257
+ ax.set_xlabel(f"Time ({time_unit})")
258
+
259
+ if title:
260
+ fig.suptitle(title)
261
+
262
+ fig.tight_layout()
263
+ return fig
264
+
265
+
266
+ def plot_xy(
267
+ x_trace: WaveformTrace | NDArray[np.float64],
268
+ y_trace: WaveformTrace | NDArray[np.float64],
269
+ *,
270
+ ax: Axes | None = None,
271
+ color: str = "C0",
272
+ marker: str = "",
273
+ alpha: float = 0.7,
274
+ title: str | None = None,
275
+ ) -> Figure:
276
+ """Plot X-Y (Lissajous) diagram.
277
+
278
+ Args:
279
+ x_trace: X-axis waveform.
280
+ y_trace: Y-axis waveform.
281
+ ax: Matplotlib axes.
282
+ color: Line/marker color.
283
+ marker: Marker style.
284
+ alpha: Transparency.
285
+ title: Plot title.
286
+
287
+ Returns:
288
+ Matplotlib Figure object.
289
+
290
+ Raises:
291
+ ImportError: If matplotlib is not available.
292
+ ValueError: If axes has no associated figure.
293
+
294
+ Example:
295
+ >>> fig = plot_xy(ch1, ch2) # Phase relationship
296
+ """
297
+ if not HAS_MATPLOTLIB:
298
+ raise ImportError("matplotlib is required for visualization")
299
+
300
+ if ax is None:
301
+ fig, ax = plt.subplots(figsize=(6, 6))
302
+ else:
303
+ fig_temp = ax.get_figure()
304
+ if fig_temp is None:
305
+ raise ValueError("Axes must have an associated figure")
306
+ fig = cast("Figure", fig_temp)
307
+
308
+ x_data = x_trace.data if isinstance(x_trace, WaveformTrace) else x_trace
309
+ y_data = y_trace.data if isinstance(y_trace, WaveformTrace) else y_trace
310
+
311
+ # Ensure same length
312
+ min_len = min(len(x_data), len(y_data))
313
+ x_data = x_data[:min_len]
314
+ y_data = y_data[:min_len]
315
+
316
+ ax.plot(x_data, y_data, color=color, marker=marker, alpha=alpha, linewidth=0.5)
317
+
318
+ ax.set_xlabel("X (V)")
319
+ ax.set_ylabel("Y (V)")
320
+ ax.set_aspect("equal")
321
+ ax.grid(True, alpha=0.3)
322
+
323
+ if title:
324
+ ax.set_title(title)
325
+
326
+ fig.tight_layout()
327
+ return fig
328
+
329
+
330
+ def _add_measurement_annotations(
331
+ ax: Axes,
332
+ trace: WaveformTrace,
333
+ measurements: dict[str, Any],
334
+ time_unit: str,
335
+ multiplier: float,
336
+ ) -> None:
337
+ """Add measurement annotations to plot."""
338
+ # Create annotation text
339
+ text_lines = []
340
+
341
+ for name, value in measurements.items():
342
+ if isinstance(value, dict):
343
+ val = value.get("value", value)
344
+ unit = value.get("unit", "")
345
+ if isinstance(val, float) and not np.isnan(val):
346
+ text_lines.append(f"{name}: {val:.4g} {unit}")
347
+ elif isinstance(value, float) and not np.isnan(value):
348
+ text_lines.append(f"{name}: {value:.4g}")
349
+
350
+ if text_lines:
351
+ text = "\n".join(text_lines)
352
+ ax.annotate(
353
+ text,
354
+ xy=(0.02, 0.98),
355
+ xycoords="axes fraction",
356
+ verticalalignment="top",
357
+ fontfamily="monospace",
358
+ fontsize=8,
359
+ bbox={"boxstyle": "round", "facecolor": "wheat", "alpha": 0.8},
360
+ )
361
+
362
+
363
+ __all__ = [
364
+ "plot_multi_channel",
365
+ "plot_waveform",
366
+ "plot_xy",
367
+ ]