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,731 @@
1
+ """Plot generation for comprehensive analysis reports.
2
+
3
+ This module provides intelligent plot generation for different analysis domains,
4
+ using the existing visualization library and returning figures for the OutputManager
5
+ to save.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import logging
11
+ from collections.abc import Callable
12
+ from pathlib import Path
13
+ from typing import TYPE_CHECKING, Any
14
+
15
+ import numpy as np
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+ try:
20
+ import matplotlib
21
+ import matplotlib.pyplot as plt
22
+
23
+ # Use non-interactive backend for automated plot generation
24
+ matplotlib.use("Agg")
25
+ HAS_MATPLOTLIB = True
26
+ except ImportError:
27
+ HAS_MATPLOTLIB = False
28
+
29
+ if TYPE_CHECKING:
30
+ from matplotlib.figure import Figure
31
+
32
+ from oscura.reporting.config import AnalysisConfig, AnalysisDomain
33
+ from oscura.reporting.output import OutputManager
34
+
35
+
36
+ class PlotGenerator:
37
+ """Generates visualization plots from analysis results.
38
+
39
+ Intelligently creates appropriate plots for each analysis domain based on
40
+ available data. Uses the existing oscura.visualization module and returns
41
+ matplotlib Figure objects for the OutputManager to save.
42
+
43
+ Attributes:
44
+ config: Analysis configuration (optional, for plot settings).
45
+
46
+ Requirements:
47
+
48
+ Example:
49
+ >>> config = AnalysisConfig(plot_format="png", plot_dpi=150)
50
+ >>> generator = PlotGenerator(config)
51
+ >>> paths = generator.generate_plots(
52
+ ... AnalysisDomain.SPECTRAL,
53
+ ... {"fft": {"frequencies": freq, "magnitude_db": mag}},
54
+ ... output_manager
55
+ ... )
56
+ """
57
+
58
+ def __init__(self, config: AnalysisConfig | None = None) -> None:
59
+ """Initialize plot generator.
60
+
61
+ Args:
62
+ config: Analysis configuration for plot settings (format, DPI, etc.).
63
+ If None, uses defaults.
64
+
65
+ Raises:
66
+ ImportError: If matplotlib is not installed.
67
+ """
68
+ if not HAS_MATPLOTLIB:
69
+ raise ImportError("matplotlib is required for plot generation")
70
+
71
+ self.config = config
72
+
73
+ def generate_plots(
74
+ self,
75
+ domain: AnalysisDomain,
76
+ results: dict[str, Any],
77
+ output_manager: OutputManager,
78
+ ) -> list[Path]:
79
+ """Generate all appropriate plots for an analysis domain.
80
+
81
+ Inspects the results dictionary and generates appropriate visualization
82
+ plots based on the domain and available data. Returns list of saved
83
+ plot paths.
84
+
85
+ Args:
86
+ domain: Analysis domain (e.g., SPECTRAL, WAVEFORM, DIGITAL).
87
+ results: Dictionary of analysis results for this domain.
88
+ output_manager: OutputManager instance for saving plots.
89
+
90
+ Returns:
91
+ List of paths to saved plot files.
92
+
93
+ Example:
94
+ >>> results = {
95
+ ... "fft": {"frequencies": freq_array, "magnitude_db": mag_array},
96
+ ... "psd": {"frequencies": freq_array, "psd": psd_array}
97
+ ... }
98
+ >>> paths = generator.generate_plots(
99
+ ... AnalysisDomain.SPECTRAL,
100
+ ... results,
101
+ ... output_manager
102
+ ... )
103
+ """
104
+ from oscura.reporting.config import AnalysisDomain
105
+
106
+ # Get plot format and DPI from config
107
+ plot_format = self.config.plot_format if self.config else "png"
108
+ plot_dpi = self.config.plot_dpi if self.config else 150
109
+
110
+ saved_paths: list[Path] = []
111
+
112
+ for analysis_name, result_data in results.items():
113
+ # Skip non-dict results
114
+ if not isinstance(result_data, dict):
115
+ continue
116
+
117
+ # Check if we have a registered plot function
118
+ key = (domain, analysis_name)
119
+ if key in PLOT_REGISTRY:
120
+ plot_func = PLOT_REGISTRY[key]
121
+ try:
122
+ fig = plot_func(result_data)
123
+ if fig is not None:
124
+ path = output_manager.save_plot(
125
+ domain,
126
+ analysis_name,
127
+ fig,
128
+ format=plot_format,
129
+ dpi=plot_dpi,
130
+ )
131
+ saved_paths.append(path)
132
+ plt.close(fig) # Prevent memory leaks
133
+ except Exception as e:
134
+ # Log error but continue with other plots
135
+ logger.warning("Failed to generate %s plot: %s", analysis_name, e)
136
+ continue
137
+
138
+ # Also try generic domain-level plots
139
+ try:
140
+ if domain == AnalysisDomain.SPECTRAL:
141
+ saved_paths.extend(
142
+ self._generate_spectral_plots(
143
+ results, domain, output_manager, plot_format, plot_dpi
144
+ )
145
+ )
146
+ elif domain == AnalysisDomain.WAVEFORM:
147
+ saved_paths.extend(
148
+ self._generate_waveform_plots(
149
+ results, domain, output_manager, plot_format, plot_dpi
150
+ )
151
+ )
152
+ elif domain == AnalysisDomain.DIGITAL:
153
+ saved_paths.extend(
154
+ self._generate_digital_plots(
155
+ results, domain, output_manager, plot_format, plot_dpi
156
+ )
157
+ )
158
+ elif domain == AnalysisDomain.STATISTICS:
159
+ saved_paths.extend(
160
+ self._generate_statistics_plots(
161
+ results, domain, output_manager, plot_format, plot_dpi
162
+ )
163
+ )
164
+ elif domain == AnalysisDomain.JITTER:
165
+ saved_paths.extend(
166
+ self._generate_jitter_plots(
167
+ results, domain, output_manager, plot_format, plot_dpi
168
+ )
169
+ )
170
+ elif domain == AnalysisDomain.EYE:
171
+ saved_paths.extend(
172
+ self._generate_eye_plots(results, domain, output_manager, plot_format, plot_dpi)
173
+ )
174
+ elif domain == AnalysisDomain.PATTERNS:
175
+ saved_paths.extend(
176
+ self._generate_pattern_plots(
177
+ results, domain, output_manager, plot_format, plot_dpi
178
+ )
179
+ )
180
+ elif domain == AnalysisDomain.POWER:
181
+ saved_paths.extend(
182
+ self._generate_power_plots(
183
+ results, domain, output_manager, plot_format, plot_dpi
184
+ )
185
+ )
186
+ except Exception as e:
187
+ logger.warning("Error in domain-level plot generation for %s: %s", domain.value, e)
188
+
189
+ return saved_paths
190
+
191
+ def _generate_spectral_plots(
192
+ self,
193
+ results: dict[str, Any],
194
+ domain: AnalysisDomain,
195
+ output_manager: OutputManager,
196
+ plot_format: str,
197
+ plot_dpi: int,
198
+ ) -> list[Path]:
199
+ """Generate spectral analysis plots (FFT, PSD, spectrogram)."""
200
+ paths: list[Path] = []
201
+
202
+ # FFT plot
203
+ if "fft" in results and isinstance(results["fft"], dict):
204
+ fft_data = results["fft"]
205
+ if "frequencies" in fft_data and "magnitude_db" in fft_data:
206
+ try:
207
+ fig = self._plot_spectrum(fft_data, title="FFT Magnitude Spectrum")
208
+ path = output_manager.save_plot(
209
+ domain, "fft_spectrum", fig, format=plot_format, dpi=plot_dpi
210
+ )
211
+ paths.append(path)
212
+ plt.close(fig)
213
+ except Exception:
214
+ pass
215
+
216
+ # PSD plot
217
+ if "psd" in results and isinstance(results["psd"], dict):
218
+ psd_data = results["psd"]
219
+ if "frequencies" in psd_data and "psd" in psd_data:
220
+ try:
221
+ fig = self._plot_spectrum(
222
+ psd_data, title="Power Spectral Density", ylabel="PSD (dB/Hz)"
223
+ )
224
+ path = output_manager.save_plot(
225
+ domain, "psd_spectrum", fig, format=plot_format, dpi=plot_dpi
226
+ )
227
+ paths.append(path)
228
+ plt.close(fig)
229
+ except Exception:
230
+ pass
231
+
232
+ # Spectrogram
233
+ if "spectrogram" in results and isinstance(results["spectrogram"], dict):
234
+ spec_data = results["spectrogram"]
235
+ if "times" in spec_data and "frequencies" in spec_data and "Sxx_db" in spec_data:
236
+ try:
237
+ fig = self._plot_spectrogram(spec_data)
238
+ path = output_manager.save_plot(
239
+ domain, "spectrogram", fig, format=plot_format, dpi=plot_dpi
240
+ )
241
+ paths.append(path)
242
+ plt.close(fig)
243
+ except Exception:
244
+ pass
245
+
246
+ return paths
247
+
248
+ def _generate_waveform_plots(
249
+ self,
250
+ results: dict[str, Any],
251
+ domain: AnalysisDomain,
252
+ output_manager: OutputManager,
253
+ plot_format: str,
254
+ plot_dpi: int,
255
+ ) -> list[Path]:
256
+ """Generate waveform analysis plots (time series, histograms)."""
257
+ paths: list[Path] = []
258
+
259
+ # Look for time-series data
260
+ for key in ["amplitude", "voltage", "signal", "data"]:
261
+ if key in results and isinstance(results[key], np.ndarray | list):
262
+ try:
263
+ fig = self._plot_time_series(
264
+ {"data": results[key]}, title=f"{key.title()} vs Time"
265
+ )
266
+ path = output_manager.save_plot(
267
+ domain, f"{key}_timeseries", fig, format=plot_format, dpi=plot_dpi
268
+ )
269
+ paths.append(path)
270
+ plt.close(fig)
271
+ break
272
+ except Exception:
273
+ pass
274
+
275
+ # Histogram of amplitudes
276
+ for key in ["amplitude", "voltage", "data"]:
277
+ if key in results and isinstance(results[key], np.ndarray | list):
278
+ try:
279
+ fig = self._plot_histogram(
280
+ {"data": results[key]}, title=f"{key.title()} Distribution"
281
+ )
282
+ path = output_manager.save_plot(
283
+ domain, f"{key}_histogram", fig, format=plot_format, dpi=plot_dpi
284
+ )
285
+ paths.append(path)
286
+ plt.close(fig)
287
+ break
288
+ except Exception:
289
+ pass
290
+
291
+ return paths
292
+
293
+ def _generate_digital_plots(
294
+ self,
295
+ results: dict[str, Any],
296
+ domain: AnalysisDomain,
297
+ output_manager: OutputManager,
298
+ plot_format: str,
299
+ plot_dpi: int,
300
+ ) -> list[Path]:
301
+ """Generate digital signal analysis plots (edges, timing)."""
302
+ paths: list[Path] = []
303
+
304
+ # Edge histogram (rise/fall time distribution)
305
+ if "edges" in results and isinstance(results["edges"], dict):
306
+ edges_data = results["edges"]
307
+ if "rise_times" in edges_data:
308
+ try:
309
+ fig = self._plot_histogram(
310
+ {"data": edges_data["rise_times"]}, title="Rise Time Distribution"
311
+ )
312
+ path = output_manager.save_plot(
313
+ domain, "rise_time_hist", fig, format=plot_format, dpi=plot_dpi
314
+ )
315
+ paths.append(path)
316
+ plt.close(fig)
317
+ except Exception:
318
+ pass
319
+
320
+ return paths
321
+
322
+ def _generate_statistics_plots(
323
+ self,
324
+ results: dict[str, Any],
325
+ domain: AnalysisDomain,
326
+ output_manager: OutputManager,
327
+ plot_format: str,
328
+ plot_dpi: int,
329
+ ) -> list[Path]:
330
+ """Generate statistical analysis plots (distributions, box plots)."""
331
+ paths: list[Path] = []
332
+
333
+ # Histogram of distribution
334
+ if "distribution" in results and isinstance(results["distribution"], dict):
335
+ dist_data = results["distribution"]
336
+ if "data" in dist_data:
337
+ try:
338
+ fig = self._plot_histogram(dist_data, title="Statistical Distribution")
339
+ path = output_manager.save_plot(
340
+ domain, "distribution", fig, format=plot_format, dpi=plot_dpi
341
+ )
342
+ paths.append(path)
343
+ plt.close(fig)
344
+ except Exception:
345
+ pass
346
+
347
+ return paths
348
+
349
+ def _generate_jitter_plots(
350
+ self,
351
+ results: dict[str, Any],
352
+ domain: AnalysisDomain,
353
+ output_manager: OutputManager,
354
+ plot_format: str,
355
+ plot_dpi: int,
356
+ ) -> list[Path]:
357
+ """Generate jitter analysis plots (TIE histogram, bathtub curve)."""
358
+ paths: list[Path] = []
359
+
360
+ # TIE (Time Interval Error) histogram
361
+ if "tie" in results and isinstance(results["tie"], np.ndarray | list):
362
+ try:
363
+ fig = self._plot_histogram(
364
+ {"data": results["tie"]}, title="Time Interval Error (TIE)"
365
+ )
366
+ path = output_manager.save_plot(
367
+ domain, "tie_histogram", fig, format=plot_format, dpi=plot_dpi
368
+ )
369
+ paths.append(path)
370
+ plt.close(fig)
371
+ except Exception:
372
+ pass
373
+
374
+ return paths
375
+
376
+ def _generate_eye_plots(
377
+ self,
378
+ results: dict[str, Any],
379
+ domain: AnalysisDomain,
380
+ output_manager: OutputManager,
381
+ plot_format: str,
382
+ plot_dpi: int,
383
+ ) -> list[Path]:
384
+ """Generate eye diagram plots."""
385
+ # Eye diagrams are typically generated by the analyzer itself
386
+ # This is a placeholder for future enhancements
387
+ return []
388
+
389
+ def _generate_pattern_plots(
390
+ self,
391
+ results: dict[str, Any],
392
+ domain: AnalysisDomain,
393
+ output_manager: OutputManager,
394
+ plot_format: str,
395
+ plot_dpi: int,
396
+ ) -> list[Path]:
397
+ """Generate pattern analysis plots (motifs, sequences)."""
398
+ paths: list[Path] = []
399
+
400
+ # Pattern occurrence histogram
401
+ if "patterns" in results and isinstance(results["patterns"], dict):
402
+ pattern_data = results["patterns"]
403
+ if "occurrences" in pattern_data:
404
+ try:
405
+ fig = self._plot_histogram(
406
+ {"data": pattern_data["occurrences"]}, title="Pattern Occurrences"
407
+ )
408
+ path = output_manager.save_plot(
409
+ domain, "pattern_occurrences", fig, format=plot_format, dpi=plot_dpi
410
+ )
411
+ paths.append(path)
412
+ plt.close(fig)
413
+ except Exception:
414
+ pass
415
+
416
+ return paths
417
+
418
+ def _generate_power_plots(
419
+ self,
420
+ results: dict[str, Any],
421
+ domain: AnalysisDomain,
422
+ output_manager: OutputManager,
423
+ plot_format: str,
424
+ plot_dpi: int,
425
+ ) -> list[Path]:
426
+ """Generate power analysis plots (power vs time, efficiency)."""
427
+ paths: list[Path] = []
428
+
429
+ # Power time series
430
+ if "power" in results and isinstance(results["power"], np.ndarray | list):
431
+ try:
432
+ fig = self._plot_time_series(
433
+ {"data": results["power"]}, title="Power vs Time", ylabel="Power (W)"
434
+ )
435
+ path = output_manager.save_plot(
436
+ domain, "power_timeseries", fig, format=plot_format, dpi=plot_dpi
437
+ )
438
+ paths.append(path)
439
+ plt.close(fig)
440
+ except Exception:
441
+ pass
442
+
443
+ return paths
444
+
445
+ # ============================================================================
446
+ # Individual plot methods
447
+ # ============================================================================
448
+
449
+ def _plot_spectrum(
450
+ self,
451
+ data: dict[str, Any],
452
+ title: str = "Spectrum",
453
+ ylabel: str = "Magnitude (dB)",
454
+ ) -> Figure:
455
+ """Plot frequency spectrum (FFT, PSD, etc.).
456
+
457
+ Args:
458
+ data: Dictionary with 'frequencies' and magnitude data.
459
+ title: Plot title.
460
+ ylabel: Y-axis label.
461
+
462
+ Returns:
463
+ Matplotlib Figure object.
464
+
465
+ Raises:
466
+ ValueError: If frequency/magnitude data is missing or empty.
467
+ """
468
+ fig, ax = plt.subplots(figsize=(10, 6))
469
+
470
+ frequencies = np.asarray(data.get("frequencies", []))
471
+ # Try multiple possible keys for magnitude data
472
+ magnitude = None
473
+ for key in ["magnitude_db", "psd", "magnitude", "power_db"]:
474
+ if key in data:
475
+ magnitude = np.asarray(data[key])
476
+ break
477
+
478
+ if magnitude is None or len(frequencies) == 0 or len(magnitude) == 0:
479
+ plt.close(fig)
480
+ raise ValueError("Missing or empty frequency/magnitude data")
481
+
482
+ # Auto-select frequency unit
483
+ max_freq = frequencies[-1] if len(frequencies) > 0 else 1.0
484
+ if max_freq >= 1e9:
485
+ freq_unit = "GHz"
486
+ freq_scale = 1e9
487
+ elif max_freq >= 1e6:
488
+ freq_unit = "MHz"
489
+ freq_scale = 1e6
490
+ elif max_freq >= 1e3:
491
+ freq_unit = "kHz"
492
+ freq_scale = 1e3
493
+ else:
494
+ freq_unit = "Hz"
495
+ freq_scale = 1.0
496
+
497
+ ax.plot(frequencies / freq_scale, magnitude, linewidth=0.8)
498
+ ax.set_xlabel(f"Frequency ({freq_unit})")
499
+ ax.set_ylabel(ylabel)
500
+ ax.set_title(title)
501
+ ax.grid(True, alpha=0.3, which="both")
502
+ ax.set_xscale("log")
503
+
504
+ fig.tight_layout()
505
+ return fig
506
+
507
+ def _plot_histogram(
508
+ self,
509
+ data: dict[str, Any],
510
+ title: str = "Histogram",
511
+ xlabel: str = "Value",
512
+ ) -> Figure:
513
+ """Plot histogram of data distribution.
514
+
515
+ Args:
516
+ data: Dictionary with 'data' array.
517
+ title: Plot title.
518
+ xlabel: X-axis label.
519
+
520
+ Returns:
521
+ Matplotlib Figure object.
522
+
523
+ Raises:
524
+ ValueError: If data array is empty or contains no finite values.
525
+ """
526
+ fig, ax = plt.subplots(figsize=(8, 6))
527
+
528
+ values = np.asarray(data.get("data", []))
529
+ if len(values) == 0:
530
+ plt.close(fig)
531
+ raise ValueError("Empty data array for histogram")
532
+
533
+ # Remove NaN/Inf values
534
+ values = values[np.isfinite(values)]
535
+ if len(values) == 0:
536
+ plt.close(fig)
537
+ raise ValueError("No finite values for histogram")
538
+
539
+ # Auto-select number of bins (Sturges' rule with limits)
540
+ n_bins = min(50, max(10, int(np.ceil(np.log2(len(values)) + 1))))
541
+
542
+ ax.hist(values, bins=n_bins, alpha=0.7, edgecolor="black")
543
+ ax.set_xlabel(xlabel)
544
+ ax.set_ylabel("Count")
545
+ ax.set_title(title)
546
+ ax.grid(True, alpha=0.3, axis="y")
547
+
548
+ fig.tight_layout()
549
+ return fig
550
+
551
+ def _plot_time_series(
552
+ self,
553
+ data: dict[str, Any],
554
+ title: str = "Time Series",
555
+ ylabel: str = "Amplitude",
556
+ ) -> Figure:
557
+ """Plot time-domain data.
558
+
559
+ Args:
560
+ data: Dictionary with 'data' and optionally 'time' arrays.
561
+ title: Plot title.
562
+ ylabel: Y-axis label.
563
+
564
+ Returns:
565
+ Matplotlib Figure object.
566
+
567
+ Raises:
568
+ ValueError: If data array is empty.
569
+ """
570
+ fig, ax = plt.subplots(figsize=(10, 6))
571
+
572
+ values = np.asarray(data.get("data", []))
573
+ if len(values) == 0:
574
+ plt.close(fig)
575
+ raise ValueError("Empty data array for time series")
576
+
577
+ time = data.get("time", np.arange(len(values)))
578
+ time = np.asarray(time)
579
+
580
+ # Auto-select time unit
581
+ max_time = time[-1] if len(time) > 0 else 1.0
582
+ if max_time < 1e-6:
583
+ time_unit = "ns"
584
+ time_scale = 1e9
585
+ elif max_time < 1e-3:
586
+ time_unit = "us"
587
+ time_scale = 1e6
588
+ elif max_time < 1:
589
+ time_unit = "ms"
590
+ time_scale = 1e3
591
+ else:
592
+ time_unit = "s"
593
+ time_scale = 1.0
594
+
595
+ ax.plot(time * time_scale, values, linewidth=0.8)
596
+ ax.set_xlabel(f"Time ({time_unit})")
597
+ ax.set_ylabel(ylabel)
598
+ ax.set_title(title)
599
+ ax.grid(True, alpha=0.3)
600
+
601
+ fig.tight_layout()
602
+ return fig
603
+
604
+ def _plot_spectrogram(self, data: dict[str, Any]) -> Figure:
605
+ """Plot spectrogram (time-frequency heatmap).
606
+
607
+ Args:
608
+ data: Dictionary with 'times', 'frequencies', and 'Sxx_db' arrays.
609
+
610
+ Returns:
611
+ Matplotlib Figure object.
612
+
613
+ Raises:
614
+ ValueError: If spectrogram data is missing or empty.
615
+ """
616
+ fig, ax = plt.subplots(figsize=(10, 6))
617
+
618
+ times = np.asarray(data.get("times", []))
619
+ frequencies = np.asarray(data.get("frequencies", []))
620
+ Sxx_db = np.asarray(data.get("Sxx_db", []))
621
+
622
+ if len(times) == 0 or len(frequencies) == 0 or Sxx_db.size == 0:
623
+ plt.close(fig)
624
+ raise ValueError("Missing spectrogram data")
625
+
626
+ # Auto-select units
627
+ max_time = times[-1] if len(times) > 0 else 1.0
628
+ if max_time < 1e-6:
629
+ time_unit = "ns"
630
+ time_scale = 1e9
631
+ elif max_time < 1e-3:
632
+ time_unit = "us"
633
+ time_scale = 1e6
634
+ elif max_time < 1:
635
+ time_unit = "ms"
636
+ time_scale = 1e3
637
+ else:
638
+ time_unit = "s"
639
+ time_scale = 1.0
640
+
641
+ max_freq = frequencies[-1] if len(frequencies) > 0 else 1.0
642
+ if max_freq >= 1e9:
643
+ freq_unit = "GHz"
644
+ freq_scale = 1e9
645
+ elif max_freq >= 1e6:
646
+ freq_unit = "MHz"
647
+ freq_scale = 1e6
648
+ elif max_freq >= 1e3:
649
+ freq_unit = "kHz"
650
+ freq_scale = 1e3
651
+ else:
652
+ freq_unit = "Hz"
653
+ freq_scale = 1.0
654
+
655
+ # Auto color limits
656
+ valid_db = Sxx_db[np.isfinite(Sxx_db)]
657
+ if len(valid_db) > 0:
658
+ vmax = np.max(valid_db)
659
+ vmin = max(np.min(valid_db), vmax - 80)
660
+ else:
661
+ vmin, vmax = None, None
662
+
663
+ pcm = ax.pcolormesh(
664
+ times * time_scale,
665
+ frequencies / freq_scale,
666
+ Sxx_db,
667
+ shading="auto",
668
+ cmap="viridis",
669
+ vmin=vmin,
670
+ vmax=vmax,
671
+ )
672
+
673
+ ax.set_xlabel(f"Time ({time_unit})")
674
+ ax.set_ylabel(f"Frequency ({freq_unit})")
675
+ ax.set_title("Spectrogram")
676
+
677
+ cbar = fig.colorbar(pcm, ax=ax)
678
+ cbar.set_label("Magnitude (dB)")
679
+
680
+ fig.tight_layout()
681
+ return fig
682
+
683
+
684
+ # ============================================================================
685
+ # Plot Registry
686
+ # ============================================================================
687
+
688
+ # Maps (domain, analysis_name) tuples to plot generation functions
689
+ # This allows custom plot functions to be registered for specific analyses
690
+ PLOT_REGISTRY: dict[
691
+ tuple[AnalysisDomain, str] | AnalysisDomain, Callable[[dict[str, Any]], Figure]
692
+ ] = {}
693
+
694
+
695
+ def register_plot(
696
+ domain: AnalysisDomain,
697
+ analysis_name: str | None = None,
698
+ ) -> Callable[[Callable[[dict[str, Any]], Figure]], Callable[[dict[str, Any]], Figure]]:
699
+ """Decorator to register a custom plot function.
700
+
701
+ Args:
702
+ domain: Analysis domain.
703
+ analysis_name: Specific analysis name (optional). If None, registers
704
+ for entire domain.
705
+
706
+ Returns:
707
+ Decorator function.
708
+
709
+ Example:
710
+ >>> @register_plot(AnalysisDomain.SPECTRAL, "custom_fft")
711
+ ... def plot_custom_fft(data: dict[str, Any]) -> Figure:
712
+ ... fig, ax = plt.subplots()
713
+ ... # Custom plotting code
714
+ ... return fig
715
+ """
716
+
717
+ def decorator(func: Callable[[dict[str, Any]], Figure]) -> Callable[[dict[str, Any]], Figure]:
718
+ if analysis_name:
719
+ PLOT_REGISTRY[(domain, analysis_name)] = func
720
+ else:
721
+ PLOT_REGISTRY[domain] = func
722
+ return func
723
+
724
+ return decorator
725
+
726
+
727
+ __all__ = [
728
+ "PLOT_REGISTRY",
729
+ "PlotGenerator",
730
+ "register_plot",
731
+ ]