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,1104 @@
1
+ """Advanced timing measurements for digital signals.
2
+
3
+ This module provides IEEE 181-2011 and JEDEC compliant timing measurements
4
+ including propagation delay, setup/hold time, slew rate, phase, and skew.
5
+
6
+
7
+ Example:
8
+ >>> from oscura.analyzers.digital.timing import propagation_delay, setup_time
9
+ >>> delay = propagation_delay(trace1, trace2)
10
+ >>> t_setup = setup_time(data_trace, clock_trace, clock_edge="rising")
11
+
12
+ References:
13
+ IEEE 181-2011: Standard for Transitional Waveform Definitions
14
+ IEEE 2414-2020: Standard for Jitter and Phase Noise
15
+ JEDEC Standard No. 65B: High-Speed Interface Timing
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ from dataclasses import dataclass
21
+ from typing import TYPE_CHECKING, Literal
22
+
23
+ import numpy as np
24
+
25
+ from oscura.core.exceptions import InsufficientDataError
26
+ from oscura.core.types import DigitalTrace, WaveformTrace
27
+
28
+ if TYPE_CHECKING:
29
+ from numpy.typing import NDArray
30
+
31
+
32
+ @dataclass
33
+ class ClockRecoveryResult:
34
+ """Result of clock recovery analysis.
35
+
36
+ Attributes:
37
+ frequency: Recovered clock frequency in Hz.
38
+ period: Recovered clock period in seconds.
39
+ method: Method used for recovery ("fft" or "edge").
40
+ confidence: Confidence score (0.0 to 1.0).
41
+ jitter_rms: RMS jitter in seconds (edge method only).
42
+ jitter_pp: Peak-to-peak jitter in seconds (edge method only).
43
+ """
44
+
45
+ frequency: float
46
+ period: float
47
+ method: str
48
+ confidence: float
49
+ jitter_rms: float | None = None
50
+ jitter_pp: float | None = None
51
+
52
+
53
+ @dataclass
54
+ class TimingViolation:
55
+ """Represents a timing violation.
56
+
57
+ Attributes:
58
+ timestamp: Time of violation in seconds.
59
+ violation_type: Type of violation ("setup" or "hold").
60
+ measured: Measured time in seconds.
61
+ required: Required time (specification) in seconds.
62
+ margin: Margin to specification (negative = violation).
63
+ """
64
+
65
+ timestamp: float
66
+ violation_type: str
67
+ measured: float
68
+ required: float
69
+ margin: float
70
+
71
+
72
+ @dataclass
73
+ class RMSJitterResult:
74
+ """Result of RMS jitter measurement.
75
+
76
+ Attributes:
77
+ rms: RMS jitter in seconds.
78
+ mean: Mean period in seconds.
79
+ samples: Number of edges used.
80
+ uncertainty: Measurement uncertainty (1-sigma) in seconds.
81
+ edge_type: Type of edges used.
82
+
83
+ References:
84
+ IEEE 2414-2020 Section 5.1
85
+ TIM-007
86
+ """
87
+
88
+ rms: float
89
+ mean: float
90
+ samples: int
91
+ uncertainty: float
92
+ edge_type: str
93
+
94
+
95
+ def propagation_delay(
96
+ input_trace: WaveformTrace | DigitalTrace,
97
+ output_trace: WaveformTrace | DigitalTrace,
98
+ *,
99
+ ref_level: float = 0.5,
100
+ edge_type: Literal["rising", "falling", "both"] = "rising",
101
+ return_all: bool = False,
102
+ ) -> float | NDArray[np.float64]:
103
+ """Measure propagation delay between two signals.
104
+
105
+ Computes the time delay from input edge to corresponding output edge
106
+ at the specified reference level per IEEE 181-2011.
107
+
108
+ Args:
109
+ input_trace: Input signal trace.
110
+ output_trace: Output signal trace.
111
+ ref_level: Reference level as fraction (0.0 to 1.0). Default 0.5 (50%).
112
+ edge_type: Type of edges to measure:
113
+ - "rising": Low-to-high transitions
114
+ - "falling": High-to-low transitions
115
+ - "both": All transitions
116
+ return_all: If True, return array of all delays. If False, return mean.
117
+
118
+ Returns:
119
+ Propagation delay in seconds (mean if return_all=False), or array of delays.
120
+
121
+ Raises:
122
+ InsufficientDataError: If traces have insufficient edges.
123
+
124
+ Example:
125
+ >>> delay = propagation_delay(input_trace, output_trace)
126
+ >>> print(f"Propagation delay: {delay * 1e9:.2f} ns")
127
+
128
+ References:
129
+ IEEE 181-2011 Section 5.6
130
+ """
131
+ # Get edge timestamps for both signals
132
+ input_edges = _get_edge_timestamps(input_trace, edge_type, ref_level)
133
+ output_edges = _get_edge_timestamps(output_trace, edge_type, ref_level)
134
+
135
+ if len(input_edges) == 0:
136
+ raise InsufficientDataError(
137
+ "No edges found in input trace",
138
+ required=1,
139
+ available=0,
140
+ analysis_type="propagation_delay",
141
+ )
142
+
143
+ if len(output_edges) == 0:
144
+ raise InsufficientDataError(
145
+ "No edges found in output trace",
146
+ required=1,
147
+ available=0,
148
+ analysis_type="propagation_delay",
149
+ )
150
+
151
+ # Match input edges to nearest subsequent output edges
152
+ delays: list[float] = []
153
+
154
+ for in_edge in input_edges:
155
+ # Find output edges after this input edge
156
+ subsequent_outputs = output_edges[output_edges > in_edge]
157
+ if len(subsequent_outputs) > 0:
158
+ # Use nearest subsequent output edge
159
+ delay = subsequent_outputs[0] - in_edge
160
+ if delay > 0:
161
+ delays.append(delay)
162
+
163
+ if len(delays) == 0:
164
+ if return_all:
165
+ return np.array([], dtype=np.float64)
166
+ return np.nan
167
+
168
+ delays_arr = np.array(delays, dtype=np.float64)
169
+
170
+ if return_all:
171
+ return delays_arr
172
+ return float(np.mean(delays_arr))
173
+
174
+
175
+ def setup_time(
176
+ data_trace: WaveformTrace | DigitalTrace,
177
+ clock_trace: WaveformTrace | DigitalTrace,
178
+ *,
179
+ clock_edge: Literal["rising", "falling"] = "rising",
180
+ data_stable_level: float = 0.5,
181
+ return_all: bool = False,
182
+ ) -> float | NDArray[np.float64]:
183
+ """Measure setup time between data and clock signals.
184
+
185
+ Computes the time from when data becomes stable to the clock edge
186
+ per JEDEC timing standards.
187
+
188
+ Args:
189
+ data_trace: Data signal trace.
190
+ clock_trace: Clock signal trace.
191
+ clock_edge: Type of clock edge to reference ("rising" or "falling").
192
+ data_stable_level: Reference level for data stability (0.0 to 1.0).
193
+ return_all: If True, return array of all setup times. If False, return mean.
194
+
195
+ Returns:
196
+ Setup time in seconds (positive = data stable before clock).
197
+
198
+ Example:
199
+ >>> t_setup = setup_time(data_trace, clock_trace, clock_edge="rising")
200
+ >>> print(f"Setup time: {t_setup * 1e9:.2f} ns")
201
+
202
+ References:
203
+ JEDEC Standard No. 65B
204
+ """
205
+ # Get clock edges
206
+ clock_edges = _get_edge_timestamps(clock_trace, clock_edge, 0.5)
207
+
208
+ if len(clock_edges) == 0:
209
+ if return_all:
210
+ return np.array([], dtype=np.float64)
211
+ return np.nan
212
+
213
+ # Get all data edges (both rising and falling)
214
+ data_edges = _get_edge_timestamps(data_trace, "both", data_stable_level)
215
+
216
+ if len(data_edges) == 0:
217
+ if return_all:
218
+ return np.array([], dtype=np.float64)
219
+ return np.nan
220
+
221
+ # For each clock edge, find the most recent data edge
222
+ setup_times: list[float] = []
223
+
224
+ for clk_edge in clock_edges:
225
+ # Find data edges before this clock edge
226
+ prior_data_edges = data_edges[data_edges < clk_edge]
227
+ if len(prior_data_edges) > 0:
228
+ # Setup time = clock edge - last data edge
229
+ setup = clk_edge - prior_data_edges[-1]
230
+ setup_times.append(setup)
231
+
232
+ if len(setup_times) == 0:
233
+ if return_all:
234
+ return np.array([], dtype=np.float64)
235
+ return np.nan
236
+
237
+ result = np.array(setup_times, dtype=np.float64)
238
+
239
+ if return_all:
240
+ return result
241
+ return float(np.mean(result))
242
+
243
+
244
+ def hold_time(
245
+ data_trace: WaveformTrace | DigitalTrace,
246
+ clock_trace: WaveformTrace | DigitalTrace,
247
+ *,
248
+ clock_edge: Literal["rising", "falling"] = "rising",
249
+ data_stable_level: float = 0.5,
250
+ return_all: bool = False,
251
+ ) -> float | NDArray[np.float64]:
252
+ """Measure hold time between clock and data signals.
253
+
254
+ Computes the time from clock edge to when data changes
255
+ per JEDEC timing standards.
256
+
257
+ Args:
258
+ data_trace: Data signal trace.
259
+ clock_trace: Clock signal trace.
260
+ clock_edge: Type of clock edge to reference ("rising" or "falling").
261
+ data_stable_level: Reference level for data transition (0.0 to 1.0).
262
+ return_all: If True, return array of all hold times. If False, return mean.
263
+
264
+ Returns:
265
+ Hold time in seconds (positive = data stable after clock).
266
+
267
+ Example:
268
+ >>> t_hold = hold_time(data_trace, clock_trace, clock_edge="rising")
269
+ >>> print(f"Hold time: {t_hold * 1e9:.2f} ns")
270
+
271
+ References:
272
+ JEDEC Standard No. 65B
273
+ """
274
+ # Get clock edges
275
+ clock_edges = _get_edge_timestamps(clock_trace, clock_edge, 0.5)
276
+
277
+ if len(clock_edges) == 0:
278
+ if return_all:
279
+ return np.array([], dtype=np.float64)
280
+ return np.nan
281
+
282
+ # Get all data edges
283
+ data_edges = _get_edge_timestamps(data_trace, "both", data_stable_level)
284
+
285
+ if len(data_edges) == 0:
286
+ if return_all:
287
+ return np.array([], dtype=np.float64)
288
+ return np.nan
289
+
290
+ # For each clock edge, find the next data edge
291
+ hold_times: list[float] = []
292
+
293
+ for clk_edge in clock_edges:
294
+ # Find data edges after this clock edge
295
+ subsequent_data_edges = data_edges[data_edges > clk_edge]
296
+ if len(subsequent_data_edges) > 0:
297
+ # Hold time = next data edge - clock edge
298
+ hold = subsequent_data_edges[0] - clk_edge
299
+ hold_times.append(hold)
300
+
301
+ if len(hold_times) == 0:
302
+ if return_all:
303
+ return np.array([], dtype=np.float64)
304
+ return np.nan
305
+
306
+ result = np.array(hold_times, dtype=np.float64)
307
+
308
+ if return_all:
309
+ return result
310
+ return float(np.mean(result))
311
+
312
+
313
+ def slew_rate(
314
+ trace: WaveformTrace,
315
+ *,
316
+ ref_levels: tuple[float, float] = (0.2, 0.8),
317
+ edge_type: Literal["rising", "falling", "both"] = "rising",
318
+ return_all: bool = False,
319
+ ) -> float | NDArray[np.float64]:
320
+ """Measure slew rate (dV/dt) during signal transitions.
321
+
322
+ Computes the rate of voltage change during edge transitions
323
+ per IEEE 181-2011.
324
+
325
+ Args:
326
+ trace: Input waveform trace.
327
+ ref_levels: Reference levels as fractions (default 20%-80%).
328
+ edge_type: Type of edges to measure.
329
+ return_all: If True, return array of all slew rates. If False, return mean.
330
+
331
+ Returns:
332
+ Slew rate in V/s (positive for rising, negative for falling).
333
+
334
+ Example:
335
+ >>> sr = slew_rate(trace)
336
+ >>> print(f"Slew rate: {sr / 1e6:.2f} V/us")
337
+
338
+ References:
339
+ IEEE 181-2011 Section 5.2
340
+ """
341
+ if len(trace.data) < 3:
342
+ if return_all:
343
+ return np.array([], dtype=np.float64)
344
+ return np.nan
345
+
346
+ data = trace.data
347
+ sample_period = trace.metadata.time_base
348
+
349
+ # Find signal levels
350
+ low, high = _find_levels(data)
351
+ amplitude = high - low
352
+
353
+ if amplitude <= 0:
354
+ if return_all:
355
+ return np.array([], dtype=np.float64)
356
+ return np.nan
357
+
358
+ # Calculate reference voltages
359
+ v_low = low + ref_levels[0] * amplitude
360
+ v_high = low + ref_levels[1] * amplitude
361
+ dv = v_high - v_low
362
+
363
+ slew_rates: list[float] = []
364
+
365
+ if edge_type in ("rising", "both"):
366
+ # Find rising transitions
367
+ rising_start = np.where((data[:-1] < v_low) & (data[1:] >= v_low))[0]
368
+
369
+ for start_idx in rising_start:
370
+ # Find where signal reaches v_high
371
+ remaining = data[start_idx:]
372
+ above_high = remaining >= v_high
373
+
374
+ if np.any(above_high):
375
+ end_offset = np.argmax(above_high)
376
+ dt = end_offset * sample_period
377
+ if dt > 0:
378
+ slew_rates.append(float(dv / dt))
379
+
380
+ if edge_type in ("falling", "both"):
381
+ # Find falling transitions
382
+ falling_start = np.where((data[:-1] > v_high) & (data[1:] <= v_high))[0]
383
+
384
+ for start_idx in falling_start:
385
+ # Find where signal reaches v_low
386
+ remaining = data[start_idx:]
387
+ below_low = remaining <= v_low
388
+
389
+ if np.any(below_low):
390
+ end_offset = np.argmax(below_low)
391
+ dt = end_offset * sample_period
392
+ if dt > 0:
393
+ slew_rates.append(float(-dv / dt)) # Negative for falling
394
+
395
+ if len(slew_rates) == 0:
396
+ if return_all:
397
+ return np.array([], dtype=np.float64)
398
+ return np.nan
399
+
400
+ result = np.array(slew_rates, dtype=np.float64)
401
+
402
+ if return_all:
403
+ return result
404
+ return float(np.mean(result))
405
+
406
+
407
+ def phase(
408
+ trace1: WaveformTrace,
409
+ trace2: WaveformTrace,
410
+ *,
411
+ method: Literal["edge", "fft"] = "edge",
412
+ unit: Literal["degrees", "radians"] = "degrees",
413
+ ) -> float:
414
+ """Measure phase difference between two signals.
415
+
416
+ Computes the phase relationship between two waveforms using
417
+ either edge-based or FFT-based methods.
418
+
419
+ Args:
420
+ trace1: Reference signal trace.
421
+ trace2: Signal to measure phase relative to reference.
422
+ method: Measurement method:
423
+ - "edge": Edge-to-edge timing (default, more accurate for digital)
424
+ - "fft": Cross-spectral phase (better for analog/noisy signals)
425
+ unit: Output unit ("degrees" or "radians").
426
+
427
+ Returns:
428
+ Phase difference in specified units. Positive = trace2 leads trace1.
429
+
430
+ Raises:
431
+ ValueError: If method is not recognized.
432
+
433
+ Example:
434
+ >>> phase_deg = phase(ref_trace, sig_trace)
435
+ >>> print(f"Phase: {phase_deg:.1f} degrees")
436
+
437
+ References:
438
+ IEEE 181-2011 Section 5.8
439
+ """
440
+ if method == "edge":
441
+ return _phase_edge(trace1, trace2, unit)
442
+ elif method == "fft":
443
+ return _phase_fft(trace1, trace2, unit)
444
+ else:
445
+ raise ValueError(f"Unknown method: {method}")
446
+
447
+
448
+ def _phase_edge(
449
+ trace1: WaveformTrace,
450
+ trace2: WaveformTrace,
451
+ unit: Literal["degrees", "radians"],
452
+ ) -> float:
453
+ """Compute phase using edge timing."""
454
+ # Get rising edges for both signals
455
+ edges1 = _get_edge_timestamps(trace1, "rising", 0.5)
456
+ edges2 = _get_edge_timestamps(trace2, "rising", 0.5)
457
+
458
+ if len(edges1) < 2 or len(edges2) < 2:
459
+ return np.nan # type: ignore[no-any-return]
460
+
461
+ # Calculate period from first signal
462
+ period1 = np.mean(np.diff(edges1))
463
+
464
+ if period1 <= 0:
465
+ return np.nan # type: ignore[no-any-return]
466
+
467
+ # Calculate phase from edge differences
468
+ phase_times: list[float] = []
469
+
470
+ for e1 in edges1:
471
+ # Find nearest edge in trace2
472
+ diffs = edges2 - e1
473
+ # Find closest edge (could be before or after)
474
+ idx = np.argmin(np.abs(diffs))
475
+ phase_times.append(diffs[idx])
476
+
477
+ if len(phase_times) == 0:
478
+ return np.nan # type: ignore[no-any-return]
479
+
480
+ mean_phase_time = np.mean(phase_times)
481
+
482
+ # Convert to phase angle
483
+ phase_rad = 2 * np.pi * mean_phase_time / period1
484
+
485
+ # Normalize to [-pi, pi]
486
+ phase_rad = (phase_rad + np.pi) % (2 * np.pi) - np.pi
487
+
488
+ if unit == "degrees":
489
+ return float(np.degrees(phase_rad))
490
+ return float(phase_rad)
491
+
492
+
493
+ def _phase_fft(
494
+ trace1: WaveformTrace,
495
+ trace2: WaveformTrace,
496
+ unit: Literal["degrees", "radians"],
497
+ ) -> float:
498
+ """Compute phase using FFT cross-spectral analysis."""
499
+ data1 = trace1.data - np.mean(trace1.data)
500
+ data2 = trace2.data - np.mean(trace2.data)
501
+
502
+ # Ensure same length
503
+ n = min(len(data1), len(data2))
504
+ data1 = data1[:n]
505
+ data2 = data2[:n]
506
+
507
+ if n < 16:
508
+ return np.nan # type: ignore[no-any-return]
509
+
510
+ # Compute FFTs
511
+ fft1 = np.fft.rfft(data1)
512
+ fft2 = np.fft.rfft(data2)
513
+
514
+ # Cross-spectrum
515
+ cross = fft2 * np.conj(fft1)
516
+
517
+ # Find fundamental frequency (strongest component after DC)
518
+ magnitudes = np.abs(cross)
519
+ fund_idx = np.argmax(magnitudes[1:]) + 1
520
+
521
+ # Phase at fundamental
522
+ phase_rad = np.angle(cross[fund_idx])
523
+
524
+ if unit == "degrees":
525
+ return float(np.degrees(phase_rad))
526
+ return float(phase_rad)
527
+
528
+
529
+ def skew(
530
+ traces: list[WaveformTrace | DigitalTrace],
531
+ *,
532
+ reference_idx: int = 0,
533
+ edge_type: Literal["rising", "falling"] = "rising",
534
+ ) -> dict[str, float | NDArray[np.float64]]:
535
+ """Measure timing skew between multiple signals.
536
+
537
+ Computes the timing offset of each signal relative to a reference
538
+ per IEEE 181-2011.
539
+
540
+ Args:
541
+ traces: List of signal traces to compare.
542
+ reference_idx: Index of reference signal (default 0).
543
+ edge_type: Type of edges to use for comparison.
544
+
545
+ Returns:
546
+ Dictionary with skew statistics:
547
+ - skew_values: Array of skew for each non-reference trace
548
+ - min: Minimum skew
549
+ - max: Maximum skew
550
+ - mean: Mean skew
551
+ - range: Max - min (total skew spread)
552
+
553
+ Raises:
554
+ ValueError: If fewer than 2 traces or reference_idx out of range.
555
+
556
+ Example:
557
+ >>> result = skew([clk1, clk2, clk3])
558
+ >>> print(f"Max skew: {result['max'] * 1e12:.0f} ps")
559
+
560
+ References:
561
+ IEEE 181-2011 Section 5.7
562
+ """
563
+ if len(traces) < 2:
564
+ raise ValueError("Need at least 2 traces for skew measurement")
565
+
566
+ if reference_idx >= len(traces):
567
+ raise ValueError(f"reference_idx {reference_idx} out of range")
568
+
569
+ # Get reference edges
570
+ ref_trace = traces[reference_idx]
571
+ ref_edges = _get_edge_timestamps(ref_trace, edge_type, 0.5)
572
+
573
+ if len(ref_edges) == 0:
574
+ return {
575
+ "skew_values": np.array([], dtype=np.float64),
576
+ "min": float(np.nan),
577
+ "max": float(np.nan),
578
+ "mean": float(np.nan),
579
+ "range": float(np.nan),
580
+ }
581
+
582
+ # Compute skew for all traces (including reference which has 0 skew)
583
+ all_skews: list[float] = []
584
+ skew_values: list[float] = []
585
+
586
+ for i, trace in enumerate(traces):
587
+ if i == reference_idx:
588
+ # Reference has zero skew by definition
589
+ all_skews.append(0.0)
590
+ continue
591
+
592
+ trace_edges = _get_edge_timestamps(trace, edge_type, 0.5)
593
+
594
+ if len(trace_edges) == 0:
595
+ skew_val = np.nan
596
+ else:
597
+ # Match edges and compute skew
598
+ edge_skews = []
599
+ for ref_edge in ref_edges:
600
+ # Find nearest edge in this trace
601
+ diffs = np.abs(trace_edges - ref_edge)
602
+ nearest_idx = np.argmin(diffs)
603
+ skew_val_edge = trace_edges[nearest_idx] - ref_edge
604
+ edge_skews.append(skew_val_edge)
605
+
606
+ skew_val = float(np.mean(edge_skews)) if len(edge_skews) > 0 else np.nan
607
+
608
+ skew_values.append(skew_val)
609
+ all_skews.append(skew_val)
610
+
611
+ skew_arr = np.array(skew_values, dtype=np.float64)
612
+ all_skews_arr = np.array(all_skews, dtype=np.float64)
613
+ valid_all_skews = all_skews_arr[~np.isnan(all_skews_arr)]
614
+
615
+ if len(valid_all_skews) == 0:
616
+ return {
617
+ "skew_values": skew_arr,
618
+ "min": np.nan,
619
+ "max": np.nan,
620
+ "mean": np.nan,
621
+ "range": np.nan,
622
+ }
623
+
624
+ # Compute statistics across ALL traces (including reference)
625
+ return {
626
+ "skew_values": skew_arr,
627
+ "min": float(np.min(valid_all_skews)),
628
+ "max": float(np.max(valid_all_skews)),
629
+ "mean": float(np.mean(valid_all_skews)),
630
+ "range": float(np.max(valid_all_skews) - np.min(valid_all_skews)),
631
+ }
632
+
633
+
634
+ def recover_clock_fft(
635
+ trace: WaveformTrace | DigitalTrace,
636
+ *,
637
+ min_freq: float | None = None,
638
+ max_freq: float | None = None,
639
+ ) -> ClockRecoveryResult:
640
+ """Recover clock frequency using FFT peak detection.
641
+
642
+ Detects the dominant frequency component in the signal using
643
+ FFT analysis, suitable for periodic digital signals.
644
+
645
+ Args:
646
+ trace: Input trace (analog or digital).
647
+ min_freq: Minimum frequency to consider (Hz). Default: sample_rate/1000.
648
+ max_freq: Maximum frequency to consider (Hz). Default: sample_rate/2.
649
+
650
+ Returns:
651
+ ClockRecoveryResult with recovered frequency and confidence.
652
+
653
+ Raises:
654
+ InsufficientDataError: If trace has fewer than 16 samples.
655
+
656
+ Example:
657
+ >>> result = recover_clock_fft(trace)
658
+ >>> print(f"Clock: {result.frequency / 1e6:.3f} MHz")
659
+
660
+ References:
661
+ IEEE 1241-2010 Section 4.1
662
+ """
663
+ data = trace.data.astype(np.float64) if isinstance(trace, DigitalTrace) else trace.data
664
+
665
+ n = len(data)
666
+ sample_rate = trace.metadata.sample_rate
667
+
668
+ if n < 16:
669
+ raise InsufficientDataError(
670
+ "FFT clock recovery requires at least 16 samples",
671
+ required=16,
672
+ available=n,
673
+ analysis_type="clock_recovery_fft",
674
+ )
675
+
676
+ # Set frequency range defaults
677
+ if min_freq is None:
678
+ min_freq = sample_rate / 1000
679
+ if max_freq is None:
680
+ max_freq = sample_rate / 2
681
+
682
+ # Remove DC and compute FFT
683
+ data_centered = data - np.mean(data)
684
+ nfft = int(2 ** np.ceil(np.log2(n)))
685
+ spectrum = np.fft.rfft(data_centered, n=nfft)
686
+ freq = np.fft.rfftfreq(nfft, d=1.0 / sample_rate)
687
+ magnitude = np.abs(spectrum)
688
+
689
+ # Apply frequency range mask
690
+ mask = (freq >= min_freq) & (freq <= max_freq)
691
+ valid_indices = np.where(mask)[0]
692
+
693
+ if len(valid_indices) == 0:
694
+ return ClockRecoveryResult(
695
+ frequency=np.nan,
696
+ period=np.nan,
697
+ method="fft",
698
+ confidence=0.0,
699
+ )
700
+
701
+ # Find peak in valid range
702
+ local_peak_idx = np.argmax(magnitude[valid_indices])
703
+ peak_idx = valid_indices[local_peak_idx]
704
+ peak_freq = freq[peak_idx]
705
+ peak_mag = magnitude[peak_idx]
706
+
707
+ # Calculate confidence (ratio of peak to RMS of spectrum)
708
+ rms_mag = np.sqrt(np.mean(magnitude[valid_indices] ** 2))
709
+ confidence = min(1.0, (peak_mag / rms_mag - 1) / 10) if rms_mag > 0 else 0.0
710
+
711
+ # Parabolic interpolation for more accurate frequency
712
+ if 0 < peak_idx < len(magnitude) - 1:
713
+ alpha = magnitude[peak_idx - 1]
714
+ beta = magnitude[peak_idx]
715
+ gamma = magnitude[peak_idx + 1]
716
+
717
+ if beta > alpha and beta > gamma:
718
+ freq_resolution = sample_rate / nfft
719
+ delta = 0.5 * (alpha - gamma) / (alpha - 2 * beta + gamma + 1e-12)
720
+ peak_freq = peak_freq + delta * freq_resolution
721
+
722
+ period = 1.0 / peak_freq if peak_freq > 0 else np.nan
723
+
724
+ return ClockRecoveryResult(
725
+ frequency=float(peak_freq),
726
+ period=float(period),
727
+ method="fft",
728
+ confidence=float(confidence),
729
+ )
730
+
731
+
732
+ def recover_clock_edge(
733
+ trace: WaveformTrace | DigitalTrace,
734
+ *,
735
+ edge_type: Literal["rising", "falling"] = "rising",
736
+ threshold: float | None = None,
737
+ ) -> ClockRecoveryResult:
738
+ """Recover clock frequency from edge timestamps.
739
+
740
+ Computes clock frequency from edge-to-edge timing, also
741
+ providing jitter statistics.
742
+
743
+ Args:
744
+ trace: Input trace (analog or digital).
745
+ edge_type: Type of edges to use ("rising" or "falling").
746
+ threshold: Threshold for edge detection (analog traces only).
747
+
748
+ Returns:
749
+ ClockRecoveryResult with frequency and jitter statistics.
750
+
751
+ Example:
752
+ >>> result = recover_clock_edge(trace)
753
+ >>> print(f"Clock: {result.frequency / 1e6:.3f} MHz")
754
+ >>> print(f"Jitter RMS: {result.jitter_rms * 1e12:.1f} ps")
755
+
756
+ References:
757
+ IEEE 2414-2020 Section 4
758
+ """
759
+ # Get edge timestamps
760
+ ref_level = 0.5 if threshold is None else threshold
761
+ edges = _get_edge_timestamps(trace, edge_type, ref_level)
762
+
763
+ if len(edges) < 3:
764
+ return ClockRecoveryResult(
765
+ frequency=np.nan,
766
+ period=np.nan,
767
+ method="edge",
768
+ confidence=0.0,
769
+ )
770
+
771
+ # Compute periods
772
+ periods = np.diff(edges)
773
+
774
+ if len(periods) == 0:
775
+ return ClockRecoveryResult(
776
+ frequency=np.nan,
777
+ period=np.nan,
778
+ method="edge",
779
+ confidence=0.0,
780
+ )
781
+
782
+ # Calculate statistics
783
+ mean_period = float(np.mean(periods))
784
+ std_period = float(np.std(periods))
785
+ frequency = 1.0 / mean_period if mean_period > 0 else np.nan
786
+
787
+ # Jitter statistics
788
+ jitter_rms = std_period
789
+ jitter_pp = float(np.max(periods) - np.min(periods))
790
+
791
+ # Confidence based on period consistency (low jitter = high confidence)
792
+ if mean_period > 0:
793
+ cv = std_period / mean_period # Coefficient of variation
794
+ confidence = max(0.0, min(1.0, 1.0 - cv * 10))
795
+ else:
796
+ confidence = 0.0
797
+
798
+ return ClockRecoveryResult(
799
+ frequency=float(frequency),
800
+ period=mean_period,
801
+ method="edge",
802
+ confidence=float(confidence),
803
+ jitter_rms=jitter_rms,
804
+ jitter_pp=jitter_pp,
805
+ )
806
+
807
+
808
+ # =============================================================================
809
+ # Helper Functions
810
+ # =============================================================================
811
+
812
+
813
+ def _get_edge_timestamps(
814
+ trace: WaveformTrace | DigitalTrace,
815
+ edge_type: Literal["rising", "falling", "both"],
816
+ ref_level: float = 0.5,
817
+ ) -> NDArray[np.float64]:
818
+ """Get edge timestamps from a trace.
819
+
820
+ Args:
821
+ trace: Input trace.
822
+ edge_type: Type of edges to find.
823
+ ref_level: Reference level for analog traces (0.0 to 1.0).
824
+
825
+ Returns:
826
+ Array of edge timestamps in seconds.
827
+ """
828
+ if isinstance(trace, DigitalTrace):
829
+ data = trace.data.astype(np.float64)
830
+ sample_rate = trace.metadata.sample_rate
831
+ else:
832
+ data = trace.data
833
+ sample_rate = trace.metadata.sample_rate
834
+
835
+ if len(data) < 2:
836
+ return np.array([], dtype=np.float64)
837
+
838
+ sample_period = 1.0 / sample_rate
839
+
840
+ # Find threshold level
841
+ low, high = _find_levels(data)
842
+ threshold = low + ref_level * (high - low)
843
+
844
+ timestamps: list[float] = []
845
+
846
+ if edge_type in ("rising", "both"):
847
+ crossings = np.where((data[:-1] < threshold) & (data[1:] >= threshold))[0]
848
+ for idx in crossings:
849
+ # Linear interpolation
850
+ if idx < len(data) - 1:
851
+ v1, v2 = data[idx], data[idx + 1]
852
+ if abs(v2 - v1) > 1e-12:
853
+ t_offset = (threshold - v1) / (v2 - v1) * sample_period
854
+ t_offset = max(0, min(sample_period, t_offset))
855
+ else:
856
+ t_offset = sample_period / 2
857
+ timestamps.append(idx * sample_period + t_offset)
858
+
859
+ if edge_type in ("falling", "both"):
860
+ crossings = np.where((data[:-1] >= threshold) & (data[1:] < threshold))[0]
861
+ for idx in crossings:
862
+ if idx < len(data) - 1:
863
+ v1, v2 = data[idx], data[idx + 1]
864
+ if abs(v2 - v1) > 1e-12:
865
+ t_offset = (threshold - v1) / (v2 - v1) * sample_period
866
+ t_offset = max(0, min(sample_period, t_offset))
867
+ else:
868
+ t_offset = sample_period / 2
869
+ timestamps.append(idx * sample_period + t_offset)
870
+
871
+ timestamps.sort()
872
+ return np.array(timestamps, dtype=np.float64)
873
+
874
+
875
+ def _find_levels(data: NDArray[np.float64]) -> tuple[float, float]:
876
+ """Find low and high levels using histogram method.
877
+
878
+ Args:
879
+ data: Waveform data array.
880
+
881
+ Returns:
882
+ Tuple of (low_level, high_level).
883
+ """
884
+ # Use percentiles for robust level detection
885
+ p10, p90 = np.percentile(data, [10, 90])
886
+
887
+ # Refine using histogram peaks
888
+ try:
889
+ hist, bin_edges = np.histogram(data, bins=50)
890
+ bin_centers = (bin_edges[:-1] + bin_edges[1:]) / 2
891
+
892
+ # Find peaks in lower and upper halves
893
+ mid_idx = len(hist) // 2
894
+ low_idx = np.argmax(hist[:mid_idx])
895
+ high_idx = mid_idx + np.argmax(hist[mid_idx:])
896
+
897
+ low = bin_centers[low_idx]
898
+ high = bin_centers[high_idx]
899
+
900
+ # Sanity check
901
+ if high <= low:
902
+ return float(p10), float(p90)
903
+
904
+ return float(low), float(high)
905
+ except (ValueError, IndexError):
906
+ return float(p10), float(p90)
907
+
908
+
909
+ def rms_jitter(
910
+ trace: WaveformTrace | DigitalTrace,
911
+ *,
912
+ edge_type: Literal["rising", "falling", "both"] = "rising",
913
+ threshold: float = 0.5,
914
+ ) -> RMSJitterResult:
915
+ """Measure RMS jitter from edge timing variations.
916
+
917
+ Computes root-mean-square jitter as the standard deviation of edge
918
+ timing variations per IEEE 2414-2020. RMS jitter characterizes the
919
+ random component of timing uncertainty.
920
+
921
+ Args:
922
+ trace: Input trace (analog or digital).
923
+ edge_type: Type of edges to measure ("rising", "falling", or "both").
924
+ threshold: Threshold for edge detection (0.0 to 1.0).
925
+
926
+ Returns:
927
+ RMSJitterResult containing RMS jitter and statistics.
928
+
929
+ Example:
930
+ >>> result = rms_jitter(clock_trace)
931
+ >>> print(f"RMS jitter: {result.rms * 1e12:.2f} ps")
932
+ >>> print(f"Uncertainty: +/- {result.uncertainty * 1e12:.2f} ps")
933
+
934
+ References:
935
+ IEEE 2414-2020 Section 5.1
936
+ TIM-007
937
+ """
938
+ # Get edge timestamps
939
+ edges = _get_edge_timestamps(trace, edge_type, threshold)
940
+
941
+ if len(edges) < 3:
942
+ return RMSJitterResult(
943
+ rms=np.nan,
944
+ mean=np.nan,
945
+ samples=0,
946
+ uncertainty=np.nan,
947
+ edge_type=edge_type,
948
+ )
949
+
950
+ # Calculate periods
951
+ periods = np.diff(edges)
952
+
953
+ if len(periods) < 2:
954
+ return RMSJitterResult(
955
+ rms=np.nan,
956
+ mean=np.nan,
957
+ samples=len(edges),
958
+ uncertainty=np.nan,
959
+ edge_type=edge_type,
960
+ )
961
+
962
+ # RMS jitter is the standard deviation of periods
963
+ mean_period = float(np.mean(periods))
964
+ jitter_rms = float(np.std(periods, ddof=1))
965
+
966
+ # Measurement uncertainty (1-sigma)
967
+ # For N samples, uncertainty of std estimate is std / sqrt(2*(N-1))
968
+ n = len(periods)
969
+ uncertainty = jitter_rms / np.sqrt(2 * (n - 1)) if n > 1 else np.nan
970
+
971
+ return RMSJitterResult(
972
+ rms=jitter_rms,
973
+ mean=mean_period,
974
+ samples=n,
975
+ uncertainty=uncertainty,
976
+ edge_type=edge_type,
977
+ )
978
+
979
+
980
+ def peak_to_peak_jitter(
981
+ trace: WaveformTrace | DigitalTrace,
982
+ *,
983
+ edge_type: Literal["rising", "falling", "both"] = "rising",
984
+ threshold: float = 0.5,
985
+ ) -> float:
986
+ """Measure peak-to-peak jitter from edge timing variations.
987
+
988
+ Pk-Pk jitter is the maximum range of edge timing deviations from
989
+ the ideal periodic timing, measured over the observation window.
990
+
991
+ Args:
992
+ trace: Input trace (analog or digital).
993
+ edge_type: Type of edges to measure ("rising", "falling", or "both").
994
+ threshold: Threshold for edge detection (0.0 to 1.0).
995
+
996
+ Returns:
997
+ Peak-to-peak jitter in seconds.
998
+
999
+ Example:
1000
+ >>> jitter_pp = peak_to_peak_jitter(clock_trace)
1001
+ >>> print(f"Pk-Pk jitter: {jitter_pp * 1e12:.2f} ps")
1002
+
1003
+ References:
1004
+ IEEE 2414-2020 Section 5.2
1005
+ TIM-008
1006
+ """
1007
+ # Get edge timestamps
1008
+ edges = _get_edge_timestamps(trace, edge_type, threshold)
1009
+
1010
+ if len(edges) < 3:
1011
+ return np.nan # type: ignore[no-any-return]
1012
+
1013
+ # Calculate periods
1014
+ periods = np.diff(edges)
1015
+
1016
+ if len(periods) < 2:
1017
+ return np.nan # type: ignore[no-any-return]
1018
+
1019
+ # Pk-Pk jitter is the range of period variations
1020
+ jitter_pp = float(np.max(periods) - np.min(periods))
1021
+
1022
+ return jitter_pp
1023
+
1024
+
1025
+ def time_interval_error(
1026
+ trace: WaveformTrace | DigitalTrace,
1027
+ *,
1028
+ edge_type: Literal["rising", "falling"] = "rising",
1029
+ nominal_period: float | None = None,
1030
+ threshold: float = 0.5,
1031
+ ) -> NDArray[np.float64]:
1032
+ """Measure Time Interval Error (TIE) from clock signal.
1033
+
1034
+ TIE is the deviation of each edge from its ideal position based on
1035
+ the recovered clock period. Provides a time series of jitter values
1036
+ for trend analysis and decomposition.
1037
+
1038
+ Args:
1039
+ trace: Input trace (analog or digital).
1040
+ edge_type: Type of edges to measure ("rising" or "falling").
1041
+ nominal_period: Expected period in seconds. If None, computed from data.
1042
+ threshold: Threshold for edge detection (0.0 to 1.0).
1043
+
1044
+ Returns:
1045
+ Array of TIE values in seconds, one per edge.
1046
+
1047
+ Raises:
1048
+ InsufficientDataError: If trace has fewer than 3 edges.
1049
+
1050
+ Example:
1051
+ >>> tie = time_interval_error(clock_trace)
1052
+ >>> plt.plot(tie * 1e12)
1053
+ >>> plt.ylabel("TIE (ps)")
1054
+ >>> plt.xlabel("Edge number")
1055
+
1056
+ References:
1057
+ IEEE 2414-2020 Section 5.1
1058
+ TIM-009
1059
+ """
1060
+ # Get edge timestamps
1061
+ edges = _get_edge_timestamps(trace, edge_type, threshold)
1062
+
1063
+ if len(edges) < 3:
1064
+ raise InsufficientDataError(
1065
+ "TIE measurement requires at least 3 edges",
1066
+ required=3,
1067
+ available=len(edges),
1068
+ analysis_type="time_interval_error",
1069
+ )
1070
+
1071
+ # Calculate actual periods
1072
+ periods = np.diff(edges)
1073
+
1074
+ # Use mean period if nominal not provided
1075
+ if nominal_period is None:
1076
+ nominal_period = np.mean(periods)
1077
+
1078
+ # Calculate ideal edge positions
1079
+ n_edges = len(edges)
1080
+ start_time = edges[0]
1081
+ ideal_positions = start_time + np.arange(n_edges) * nominal_period
1082
+
1083
+ # TIE is actual - ideal
1084
+ tie: NDArray[np.float64] = edges - ideal_positions
1085
+
1086
+ return tie
1087
+
1088
+
1089
+ __all__ = [
1090
+ "ClockRecoveryResult",
1091
+ "RMSJitterResult",
1092
+ "TimingViolation",
1093
+ "hold_time",
1094
+ "peak_to_peak_jitter",
1095
+ "phase",
1096
+ "propagation_delay",
1097
+ "recover_clock_edge",
1098
+ "recover_clock_fft",
1099
+ "rms_jitter",
1100
+ "setup_time",
1101
+ "skew",
1102
+ "slew_rate",
1103
+ "time_interval_error",
1104
+ ]