oscura 0.0.1__py3-none-any.whl → 0.1.1__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.1.dist-info/METADATA +300 -0
  460. oscura-0.1.1.dist-info/RECORD +463 -0
  461. oscura-0.1.1.dist-info/entry_points.txt +2 -0
  462. {oscura-0.0.1.dist-info → oscura-0.1.1.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.1.dist-info}/WHEEL +0 -0
@@ -0,0 +1,878 @@
1
+ """Signal quality analysis for digital signals.
2
+
3
+ This module provides signal quality metrics including noise margin
4
+ calculation, setup/hold violation detection, and glitch detection.
5
+
6
+
7
+ Example:
8
+ >>> from oscura.analyzers.digital.quality import noise_margin, detect_glitches
9
+ >>> margins = noise_margin(trace, family="TTL")
10
+ >>> glitches = detect_glitches(trace, min_width=10e-9)
11
+
12
+ References:
13
+ JEDEC Standard No. 8C: High-Speed CMOS Interface
14
+ Various IC manufacturer datasheets for logic family specifications
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from dataclasses import dataclass
20
+ from typing import TYPE_CHECKING, Literal
21
+
22
+ import numpy as np
23
+
24
+ from oscura.analyzers.digital.extraction import LOGIC_FAMILIES
25
+ from oscura.analyzers.digital.timing import hold_time, setup_time
26
+ from oscura.core.exceptions import InsufficientDataError
27
+ from oscura.core.types import DigitalTrace, WaveformTrace
28
+
29
+ if TYPE_CHECKING:
30
+ from typing import Any
31
+
32
+ from numpy.typing import NDArray
33
+
34
+
35
+ @dataclass
36
+ class NoiseMarginResult:
37
+ """Result of noise margin calculation.
38
+
39
+ Attributes:
40
+ nm_high: Noise margin high (VOH_min - VIH_min).
41
+ nm_low: Noise margin low (VIL_max - VOL_max).
42
+ logic_family: Logic family used for calculation.
43
+ voh: Output high voltage (measured or spec).
44
+ vol: Output low voltage (measured or spec).
45
+ vih: Input high threshold (from spec).
46
+ vil: Input low threshold (from spec).
47
+ """
48
+
49
+ nm_high: float
50
+ nm_low: float
51
+ logic_family: str
52
+ voh: float
53
+ vol: float
54
+ vih: float
55
+ vil: float
56
+
57
+
58
+ @dataclass
59
+ class Violation:
60
+ """Represents a timing or signal violation.
61
+
62
+ Attributes:
63
+ timestamp: Time of violation in seconds.
64
+ violation_type: Type of violation.
65
+ measured: Measured value.
66
+ limit: Specification limit.
67
+ margin: Margin to specification (negative = violation).
68
+ end_timestamp: End time of violation (if applicable).
69
+ """
70
+
71
+ timestamp: float
72
+ violation_type: str
73
+ measured: float
74
+ limit: float
75
+ margin: float
76
+ end_timestamp: float | None = None
77
+
78
+
79
+ @dataclass
80
+ class Glitch:
81
+ """Represents a detected glitch.
82
+
83
+ Attributes:
84
+ timestamp: Start time of glitch in seconds.
85
+ width: Duration of glitch in seconds.
86
+ polarity: "positive" (spike high) or "negative" (spike low).
87
+ amplitude: Peak amplitude of glitch.
88
+ """
89
+
90
+ timestamp: float
91
+ width: float
92
+ polarity: Literal["positive", "negative"]
93
+ amplitude: float
94
+
95
+
96
+ def noise_margin(
97
+ trace: WaveformTrace,
98
+ *,
99
+ family: str = "LVCMOS_3V3",
100
+ use_measured_levels: bool = True,
101
+ ) -> NoiseMarginResult:
102
+ """Calculate noise margins for a digital signal.
103
+
104
+ Computes noise margin high (NMH) and noise margin low (NML) based on
105
+ measured signal levels or logic family specifications.
106
+
107
+ Args:
108
+ trace: Input waveform trace.
109
+ family: Logic family for threshold levels.
110
+ Options: TTL, CMOS_5V, LVTTL, LVCMOS_3V3, LVCMOS_2V5, LVCMOS_1V8, LVCMOS_1V2
111
+ use_measured_levels: If True, use measured VOH/VOL from signal.
112
+ If False, use spec values from logic family.
113
+
114
+ Returns:
115
+ NoiseMarginResult with calculated margins.
116
+
117
+ Raises:
118
+ ValueError: If logic family is not recognized.
119
+
120
+ Example:
121
+ >>> result = noise_margin(trace, family="TTL")
122
+ >>> print(f"NMH: {result.nm_high:.3f} V")
123
+ >>> print(f"NML: {result.nm_low:.3f} V")
124
+
125
+ References:
126
+ JEDEC Standard No. 8C
127
+ """
128
+ if family not in LOGIC_FAMILIES:
129
+ available = ", ".join(LOGIC_FAMILIES.keys())
130
+ raise ValueError(f"Unknown logic family: {family}. Available: {available}")
131
+
132
+ specs = LOGIC_FAMILIES[family]
133
+ vih = specs["VIH_min"]
134
+ vil = specs["VIL_max"]
135
+
136
+ if use_measured_levels and len(trace.data) > 0:
137
+ # Measure actual output levels from signal
138
+ data = trace.data
139
+ low, high = _find_logic_levels(data)
140
+ voh = high
141
+ vol = low
142
+ else:
143
+ # Use specification values
144
+ voh = specs["VOH_min"]
145
+ vol = specs["VOL_max"]
146
+
147
+ # Calculate noise margins
148
+ # NMH = VOH - VIH (margin when output is high)
149
+ # NML = VIL - VOL (margin when output is low)
150
+ nm_high = voh - vih
151
+ nm_low = vil - vol
152
+
153
+ return NoiseMarginResult(
154
+ nm_high=nm_high,
155
+ nm_low=nm_low,
156
+ logic_family=family,
157
+ voh=voh,
158
+ vol=vol,
159
+ vih=vih,
160
+ vil=vil,
161
+ )
162
+
163
+
164
+ def detect_violations(
165
+ data_trace: WaveformTrace | DigitalTrace,
166
+ clock_trace: WaveformTrace | DigitalTrace,
167
+ *,
168
+ setup_spec: float,
169
+ hold_spec: float,
170
+ clock_edge: Literal["rising", "falling"] = "rising",
171
+ ) -> list[Violation]:
172
+ """Detect setup and hold time violations.
173
+
174
+ Compares measured setup and hold times to specifications and
175
+ reports any violations with timestamps and margins.
176
+
177
+ Args:
178
+ data_trace: Data signal trace.
179
+ clock_trace: Clock signal trace.
180
+ setup_spec: Required setup time in seconds.
181
+ hold_spec: Required hold time in seconds.
182
+ clock_edge: Clock edge to reference ("rising" or "falling").
183
+
184
+ Returns:
185
+ List of Violation objects for each detected violation.
186
+
187
+ Example:
188
+ >>> violations = detect_violations(
189
+ ... data_trace, clock_trace,
190
+ ... setup_spec=2e-9, hold_spec=1e-9
191
+ ... )
192
+ >>> for v in violations:
193
+ ... print(f"{v.violation_type}: {v.margin*1e12:.0f} ps margin")
194
+
195
+ References:
196
+ JEDEC Standard No. 65B
197
+ """
198
+ violations: list[Violation] = []
199
+
200
+ # Get all setup times
201
+ setup_times = setup_time(data_trace, clock_trace, clock_edge=clock_edge, return_all=True)
202
+
203
+ if isinstance(setup_times, np.ndarray) and len(setup_times) > 0:
204
+ clock_edges = _get_clock_edges(clock_trace, clock_edge)
205
+
206
+ for _i, (t_setup, clk_edge) in enumerate(
207
+ zip(setup_times, clock_edges[: len(setup_times)], strict=False)
208
+ ):
209
+ margin = t_setup - setup_spec
210
+ if margin < 0: # Violation
211
+ violations.append(
212
+ Violation(
213
+ timestamp=clk_edge,
214
+ violation_type="setup",
215
+ measured=t_setup,
216
+ limit=setup_spec,
217
+ margin=margin,
218
+ )
219
+ )
220
+
221
+ # Get all hold times
222
+ hold_times = hold_time(data_trace, clock_trace, clock_edge=clock_edge, return_all=True)
223
+
224
+ if isinstance(hold_times, np.ndarray) and len(hold_times) > 0:
225
+ clock_edges = _get_clock_edges(clock_trace, clock_edge)
226
+
227
+ for _i, (t_hold, clk_edge) in enumerate(
228
+ zip(hold_times, clock_edges[: len(hold_times)], strict=False)
229
+ ):
230
+ margin = t_hold - hold_spec
231
+ if margin < 0: # Violation
232
+ violations.append(
233
+ Violation(
234
+ timestamp=clk_edge,
235
+ violation_type="hold",
236
+ measured=t_hold,
237
+ limit=hold_spec,
238
+ margin=margin,
239
+ )
240
+ )
241
+
242
+ # Sort by timestamp
243
+ violations.sort(key=lambda v: v.timestamp)
244
+
245
+ return violations
246
+
247
+
248
+ def detect_glitches(
249
+ trace: WaveformTrace | DigitalTrace,
250
+ *,
251
+ min_width: float,
252
+ threshold: float | None = None,
253
+ ) -> list[Glitch]:
254
+ """Detect glitches (pulses shorter than minimum width).
255
+
256
+ Identifies short pulses that violate minimum pulse width specifications,
257
+ which may cause logic errors or be artifacts.
258
+
259
+ Args:
260
+ trace: Input trace (analog or digital).
261
+ min_width: Minimum valid pulse width in seconds.
262
+ threshold: Threshold for digital conversion (analog traces only).
263
+ If None, auto-detected from signal.
264
+
265
+ Returns:
266
+ List of Glitch objects for each detected glitch.
267
+
268
+ Example:
269
+ >>> glitches = detect_glitches(trace, min_width=10e-9)
270
+ >>> for g in glitches:
271
+ ... print(f"Glitch at {g.timestamp*1e6:.2f} us, width={g.width*1e9:.1f} ns")
272
+
273
+ References:
274
+ Application note AN-905: Understanding Glitch Detection
275
+ """
276
+ if isinstance(trace, DigitalTrace):
277
+ # Already digital - use directly
278
+ digital = trace.data
279
+ sample_rate = trace.metadata.sample_rate
280
+ threshold_used = 0.5 # Not used for amplitude calc on digital
281
+ data = trace.data.astype(np.float64)
282
+ else:
283
+ # Analog trace - need to threshold
284
+ data = trace.data
285
+ sample_rate = trace.metadata.sample_rate
286
+
287
+ if len(data) < 3:
288
+ return []
289
+
290
+ # Find threshold
291
+ low, high = _find_logic_levels(data)
292
+ threshold_used = (low + high) / 2 if threshold is None else threshold
293
+
294
+ amplitude = high - low
295
+ if amplitude <= 0:
296
+ return []
297
+
298
+ # Convert to binary
299
+ digital = data >= threshold_used
300
+
301
+ if len(digital) < 3:
302
+ return []
303
+
304
+ sample_period = 1.0 / sample_rate
305
+
306
+ glitches: list[Glitch] = []
307
+
308
+ # Find all pulse edges
309
+ transitions = np.diff(digital.astype(np.int8))
310
+ rising_edges = np.where(transitions == 1)[0]
311
+ falling_edges = np.where(transitions == -1)[0]
312
+
313
+ # Check positive pulses (rising to falling)
314
+ for rising_idx in rising_edges:
315
+ # Find next falling edge
316
+ subsequent_falling = falling_edges[falling_edges > rising_idx]
317
+ if len(subsequent_falling) > 0:
318
+ falling_idx = subsequent_falling[0]
319
+ width = (falling_idx - rising_idx) * sample_period
320
+
321
+ if width < min_width:
322
+ # Calculate amplitude within pulse
323
+ pulse_data = data[rising_idx : falling_idx + 1]
324
+ if isinstance(trace, DigitalTrace):
325
+ # For digital trace, amplitude is just 1.0 (logic high)
326
+ pulse_amplitude = 1.0
327
+ else:
328
+ pulse_amplitude = float(np.max(pulse_data) - threshold_used)
329
+
330
+ glitches.append(
331
+ Glitch(
332
+ timestamp=rising_idx * sample_period,
333
+ width=width,
334
+ polarity="positive",
335
+ amplitude=pulse_amplitude,
336
+ )
337
+ )
338
+
339
+ # Check negative pulses (falling to rising)
340
+ for falling_idx in falling_edges:
341
+ # Find next rising edge
342
+ subsequent_rising = rising_edges[rising_edges > falling_idx]
343
+ if len(subsequent_rising) > 0:
344
+ rising_idx = subsequent_rising[0]
345
+ width = (rising_idx - falling_idx) * sample_period
346
+
347
+ if width < min_width:
348
+ # Calculate amplitude within pulse
349
+ pulse_data = data[falling_idx : rising_idx + 1]
350
+ if isinstance(trace, DigitalTrace):
351
+ # For digital trace, amplitude is just 1.0 (logic low)
352
+ pulse_amplitude = 1.0
353
+ else:
354
+ pulse_amplitude = float(threshold_used - np.min(pulse_data))
355
+
356
+ glitches.append(
357
+ Glitch(
358
+ timestamp=falling_idx * sample_period,
359
+ width=width,
360
+ polarity="negative",
361
+ amplitude=pulse_amplitude,
362
+ )
363
+ )
364
+
365
+ # Sort by timestamp
366
+ glitches.sort(key=lambda g: g.timestamp)
367
+
368
+ return glitches
369
+
370
+
371
+ def signal_quality_summary(
372
+ trace: WaveformTrace,
373
+ *,
374
+ family: str = "LVCMOS_3V3",
375
+ min_pulse_width: float = 10e-9,
376
+ ) -> dict: # type: ignore[type-arg]
377
+ """Generate comprehensive signal quality summary.
378
+
379
+ Combines multiple quality metrics into a single report.
380
+
381
+ Args:
382
+ trace: Input waveform trace.
383
+ family: Logic family for noise margin calculation.
384
+ min_pulse_width: Minimum pulse width for glitch detection.
385
+
386
+ Returns:
387
+ Dictionary with quality metrics:
388
+ - noise_margin: NoiseMarginResult
389
+ - glitch_count: Number of detected glitches
390
+ - glitches: List of Glitch objects
391
+ - signal_levels: Measured low/high levels
392
+ - transition_count: Number of transitions
393
+
394
+ Example:
395
+ >>> summary = signal_quality_summary(trace)
396
+ >>> print(f"NMH: {summary['noise_margin'].nm_high:.3f} V")
397
+ >>> print(f"Glitches: {summary['glitch_count']}")
398
+ """
399
+ # Noise margin analysis
400
+ nm_result = noise_margin(trace, family=family)
401
+
402
+ # Glitch detection
403
+ glitches = detect_glitches(trace, min_width=min_pulse_width)
404
+
405
+ # Signal levels
406
+ low, high = _find_logic_levels(trace.data)
407
+
408
+ # Transition count
409
+ threshold = (low + high) / 2
410
+ digital = trace.data >= threshold
411
+ transitions = np.sum(np.abs(np.diff(digital.astype(np.int8))))
412
+
413
+ return {
414
+ "noise_margin": nm_result,
415
+ "glitch_count": len(glitches),
416
+ "glitches": glitches,
417
+ "signal_levels": {"low": low, "high": high},
418
+ "transition_count": int(transitions),
419
+ }
420
+
421
+
422
+ # =============================================================================
423
+ # Helper Functions
424
+ # =============================================================================
425
+
426
+
427
+ def _find_logic_levels(data: NDArray[np.floating[Any]]) -> tuple[float, float]:
428
+ """Find low and high logic levels from signal data.
429
+
430
+ Uses histogram analysis to identify stable high and low levels.
431
+
432
+ Args:
433
+ data: Waveform data array.
434
+
435
+ Returns:
436
+ Tuple of (low_level, high_level).
437
+ """
438
+ if len(data) == 0:
439
+ return 0.0, 0.0
440
+
441
+ # Use percentiles for robust level detection
442
+ p10, p90 = np.percentile(data, [10, 90])
443
+
444
+ try:
445
+ # Refine using histogram peaks
446
+ hist, bin_edges = np.histogram(data, bins=50)
447
+ bin_centers = (bin_edges[:-1] + bin_edges[1:]) / 2
448
+
449
+ # Find peaks in lower and upper halves
450
+ mid_idx = len(hist) // 2
451
+ low_idx = np.argmax(hist[:mid_idx])
452
+ high_idx = mid_idx + np.argmax(hist[mid_idx:])
453
+
454
+ low = bin_centers[low_idx]
455
+ high = bin_centers[high_idx]
456
+
457
+ # Sanity check
458
+ if high <= low:
459
+ return float(p10), float(p90)
460
+
461
+ return float(low), float(high)
462
+ except (ValueError, IndexError):
463
+ return float(p10), float(p90)
464
+
465
+
466
+ def _get_clock_edges(
467
+ trace: WaveformTrace | DigitalTrace,
468
+ edge_type: Literal["rising", "falling"],
469
+ ) -> NDArray[np.float64]:
470
+ """Get clock edge timestamps.
471
+
472
+ Args:
473
+ trace: Clock trace.
474
+ edge_type: Type of edges to find.
475
+
476
+ Returns:
477
+ Array of edge timestamps in seconds.
478
+ """
479
+ data = trace.data.astype(np.float64) if isinstance(trace, DigitalTrace) else trace.data
480
+
481
+ if len(data) < 2:
482
+ return np.array([], dtype=np.float64)
483
+
484
+ sample_period = trace.metadata.time_base
485
+
486
+ # Find threshold
487
+ low, high = _find_logic_levels(data)
488
+ threshold = (low + high) / 2
489
+
490
+ if edge_type == "rising":
491
+ crossings = np.where((data[:-1] < threshold) & (data[1:] >= threshold))[0]
492
+ else:
493
+ crossings = np.where((data[:-1] >= threshold) & (data[1:] < threshold))[0]
494
+
495
+ # Convert to timestamps with interpolation
496
+ timestamps = np.zeros(len(crossings), dtype=np.float64)
497
+
498
+ for i, idx in enumerate(crossings):
499
+ base_time = idx * sample_period
500
+ if idx < len(data) - 1:
501
+ v1, v2 = data[idx], data[idx + 1]
502
+ if abs(v2 - v1) > 1e-12:
503
+ t_offset = (threshold - v1) / (v2 - v1) * sample_period
504
+ t_offset = max(0, min(sample_period, t_offset))
505
+ timestamps[i] = base_time + t_offset
506
+ else:
507
+ timestamps[i] = base_time + sample_period / 2
508
+ else:
509
+ timestamps[i] = base_time
510
+
511
+ return timestamps
512
+
513
+
514
+ @dataclass
515
+ class MaskTestResult:
516
+ """Result of mask testing.
517
+
518
+ Attributes:
519
+ pass_fail: Overall pass/fail result.
520
+ hit_count: Number of samples violating the mask.
521
+ total_samples: Total number of samples tested.
522
+ margin_top: Margin to top mask boundary in volts (minimum).
523
+ margin_bottom: Margin to bottom mask boundary in volts (minimum).
524
+ violations: List of violation timestamps and amplitudes.
525
+ """
526
+
527
+ pass_fail: bool
528
+ hit_count: int
529
+ total_samples: int
530
+ margin_top: float
531
+ margin_bottom: float
532
+ violations: list[tuple[float, float]] # (time, voltage) pairs
533
+
534
+
535
+ @dataclass
536
+ class PLLRecoveryResult:
537
+ """Result of PLL clock recovery.
538
+
539
+ Attributes:
540
+ recovered_frequency: Recovered clock frequency in Hz.
541
+ recovered_phase: Recovered phase trajectory (radians).
542
+ vco_control: VCO control voltage trajectory.
543
+ lock_status: True if PLL is locked.
544
+ lock_time: Time to achieve lock in seconds (if locked).
545
+ frequency_error: Final frequency error in Hz.
546
+ """
547
+
548
+ recovered_frequency: float
549
+ recovered_phase: NDArray[np.float64]
550
+ vco_control: NDArray[np.float64]
551
+ lock_status: bool
552
+ lock_time: float | None
553
+ frequency_error: float
554
+
555
+
556
+ def mask_test(
557
+ trace: WaveformTrace,
558
+ mask: dict[str, NDArray[np.float64]] | str = "usb2",
559
+ *,
560
+ bit_period: float | None = None,
561
+ ) -> MaskTestResult:
562
+ """Test signal against compliance mask template.
563
+
564
+ Performs mask testing for signal integrity verification against
565
+ predefined templates (USB, PCIe, etc.) or custom masks.
566
+
567
+ Args:
568
+ trace: Input waveform trace.
569
+ mask: Either mask name ("usb2", "pcie_gen3") or custom mask dict with:
570
+ - "time_ui": Time coordinates in UI (0.0 to 2.0 for 2-UI mask)
571
+ - "voltage_top": Upper voltage boundary
572
+ - "voltage_bottom": Lower voltage boundary
573
+ bit_period: Bit period in seconds (required if mask uses UI coordinates).
574
+
575
+ Returns:
576
+ MaskTestResult with pass/fail and violation statistics.
577
+
578
+ Raises:
579
+ ValueError: If bit_period is not provided or mask name is not recognized.
580
+
581
+ Example:
582
+ >>> result = mask_test(signal_trace, mask="usb2", bit_period=3.33e-9)
583
+ >>> print(f"Pass: {result.pass_fail}, Violations: {result.hit_count}")
584
+
585
+ References:
586
+ USB 2.0 Specification, PCIe Base Specification
587
+ """
588
+ # Load predefined mask if string
589
+ mask_data = _get_predefined_mask(mask) if isinstance(mask, str) else mask
590
+
591
+ # Extract mask boundaries
592
+ time_ui = mask_data["time_ui"]
593
+ v_top = mask_data["voltage_top"]
594
+ v_bottom = mask_data["voltage_bottom"]
595
+
596
+ # Get signal data
597
+ data = trace.data
598
+ n_samples = len(data)
599
+ sample_rate = trace.metadata.sample_rate
600
+
601
+ if bit_period is None:
602
+ raise ValueError("bit_period is required for mask testing with UI coordinates")
603
+
604
+ # Convert UI to sample indices
605
+ samples_per_ui = bit_period * sample_rate
606
+ time_samples = time_ui * samples_per_ui
607
+
608
+ # For simplicity, test over one or two bit periods
609
+ # Align signal to start of bit period
610
+ n_ui = int(np.max(time_ui)) # 1 or 2 UI mask
611
+
612
+ violations: list[tuple[float, float]] = []
613
+ hit_count = 0
614
+
615
+ # Test all complete bit periods in the signal
616
+ n_periods = n_samples // int(samples_per_ui * n_ui)
617
+
618
+ for period_idx in range(n_periods):
619
+ period_start_sample = int(period_idx * samples_per_ui * n_ui)
620
+
621
+ # Extract samples for this period
622
+ for i, _t_ui in enumerate(time_ui):
623
+ sample_idx = period_start_sample + int(time_samples[i])
624
+
625
+ if sample_idx >= n_samples:
626
+ break
627
+
628
+ voltage = data[sample_idx]
629
+
630
+ # Check if voltage violates mask
631
+ if voltage > v_top[i] or voltage < v_bottom[i]:
632
+ timestamp = sample_idx / sample_rate
633
+ violations.append((timestamp, voltage))
634
+ hit_count += 1
635
+
636
+ # Calculate margins (minimum distance to mask boundaries)
637
+ margin_top = float(np.inf)
638
+ margin_bottom = float(np.inf)
639
+
640
+ for period_idx in range(n_periods):
641
+ period_start_sample = int(period_idx * samples_per_ui * n_ui)
642
+
643
+ for i, _t_ui in enumerate(time_ui):
644
+ sample_idx = period_start_sample + int(time_samples[i])
645
+
646
+ if sample_idx >= n_samples:
647
+ break
648
+
649
+ voltage = data[sample_idx]
650
+
651
+ # Margin to top
652
+ margin_top = min(margin_top, v_top[i] - voltage)
653
+
654
+ # Margin to bottom
655
+ margin_bottom = min(margin_bottom, voltage - v_bottom[i])
656
+
657
+ # Pass if no hits
658
+ pass_fail = hit_count == 0
659
+
660
+ return MaskTestResult(
661
+ pass_fail=pass_fail,
662
+ hit_count=hit_count,
663
+ total_samples=n_periods * len(time_ui),
664
+ margin_top=margin_top if margin_top != np.inf else 0.0,
665
+ margin_bottom=margin_bottom if margin_bottom != np.inf else 0.0,
666
+ violations=violations,
667
+ )
668
+
669
+
670
+ def _get_predefined_mask(mask_name: str) -> dict[str, NDArray[np.float64]]:
671
+ """Get predefined mask template.
672
+
673
+ Args:
674
+ mask_name: Name of standard mask ("usb2", "pcie_gen3", etc.).
675
+
676
+ Returns:
677
+ Dictionary with time_ui, voltage_top, voltage_bottom arrays.
678
+
679
+ Raises:
680
+ ValueError: If mask name is not recognized.
681
+ """
682
+ if mask_name == "usb2":
683
+ # USB 2.0 high-speed eye mask (simplified)
684
+ # 2-UI mask, normalized to ±1V amplitude
685
+ time_ui = np.array([0.0, 0.2, 0.4, 0.5, 0.6, 0.8, 1.0, 1.2, 1.4, 1.5, 1.6, 1.8, 2.0])
686
+ v_top = np.array([0.6, 0.6, 0.8, 0.9, 0.8, 0.6, 0.4, 0.6, 0.8, 0.9, 0.8, 0.6, 0.6])
687
+ v_bottom = np.array(
688
+ [
689
+ -0.6,
690
+ -0.6,
691
+ -0.8,
692
+ -0.9,
693
+ -0.8,
694
+ -0.6,
695
+ -0.4,
696
+ -0.6,
697
+ -0.8,
698
+ -0.9,
699
+ -0.8,
700
+ -0.6,
701
+ -0.6,
702
+ ]
703
+ )
704
+
705
+ elif mask_name == "pcie_gen3":
706
+ # PCIe Gen 3 eye mask (simplified)
707
+ time_ui = np.array([0.0, 0.15, 0.35, 0.5, 0.65, 0.85, 1.0])
708
+ v_top = np.array([0.5, 0.5, 0.7, 0.8, 0.7, 0.5, 0.5])
709
+ v_bottom = np.array([-0.5, -0.5, -0.7, -0.8, -0.7, -0.5, -0.5])
710
+
711
+ else:
712
+ raise ValueError(
713
+ f"Unknown mask: {mask_name}. Available: usb2, pcie_gen3. Or provide custom mask dict."
714
+ )
715
+
716
+ return {"time_ui": time_ui, "voltage_top": v_top, "voltage_bottom": v_bottom}
717
+
718
+
719
+ def pll_clock_recovery(
720
+ trace: WaveformTrace | DigitalTrace,
721
+ *,
722
+ nominal_frequency: float,
723
+ loop_bandwidth: float = 1e6,
724
+ damping: float = 0.707,
725
+ vco_gain: float = 1e6,
726
+ ) -> PLLRecoveryResult:
727
+ """Recover clock using PLL emulation.
728
+
729
+ Emulates a second-order PLL to recover embedded clock from NRZ,
730
+ NRZI, or Manchester-encoded data streams.
731
+
732
+ Args:
733
+ trace: Input data trace.
734
+ nominal_frequency: Nominal clock frequency in Hz.
735
+ loop_bandwidth: PLL loop bandwidth in Hz (default 1 MHz).
736
+ damping: Damping factor (default 0.707 for critical damping).
737
+ vco_gain: VCO gain in Hz/V (default 1 MHz/V).
738
+
739
+ Returns:
740
+ PLLRecoveryResult with recovered clock parameters.
741
+
742
+ Raises:
743
+ InsufficientDataError: If trace has fewer than 100 samples.
744
+
745
+ Example:
746
+ >>> result = pll_clock_recovery(data_trace, nominal_frequency=1e9)
747
+ >>> print(f"Recovered: {result.recovered_frequency / 1e9:.3f} GHz")
748
+ >>> print(f"Locked: {result.lock_status}")
749
+
750
+ References:
751
+ Gardner, F. M. (2005). Phaselock Techniques, 3rd ed.
752
+ """
753
+ data = trace.data.astype(np.float64) if isinstance(trace, DigitalTrace) else trace.data
754
+
755
+ sample_rate = trace.metadata.sample_rate
756
+ n_samples = len(data)
757
+
758
+ if n_samples < 100:
759
+ raise InsufficientDataError(
760
+ "PLL recovery requires at least 100 samples",
761
+ required=100,
762
+ available=n_samples,
763
+ analysis_type="pll_clock_recovery",
764
+ )
765
+
766
+ dt = 1.0 / sample_rate
767
+
768
+ # PLL parameters
769
+ omega_n = 2 * np.pi * loop_bandwidth # Natural frequency
770
+ K_vco = 2 * np.pi * vco_gain # VCO gain in rad/s/V
771
+
772
+ # Loop filter coefficients (2nd order)
773
+ # Transfer function: F(s) = K1 + K2/s
774
+ K1 = (2 * damping * omega_n) / K_vco
775
+ K2 = (omega_n**2) / K_vco
776
+
777
+ # Initialize PLL state
778
+ phase = np.zeros(n_samples)
779
+ vco_control = np.zeros(n_samples)
780
+ integrator = 0.0
781
+ theta = 0.0
782
+
783
+ # Nominal phase increment per sample
784
+ nominal_phase_inc = 2 * np.pi * nominal_frequency * dt
785
+
786
+ # Find edges for phase detection (simplified)
787
+ threshold = (np.max(data) + np.min(data)) / 2
788
+ edges = np.where(np.abs(np.diff(np.sign(data - threshold))) > 0)[0]
789
+
790
+ edge_idx = 0
791
+
792
+ # Run PLL loop
793
+ for i in range(n_samples):
794
+ # Phase detector: compare VCO phase to input transitions
795
+ if edge_idx < len(edges) and i == edges[edge_idx]:
796
+ # Edge detected - compute phase error
797
+ # Phase error = expected phase - actual VCO phase
798
+ expected_phase = (edges[edge_idx] * nominal_phase_inc) % (2 * np.pi)
799
+ phase_error = expected_phase - (theta % (2 * np.pi))
800
+
801
+ # Wrap to [-pi, pi]
802
+ phase_error = (phase_error + np.pi) % (2 * np.pi) - np.pi
803
+
804
+ edge_idx += 1
805
+ else:
806
+ phase_error = 0.0
807
+
808
+ # Loop filter (proportional + integral)
809
+ integrator += K2 * phase_error * dt
810
+ vco_input = K1 * phase_error + integrator
811
+
812
+ # VCO: frequency = nominal + K_vco * control voltage
813
+ vco_freq = nominal_frequency + K_vco * vco_input / (2 * np.pi)
814
+ phase_increment = 2 * np.pi * vco_freq * dt
815
+
816
+ # Update phase
817
+ theta += phase_increment
818
+
819
+ # Store results
820
+ phase[i] = theta
821
+ vco_control[i] = vco_input
822
+
823
+ # Analyze lock status
824
+ # Consider locked if VCO control voltage is stable in last 20%
825
+ lock_threshold = 0.1 # 10% variation
826
+ last_20_percent = vco_control[int(0.8 * n_samples) :]
827
+
828
+ if len(last_20_percent) > 0:
829
+ vco_std = np.std(last_20_percent)
830
+ vco_mean = np.abs(np.mean(last_20_percent))
831
+ lock_status = vco_std < lock_threshold * max(vco_mean, 1.0)
832
+
833
+ # Find lock time (when variation drops below threshold)
834
+ if lock_status:
835
+ # Search for first point where subsequent variance is low
836
+ window = int(0.1 * n_samples) # 10% window
837
+ for i in range(window, n_samples - window):
838
+ window_std = np.std(vco_control[i : i + window])
839
+ if window_std < lock_threshold:
840
+ lock_time = i * dt
841
+ break
842
+ else:
843
+ lock_time = None
844
+ else:
845
+ lock_time = None
846
+ else:
847
+ lock_status = False
848
+ lock_time = None
849
+
850
+ # Recovered frequency from final VCO state
851
+ final_vco = np.mean(vco_control[-int(0.1 * n_samples) :])
852
+ recovered_frequency = nominal_frequency + K_vco * final_vco / (2 * np.pi)
853
+
854
+ frequency_error = recovered_frequency - nominal_frequency
855
+
856
+ return PLLRecoveryResult(
857
+ recovered_frequency=float(recovered_frequency),
858
+ recovered_phase=phase,
859
+ vco_control=vco_control,
860
+ lock_status=lock_status,
861
+ lock_time=lock_time,
862
+ frequency_error=float(frequency_error),
863
+ )
864
+
865
+
866
+ __all__ = [
867
+ "Glitch",
868
+ "MaskTestResult",
869
+ "NoiseMarginResult",
870
+ "PLLRecoveryResult",
871
+ "Violation",
872
+ "detect_glitches",
873
+ "detect_violations",
874
+ "mask_test",
875
+ "noise_margin",
876
+ "pll_clock_recovery",
877
+ "signal_quality_summary",
878
+ ]