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,811 @@
1
+ """Spectral visualization functions.
2
+
3
+ This module provides spectrum and spectrogram plots for
4
+ frequency-domain analysis visualization.
5
+
6
+
7
+ Example:
8
+ >>> from oscura.visualization.spectral import plot_spectrum, plot_spectrogram
9
+ >>> plot_spectrum(trace)
10
+ >>> plot_spectrogram(trace)
11
+
12
+ References:
13
+ matplotlib best practices for scientific visualization
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from pathlib import Path
19
+ from typing import TYPE_CHECKING, Any, Literal, cast
20
+
21
+ import numpy as np
22
+
23
+ try:
24
+ import matplotlib.pyplot as plt
25
+ from matplotlib.colors import Normalize # noqa: F401
26
+
27
+ HAS_MATPLOTLIB = True
28
+ except ImportError:
29
+ HAS_MATPLOTLIB = False
30
+
31
+
32
+ if TYPE_CHECKING:
33
+ from matplotlib.axes import Axes
34
+ from matplotlib.figure import Figure
35
+ from numpy.typing import NDArray
36
+
37
+ from oscura.core.types import WaveformTrace
38
+
39
+
40
+ def plot_spectrum(
41
+ trace: WaveformTrace,
42
+ *,
43
+ ax: Axes | None = None,
44
+ freq_unit: str = "auto",
45
+ db_ref: float | None = None,
46
+ freq_range: tuple[float, float] | None = None,
47
+ show_grid: bool = True,
48
+ color: str = "C0",
49
+ title: str | None = None,
50
+ window: str = "hann",
51
+ xscale: Literal["linear", "log"] = "log",
52
+ show: bool = True,
53
+ save_path: str | None = None,
54
+ figsize: tuple[float, float] = (10, 6),
55
+ xlim: tuple[float, float] | None = None,
56
+ ylim: tuple[float, float] | None = None,
57
+ fft_result: tuple[Any, Any] | None = None,
58
+ log_scale: bool = True,
59
+ db_scale: bool | None = None,
60
+ ) -> Figure:
61
+ """Plot magnitude spectrum.
62
+
63
+ Args:
64
+ trace: Waveform trace to analyze.
65
+ ax: Matplotlib axes. If None, creates new figure.
66
+ freq_unit: Frequency unit ("Hz", "kHz", "MHz", "auto").
67
+ db_ref: Reference for dB scaling. If None, uses max value.
68
+ freq_range: Frequency range (min, max) in Hz to display.
69
+ show_grid: Show grid lines.
70
+ color: Line color.
71
+ title: Plot title.
72
+ window: Window function for FFT.
73
+ xscale: X-axis scale ("linear" or "log"). Deprecated, use log_scale instead.
74
+ show: If True, call plt.show() to display the plot.
75
+ save_path: Path to save the figure. If None, figure is not saved.
76
+ figsize: Figure size (width, height) in inches. Only used if ax is None.
77
+ xlim: X-axis limits (min, max) in selected frequency units.
78
+ ylim: Y-axis limits (min, max) in dB.
79
+ fft_result: Pre-computed FFT result (frequencies, magnitudes). If None, computes FFT.
80
+ log_scale: Use logarithmic scale for frequency axis (default True).
81
+ db_scale: Deprecated alias for log_scale. If provided, overrides log_scale.
82
+
83
+ Returns:
84
+ Matplotlib Figure object.
85
+
86
+ Raises:
87
+ ImportError: If matplotlib is not installed.
88
+ ValueError: If axes must have an associated figure.
89
+
90
+ Example:
91
+ >>> import oscura as tk
92
+ >>> trace = tk.load("signal.wfm")
93
+ >>> fig = tk.plot_spectrum(trace, freq_unit="MHz", log_scale=True)
94
+
95
+ >>> # With pre-computed FFT
96
+ >>> freq, mag = tk.fft(trace)
97
+ >>> fig = tk.plot_spectrum(trace, fft_result=(freq, mag), show=False)
98
+ >>> fig.savefig("spectrum.png")
99
+ """
100
+ if not HAS_MATPLOTLIB:
101
+ raise ImportError("matplotlib is required for visualization")
102
+
103
+ # Handle deprecated db_scale parameter
104
+ if db_scale is not None:
105
+ log_scale = db_scale
106
+
107
+ from oscura.analyzers.waveform.spectral import fft
108
+
109
+ if ax is None:
110
+ fig, ax = plt.subplots(figsize=figsize)
111
+ else:
112
+ fig_temp = ax.get_figure()
113
+ if fig_temp is None:
114
+ raise ValueError("Axes must have an associated figure")
115
+ fig = cast("Figure", fig_temp)
116
+
117
+ # Compute FFT if not provided
118
+ if fft_result is not None:
119
+ freq, mag_db = fft_result
120
+ else:
121
+ freq, mag_db = fft(trace, window=window) # type: ignore[misc]
122
+
123
+ # Auto-select frequency unit
124
+ if freq_unit == "auto":
125
+ max_freq = freq[-1]
126
+ if max_freq >= 1e9:
127
+ freq_unit = "GHz"
128
+ elif max_freq >= 1e6:
129
+ freq_unit = "MHz"
130
+ elif max_freq >= 1e3:
131
+ freq_unit = "kHz"
132
+ else:
133
+ freq_unit = "Hz"
134
+
135
+ freq_divisors = {"Hz": 1.0, "kHz": 1e3, "MHz": 1e6, "GHz": 1e9}
136
+ divisor = freq_divisors.get(freq_unit, 1.0)
137
+ freq_scaled = freq / divisor
138
+
139
+ # Adjust dB reference if specified
140
+ if db_ref is not None:
141
+ mag_db = mag_db - db_ref
142
+
143
+ # Plot
144
+ ax.plot(freq_scaled, mag_db, color=color, linewidth=0.8)
145
+
146
+ ax.set_xlabel(f"Frequency ({freq_unit})")
147
+ ax.set_ylabel("Magnitude (dB)")
148
+
149
+ # Use log_scale parameter, fall back to xscale for backward compatibility
150
+ # Note: xscale is Literal["linear", "log"] so can never be "log" at this point
151
+ ax.set_xscale("log" if log_scale else "linear")
152
+
153
+ if title:
154
+ ax.set_title(title)
155
+ else:
156
+ ax.set_title("Magnitude Spectrum")
157
+
158
+ if show_grid:
159
+ ax.grid(True, alpha=0.3, which="both")
160
+
161
+ # Set reasonable y-limits
162
+ valid_db = mag_db[np.isfinite(mag_db)]
163
+ if len(valid_db) > 0:
164
+ y_max = np.max(valid_db)
165
+ y_min = max(np.min(valid_db), y_max - 120) # Limit dynamic range
166
+ ax.set_ylim(y_min, y_max + 5)
167
+
168
+ # Apply custom limits if specified
169
+ if freq_range is not None:
170
+ ax.set_xlim(freq_range[0] / divisor, freq_range[1] / divisor)
171
+ elif xlim is not None:
172
+ ax.set_xlim(xlim)
173
+
174
+ if ylim is not None:
175
+ ax.set_ylim(ylim)
176
+
177
+ fig.tight_layout()
178
+
179
+ # Save if path provided
180
+ if save_path is not None:
181
+ fig.savefig(save_path, dpi=300, bbox_inches="tight")
182
+
183
+ # Show if requested
184
+ if show:
185
+ plt.show()
186
+
187
+ return fig
188
+
189
+
190
+ def plot_spectrogram(
191
+ trace: WaveformTrace,
192
+ *,
193
+ ax: Axes | None = None,
194
+ time_unit: str = "auto",
195
+ freq_unit: str = "auto",
196
+ cmap: str = "viridis",
197
+ vmin: float | None = None,
198
+ vmax: float | None = None,
199
+ title: str | None = None,
200
+ window: str = "hann",
201
+ nperseg: int | None = None,
202
+ nfft: int | None = None,
203
+ overlap: float | None = None,
204
+ ) -> Figure:
205
+ """Plot spectrogram (time-frequency representation).
206
+
207
+ Args:
208
+ trace: Waveform trace to analyze.
209
+ ax: Matplotlib axes. If None, creates new figure.
210
+ time_unit: Time unit ("s", "ms", "us", "auto").
211
+ freq_unit: Frequency unit ("Hz", "kHz", "MHz", "auto").
212
+ cmap: Colormap name.
213
+ vmin: Minimum dB value for color scaling.
214
+ vmax: Maximum dB value for color scaling.
215
+ title: Plot title.
216
+ window: Window function.
217
+ nperseg: Segment length for STFT.
218
+ nfft: FFT length. If specified, overrides nperseg.
219
+ overlap: Overlap fraction (0.0 to 1.0). Default is 0.5 (50%).
220
+
221
+ Returns:
222
+ Matplotlib Figure object.
223
+
224
+ Raises:
225
+ ImportError: If matplotlib is not installed.
226
+ ValueError: If axes must have an associated figure.
227
+
228
+ Example:
229
+ >>> fig = plot_spectrogram(trace)
230
+ >>> plt.show()
231
+ """
232
+ if not HAS_MATPLOTLIB:
233
+ raise ImportError("matplotlib is required for visualization")
234
+
235
+ from oscura.analyzers.waveform.spectral import spectrogram
236
+
237
+ if ax is None:
238
+ fig, ax = plt.subplots(figsize=(10, 4))
239
+ else:
240
+ fig_temp = ax.get_figure()
241
+ if fig_temp is None:
242
+ raise ValueError("Axes must have an associated figure")
243
+ fig = cast("Figure", fig_temp)
244
+
245
+ # Handle nfft as alias for nperseg
246
+ if nfft is not None:
247
+ nperseg = nfft
248
+
249
+ # Compute spectrogram with optional overlap
250
+ noverlap = None
251
+ if overlap is not None and nperseg is not None:
252
+ noverlap = int(nperseg * overlap)
253
+ times, freq, Sxx_db = spectrogram(trace, window=window, nperseg=nperseg, noverlap=noverlap)
254
+
255
+ # Auto-select units
256
+ if time_unit == "auto":
257
+ max_time = times[-1] if len(times) > 0 else 0
258
+ if max_time < 1e-6:
259
+ time_unit = "ns"
260
+ elif max_time < 1e-3:
261
+ time_unit = "us"
262
+ elif max_time < 1:
263
+ time_unit = "ms"
264
+ else:
265
+ time_unit = "s"
266
+
267
+ if freq_unit == "auto":
268
+ max_freq = freq[-1] if len(freq) > 0 else 0
269
+ if max_freq >= 1e9:
270
+ freq_unit = "GHz"
271
+ elif max_freq >= 1e6:
272
+ freq_unit = "MHz"
273
+ elif max_freq >= 1e3:
274
+ freq_unit = "kHz"
275
+ else:
276
+ freq_unit = "Hz"
277
+
278
+ time_multipliers = {"s": 1.0, "ms": 1e3, "us": 1e6, "ns": 1e9}
279
+ freq_divisors = {"Hz": 1.0, "kHz": 1e3, "MHz": 1e6, "GHz": 1e9}
280
+
281
+ time_mult = time_multipliers.get(time_unit, 1.0)
282
+ freq_div = freq_divisors.get(freq_unit, 1.0)
283
+
284
+ times_scaled = times * time_mult
285
+ freq_scaled = freq / freq_div
286
+
287
+ # Auto color limits
288
+ if vmin is None or vmax is None:
289
+ valid_db = Sxx_db[np.isfinite(Sxx_db)]
290
+ if len(valid_db) > 0:
291
+ if vmax is None:
292
+ vmax = np.max(valid_db)
293
+ if vmin is None:
294
+ vmin = max(np.min(valid_db), vmax - 80)
295
+
296
+ # Plot
297
+ pcm = ax.pcolormesh(
298
+ times_scaled,
299
+ freq_scaled,
300
+ Sxx_db,
301
+ shading="auto",
302
+ cmap=cmap,
303
+ vmin=vmin,
304
+ vmax=vmax,
305
+ )
306
+
307
+ ax.set_xlabel(f"Time ({time_unit})")
308
+ ax.set_ylabel(f"Frequency ({freq_unit})")
309
+
310
+ if title:
311
+ ax.set_title(title)
312
+ else:
313
+ ax.set_title("Spectrogram")
314
+
315
+ # Colorbar
316
+ cbar = fig.colorbar(pcm, ax=ax)
317
+ cbar.set_label("Magnitude (dB)")
318
+
319
+ fig.tight_layout()
320
+ return fig
321
+
322
+
323
+ def plot_psd(
324
+ trace: WaveformTrace,
325
+ *,
326
+ ax: Axes | None = None,
327
+ freq_unit: str = "auto",
328
+ show_grid: bool = True,
329
+ color: str = "C0",
330
+ title: str | None = None,
331
+ window: str = "hann",
332
+ xscale: Literal["linear", "log"] = "log",
333
+ ) -> Figure:
334
+ """Plot Power Spectral Density.
335
+
336
+ Args:
337
+ trace: Waveform trace to analyze.
338
+ ax: Matplotlib axes.
339
+ freq_unit: Frequency unit.
340
+ show_grid: Show grid lines.
341
+ color: Line color.
342
+ title: Plot title.
343
+ window: Window function.
344
+ xscale: X-axis scale.
345
+
346
+ Returns:
347
+ Matplotlib Figure object.
348
+
349
+ Raises:
350
+ ImportError: If matplotlib is not installed.
351
+ ValueError: If axes must have an associated figure.
352
+
353
+ Example:
354
+ >>> fig = plot_psd(trace)
355
+ >>> plt.show()
356
+ """
357
+ if not HAS_MATPLOTLIB:
358
+ raise ImportError("matplotlib is required for visualization")
359
+
360
+ from oscura.analyzers.waveform.spectral import psd
361
+
362
+ if ax is None:
363
+ fig, ax = plt.subplots(figsize=(10, 4))
364
+ else:
365
+ fig_temp = ax.get_figure()
366
+ if fig_temp is None:
367
+ raise ValueError("Axes must have an associated figure")
368
+ fig = cast("Figure", fig_temp)
369
+
370
+ # Compute PSD
371
+ freq, psd_db = psd(trace, window=window)
372
+
373
+ # Auto-select frequency unit
374
+ if freq_unit == "auto":
375
+ max_freq = freq[-1]
376
+ if max_freq >= 1e9:
377
+ freq_unit = "GHz"
378
+ elif max_freq >= 1e6:
379
+ freq_unit = "MHz"
380
+ elif max_freq >= 1e3:
381
+ freq_unit = "kHz"
382
+ else:
383
+ freq_unit = "Hz"
384
+
385
+ freq_divisors = {"Hz": 1.0, "kHz": 1e3, "MHz": 1e6, "GHz": 1e9}
386
+ divisor = freq_divisors.get(freq_unit, 1.0)
387
+ freq_scaled = freq / divisor
388
+
389
+ # Plot
390
+ ax.plot(freq_scaled, psd_db, color=color, linewidth=0.8)
391
+
392
+ ax.set_xlabel(f"Frequency ({freq_unit})")
393
+ ax.set_ylabel("PSD (dB/Hz)")
394
+ ax.set_xscale(xscale)
395
+
396
+ if title:
397
+ ax.set_title(title)
398
+ else:
399
+ ax.set_title("Power Spectral Density")
400
+
401
+ if show_grid:
402
+ ax.grid(True, alpha=0.3, which="both")
403
+
404
+ fig.tight_layout()
405
+ return fig
406
+
407
+
408
+ def plot_fft(
409
+ trace: WaveformTrace,
410
+ *,
411
+ ax: Axes | None = None,
412
+ show: bool = True,
413
+ save_path: str | None = None,
414
+ title: str | None = None,
415
+ xlabel: str = "Frequency",
416
+ ylabel: str = "Magnitude (dB)",
417
+ figsize: tuple[float, float] = (10, 6),
418
+ freq_unit: str = "auto",
419
+ log_scale: bool = True,
420
+ show_grid: bool = True,
421
+ color: str = "C0",
422
+ window: str = "hann",
423
+ xlim: tuple[float, float] | None = None,
424
+ ylim: tuple[float, float] | None = None,
425
+ ) -> Figure:
426
+ """Plot FFT magnitude spectrum.
427
+
428
+ Computes and plots the FFT magnitude spectrum of a waveform trace.
429
+ This is a convenience function that combines FFT computation and visualization.
430
+
431
+ Args:
432
+ trace: Waveform trace to analyze and plot.
433
+ ax: Matplotlib axes. If None, creates new figure.
434
+ show: If True, call plt.show() to display the plot.
435
+ save_path: Path to save the figure. If None, figure is not saved.
436
+ title: Plot title. If None, uses default "FFT Magnitude Spectrum".
437
+ xlabel: X-axis label (appended with frequency unit).
438
+ ylabel: Y-axis label.
439
+ figsize: Figure size (width, height) in inches. Only used if ax is None.
440
+ freq_unit: Frequency unit ("Hz", "kHz", "MHz", "GHz", "auto").
441
+ log_scale: Use logarithmic scale for frequency axis.
442
+ show_grid: Show grid lines.
443
+ color: Line color.
444
+ window: Window function for FFT computation.
445
+ xlim: X-axis limits (min, max) in selected frequency units.
446
+ ylim: Y-axis limits (min, max) in dB.
447
+
448
+ Returns:
449
+ Matplotlib Figure object.
450
+
451
+ Raises:
452
+ ImportError: If matplotlib is not installed.
453
+ ValueError: If axes must have an associated figure.
454
+
455
+ Example:
456
+ >>> import oscura as tk
457
+ >>> trace = tk.load("signal.wfm")
458
+ >>> fig = tk.plot_fft(trace, freq_unit="MHz", show=False)
459
+ >>> fig.savefig("spectrum.png")
460
+
461
+ >>> # With custom styling
462
+ >>> fig = tk.plot_fft(trace,
463
+ ... title="Signal FFT",
464
+ ... log_scale=True,
465
+ ... xlim=(1e3, 1e6),
466
+ ... ylim=(-100, 0))
467
+
468
+ References:
469
+ IEEE 1241-2010: Standard for Terminology and Test Methods for
470
+ Analog-to-Digital Converters
471
+ """
472
+ if not HAS_MATPLOTLIB:
473
+ raise ImportError("matplotlib is required for visualization")
474
+
475
+ # Create figure if needed
476
+ if ax is None:
477
+ fig, ax = plt.subplots(figsize=figsize)
478
+ else:
479
+ fig_temp = ax.get_figure()
480
+ if fig_temp is None:
481
+ raise ValueError("Axes must have an associated figure")
482
+ fig = cast("Figure", fig_temp)
483
+
484
+ # Use plot_spectrum to do the actual plotting
485
+ xscale_value: Literal["linear", "log"] = "log" if log_scale else "linear"
486
+ plot_spectrum(
487
+ trace,
488
+ ax=ax,
489
+ freq_unit=freq_unit,
490
+ show_grid=show_grid,
491
+ color=color,
492
+ title=title if title else "FFT Magnitude Spectrum",
493
+ window=window,
494
+ xscale=xscale_value,
495
+ )
496
+
497
+ # Apply custom labels if different from defaults
498
+ if xlabel != "Frequency":
499
+ # Get current label to preserve unit
500
+ current_label = ax.get_xlabel()
501
+ if "(" in current_label and ")" in current_label:
502
+ unit = current_label[current_label.find("(") : current_label.find(")") + 1]
503
+ ax.set_xlabel(f"{xlabel} {unit}")
504
+ else:
505
+ ax.set_xlabel(xlabel)
506
+
507
+ if ylabel != "Magnitude (dB)":
508
+ ax.set_ylabel(ylabel)
509
+
510
+ # Apply custom limits if specified
511
+ if xlim is not None:
512
+ ax.set_xlim(xlim)
513
+
514
+ if ylim is not None:
515
+ ax.set_ylim(ylim)
516
+
517
+ # Save if path provided
518
+ if save_path is not None:
519
+ fig.savefig(save_path, dpi=300, bbox_inches="tight")
520
+
521
+ # Show if requested
522
+ if show:
523
+ plt.show()
524
+
525
+ return fig
526
+
527
+
528
+ def plot_thd_bars(
529
+ harmonic_magnitudes: NDArray[np.floating[Any]],
530
+ *,
531
+ fundamental_freq: float | None = None,
532
+ ax: Axes | None = None,
533
+ figsize: tuple[float, float] = (10, 6),
534
+ title: str | None = None,
535
+ thd_value: float | None = None,
536
+ show_thd: bool = True,
537
+ reference_db: float = 0.0,
538
+ show: bool = True,
539
+ save_path: str | Path | None = None,
540
+ ) -> Figure:
541
+ """Plot THD harmonic bar chart.
542
+
543
+ Creates a bar chart showing harmonic content relative to the fundamental,
544
+ useful for visualizing Total Harmonic Distortion analysis results.
545
+
546
+ Args:
547
+ harmonic_magnitudes: Array of harmonic magnitudes in dB (relative to fundamental).
548
+ Index 0 = fundamental (0 dB), Index 1 = 2nd harmonic, etc.
549
+ fundamental_freq: Fundamental frequency in Hz (for x-axis labels).
550
+ ax: Matplotlib axes. If None, creates new figure.
551
+ figsize: Figure size in inches.
552
+ title: Plot title.
553
+ thd_value: Pre-calculated THD value in dB or % to display.
554
+ show_thd: Show THD annotation on plot.
555
+ reference_db: Reference level for the fundamental (default 0 dB).
556
+ show: Display plot interactively.
557
+ save_path: Save plot to file.
558
+
559
+ Returns:
560
+ Matplotlib Figure object.
561
+
562
+ Example:
563
+ >>> # Harmonic magnitudes relative to fundamental (in dB)
564
+ >>> harmonics = np.array([0, -40, -60, -55, -70, -65]) # Fund, H2, H3, H4, H5, H6
565
+ >>> fig = plot_thd_bars(harmonics, fundamental_freq=1000, thd_value=-38.5)
566
+
567
+ References:
568
+ IEEE 1241-2010: ADC Testing Standards
569
+ IEC 61000-4-7: Harmonics measurement
570
+ """
571
+ if not HAS_MATPLOTLIB:
572
+ raise ImportError("matplotlib is required for visualization")
573
+
574
+ if ax is None:
575
+ fig, ax = plt.subplots(figsize=figsize)
576
+ else:
577
+ fig_temp = ax.get_figure()
578
+ if fig_temp is None:
579
+ raise ValueError("Axes must have an associated figure")
580
+ fig = cast("Figure", fig_temp)
581
+
582
+ n_harmonics = len(harmonic_magnitudes)
583
+
584
+ # Create x-positions for harmonics
585
+ x_pos = np.arange(n_harmonics)
586
+
587
+ # Create labels
588
+ if fundamental_freq is not None:
589
+ labels = [
590
+ f"H{i + 1}\n({(i + 1) * fundamental_freq / 1e3:.1f} kHz)"
591
+ if fundamental_freq >= 1000
592
+ else f"H{i + 1}\n({(i + 1) * fundamental_freq:.0f} Hz)"
593
+ for i in range(n_harmonics)
594
+ ]
595
+ labels[0] = (
596
+ f"Fund\n({fundamental_freq / 1e3:.1f} kHz)"
597
+ if fundamental_freq >= 1000
598
+ else f"Fund\n({fundamental_freq:.0f} Hz)"
599
+ )
600
+ else:
601
+ labels = [f"H{i + 1}" for i in range(n_harmonics)]
602
+ labels[0] = "Fund"
603
+
604
+ # Color code: fundamental in blue, harmonics in orange/red based on magnitude
605
+ colors = []
606
+ for i, mag in enumerate(harmonic_magnitudes):
607
+ if i == 0:
608
+ colors.append("#3498DB") # Blue for fundamental
609
+ elif mag > -30:
610
+ colors.append("#E74C3C") # Red for significant harmonics
611
+ elif mag > -50:
612
+ colors.append("#F39C12") # Orange for moderate
613
+ else:
614
+ colors.append("#95A5A6") # Gray for low
615
+
616
+ # Plot bars
617
+ ax.bar(
618
+ x_pos, harmonic_magnitudes - reference_db, color=colors, edgecolor="black", linewidth=0.5
619
+ )
620
+
621
+ # Reference line at fundamental level
622
+ ax.axhline(0, color="gray", linestyle="--", linewidth=1, alpha=0.7)
623
+
624
+ # THD annotation
625
+ if show_thd and thd_value is not None:
626
+ # Position in upper right
627
+ if thd_value > 0:
628
+ thd_text = f"THD: {thd_value:.2f}%"
629
+ else:
630
+ thd_text = f"THD: {thd_value:.1f} dB"
631
+
632
+ ax.text(
633
+ 0.98,
634
+ 0.98,
635
+ thd_text,
636
+ transform=ax.transAxes,
637
+ fontsize=12,
638
+ fontweight="bold",
639
+ ha="right",
640
+ va="top",
641
+ bbox={"boxstyle": "round,pad=0.5", "facecolor": "wheat", "alpha": 0.9},
642
+ )
643
+
644
+ # Labels
645
+ ax.set_xticks(x_pos)
646
+ ax.set_xticklabels(labels, fontsize=9)
647
+ ax.set_xlabel("Harmonic", fontsize=11)
648
+ ax.set_ylabel("Magnitude (dB rel. to fundamental)", fontsize=11)
649
+ ax.grid(True, axis="y", alpha=0.3)
650
+
651
+ # Y-axis limits
652
+ min_mag = min(harmonic_magnitudes) - reference_db
653
+ ax.set_ylim(min(min_mag - 10, -80), 10)
654
+
655
+ if title:
656
+ ax.set_title(title, fontsize=12, fontweight="bold")
657
+ else:
658
+ ax.set_title("Harmonic Distortion Analysis", fontsize=12, fontweight="bold")
659
+
660
+ fig.tight_layout()
661
+
662
+ if save_path is not None:
663
+ fig.savefig(save_path, dpi=300, bbox_inches="tight")
664
+
665
+ if show:
666
+ plt.show()
667
+
668
+ return fig
669
+
670
+
671
+ def plot_quality_summary(
672
+ metrics: dict[str, float],
673
+ *,
674
+ ax: Axes | None = None,
675
+ figsize: tuple[float, float] = (10, 6),
676
+ title: str | None = None,
677
+ show_specs: dict[str, float] | None = None,
678
+ show: bool = True,
679
+ save_path: str | Path | None = None,
680
+ ) -> Figure:
681
+ """Plot ADC/signal quality summary with metrics.
682
+
683
+ Creates a summary panel showing SNR, SINAD, THD, ENOB, and SFDR
684
+ with optional pass/fail indication against specifications.
685
+
686
+ Args:
687
+ metrics: Dictionary with keys like "snr", "sinad", "thd", "enob", "sfdr".
688
+ ax: Matplotlib axes.
689
+ figsize: Figure size.
690
+ title: Plot title.
691
+ show_specs: Dictionary of specification values for pass/fail.
692
+ show: Display plot.
693
+ save_path: Save path.
694
+
695
+ Returns:
696
+ Matplotlib Figure object.
697
+
698
+ Example:
699
+ >>> metrics = {"snr": 72.5, "sinad": 70.2, "thd": -65.3, "enob": 11.2, "sfdr": 75.8}
700
+ >>> specs = {"snr": 70.0, "enob": 10.0}
701
+ >>> fig = plot_quality_summary(metrics, show_specs=specs)
702
+
703
+ References:
704
+ IEEE 1241-2010: ADC Testing Standards
705
+ """
706
+ if not HAS_MATPLOTLIB:
707
+ raise ImportError("matplotlib is required for visualization")
708
+
709
+ if ax is None:
710
+ fig, ax = plt.subplots(figsize=figsize)
711
+ else:
712
+ fig_temp = ax.get_figure()
713
+ if fig_temp is None:
714
+ raise ValueError("Axes must have an associated figure")
715
+ fig = cast("Figure", fig_temp)
716
+
717
+ # Define metric display info
718
+ metric_info = {
719
+ "snr": {"name": "SNR", "unit": "dB", "higher_better": True},
720
+ "sinad": {"name": "SINAD", "unit": "dB", "higher_better": True},
721
+ "thd": {
722
+ "name": "THD",
723
+ "unit": "dB",
724
+ "higher_better": False,
725
+ }, # Lower (more negative) is better
726
+ "enob": {"name": "ENOB", "unit": "bits", "higher_better": True},
727
+ "sfdr": {"name": "SFDR", "unit": "dBc", "higher_better": True},
728
+ }
729
+
730
+ # Filter to available metrics
731
+ available_metrics = [(k, v) for k, v in metrics.items() if k in metric_info]
732
+ n_metrics = len(available_metrics)
733
+
734
+ if n_metrics == 0:
735
+ ax.text(0.5, 0.5, "No metrics available", ha="center", va="center", fontsize=14)
736
+ ax.axis("off")
737
+ return fig
738
+
739
+ # Create horizontal bar chart
740
+ y_pos = np.arange(n_metrics)
741
+ values = [v for _, v in available_metrics]
742
+ names = [metric_info[k]["name"] for k, _ in available_metrics]
743
+
744
+ # Determine colors based on pass/fail
745
+ colors = []
746
+ for key, value in available_metrics:
747
+ if show_specs and key in show_specs:
748
+ spec = show_specs[key]
749
+ info = metric_info[key]
750
+ if info["higher_better"]:
751
+ passed = value >= spec
752
+ else:
753
+ # For THD, more negative is better
754
+ passed = value <= spec
755
+ colors.append("#27AE60" if passed else "#E74C3C")
756
+ else:
757
+ colors.append("#3498DB")
758
+
759
+ # Plot horizontal bars
760
+ ax.barh(y_pos, values, color=colors, edgecolor="black", linewidth=0.5)
761
+
762
+ # Add value labels
763
+ for i, (key, value) in enumerate(available_metrics):
764
+ unit = metric_info[key]["unit"]
765
+ label_text = f"{value:.1f} {unit}"
766
+ ax.text(
767
+ value + 2 if value >= 0 else value - 2,
768
+ i,
769
+ label_text,
770
+ va="center",
771
+ ha="left" if value >= 0 else "right",
772
+ fontsize=10,
773
+ fontweight="bold",
774
+ )
775
+
776
+ # Add spec markers
777
+ if show_specs:
778
+ for i, (key, _) in enumerate(available_metrics):
779
+ if key in show_specs:
780
+ spec = show_specs[key]
781
+ ax.plot(spec, i, "k|", markersize=20, markeredgewidth=2)
782
+ ax.text(spec, i + 0.3, f"Spec: {spec}", fontsize=8, ha="center")
783
+
784
+ ax.set_yticks(y_pos)
785
+ ax.set_yticklabels([str(name) for name in names], fontsize=11)
786
+ ax.set_xlabel("Value", fontsize=11)
787
+ ax.grid(True, axis="x", alpha=0.3)
788
+ ax.invert_yaxis()
789
+
790
+ if title:
791
+ ax.set_title(title, fontsize=12, fontweight="bold")
792
+ else:
793
+ ax.set_title("Signal Quality Summary (IEEE 1241-2010)", fontsize=12, fontweight="bold")
794
+
795
+ fig.tight_layout()
796
+
797
+ if save_path is not None:
798
+ fig.savefig(save_path, dpi=300, bbox_inches="tight")
799
+
800
+ if show:
801
+ plt.show()
802
+
803
+ return fig
804
+
805
+
806
+ __all__ = [
807
+ "plot_fft",
808
+ "plot_psd",
809
+ "plot_spectrogram",
810
+ "plot_spectrum",
811
+ ]