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,858 @@
1
+ """Interactive visualization features.
2
+
3
+ This module provides interactive plotting capabilities including zoom,
4
+ pan, cursors, and specialized plot types.
5
+
6
+
7
+ Example:
8
+ >>> from oscura.visualization.interactive import (
9
+ ... plot_with_cursors, plot_phase, plot_bode,
10
+ ... plot_waterfall, plot_histogram
11
+ ... )
12
+ >>> fig, ax = plot_with_cursors(trace)
13
+ >>> plot_bode(frequencies, magnitude, phase)
14
+
15
+ References:
16
+ matplotlib interactive features
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ from dataclasses import dataclass, field
22
+ from typing import TYPE_CHECKING, Any, Literal, cast
23
+
24
+ import numpy as np
25
+ from scipy import signal as scipy_signal
26
+
27
+ if TYPE_CHECKING:
28
+ from matplotlib.axes import Axes
29
+ from matplotlib.backend_bases import MouseEvent
30
+ from matplotlib.figure import Figure
31
+ from numpy.typing import NDArray
32
+
33
+ from oscura.core.types import WaveformTrace
34
+
35
+ # Optional matplotlib import
36
+ try:
37
+ import matplotlib.pyplot as plt
38
+ from matplotlib.widgets import Cursor, MultiCursor, SpanSelector # noqa: F401
39
+
40
+ MATPLOTLIB_AVAILABLE = True
41
+ except ImportError:
42
+ MATPLOTLIB_AVAILABLE = False
43
+
44
+
45
+ @dataclass
46
+ class CursorMeasurement:
47
+ """Measurement result from cursors.
48
+
49
+ Attributes:
50
+ x1: First cursor X position.
51
+ x2: Second cursor X position.
52
+ y1: First cursor Y position.
53
+ y2: Second cursor Y position.
54
+ delta_x: X difference (x2 - x1).
55
+ delta_y: Y difference (y2 - y1).
56
+ frequency: 1/delta_x if delta_x > 0.
57
+ slope: delta_y/delta_x if delta_x != 0.
58
+
59
+ References:
60
+ VIS-008
61
+ """
62
+
63
+ x1: float
64
+ x2: float
65
+ y1: float
66
+ y2: float
67
+ delta_x: float
68
+ delta_y: float
69
+ frequency: float | None = None
70
+ slope: float | None = None
71
+
72
+
73
+ @dataclass
74
+ class ZoomState:
75
+ """Current zoom/pan state.
76
+
77
+ Attributes:
78
+ xlim: Current X-axis limits.
79
+ ylim: Current Y-axis limits.
80
+ history: Stack of previous zoom states.
81
+ home_xlim: Original X-axis limits.
82
+ home_ylim: Original Y-axis limits.
83
+
84
+ References:
85
+ VIS-007
86
+ """
87
+
88
+ xlim: tuple[float, float]
89
+ ylim: tuple[float, float]
90
+ history: list[tuple[tuple[float, float], tuple[float, float]]] = field(default_factory=list)
91
+ home_xlim: tuple[float, float] | None = None
92
+ home_ylim: tuple[float, float] | None = None
93
+
94
+
95
+ def enable_zoom_pan(
96
+ ax: Axes,
97
+ *,
98
+ enable_zoom: bool = True,
99
+ enable_pan: bool = True,
100
+ zoom_factor: float = 1.5,
101
+ ) -> ZoomState:
102
+ """Enable interactive zoom and pan on an axes.
103
+
104
+ Adds scroll wheel zoom and click-drag pan functionality.
105
+
106
+ Args:
107
+ ax: Matplotlib axes to enable zoom/pan on.
108
+ enable_zoom: Enable scroll wheel zoom.
109
+ enable_pan: Enable click-drag pan.
110
+ zoom_factor: Zoom factor per scroll step.
111
+
112
+ Returns:
113
+ ZoomState object tracking zoom history.
114
+
115
+ Raises:
116
+ ImportError: If matplotlib is not available.
117
+
118
+ Example:
119
+ >>> fig, ax = plt.subplots()
120
+ >>> ax.plot(trace.time_vector, trace.data)
121
+ >>> state = enable_zoom_pan(ax)
122
+
123
+ References:
124
+ VIS-007
125
+ """
126
+ if not MATPLOTLIB_AVAILABLE:
127
+ raise ImportError("matplotlib is required for interactive visualization")
128
+
129
+ # Store initial state
130
+ xlim = ax.get_xlim()
131
+ ylim = ax.get_ylim()
132
+ state = ZoomState(
133
+ xlim=xlim,
134
+ ylim=ylim,
135
+ home_xlim=xlim,
136
+ home_ylim=ylim,
137
+ )
138
+
139
+ def on_scroll(event): # type: ignore[no-untyped-def]
140
+ if event.inaxes != ax:
141
+ return
142
+
143
+ # Get mouse position
144
+ x_data = event.xdata
145
+ y_data = event.ydata
146
+
147
+ if x_data is None or y_data is None:
148
+ return
149
+
150
+ # Determine zoom direction
151
+ if event.button == "up":
152
+ factor = 1 / zoom_factor
153
+ elif event.button == "down":
154
+ factor = zoom_factor
155
+ else:
156
+ return
157
+
158
+ # Save current state
159
+ state.history.append((state.xlim, state.ylim))
160
+
161
+ # Calculate new limits centered on mouse position
162
+ cur_xlim = ax.get_xlim()
163
+ cur_ylim = ax.get_ylim()
164
+
165
+ new_width = (cur_xlim[1] - cur_xlim[0]) * factor
166
+ new_height = (cur_ylim[1] - cur_ylim[0]) * factor
167
+
168
+ rel_x = (x_data - cur_xlim[0]) / (cur_xlim[1] - cur_xlim[0])
169
+ rel_y = (y_data - cur_ylim[0]) / (cur_ylim[1] - cur_ylim[0])
170
+
171
+ new_xlim = (
172
+ x_data - new_width * rel_x,
173
+ x_data + new_width * (1 - rel_x),
174
+ )
175
+ new_ylim = (
176
+ y_data - new_height * rel_y,
177
+ y_data + new_height * (1 - rel_y),
178
+ )
179
+
180
+ ax.set_xlim(new_xlim)
181
+ ax.set_ylim(new_ylim)
182
+ state.xlim = new_xlim
183
+ state.ylim = new_ylim
184
+
185
+ ax.figure.canvas.draw_idle()
186
+
187
+ if enable_zoom:
188
+ ax.figure.canvas.mpl_connect("scroll_event", on_scroll)
189
+
190
+ # Pan state
191
+ pan_active = [False]
192
+ pan_start: list[float | None] = [None, None]
193
+
194
+ def on_press(event): # type: ignore[no-untyped-def]
195
+ if event.inaxes != ax:
196
+ return
197
+ if event.button == 1: # Left click
198
+ pan_active[0] = True
199
+ pan_start[0] = event.xdata
200
+ pan_start[1] = event.ydata
201
+
202
+ def on_release(event: MouseEvent) -> None:
203
+ pan_active[0] = False
204
+
205
+ def on_motion(event: MouseEvent) -> None:
206
+ if not pan_active[0]:
207
+ return
208
+ if event.inaxes != ax:
209
+ return
210
+ if event.xdata is None or event.ydata is None:
211
+ return
212
+ if pan_start[0] is None or pan_start[1] is None:
213
+ return
214
+
215
+ dx = pan_start[0] - event.xdata
216
+ dy = pan_start[1] - event.ydata
217
+
218
+ cur_xlim = ax.get_xlim()
219
+ cur_ylim = ax.get_ylim()
220
+
221
+ new_xlim = (cur_xlim[0] + dx, cur_xlim[1] + dx)
222
+ new_ylim = (cur_ylim[0] + dy, cur_ylim[1] + dy)
223
+
224
+ ax.set_xlim(new_xlim)
225
+ ax.set_ylim(new_ylim)
226
+ state.xlim = new_xlim
227
+ state.ylim = new_ylim
228
+
229
+ ax.figure.canvas.draw_idle()
230
+
231
+ if enable_pan:
232
+ ax.figure.canvas.mpl_connect("button_press_event", on_press)
233
+ ax.figure.canvas.mpl_connect("button_release_event", on_release) # type: ignore[arg-type]
234
+ ax.figure.canvas.mpl_connect("motion_notify_event", on_motion) # type: ignore[arg-type]
235
+
236
+ return state
237
+
238
+
239
+ def plot_with_cursors(
240
+ trace: WaveformTrace | NDArray[np.floating[Any]],
241
+ *,
242
+ sample_rate: float | None = None,
243
+ cursor_type: Literal["vertical", "horizontal", "cross"] = "cross",
244
+ ax: Axes | None = None,
245
+ **plot_kwargs: Any,
246
+ ) -> tuple[Figure, Axes, Cursor]:
247
+ """Plot waveform with interactive measurement cursors.
248
+
249
+ Args:
250
+ trace: Input trace or numpy array.
251
+ sample_rate: Sample rate (required for arrays).
252
+ cursor_type: Type of cursor lines.
253
+ ax: Existing axes to plot on.
254
+ **plot_kwargs: Additional arguments to plot().
255
+
256
+ Returns:
257
+ Tuple of (figure, axes, cursor widget).
258
+
259
+ Raises:
260
+ ImportError: If matplotlib is not available.
261
+ ValueError: If axes has no associated figure.
262
+
263
+ Example:
264
+ >>> fig, ax, cursor = plot_with_cursors(trace)
265
+ >>> plt.show()
266
+
267
+ References:
268
+ VIS-008
269
+ """
270
+ if not MATPLOTLIB_AVAILABLE:
271
+ raise ImportError("matplotlib is required for interactive visualization")
272
+
273
+ # Get data and time vector
274
+ if isinstance(trace, WaveformTrace):
275
+ data = trace.data
276
+ time = trace.time_vector
277
+ else:
278
+ data = np.asarray(trace)
279
+ if sample_rate is None:
280
+ sample_rate = 1.0
281
+ time = np.arange(len(data)) / sample_rate
282
+
283
+ # Create figure if needed
284
+ if ax is None:
285
+ fig, ax = plt.subplots(figsize=(10, 6))
286
+ else:
287
+ fig_temp = ax.figure
288
+ if fig_temp is None:
289
+ raise ValueError("Axes must have an associated figure")
290
+ fig = cast("Figure", fig_temp)
291
+
292
+ # Plot data
293
+ ax.plot(time, data, **plot_kwargs)
294
+ ax.set_xlabel("Time (s)")
295
+ ax.set_ylabel("Amplitude")
296
+ ax.grid(True, alpha=0.3)
297
+
298
+ # Create cursor
299
+ if cursor_type == "vertical":
300
+ cursor = Cursor(ax, useblit=True, color="red", linewidth=1, vertOn=True, horizOn=False)
301
+ elif cursor_type == "horizontal":
302
+ cursor = Cursor(ax, useblit=True, color="red", linewidth=1, vertOn=False, horizOn=True)
303
+ else: # cross
304
+ cursor = Cursor(ax, useblit=True, color="red", linewidth=1)
305
+
306
+ return fig, ax, cursor
307
+
308
+
309
+ def add_measurement_cursors(
310
+ ax: Axes,
311
+ *,
312
+ color: str = "red",
313
+ linestyle: str = "--",
314
+ ) -> dict: # type: ignore[type-arg]
315
+ """Add dual measurement cursors to an axes.
316
+
317
+ Click and drag to define measurement region. Returns measurement
318
+ data in the callback.
319
+
320
+ Args:
321
+ ax: Axes to add cursors to.
322
+ color: Cursor line color.
323
+ linestyle: Cursor line style.
324
+
325
+ Returns:
326
+ Dictionary with cursor state and get_measurement() function.
327
+
328
+ Raises:
329
+ ImportError: If matplotlib is not available.
330
+
331
+ Example:
332
+ >>> cursors = add_measurement_cursors(ax)
333
+ >>> measurement = cursors['get_measurement']()
334
+ >>> print(f"Delta X: {measurement.delta_x}")
335
+
336
+ References:
337
+ VIS-008
338
+ """
339
+ if not MATPLOTLIB_AVAILABLE:
340
+ raise ImportError("matplotlib is required for interactive visualization")
341
+
342
+ state: dict[str, float | None | Any] = {
343
+ "x1": None,
344
+ "x2": None,
345
+ "y1": None,
346
+ "y2": None,
347
+ "line1": None,
348
+ "line2": None,
349
+ }
350
+
351
+ def onselect(xmin: float, xmax: float) -> None:
352
+ state["x1"] = xmin
353
+ state["x2"] = xmax
354
+
355
+ # Get Y values at cursor positions
356
+ for line in ax.get_lines():
357
+ xdata = line.get_xdata()
358
+ ydata = line.get_ydata()
359
+ # Type narrowing: these return ArrayLike from Line2D
360
+ xdata_arr = np.asarray(xdata)
361
+ ydata_arr = np.asarray(ydata)
362
+ if len(xdata_arr) > 0:
363
+ # Interpolate Y at cursor positions
364
+ y1_interp: float = float(np.interp(xmin, xdata_arr, ydata_arr))
365
+ y2_interp: float = float(np.interp(xmax, xdata_arr, ydata_arr))
366
+ state["y1"] = y1_interp
367
+ state["y2"] = y2_interp
368
+ break
369
+
370
+ span = SpanSelector(
371
+ ax,
372
+ onselect,
373
+ "horizontal",
374
+ useblit=True,
375
+ props={"alpha": 0.3, "facecolor": color},
376
+ interactive=True,
377
+ )
378
+
379
+ def get_measurement() -> CursorMeasurement | None:
380
+ x1 = state["x1"]
381
+ x2 = state["x2"]
382
+ y1 = state["y1"]
383
+ y2 = state["y2"]
384
+
385
+ if (
386
+ x1 is None
387
+ or x2 is None
388
+ or not isinstance(x1, int | float)
389
+ or not isinstance(x2, int | float)
390
+ ):
391
+ return None
392
+
393
+ delta_x = x2 - x1
394
+ y1_val = float(y1) if y1 is not None else 0.0
395
+ y2_val = float(y2) if y2 is not None else 0.0
396
+ delta_y = y2_val - y1_val
397
+
398
+ return CursorMeasurement(
399
+ x1=x1,
400
+ x2=x2,
401
+ y1=y1_val,
402
+ y2=y2_val,
403
+ delta_x=delta_x,
404
+ delta_y=delta_y,
405
+ frequency=1 / delta_x if delta_x > 0 else None,
406
+ slope=delta_y / delta_x if delta_x != 0 else None,
407
+ )
408
+
409
+ return {
410
+ "span": span,
411
+ "state": state,
412
+ "get_measurement": get_measurement,
413
+ }
414
+
415
+
416
+ def plot_phase(
417
+ trace1: WaveformTrace | NDArray[np.floating[Any]],
418
+ trace2: WaveformTrace | NDArray[np.floating[Any]] | None = None,
419
+ *,
420
+ delay: int = 1,
421
+ delay_samples: int | None = None,
422
+ ax: Axes | None = None,
423
+ **plot_kwargs: Any,
424
+ ) -> tuple[Figure, Axes]:
425
+ """Create phase plot (X-Y plot) of two signals.
426
+
427
+ Plots trace1 on X-axis vs trace2 on Y-axis, useful for
428
+ visualizing phase relationships and Lissajous figures.
429
+ If trace2 is not provided, creates a self-phase plot using
430
+ time-delayed version of trace1.
431
+
432
+ Args:
433
+ trace1: Signal for X-axis.
434
+ trace2: Signal for Y-axis. If None, uses delayed trace1.
435
+ delay: Sample delay for self-phase plot (when trace2=None).
436
+ delay_samples: Alias for delay parameter.
437
+ ax: Existing axes to plot on.
438
+ **plot_kwargs: Additional arguments to plot().
439
+
440
+ Returns:
441
+ Tuple of (figure, axes).
442
+
443
+ Raises:
444
+ ImportError: If matplotlib is not available.
445
+ ValueError: If axes has no associated figure.
446
+
447
+ Example:
448
+ >>> fig, ax = plot_phase(signal_x, signal_y)
449
+ >>> plt.show()
450
+ >>> # Self-phase plot
451
+ >>> fig, ax = plot_phase(signal, delay_samples=10)
452
+
453
+ References:
454
+ VIS-009
455
+ """
456
+ if not MATPLOTLIB_AVAILABLE:
457
+ raise ImportError("matplotlib is required for interactive visualization")
458
+
459
+ # Handle delay_samples alias
460
+ if delay_samples is not None:
461
+ delay = delay_samples
462
+
463
+ # Get data
464
+ data1 = trace1.data if isinstance(trace1, WaveformTrace) else np.asarray(trace1)
465
+
466
+ # If trace2 not provided, create self-phase plot with delay
467
+ if trace2 is None:
468
+ data2 = np.roll(data1, -delay)
469
+ else:
470
+ data2 = trace2.data if isinstance(trace2, WaveformTrace) else np.asarray(trace2)
471
+
472
+ # Ensure same length
473
+ n = min(len(data1), len(data2))
474
+ data1 = data1[:n]
475
+ data2 = data2[:n]
476
+
477
+ # Create figure if needed
478
+ if ax is None:
479
+ fig, ax = plt.subplots(figsize=(8, 8))
480
+ else:
481
+ fig_temp = ax.figure
482
+ if fig_temp is None:
483
+ raise ValueError("Axes must have an associated figure")
484
+ fig = cast("Figure", fig_temp)
485
+
486
+ # Plot
487
+ defaults: dict[str, Any] = {"alpha": 0.5, "marker": ".", "linestyle": "-", "markersize": 2}
488
+ defaults.update(plot_kwargs)
489
+ ax.plot(data1, data2, **defaults)
490
+
491
+ # Equal aspect ratio for proper phase visualization
492
+ ax.set_aspect("equal", adjustable="datalim")
493
+ ax.set_xlabel("Signal 1")
494
+ ax.set_ylabel("Signal 2")
495
+ ax.set_title("Phase Plot (X-Y)")
496
+ ax.grid(True, alpha=0.3)
497
+
498
+ return fig, ax
499
+
500
+
501
+ def plot_bode(
502
+ frequencies: NDArray[np.floating[Any]],
503
+ magnitude: NDArray[np.floating[Any]] | NDArray[np.complexfloating[Any, Any]],
504
+ phase: NDArray[np.floating[Any]] | None = None,
505
+ *,
506
+ magnitude_db: bool = True,
507
+ phase_degrees: bool = True,
508
+ fig: Figure | None = None,
509
+ **plot_kwargs: Any,
510
+ ) -> Figure:
511
+ """Create Bode plot with magnitude and phase.
512
+
513
+ Standard frequency response visualization with logarithmic
514
+ frequency axis.
515
+
516
+ Args:
517
+ frequencies: Frequency array in Hz.
518
+ magnitude: Magnitude array (linear or dB), or complex transfer function H(s).
519
+ If complex, magnitude and phase are extracted automatically.
520
+ phase: Phase array in radians (optional). Ignored if magnitude is complex.
521
+ magnitude_db: If True, magnitude is already in dB. Ignored if complex input.
522
+ phase_degrees: If True, convert phase to degrees.
523
+ fig: Existing figure to plot on.
524
+ **plot_kwargs: Additional arguments to plot().
525
+
526
+ Returns:
527
+ Matplotlib Figure object with magnitude and optionally phase axes.
528
+
529
+ Raises:
530
+ ImportError: If matplotlib is not available.
531
+
532
+ Example:
533
+ >>> # With complex transfer function
534
+ >>> H = 1 / (1 + 1j * freqs / 1000)
535
+ >>> fig = plot_bode(freqs, H)
536
+ >>> ax_mag, ax_phase = fig.axes[:2] # Access axes from figure
537
+ >>> plt.show()
538
+
539
+ References:
540
+ VIS-010
541
+ """
542
+ if not MATPLOTLIB_AVAILABLE:
543
+ raise ImportError("matplotlib is required for interactive visualization")
544
+
545
+ frequencies = np.asarray(frequencies)
546
+ magnitude = np.asarray(magnitude)
547
+
548
+ # Handle complex transfer function input
549
+ if np.iscomplexobj(magnitude):
550
+ # Extract phase from complex input
551
+ phase = np.angle(magnitude)
552
+ # Convert to magnitude in dB
553
+ with np.errstate(divide="ignore"):
554
+ magnitude = 20 * np.log10(np.abs(magnitude))
555
+ magnitude = np.nan_to_num(magnitude, neginf=-200)
556
+ elif not magnitude_db:
557
+ # Convert magnitude to dB if needed
558
+ with np.errstate(divide="ignore"):
559
+ magnitude = 20 * np.log10(np.abs(magnitude))
560
+ magnitude = np.nan_to_num(magnitude, neginf=-200)
561
+
562
+ # Create figure
563
+ if phase is not None:
564
+ if fig is None:
565
+ fig, (ax_mag, ax_phase) = plt.subplots(2, 1, figsize=(10, 8), sharex=True)
566
+ else:
567
+ axes = fig.subplots(2, 1, sharex=True)
568
+ ax_mag, ax_phase = axes
569
+ else:
570
+ if fig is None:
571
+ fig, ax_mag = plt.subplots(figsize=(10, 5))
572
+ else:
573
+ ax_mag = fig.subplots()
574
+ ax_phase = None
575
+
576
+ # Plot magnitude
577
+ ax_mag.semilogx(frequencies, magnitude, **plot_kwargs)
578
+ ax_mag.set_ylabel("Magnitude (dB)")
579
+ ax_mag.grid(True, which="both", alpha=0.3)
580
+ ax_mag.set_title("Bode Plot")
581
+
582
+ # Plot phase if provided
583
+ if phase is not None and ax_phase is not None:
584
+ phase = np.asarray(phase)
585
+ if phase_degrees:
586
+ phase = np.degrees(phase)
587
+ ylabel = "Phase (degrees)"
588
+ else:
589
+ ylabel = "Phase (radians)"
590
+
591
+ ax_phase.semilogx(frequencies, phase, **plot_kwargs)
592
+ ax_phase.set_ylabel(ylabel)
593
+ ax_phase.set_xlabel("Frequency (Hz)")
594
+ ax_phase.grid(True, which="both", alpha=0.3)
595
+ else:
596
+ ax_mag.set_xlabel("Frequency (Hz)")
597
+
598
+ fig.tight_layout()
599
+
600
+ return fig
601
+
602
+
603
+ def plot_waterfall(
604
+ data: NDArray[np.floating[Any]],
605
+ *,
606
+ time_axis: NDArray[np.floating[Any]] | None = None,
607
+ freq_axis: NDArray[np.floating[Any]] | None = None,
608
+ sample_rate: float = 1.0,
609
+ nperseg: int = 256,
610
+ noverlap: int | None = None,
611
+ cmap: str = "viridis",
612
+ ax: Axes | None = None,
613
+ **kwargs: Any,
614
+ ) -> tuple[Figure, Axes]:
615
+ """Create 3D waterfall plot (spectrogram with depth).
616
+
617
+ Shows spectrum evolution over time as stacked frequency slices.
618
+
619
+ Args:
620
+ data: Input signal array (1D) or pre-computed spectrogram (2D).
621
+ If 2D, treated as (n_traces, n_points) spectrogram data.
622
+ time_axis: Time axis for signal.
623
+ freq_axis: Frequency axis (if pre-computed).
624
+ sample_rate: Sample rate in Hz.
625
+ nperseg: Segment length for FFT.
626
+ noverlap: Overlap between segments.
627
+ cmap: Colormap for amplitude coloring.
628
+ ax: Existing 3D axes to plot on.
629
+ **kwargs: Additional arguments.
630
+
631
+ Returns:
632
+ Tuple of (figure, axes).
633
+
634
+ Raises:
635
+ ImportError: If matplotlib is not available.
636
+ TypeError: If axes is not a 3D axes.
637
+ ValueError: If axes has no associated figure.
638
+
639
+ Example:
640
+ >>> fig, ax = plot_waterfall(signal, sample_rate=1e6)
641
+ >>> plt.show()
642
+ >>> # With 2D precomputed data
643
+ >>> fig, ax = plot_waterfall(spectrogram_data)
644
+
645
+ References:
646
+ VIS-011
647
+ """
648
+ if not MATPLOTLIB_AVAILABLE:
649
+ raise ImportError("matplotlib is required for interactive visualization")
650
+
651
+ data = np.asarray(data)
652
+
653
+ # Check if data is 2D (precomputed spectrogram)
654
+ if data.ndim == 2:
655
+ # Treat as precomputed spectrogram (n_traces, n_points)
656
+ Sxx_db = data
657
+ n_traces, n_points = data.shape
658
+ frequencies = freq_axis if freq_axis is not None else np.arange(n_points)
659
+ times = time_axis if time_axis is not None else np.arange(n_traces)
660
+ elif freq_axis is not None:
661
+ # 1D data with explicit freq_axis means precomputed
662
+ Sxx_db = data
663
+ frequencies = freq_axis
664
+ times = (
665
+ time_axis
666
+ if time_axis is not None
667
+ else np.arange(Sxx_db.shape[1] if Sxx_db.ndim > 1 else 1)
668
+ )
669
+ else:
670
+ # Compute spectrogram from 1D signal
671
+ if noverlap is None:
672
+ noverlap = nperseg // 2
673
+
674
+ frequencies, times, Sxx = scipy_signal.spectrogram(
675
+ data, fs=sample_rate, nperseg=nperseg, noverlap=noverlap
676
+ )
677
+ Sxx_db = 10 * np.log10(Sxx + 1e-10)
678
+ times = time_axis if time_axis is not None else np.arange(Sxx_db.shape[1])
679
+
680
+ # Create 3D figure
681
+ if ax is None:
682
+ fig = plt.figure(figsize=(12, 8))
683
+ ax = fig.add_subplot(111, projection="3d")
684
+ else:
685
+ fig_temp = ax.figure
686
+ if fig_temp is None:
687
+ raise ValueError("Axes must have an associated figure")
688
+ fig = cast("Figure", fig_temp)
689
+
690
+ # Create meshgrid
691
+ T, F = np.meshgrid(times, frequencies)
692
+
693
+ # Ensure Sxx_db matches meshgrid shape (n_frequencies, n_times)
694
+ if Sxx_db.shape != T.shape:
695
+ if Sxx_db.T.shape == T.shape:
696
+ Sxx_db = Sxx_db.T
697
+ # If still mismatched, the data dimensions may be incompatible
698
+ # but we'll let plot_surface raise a more informative error
699
+
700
+ # Plot surface
701
+ # Type checking: ax must be a 3D axes at this point
702
+ if not hasattr(ax, "plot_surface"):
703
+ raise TypeError("Axes must be a 3D axes for waterfall plot")
704
+ surf = ax.plot_surface( # type: ignore[attr-defined,union-attr]
705
+ T,
706
+ F,
707
+ Sxx_db,
708
+ cmap=cmap,
709
+ linewidth=0,
710
+ antialiased=True,
711
+ alpha=0.8,
712
+ )
713
+
714
+ ax.set_xlabel("Time (s)")
715
+ ax.set_ylabel("Frequency (Hz)")
716
+ if hasattr(ax, "set_zlabel"):
717
+ ax.set_zlabel("Power (dB)") # type: ignore[attr-defined]
718
+ ax.set_title("Waterfall Plot (Spectrogram)")
719
+
720
+ fig.colorbar(surf, ax=ax, label="Power (dB)", shrink=0.5)
721
+
722
+ return fig, ax
723
+
724
+
725
+ def plot_histogram(
726
+ trace: WaveformTrace | NDArray[np.floating[Any]],
727
+ *,
728
+ bins: int | str | NDArray[np.floating[Any]] = "auto",
729
+ density: bool = True,
730
+ show_stats: bool = True,
731
+ show_kde: bool = False,
732
+ ax: Axes | None = None,
733
+ save_path: str | None = None,
734
+ show: bool = True,
735
+ **hist_kwargs: Any,
736
+ ) -> tuple[Figure, Axes, dict[str, Any]]:
737
+ """Create histogram plot of signal amplitude distribution.
738
+
739
+ Optionally overlays kernel density estimate and statistics.
740
+
741
+ Args:
742
+ trace: Input trace or numpy array.
743
+ bins: Number of bins or binning strategy.
744
+ density: If True, normalize to probability density.
745
+ show_stats: Show mean and standard deviation lines.
746
+ show_kde: Overlay kernel density estimate.
747
+ ax: Existing axes to plot on.
748
+ save_path: Path to save figure. If None, figure is not saved.
749
+ show: If True, display the figure. If False, close it.
750
+ **hist_kwargs: Additional arguments to hist().
751
+
752
+ Returns:
753
+ Tuple of (Figure, Axes, statistics dict).
754
+
755
+ Raises:
756
+ ImportError: If matplotlib is not available.
757
+ ValueError: If axes has no associated figure.
758
+
759
+ Example:
760
+ >>> fig = plot_histogram(trace, bins=50, show_kde=True)
761
+ >>> # With save
762
+ >>> fig = plot_histogram(trace, save_path="hist.png", show=False)
763
+
764
+ References:
765
+ VIS-012
766
+ """
767
+ if not MATPLOTLIB_AVAILABLE:
768
+ raise ImportError("matplotlib is required for interactive visualization")
769
+
770
+ # Get data
771
+ data = trace.data if isinstance(trace, WaveformTrace) else np.asarray(trace)
772
+
773
+ # Create figure if needed
774
+ if ax is None:
775
+ fig, ax = plt.subplots(figsize=(10, 6))
776
+ else:
777
+ fig_temp = ax.figure
778
+ if fig_temp is None:
779
+ raise ValueError("Axes must have an associated figure")
780
+ fig = cast("Figure", fig_temp)
781
+
782
+ # Calculate statistics
783
+ mean = float(np.mean(data))
784
+ std = float(np.std(data))
785
+ median = float(np.median(data))
786
+ min_val = float(np.min(data))
787
+ max_val = float(np.max(data))
788
+
789
+ stats = {
790
+ "mean": mean,
791
+ "std": std,
792
+ "median": median,
793
+ "min": min_val,
794
+ "max": max_val,
795
+ "count": len(data),
796
+ }
797
+
798
+ # Plot histogram
799
+ defaults: dict[str, Any] = {"alpha": 0.7, "edgecolor": "black", "linewidth": 0.5}
800
+ defaults.update(hist_kwargs)
801
+ _counts, bin_edges, _patches = ax.hist(data, bins=bins, density=density, **defaults) # type: ignore[arg-type]
802
+
803
+ stats["bins"] = len(bin_edges) - 1
804
+
805
+ # Show statistics lines
806
+ if show_stats:
807
+ ax.get_ylim()
808
+ ax.axvline(mean, color="red", linestyle="--", linewidth=2, label=f"Mean: {mean:.3g}")
809
+ ax.axvline(mean - std, color="orange", linestyle=":", linewidth=1.5, label="Mean - Std")
810
+ ax.axvline(mean + std, color="orange", linestyle=":", linewidth=1.5, label="Mean + Std")
811
+
812
+ # Show KDE
813
+ if show_kde:
814
+ from scipy.stats import gaussian_kde
815
+
816
+ kde = gaussian_kde(data)
817
+ x_kde = np.linspace(min_val, max_val, 200)
818
+ y_kde = kde(x_kde)
819
+
820
+ if density:
821
+ ax.plot(x_kde, y_kde, "r-", linewidth=2, label="KDE")
822
+ else:
823
+ # Scale KDE to histogram
824
+ bin_width = bin_edges[1] - bin_edges[0]
825
+ ax.plot(x_kde, y_kde * len(data) * bin_width, "r-", linewidth=2, label="KDE")
826
+
827
+ ax.set_xlabel("Amplitude")
828
+ ax.set_ylabel("Density" if density else "Count")
829
+ ax.set_title("Amplitude Distribution")
830
+ # Only show legend if there are labeled artists
831
+ if show_stats or show_kde:
832
+ ax.legend(loc="upper right")
833
+ ax.grid(True, alpha=0.3)
834
+
835
+ # Save if requested
836
+ if save_path is not None:
837
+ fig.savefig(save_path, dpi=150, bbox_inches="tight")
838
+
839
+ # Show or close
840
+ if show:
841
+ plt.show()
842
+ else:
843
+ plt.close(fig)
844
+
845
+ return fig, ax, stats
846
+
847
+
848
+ __all__ = [
849
+ "CursorMeasurement",
850
+ "ZoomState",
851
+ "add_measurement_cursors",
852
+ "enable_zoom_pan",
853
+ "plot_bode",
854
+ "plot_histogram",
855
+ "plot_phase",
856
+ "plot_waterfall",
857
+ "plot_with_cursors",
858
+ ]