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,808 @@
1
+ """Signal Integrity Visualization Functions.
2
+
3
+ This module provides visualization functions for signal integrity analysis
4
+ including TDR impedance plots, S-parameter displays, setup/hold timing
5
+ diagrams, and eye diagram enhancements.
6
+
7
+ Example:
8
+ >>> from oscura.visualization.signal_integrity import plot_tdr, plot_sparams
9
+ >>> fig = plot_tdr(impedance_profile, distance_axis)
10
+ >>> fig = plot_sparams(frequencies, s11, s21)
11
+
12
+ References:
13
+ - IEEE 370-2020: Electrical Characterization of Printed Circuit Board
14
+ - TDR impedance measurement best practices
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from pathlib import Path
20
+ from typing import TYPE_CHECKING, Any, cast
21
+
22
+ import numpy as np
23
+
24
+ try:
25
+ import matplotlib.pyplot as plt
26
+
27
+ HAS_MATPLOTLIB = True
28
+ except ImportError:
29
+ HAS_MATPLOTLIB = False
30
+
31
+ if TYPE_CHECKING:
32
+ from matplotlib.axes import Axes
33
+ from matplotlib.figure import Figure
34
+ from numpy.typing import NDArray
35
+
36
+ __all__ = [
37
+ "plot_setup_hold_timing",
38
+ "plot_sparams_magnitude",
39
+ "plot_sparams_phase",
40
+ "plot_tdr",
41
+ "plot_timing_margin",
42
+ ]
43
+
44
+
45
+ def plot_tdr(
46
+ impedance: NDArray[np.floating[Any]],
47
+ distance: NDArray[np.floating[Any]],
48
+ *,
49
+ z0: float = 50.0,
50
+ ax: Axes | None = None,
51
+ figsize: tuple[float, float] = (12, 6),
52
+ title: str | None = None,
53
+ distance_unit: str = "auto",
54
+ show_reference: bool = True,
55
+ show_discontinuities: bool = True,
56
+ discontinuity_threshold: float = 5.0,
57
+ show: bool = True,
58
+ save_path: str | Path | None = None,
59
+ ) -> Figure:
60
+ """Plot TDR impedance profile vs distance.
61
+
62
+ Creates a Time Domain Reflectometry impedance plot showing impedance
63
+ as a function of distance along a transmission line, with annotations
64
+ for discontinuities and reference impedance.
65
+
66
+ Args:
67
+ impedance: Impedance values in Ohms.
68
+ distance: Distance values (in meters).
69
+ z0: Reference impedance (Ohms) for the reference line.
70
+ ax: Matplotlib axes. If None, creates new figure.
71
+ figsize: Figure size in inches (only if ax is None).
72
+ title: Plot title.
73
+ distance_unit: Distance unit ("m", "cm", "mm", "auto").
74
+ show_reference: Show reference impedance line at z0.
75
+ show_discontinuities: Annotate significant discontinuities.
76
+ discontinuity_threshold: Impedance change threshold (Ohms) for marking.
77
+ show: Display plot interactively.
78
+ save_path: Save plot to file.
79
+
80
+ Returns:
81
+ Matplotlib Figure object.
82
+
83
+ Raises:
84
+ ImportError: If matplotlib is not available.
85
+ ValueError: If input arrays have different lengths.
86
+
87
+ Example:
88
+ >>> z_profile = np.array([50, 50, 75, 75, 50, 50])
89
+ >>> dist = np.linspace(0, 0.5, 6) # 0 to 50 cm
90
+ >>> fig = plot_tdr(z_profile, dist, z0=50, show=False)
91
+ >>> fig.savefig("tdr_impedance.png")
92
+ """
93
+ if not HAS_MATPLOTLIB:
94
+ raise ImportError("matplotlib is required for visualization")
95
+
96
+ if len(impedance) != len(distance):
97
+ raise ValueError(
98
+ f"impedance and distance must have same length "
99
+ f"(got {len(impedance)} and {len(distance)})"
100
+ )
101
+
102
+ # Create figure if needed
103
+ if ax is None:
104
+ fig, ax = plt.subplots(figsize=figsize)
105
+ else:
106
+ fig_temp = ax.get_figure()
107
+ if fig_temp is None:
108
+ raise ValueError("Axes must have an associated figure")
109
+ fig = cast("Figure", fig_temp)
110
+
111
+ # Convert distance units
112
+ if distance_unit == "auto":
113
+ max_dist = np.max(distance)
114
+ if max_dist < 0.01:
115
+ distance_unit = "mm"
116
+ distance_mult = 1000.0
117
+ elif max_dist < 1.0:
118
+ distance_unit = "cm"
119
+ distance_mult = 100.0
120
+ else:
121
+ distance_unit = "m"
122
+ distance_mult = 1.0
123
+ else:
124
+ distance_mult = {"m": 1.0, "cm": 100.0, "mm": 1000.0}.get(distance_unit, 1.0)
125
+
126
+ dist_scaled = distance * distance_mult
127
+
128
+ # Clip impedance for display (handle inf values)
129
+ impedance_display = np.clip(impedance, 0, 500)
130
+
131
+ # Plot impedance profile
132
+ ax.plot(dist_scaled, impedance_display, "b-", linewidth=2, label="Impedance")
133
+
134
+ # Fill regions based on impedance deviation from z0
135
+ for i in range(len(dist_scaled) - 1):
136
+ z = impedance_display[i]
137
+ if z > z0 + discontinuity_threshold:
138
+ color = "#FFA500" # Orange for high-Z
139
+ alpha = 0.3
140
+ elif z < z0 - discontinuity_threshold:
141
+ color = "#1E90FF" # Blue for low-Z
142
+ alpha = 0.3
143
+ else:
144
+ color = "#90EE90" # Light green for matched
145
+ alpha = 0.2
146
+
147
+ ax.fill_between(
148
+ [dist_scaled[i], dist_scaled[i + 1]],
149
+ [z0, z0],
150
+ [z, impedance_display[i + 1]],
151
+ color=color,
152
+ alpha=alpha,
153
+ )
154
+
155
+ # Reference line
156
+ if show_reference:
157
+ ax.axhline(z0, color="gray", linestyle="--", linewidth=1.5, label=f"Z0 = {z0} Ω")
158
+
159
+ # Find and annotate discontinuities
160
+ if show_discontinuities:
161
+ # Find significant changes
162
+ z_diff = np.abs(np.diff(impedance_display))
163
+ discontinuities = np.where(z_diff > discontinuity_threshold)[0]
164
+
165
+ for idx in discontinuities:
166
+ z_before = impedance_display[idx]
167
+ z_after = impedance_display[idx + 1]
168
+ d = dist_scaled[idx]
169
+
170
+ # Determine discontinuity type
171
+ if z_after > z_before + discontinuity_threshold:
172
+ disc_type = "High-Z"
173
+ color = "orange"
174
+ elif z_after < z_before - discontinuity_threshold:
175
+ disc_type = "Low-Z"
176
+ color = "blue"
177
+ else:
178
+ continue
179
+
180
+ # Add marker
181
+ ax.plot(d, z_after, "o", color=color, markersize=8)
182
+
183
+ # Add annotation
184
+ z_str = f"{z_after:.0f}" if z_after < 500 else "Open"
185
+ ax.annotate(
186
+ f"{disc_type}\n{z_str} Ω",
187
+ xy=(d, z_after),
188
+ xytext=(10, 10),
189
+ textcoords="offset points",
190
+ fontsize=8,
191
+ ha="left",
192
+ bbox={"boxstyle": "round,pad=0.3", "facecolor": "white", "alpha": 0.8},
193
+ )
194
+
195
+ # Labels and formatting
196
+ ax.set_xlabel(f"Distance ({distance_unit})", fontsize=11)
197
+ ax.set_ylabel("Impedance (Ω)", fontsize=11)
198
+ ax.set_xlim(0, dist_scaled[-1])
199
+
200
+ # Set y-axis limits with padding
201
+ y_min = max(0, np.min(impedance_display) - 10)
202
+ y_max = min(200, np.max(impedance_display) + 10)
203
+ ax.set_ylim(y_min, y_max)
204
+
205
+ ax.grid(True, alpha=0.3)
206
+ ax.legend(loc="upper right")
207
+
208
+ if title:
209
+ ax.set_title(title, fontsize=12, fontweight="bold")
210
+ else:
211
+ ax.set_title("TDR Impedance Profile", fontsize=12, fontweight="bold")
212
+
213
+ fig.tight_layout()
214
+
215
+ # Save if requested
216
+ if save_path is not None:
217
+ fig.savefig(save_path, dpi=300, bbox_inches="tight")
218
+
219
+ # Show if requested
220
+ if show:
221
+ plt.show()
222
+
223
+ return fig
224
+
225
+
226
+ def plot_sparams_magnitude(
227
+ frequencies: NDArray[np.floating[Any]],
228
+ s11: NDArray[np.complexfloating[Any, Any]] | NDArray[np.floating[Any]] | None = None,
229
+ s21: NDArray[np.complexfloating[Any, Any]] | NDArray[np.floating[Any]] | None = None,
230
+ s12: NDArray[np.complexfloating[Any, Any]] | NDArray[np.floating[Any]] | None = None,
231
+ s22: NDArray[np.complexfloating[Any, Any]] | NDArray[np.floating[Any]] | None = None,
232
+ *,
233
+ ax: Axes | None = None,
234
+ figsize: tuple[float, float] = (12, 6),
235
+ title: str | None = None,
236
+ freq_unit: str = "auto",
237
+ show_markers: bool = True,
238
+ db_3_marker: bool = True,
239
+ show: bool = True,
240
+ save_path: str | Path | None = None,
241
+ ) -> Figure:
242
+ """Plot S-parameter magnitude vs frequency.
243
+
244
+ Creates a frequency response plot showing S-parameter magnitudes
245
+ in dB with optional -3dB marker for bandwidth measurement.
246
+
247
+ Args:
248
+ frequencies: Frequency array in Hz.
249
+ s11: S11 (input reflection) - complex or dB values.
250
+ s21: S21 (forward transmission) - complex or dB values.
251
+ s12: S12 (reverse transmission) - complex or dB values.
252
+ s22: S22 (output reflection) - complex or dB values.
253
+ ax: Matplotlib axes. If None, creates new figure.
254
+ figsize: Figure size in inches.
255
+ title: Plot title.
256
+ freq_unit: Frequency unit ("Hz", "kHz", "MHz", "GHz", "auto").
257
+ show_markers: Show markers at key frequencies.
258
+ db_3_marker: Show -3dB bandwidth marker for S21.
259
+ show: Display plot interactively.
260
+ save_path: Save plot to file.
261
+
262
+ Returns:
263
+ Matplotlib Figure object.
264
+
265
+ Example:
266
+ >>> freq = np.linspace(1e6, 1e9, 1000)
267
+ >>> s21 = 1 / (1 + 1j * freq / 100e6) # Low-pass response
268
+ >>> fig = plot_sparams_magnitude(freq, s21=s21)
269
+ """
270
+ if not HAS_MATPLOTLIB:
271
+ raise ImportError("matplotlib is required for visualization")
272
+
273
+ # Create figure if needed
274
+ if ax is None:
275
+ fig, ax = plt.subplots(figsize=figsize)
276
+ else:
277
+ fig_temp = ax.get_figure()
278
+ if fig_temp is None:
279
+ raise ValueError("Axes must have an associated figure")
280
+ fig = cast("Figure", fig_temp)
281
+
282
+ # Select frequency unit
283
+ if freq_unit == "auto":
284
+ max_freq = np.max(frequencies)
285
+ if max_freq >= 1e9:
286
+ freq_unit = "GHz"
287
+ freq_div = 1e9
288
+ elif max_freq >= 1e6:
289
+ freq_unit = "MHz"
290
+ freq_div = 1e6
291
+ elif max_freq >= 1e3:
292
+ freq_unit = "kHz"
293
+ freq_div = 1e3
294
+ else:
295
+ freq_unit = "Hz"
296
+ freq_div = 1.0
297
+ else:
298
+ freq_div = {"Hz": 1.0, "kHz": 1e3, "MHz": 1e6, "GHz": 1e9}.get(freq_unit, 1.0)
299
+
300
+ freq_scaled = frequencies / freq_div
301
+
302
+ def to_db(s: NDArray[Any]) -> NDArray[np.floating[Any]]:
303
+ """Convert S-parameter to dB."""
304
+ if np.iscomplexobj(s):
305
+ result: NDArray[np.floating[Any]] = 20 * np.log10(np.abs(s) + 1e-12)
306
+ return result
307
+ return np.asarray(s, dtype=np.float64)
308
+
309
+ # Color scheme
310
+ colors = {"S11": "#E74C3C", "S21": "#3498DB", "S12": "#2ECC71", "S22": "#9B59B6"}
311
+ linestyles = {"S11": "-", "S21": "-", "S12": "--", "S22": "--"}
312
+
313
+ params = [("S11", s11), ("S21", s21), ("S12", s12), ("S22", s22)]
314
+
315
+ for name, s_param in params:
316
+ if s_param is not None:
317
+ s_db = to_db(s_param)
318
+ ax.semilogx(
319
+ freq_scaled,
320
+ s_db,
321
+ color=colors[name],
322
+ linestyle=linestyles[name],
323
+ linewidth=2,
324
+ label=name,
325
+ )
326
+
327
+ # -3dB marker for S21
328
+ if name == "S21" and db_3_marker:
329
+ max_db = np.max(s_db)
330
+ db_3_level = max_db - 3
331
+
332
+ # Find -3dB crossover
333
+ crossings = np.where(np.diff(np.sign(s_db - db_3_level)))[0]
334
+ if len(crossings) > 0:
335
+ f_3db = float(freq_scaled[crossings[0]])
336
+ db_3_level_float = float(db_3_level)
337
+ ax.axhline(
338
+ db_3_level_float, color="gray", linestyle=":", alpha=0.7, linewidth=1
339
+ )
340
+ ax.axvline(f_3db, color="gray", linestyle=":", alpha=0.7, linewidth=1)
341
+ ax.plot(f_3db, db_3_level_float, "ko", markersize=6)
342
+ ax.annotate(
343
+ f"-3dB: {f_3db:.2f} {freq_unit}",
344
+ xy=(f_3db, db_3_level_float),
345
+ xytext=(10, -15),
346
+ textcoords="offset points",
347
+ fontsize=9,
348
+ ha="left",
349
+ )
350
+
351
+ # Labels and formatting
352
+ ax.set_xlabel(f"Frequency ({freq_unit})", fontsize=11)
353
+ ax.set_ylabel("Magnitude (dB)", fontsize=11)
354
+ ax.grid(True, which="both", alpha=0.3)
355
+ ax.legend(loc="best")
356
+
357
+ if title:
358
+ ax.set_title(title, fontsize=12, fontweight="bold")
359
+ else:
360
+ ax.set_title("S-Parameter Magnitude Response", fontsize=12, fontweight="bold")
361
+
362
+ fig.tight_layout()
363
+
364
+ if save_path is not None:
365
+ fig.savefig(save_path, dpi=300, bbox_inches="tight")
366
+
367
+ if show:
368
+ plt.show()
369
+
370
+ return fig
371
+
372
+
373
+ def plot_sparams_phase(
374
+ frequencies: NDArray[np.floating[Any]],
375
+ s11: NDArray[np.complexfloating[Any, Any]] | None = None,
376
+ s21: NDArray[np.complexfloating[Any, Any]] | None = None,
377
+ *,
378
+ ax: Axes | None = None,
379
+ figsize: tuple[float, float] = (12, 6),
380
+ title: str | None = None,
381
+ freq_unit: str = "auto",
382
+ unwrap: bool = True,
383
+ show: bool = True,
384
+ save_path: str | Path | None = None,
385
+ ) -> Figure:
386
+ """Plot S-parameter phase vs frequency.
387
+
388
+ Args:
389
+ frequencies: Frequency array in Hz.
390
+ s11: S11 complex values.
391
+ s21: S21 complex values.
392
+ ax: Matplotlib axes.
393
+ figsize: Figure size.
394
+ title: Plot title.
395
+ freq_unit: Frequency unit.
396
+ unwrap: Unwrap phase discontinuities.
397
+ show: Display plot.
398
+ save_path: Save path.
399
+
400
+ Returns:
401
+ Matplotlib Figure object.
402
+ """
403
+ if not HAS_MATPLOTLIB:
404
+ raise ImportError("matplotlib is required for visualization")
405
+
406
+ if ax is None:
407
+ fig, ax = plt.subplots(figsize=figsize)
408
+ else:
409
+ fig_temp = ax.get_figure()
410
+ if fig_temp is None:
411
+ raise ValueError("Axes must have an associated figure")
412
+ fig = cast("Figure", fig_temp)
413
+
414
+ # Select frequency unit
415
+ if freq_unit == "auto":
416
+ max_freq = np.max(frequencies)
417
+ if max_freq >= 1e9:
418
+ freq_unit = "GHz"
419
+ freq_div = 1e9
420
+ elif max_freq >= 1e6:
421
+ freq_unit = "MHz"
422
+ freq_div = 1e6
423
+ else:
424
+ freq_unit = "kHz"
425
+ freq_div = 1e3
426
+ else:
427
+ freq_div = {"Hz": 1.0, "kHz": 1e3, "MHz": 1e6, "GHz": 1e9}.get(freq_unit, 1.0)
428
+
429
+ freq_scaled = frequencies / freq_div
430
+
431
+ colors = {"S11": "#E74C3C", "S21": "#3498DB"}
432
+
433
+ for name, s_param in [("S11", s11), ("S21", s21)]:
434
+ if s_param is not None:
435
+ phase = np.angle(s_param, deg=True)
436
+ if unwrap:
437
+ phase = np.rad2deg(np.unwrap(np.deg2rad(phase)))
438
+
439
+ ax.semilogx(freq_scaled, phase, color=colors[name], linewidth=2, label=name)
440
+
441
+ ax.set_xlabel(f"Frequency ({freq_unit})", fontsize=11)
442
+ ax.set_ylabel("Phase (degrees)", fontsize=11)
443
+ ax.grid(True, which="both", alpha=0.3)
444
+ ax.legend(loc="best")
445
+
446
+ if title:
447
+ ax.set_title(title, fontsize=12, fontweight="bold")
448
+ else:
449
+ ax.set_title("S-Parameter Phase Response", fontsize=12, fontweight="bold")
450
+
451
+ fig.tight_layout()
452
+
453
+ if save_path is not None:
454
+ fig.savefig(save_path, dpi=300, bbox_inches="tight")
455
+
456
+ if show:
457
+ plt.show()
458
+
459
+ return fig
460
+
461
+
462
+ def plot_setup_hold_timing(
463
+ clock_edges: NDArray[np.floating[Any]],
464
+ data_edges: NDArray[np.floating[Any]],
465
+ setup_time: float,
466
+ hold_time: float,
467
+ *,
468
+ clock_data: NDArray[np.floating[Any]] | None = None,
469
+ data_data: NDArray[np.floating[Any]] | None = None,
470
+ time_axis: NDArray[np.floating[Any]] | None = None,
471
+ ax: Axes | None = None,
472
+ figsize: tuple[float, float] = (14, 8),
473
+ title: str | None = None,
474
+ time_unit: str = "auto",
475
+ show_margins: bool = True,
476
+ setup_spec: float | None = None,
477
+ hold_spec: float | None = None,
478
+ show: bool = True,
479
+ save_path: str | Path | None = None,
480
+ ) -> Figure:
481
+ """Plot setup/hold timing diagram with annotations.
482
+
483
+ Creates a timing diagram showing clock and data relationships
484
+ with setup and hold time annotations and optional pass/fail
485
+ indication against specifications.
486
+
487
+ Args:
488
+ clock_edges: Array of clock edge times (rising edges).
489
+ data_edges: Array of data transition times.
490
+ setup_time: Measured setup time (seconds).
491
+ hold_time: Measured hold time (seconds).
492
+ clock_data: Optional clock waveform for display.
493
+ data_data: Optional data waveform for display.
494
+ time_axis: Time axis for waveforms.
495
+ ax: Matplotlib axes.
496
+ figsize: Figure size.
497
+ title: Plot title.
498
+ time_unit: Time unit ("s", "ms", "us", "ns", "ps", "auto").
499
+ show_margins: Show setup/hold timing arrows.
500
+ setup_spec: Setup time specification for pass/fail.
501
+ hold_spec: Hold time specification for pass/fail.
502
+ show: Display plot.
503
+ save_path: Save path.
504
+
505
+ Returns:
506
+ Matplotlib Figure object.
507
+
508
+ Example:
509
+ >>> clk_edges = np.array([0, 10e-9, 20e-9])
510
+ >>> data_edges = np.array([8e-9, 18e-9])
511
+ >>> fig = plot_setup_hold_timing(
512
+ ... clk_edges, data_edges,
513
+ ... setup_time=2e-9, hold_time=1e-9,
514
+ ... setup_spec=1e-9, hold_spec=0.5e-9
515
+ ... )
516
+ """
517
+ if not HAS_MATPLOTLIB:
518
+ raise ImportError("matplotlib is required for visualization")
519
+
520
+ # Create figure with multiple rows
521
+ if ax is not None:
522
+ fig_temp = ax.get_figure()
523
+ if fig_temp is None:
524
+ raise ValueError("Axes must have an associated figure")
525
+ fig = cast("Figure", fig_temp)
526
+ axes = [ax]
527
+ n_rows = 1
528
+ else:
529
+ n_rows = 3 if clock_data is not None else 1
530
+ fig, axes = plt.subplots(
531
+ n_rows, 1, figsize=figsize, sharex=True, gridspec_kw={"height_ratios": [1] * n_rows}
532
+ )
533
+ if n_rows == 1:
534
+ axes = [axes]
535
+
536
+ # Select time unit
537
+ if time_unit == "auto":
538
+ max_time = max(np.max(clock_edges), np.max(data_edges))
539
+ if max_time < 1e-9:
540
+ time_unit = "ps"
541
+ time_mult = 1e12
542
+ elif max_time < 1e-6:
543
+ time_unit = "ns"
544
+ time_mult = 1e9
545
+ elif max_time < 1e-3:
546
+ time_unit = "us"
547
+ time_mult = 1e6
548
+ else:
549
+ time_unit = "ms"
550
+ time_mult = 1e3
551
+ else:
552
+ time_mult = {"s": 1, "ms": 1e3, "us": 1e6, "ns": 1e9, "ps": 1e12}.get(time_unit, 1e9)
553
+
554
+ setup_scaled = setup_time * time_mult
555
+ hold_scaled = hold_time * time_mult
556
+
557
+ # If waveforms provided, plot them
558
+ if clock_data is not None and data_data is not None and time_axis is not None:
559
+ time_scaled = time_axis * time_mult
560
+
561
+ # Clock waveform
562
+ ax_clk = axes[0]
563
+ ax_clk.step(time_scaled, clock_data, where="post", color="#3498DB", linewidth=2)
564
+ ax_clk.set_ylabel(
565
+ "CLK", rotation=0, ha="right", va="center", fontsize=11, fontweight="bold"
566
+ )
567
+ ax_clk.set_ylim(-0.2, 1.3)
568
+ ax_clk.set_yticks([0, 1])
569
+ ax_clk.grid(True, axis="x", alpha=0.3)
570
+
571
+ # Data waveform
572
+ ax_data = axes[1]
573
+ ax_data.step(time_scaled, data_data, where="post", color="#E74C3C", linewidth=2)
574
+ ax_data.set_ylabel(
575
+ "DATA", rotation=0, ha="right", va="center", fontsize=11, fontweight="bold"
576
+ )
577
+ ax_data.set_ylim(-0.2, 1.3)
578
+ ax_data.set_yticks([0, 1])
579
+ ax_data.grid(True, axis="x", alpha=0.3)
580
+
581
+ ax_timing = axes[2] if len(axes) > 2 else axes[-1]
582
+ else:
583
+ ax_timing = axes[0]
584
+
585
+ # Timing annotation panel
586
+ ax_timing.set_ylim(0, 1)
587
+ ax_timing.set_xlim(0, max(clock_edges[-1], data_edges[-1]) * time_mult * 1.1)
588
+ ax_timing.axis("off")
589
+
590
+ # Draw timing arrows for first clock edge
591
+ if len(clock_edges) > 0 and len(data_edges) > 0:
592
+ clk_edge = clock_edges[0] * time_mult
593
+
594
+ # Find nearest data edge before clock
595
+ data_before = data_edges[data_edges < clock_edges[0]]
596
+ if len(data_before) > 0:
597
+ data_edge = data_before[-1] * time_mult
598
+
599
+ # Setup time arrow (data_edge to clk_edge)
600
+ if show_margins:
601
+ y_setup = 0.7
602
+ ax_timing.annotate(
603
+ "",
604
+ xy=(clk_edge, y_setup),
605
+ xytext=(data_edge, y_setup),
606
+ arrowprops={
607
+ "arrowstyle": "<->",
608
+ "color": "#27AE60",
609
+ "lw": 2,
610
+ },
611
+ )
612
+ ax_timing.text(
613
+ (data_edge + clk_edge) / 2,
614
+ y_setup + 0.1,
615
+ f"Setup: {setup_scaled:.2f} {time_unit}",
616
+ ha="center",
617
+ va="bottom",
618
+ fontsize=10,
619
+ fontweight="bold",
620
+ color="#27AE60",
621
+ )
622
+
623
+ # Find nearest data edge after clock
624
+ data_after = data_edges[data_edges > clock_edges[0]]
625
+ if len(data_after) > 0:
626
+ data_edge_after = data_after[0] * time_mult
627
+
628
+ # Hold time arrow (clk_edge to data_edge_after)
629
+ if show_margins:
630
+ y_hold = 0.3
631
+ ax_timing.annotate(
632
+ "",
633
+ xy=(data_edge_after, y_hold),
634
+ xytext=(clk_edge, y_hold),
635
+ arrowprops={
636
+ "arrowstyle": "<->",
637
+ "color": "#E67E22",
638
+ "lw": 2,
639
+ },
640
+ )
641
+ ax_timing.text(
642
+ (clk_edge + data_edge_after) / 2,
643
+ y_hold + 0.1,
644
+ f"Hold: {hold_scaled:.2f} {time_unit}",
645
+ ha="center",
646
+ va="bottom",
647
+ fontsize=10,
648
+ fontweight="bold",
649
+ color="#E67E22",
650
+ )
651
+
652
+ # Add pass/fail status
653
+ status_y = 0.9
654
+ if setup_spec is not None:
655
+ setup_pass = setup_time >= setup_spec
656
+ status = "PASS" if setup_pass else "FAIL"
657
+ color = "#27AE60" if setup_pass else "#E74C3C"
658
+ ax_timing.text(
659
+ 0.02,
660
+ status_y,
661
+ f"Setup: {status} (spec: {setup_spec * time_mult:.2f} {time_unit})",
662
+ transform=ax_timing.transAxes,
663
+ fontsize=10,
664
+ color=color,
665
+ fontweight="bold",
666
+ )
667
+ status_y -= 0.15
668
+
669
+ if hold_spec is not None:
670
+ hold_pass = hold_time >= hold_spec
671
+ status = "PASS" if hold_pass else "FAIL"
672
+ color = "#27AE60" if hold_pass else "#E74C3C"
673
+ ax_timing.text(
674
+ 0.02,
675
+ status_y,
676
+ f"Hold: {status} (spec: {hold_spec * time_mult:.2f} {time_unit})",
677
+ transform=ax_timing.transAxes,
678
+ fontsize=10,
679
+ color=color,
680
+ fontweight="bold",
681
+ )
682
+
683
+ # Set x-label on bottom axes
684
+ axes[-1].set_xlabel(f"Time ({time_unit})", fontsize=11)
685
+
686
+ if title:
687
+ fig.suptitle(title, fontsize=14, fontweight="bold")
688
+ else:
689
+ fig.suptitle("Setup/Hold Timing Analysis", fontsize=14, fontweight="bold")
690
+
691
+ fig.tight_layout()
692
+
693
+ if save_path is not None:
694
+ fig.savefig(save_path, dpi=300, bbox_inches="tight")
695
+
696
+ if show:
697
+ plt.show()
698
+
699
+ return fig
700
+
701
+
702
+ def plot_timing_margin(
703
+ setup_times: NDArray[np.floating[Any]],
704
+ hold_times: NDArray[np.floating[Any]],
705
+ *,
706
+ setup_spec: float | None = None,
707
+ hold_spec: float | None = None,
708
+ ax: Axes | None = None,
709
+ figsize: tuple[float, float] = (10, 8),
710
+ title: str | None = None,
711
+ time_unit: str = "ns",
712
+ show: bool = True,
713
+ save_path: str | Path | None = None,
714
+ ) -> Figure:
715
+ """Plot setup vs hold timing margin scatter plot.
716
+
717
+ Creates a scatter plot showing the distribution of setup and hold
718
+ times with specification regions marked.
719
+
720
+ Args:
721
+ setup_times: Array of setup time measurements.
722
+ hold_times: Array of hold time measurements.
723
+ setup_spec: Setup time specification.
724
+ hold_spec: Hold time specification.
725
+ ax: Matplotlib axes.
726
+ figsize: Figure size.
727
+ title: Plot title.
728
+ time_unit: Time unit for display.
729
+ show: Display plot.
730
+ save_path: Save path.
731
+
732
+ Returns:
733
+ Matplotlib Figure object.
734
+ """
735
+ if not HAS_MATPLOTLIB:
736
+ raise ImportError("matplotlib is required for visualization")
737
+
738
+ if ax is None:
739
+ fig, ax = plt.subplots(figsize=figsize)
740
+ else:
741
+ fig_temp = ax.get_figure()
742
+ if fig_temp is None:
743
+ raise ValueError("Axes must have an associated figure")
744
+ fig = cast("Figure", fig_temp)
745
+
746
+ time_mult = {"s": 1, "ms": 1e3, "us": 1e6, "ns": 1e9, "ps": 1e12}.get(time_unit, 1e9)
747
+
748
+ setup_scaled = setup_times * time_mult
749
+ hold_scaled = hold_times * time_mult
750
+
751
+ # Scatter plot
752
+ ax.scatter(setup_scaled, hold_scaled, c="#3498DB", alpha=0.6, s=50)
753
+
754
+ # Add specification lines if provided
755
+ if setup_spec is not None:
756
+ spec_scaled = setup_spec * time_mult
757
+ ax.axvline(
758
+ spec_scaled,
759
+ color="#E74C3C",
760
+ linestyle="--",
761
+ linewidth=2,
762
+ label=f"Setup Spec ({spec_scaled:.2f} {time_unit})",
763
+ )
764
+
765
+ if hold_spec is not None:
766
+ spec_scaled = hold_spec * time_mult
767
+ ax.axhline(
768
+ spec_scaled,
769
+ color="#E67E22",
770
+ linestyle="--",
771
+ linewidth=2,
772
+ label=f"Hold Spec ({spec_scaled:.2f} {time_unit})",
773
+ )
774
+
775
+ # Mark pass/fail regions
776
+ if setup_spec is not None and hold_spec is not None:
777
+ x_lim = ax.get_xlim()
778
+ y_lim = ax.get_ylim()
779
+
780
+ # Pass region (upper right)
781
+ ax.fill_between(
782
+ [setup_spec * time_mult, x_lim[1]],
783
+ [hold_spec * time_mult, hold_spec * time_mult],
784
+ [y_lim[1], y_lim[1]],
785
+ color="#27AE60",
786
+ alpha=0.1,
787
+ label="Pass Region",
788
+ )
789
+
790
+ ax.set_xlabel(f"Setup Time ({time_unit})", fontsize=11)
791
+ ax.set_ylabel(f"Hold Time ({time_unit})", fontsize=11)
792
+ ax.grid(True, alpha=0.3)
793
+ ax.legend(loc="best")
794
+
795
+ if title:
796
+ ax.set_title(title, fontsize=12, fontweight="bold")
797
+ else:
798
+ ax.set_title("Setup/Hold Timing Margin", fontsize=12, fontweight="bold")
799
+
800
+ fig.tight_layout()
801
+
802
+ if save_path is not None:
803
+ fig.savefig(save_path, dpi=300, bbox_inches="tight")
804
+
805
+ if show:
806
+ plt.show()
807
+
808
+ return fig