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,805 @@
1
+ """Advanced clock recovery for digital signals.
2
+
3
+ This module provides comprehensive clock recovery and analysis tools for digital
4
+ signals, including frequency detection, clock reconstruction, baud rate detection,
5
+ and jitter measurement.
6
+
7
+
8
+ Example:
9
+ >>> from oscura.analyzers.digital.clock import detect_clock_frequency, recover_clock
10
+ >>> freq = detect_clock_frequency(data_trace, sample_rate=1e9)
11
+ >>> print(f"Detected clock: {freq/1e6:.2f} MHz")
12
+ >>> clock = recover_clock(data_trace, sample_rate=1e9, method='edge')
13
+ >>> metrics = measure_clock_jitter(clock, sample_rate=1e9)
14
+
15
+ References:
16
+ Gardner, F.M.: "Phaselock Techniques" (3rd Ed), Wiley, 2005
17
+ Lee, E.A. & Messerschmitt, D.G.: "Digital Communication" (2nd Ed), 1994
18
+ IEEE 1241-2010: Standard for Terminology and Test Methods for ADCs
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ from dataclasses import dataclass
24
+ from typing import TYPE_CHECKING, Any, ClassVar, Literal
25
+
26
+ import numpy as np
27
+ from scipy import signal
28
+
29
+ from oscura.core.exceptions import InsufficientDataError, ValidationError
30
+
31
+ if TYPE_CHECKING:
32
+ from numpy.typing import NDArray
33
+
34
+
35
+ @dataclass
36
+ class ClockMetrics:
37
+ """Clock signal quality metrics.
38
+
39
+
40
+
41
+ Attributes:
42
+ frequency: Detected frequency in Hz.
43
+ period_samples: Period in samples.
44
+ period_seconds: Period in seconds.
45
+ jitter_rms: RMS jitter in seconds.
46
+ jitter_pp: Peak-to-peak jitter in seconds.
47
+ duty_cycle: Duty cycle (0.0 to 1.0).
48
+ stability: Stability score (0.0 to 1.0).
49
+ confidence: Detection confidence (0.0 to 1.0).
50
+ """
51
+
52
+ frequency: float
53
+ period_samples: float
54
+ period_seconds: float
55
+ jitter_rms: float
56
+ jitter_pp: float
57
+ duty_cycle: float
58
+ stability: float
59
+ confidence: float
60
+
61
+
62
+ @dataclass
63
+ class BaudRateResult:
64
+ """Result of baud rate detection.
65
+
66
+
67
+
68
+ Attributes:
69
+ baud_rate: Detected baud rate in bits per second.
70
+ bit_period_samples: Bit period in samples.
71
+ confidence: Detection confidence (0.0 to 1.0).
72
+ method: Method used for detection.
73
+ """
74
+
75
+ baud_rate: int
76
+ bit_period_samples: float
77
+ confidence: float
78
+ method: str
79
+
80
+
81
+ class ClockRecovery:
82
+ """Recover clock signal from data.
83
+
84
+
85
+
86
+ This class provides multiple methods for clock recovery including edge-based,
87
+ FFT-based, and autocorrelation-based detection, as well as PLL tracking and
88
+ baud rate detection for asynchronous protocols.
89
+
90
+ Can be initialized with or without sample_rate:
91
+ - With sample_rate: ClockRecovery(sample_rate=1e9)
92
+ - Without: ClockRecovery() - sample_rate extracted from trace metadata
93
+ """
94
+
95
+ # Standard baud rates for async protocols
96
+ STANDARD_BAUD_RATES: ClassVar[list[int]] = [
97
+ 300,
98
+ 600,
99
+ 1200,
100
+ 2400,
101
+ 4800,
102
+ 9600,
103
+ 14400,
104
+ 19200,
105
+ 28800,
106
+ 38400,
107
+ 57600,
108
+ 115200,
109
+ 230400,
110
+ 460800,
111
+ 921600,
112
+ 1000000,
113
+ 2000000,
114
+ ]
115
+
116
+ def __init__(self, sample_rate: float | None = None):
117
+ """Initialize with optional sample rate.
118
+
119
+ Args:
120
+ sample_rate: Sample rate in Hz. If None, will be extracted from trace metadata.
121
+
122
+ Raises:
123
+ ValidationError: If sample rate is provided and invalid.
124
+ """
125
+ if sample_rate is not None and sample_rate <= 0:
126
+ raise ValidationError(f"Sample rate must be positive, got {sample_rate}")
127
+
128
+ self.sample_rate: float | None = float(sample_rate) if sample_rate is not None else None
129
+
130
+ def _get_sample_rate(self, trace: Any) -> float:
131
+ """Extract sample rate from trace or use stored value.
132
+
133
+ Args:
134
+ trace: A DigitalTrace/WaveformTrace with metadata, or a numpy array.
135
+
136
+ Returns:
137
+ Sample rate in Hz.
138
+
139
+ Raises:
140
+ ValidationError: If sample rate cannot be determined.
141
+ """
142
+ if self.sample_rate is not None:
143
+ return self.sample_rate
144
+
145
+ # Try to extract from trace metadata
146
+ if hasattr(trace, "metadata") and hasattr(trace.metadata, "sample_rate"):
147
+ return float(trace.metadata.sample_rate)
148
+
149
+ raise ValidationError(
150
+ "Sample rate not set and cannot be extracted from trace. "
151
+ "Either provide sample_rate to constructor or use a trace with metadata."
152
+ )
153
+
154
+ def _get_trace_data(self, trace: Any) -> NDArray[np.float64]:
155
+ """Extract numpy array from trace object.
156
+
157
+ Args:
158
+ trace: A DigitalTrace/WaveformTrace or numpy array.
159
+
160
+ Returns:
161
+ Numpy array of signal data.
162
+ """
163
+ if hasattr(trace, "data"):
164
+ return np.asarray(trace.data, dtype=np.float64)
165
+ return np.asarray(trace, dtype=np.float64)
166
+
167
+ def detect_frequency(
168
+ self, trace: Any, method: Literal["edge", "fft", "autocorr"] = "edge"
169
+ ) -> float:
170
+ """Detect clock frequency from signal (supports DigitalTrace).
171
+
172
+
173
+
174
+ This method supports both raw numpy arrays and DigitalTrace objects.
175
+ Sample rate is extracted from trace metadata if not set in constructor.
176
+
177
+ Args:
178
+ trace: Signal trace data (DigitalTrace or numpy array).
179
+ method: Detection method to use.
180
+
181
+ Returns:
182
+ Detected frequency in Hz.
183
+
184
+ Example:
185
+ >>> recovery = ClockRecovery()
186
+ >>> freq = recovery.detect_frequency(digital_trace)
187
+ """
188
+ sample_rate = self._get_sample_rate(trace)
189
+ data = self._get_trace_data(trace)
190
+
191
+ # Temporarily set sample rate for internal methods
192
+ old_rate = self.sample_rate
193
+ self.sample_rate = sample_rate
194
+
195
+ try:
196
+ return self.detect_clock_frequency(data, method)
197
+ finally:
198
+ self.sample_rate = old_rate
199
+
200
+ def detect_clock_frequency(
201
+ self, trace: NDArray[np.float64], method: Literal["edge", "fft", "autocorr"] = "edge"
202
+ ) -> float:
203
+ """Detect clock frequency from signal.
204
+
205
+
206
+
207
+ Detects the dominant clock frequency using the specified method.
208
+ Each method has different strengths:
209
+ - edge: Best for clean digital signals with clear transitions
210
+ - fft: Best for noisy signals or periodic analog waveforms
211
+ - autocorr: Best for periodic patterns with timing jitter
212
+
213
+ Args:
214
+ trace: Signal trace data.
215
+ method: Detection method to use.
216
+
217
+ Returns:
218
+ Detected frequency in Hz.
219
+
220
+ Raises:
221
+ InsufficientDataError: If trace is too short.
222
+ ValidationError: If method is invalid or detection fails.
223
+ """
224
+ if len(trace) < 10:
225
+ raise InsufficientDataError("Trace must have at least 10 samples")
226
+
227
+ if self.sample_rate is None:
228
+ raise ValidationError(
229
+ "Sample rate not set. Use detect_frequency() with trace metadata."
230
+ )
231
+
232
+ if method == "edge":
233
+ return self._detect_frequency_edge(trace)
234
+ elif method == "fft":
235
+ return self._detect_frequency_fft(trace)
236
+ elif method == "autocorr":
237
+ return self._detect_frequency_autocorr(trace)
238
+ else:
239
+ raise ValidationError(f"Unknown method: {method}")
240
+
241
+ def recover_clock(
242
+ self, data_trace: NDArray[np.float64], method: Literal["edge", "pll", "fft"] = "edge"
243
+ ) -> NDArray[np.float64]:
244
+ """Recover clock signal from data.
245
+
246
+
247
+
248
+ Reconstructs a clock signal from the data trace. The recovered clock
249
+ is a square wave aligned to the detected clock transitions.
250
+
251
+ Args:
252
+ data_trace: Data signal trace.
253
+ method: Recovery method to use.
254
+
255
+ Returns:
256
+ Recovered clock trace (same length as input).
257
+
258
+ Raises:
259
+ InsufficientDataError: If trace is too short.
260
+ ValidationError: If method is invalid or recovery fails.
261
+ """
262
+ if len(data_trace) < 10:
263
+ raise InsufficientDataError("Trace must have at least 10 samples")
264
+
265
+ if self.sample_rate is None:
266
+ raise ValidationError("Sample rate not set")
267
+
268
+ # Detect clock frequency first
269
+ freq = self.detect_clock_frequency(data_trace, method=method if method != "pll" else "edge")
270
+
271
+ if freq <= 0:
272
+ raise ValidationError("Failed to detect valid clock frequency")
273
+
274
+ if method == "pll":
275
+ # Use PLL tracking for robust recovery
276
+ return self._pll_track(data_trace, freq)
277
+ else:
278
+ # Generate ideal square wave at detected frequency
279
+ _period_samples = self.sample_rate / freq
280
+ n_samples = len(data_trace)
281
+ t = np.arange(n_samples)
282
+
283
+ # Generate square wave (50% duty cycle)
284
+ clock_raw = signal.square(2 * np.pi * freq * t / self.sample_rate)
285
+
286
+ # Normalize to 0-1 range
287
+ clock = (clock_raw + 1.0) / 2.0
288
+
289
+ return np.asarray(clock, dtype=np.float64)
290
+
291
+ def detect_baud_rate(
292
+ self, trace: NDArray[np.float64], candidates: list[int] | None = None
293
+ ) -> BaudRateResult:
294
+ """Auto-detect baud rate for async protocols.
295
+
296
+
297
+
298
+ Detects the baud rate by analyzing bit timing. Works best with traces
299
+ containing start bits or transitions between different bit values.
300
+
301
+ Args:
302
+ trace: Signal trace data.
303
+ candidates: List of candidate baud rates to test. If None, uses
304
+ standard rates.
305
+
306
+ Returns:
307
+ BaudRateResult with detected baud rate and confidence.
308
+
309
+ Raises:
310
+ InsufficientDataError: If trace is too short or not enough edges found.
311
+ ValidationError: If sample rate is not set.
312
+ """
313
+ if len(trace) < 100:
314
+ raise InsufficientDataError("Need at least 100 samples for baud rate detection")
315
+
316
+ if self.sample_rate is None:
317
+ raise ValidationError("Sample rate not set")
318
+
319
+ if candidates is None:
320
+ candidates = self.STANDARD_BAUD_RATES
321
+
322
+ # Detect edges to find bit transitions
323
+ edges = self._detect_edges_simple(trace)
324
+
325
+ if len(edges) < 3:
326
+ raise InsufficientDataError("Not enough edges to detect baud rate")
327
+
328
+ # Calculate inter-edge intervals
329
+ intervals = np.diff(edges)
330
+
331
+ # The minimum interval should be close to one bit period
332
+ # (assuming we have at least some single-bit pulses)
333
+ # Use histogram to find most common interval
334
+ hist, bin_edges = np.histogram(intervals, bins=50)
335
+ most_common_interval = bin_edges[np.argmax(hist)]
336
+
337
+ # Convert to frequency
338
+ detected_freq = self.sample_rate / most_common_interval
339
+
340
+ # Find closest standard baud rate
341
+ candidates_array = np.array(candidates)
342
+ errors = np.abs(candidates_array - detected_freq)
343
+ best_idx = np.argmin(errors)
344
+ best_baud = candidates_array[best_idx]
345
+
346
+ # Calculate confidence based on how close we are to standard rate
347
+ relative_error = errors[best_idx] / best_baud
348
+ confidence = max(0.0, 1.0 - relative_error * 10)
349
+
350
+ bit_period_samples = self.sample_rate / best_baud
351
+
352
+ return BaudRateResult(
353
+ baud_rate=int(best_baud),
354
+ bit_period_samples=float(bit_period_samples),
355
+ confidence=float(confidence),
356
+ method="edge_histogram",
357
+ )
358
+
359
+ def measure_clock_jitter(self, clock_trace: NDArray[np.float64]) -> ClockMetrics:
360
+ """Measure clock jitter and quality metrics.
361
+
362
+
363
+
364
+ Analyzes a clock signal to measure jitter, duty cycle, and stability.
365
+ Works best with recovered or measured clock signals.
366
+
367
+ Args:
368
+ clock_trace: Clock signal trace.
369
+
370
+ Returns:
371
+ ClockMetrics with comprehensive quality measurements.
372
+
373
+ Raises:
374
+ InsufficientDataError: If trace is too short or has too few edges.
375
+ ValidationError: If sample rate is not set.
376
+ """
377
+ if len(clock_trace) < 10:
378
+ raise InsufficientDataError("Trace must have at least 10 samples")
379
+
380
+ if self.sample_rate is None:
381
+ raise ValidationError("Sample rate not set")
382
+
383
+ # Detect rising and falling edges
384
+ rising_edges = self._detect_edges_by_type(clock_trace, "rising")
385
+ falling_edges = self._detect_edges_by_type(clock_trace, "falling")
386
+
387
+ if len(rising_edges) < 3:
388
+ raise InsufficientDataError("Need at least 3 rising edges for jitter measurement")
389
+
390
+ # Calculate periods from rising edge to rising edge
391
+ periods = np.diff(rising_edges)
392
+
393
+ if len(periods) == 0:
394
+ raise InsufficientDataError("Cannot calculate period from single edge")
395
+
396
+ # Mean period
397
+ mean_period_samples = np.mean(periods)
398
+ mean_period_seconds = mean_period_samples / self.sample_rate
399
+ frequency = 1.0 / mean_period_seconds
400
+
401
+ # RMS jitter (standard deviation of periods)
402
+ jitter_rms_samples = np.std(periods)
403
+ jitter_rms = jitter_rms_samples / self.sample_rate
404
+
405
+ # Peak-to-peak jitter
406
+ jitter_pp_samples = np.ptp(periods)
407
+ jitter_pp = jitter_pp_samples / self.sample_rate
408
+
409
+ # Duty cycle (high time / period)
410
+ if len(falling_edges) >= len(rising_edges):
411
+ # Can measure duty cycle
412
+ high_times = []
413
+ for _i, rise in enumerate(rising_edges):
414
+ # Find next falling edge
415
+ fall_idx = np.searchsorted(falling_edges, rise)
416
+ if fall_idx < len(falling_edges):
417
+ high_time = falling_edges[fall_idx] - rise
418
+ high_times.append(high_time)
419
+
420
+ if high_times:
421
+ mean_high_time = np.mean(high_times)
422
+ duty_cycle = mean_high_time / mean_period_samples
423
+ else:
424
+ duty_cycle = 0.5 # Assume 50% if cannot measure
425
+ else:
426
+ duty_cycle = 0.5
427
+
428
+ # Stability score (inverse of relative jitter)
429
+ relative_jitter = (
430
+ jitter_rms_samples / mean_period_samples if mean_period_samples > 0 else 1.0
431
+ )
432
+ stability = max(0.0, 1.0 - relative_jitter * 10)
433
+
434
+ # Confidence based on number of periods and stability
435
+ confidence = min(1.0, len(periods) / 100.0) * stability
436
+
437
+ return ClockMetrics(
438
+ frequency=float(frequency),
439
+ period_samples=float(mean_period_samples),
440
+ period_seconds=float(mean_period_seconds),
441
+ jitter_rms=float(jitter_rms),
442
+ jitter_pp=float(jitter_pp),
443
+ duty_cycle=float(np.clip(duty_cycle, 0.0, 1.0)),
444
+ stability=float(stability),
445
+ confidence=float(confidence),
446
+ )
447
+
448
+ def _detect_frequency_edge(self, trace: NDArray[np.float64]) -> float:
449
+ """Detect frequency using edge timing histogram.
450
+
451
+
452
+
453
+ Args:
454
+ trace: Signal trace.
455
+
456
+ Returns:
457
+ Detected frequency in Hz.
458
+
459
+ Raises:
460
+ ValidationError: If not enough edges found to detect frequency.
461
+ """
462
+ edges = self._detect_edges_simple(trace)
463
+
464
+ if len(edges) < 3:
465
+ raise ValidationError("Not enough edges to detect frequency")
466
+
467
+ # Calculate inter-edge intervals
468
+ intervals = np.diff(edges)
469
+
470
+ # Build histogram of intervals
471
+ # The peak should correspond to half the period (edge to edge)
472
+ hist, bin_edges = np.histogram(intervals, bins=50)
473
+ _peak_interval = bin_edges[np.argmax(hist)]
474
+
475
+ # Frequency is sample_rate / (2 * interval) for edge-to-edge
476
+ # But we need to check if these are half-periods or full periods
477
+ # Use median interval as robust estimator
478
+ median_interval = np.median(intervals)
479
+
480
+ # Assume median represents half-period (rising to falling or vice versa)
481
+ # So full period is 2x median interval
482
+ period_samples = 2 * median_interval
483
+ frequency = self.sample_rate / period_samples
484
+
485
+ return float(frequency)
486
+
487
+ def _detect_frequency_fft(self, trace: NDArray[np.float64]) -> float:
488
+ """Detect frequency using FFT spectral analysis.
489
+
490
+
491
+
492
+ Args:
493
+ trace: Signal trace.
494
+
495
+ Returns:
496
+ Detected frequency in Hz.
497
+
498
+ Raises:
499
+ ValidationError: If sample rate is not set.
500
+ """
501
+ # Remove DC component
502
+ trace_ac = trace - np.mean(trace)
503
+
504
+ # Apply window to reduce spectral leakage
505
+ window = signal.windows.hann(len(trace_ac))
506
+ trace_windowed = trace_ac * window
507
+
508
+ # Compute FFT
509
+ fft = np.fft.rfft(trace_windowed)
510
+ if self.sample_rate is None:
511
+ raise ValidationError("Sample rate not set")
512
+ freqs = np.fft.rfftfreq(len(trace_windowed), 1.0 / self.sample_rate)
513
+
514
+ # Find peak in magnitude spectrum
515
+ magnitude = np.abs(fft)
516
+
517
+ # Ignore DC and very low frequencies (below 10 Hz)
518
+ min_freq_hz = 10.0
519
+ min_freq_idx = np.searchsorted(freqs, min_freq_hz)
520
+ if min_freq_idx >= len(magnitude):
521
+ min_freq_idx = np.intp(1)
522
+
523
+ peak_idx = min_freq_idx + np.argmax(magnitude[min_freq_idx:])
524
+ frequency = freqs[peak_idx]
525
+
526
+ return float(frequency)
527
+
528
+ def _detect_frequency_autocorr(self, trace: NDArray[np.float64]) -> float:
529
+ """Detect frequency using autocorrelation.
530
+
531
+
532
+
533
+ Args:
534
+ trace: Signal trace.
535
+
536
+ Returns:
537
+ Detected frequency in Hz.
538
+
539
+ Raises:
540
+ ValidationError: If no periodic pattern detected or sample rate not set.
541
+ """
542
+ # Remove mean
543
+ trace_centered = trace - np.mean(trace)
544
+
545
+ # Compute autocorrelation
546
+ autocorr = signal.correlate(trace_centered, trace_centered, mode="full")
547
+ autocorr = autocorr[len(autocorr) // 2 :] # Keep only positive lags
548
+
549
+ # Normalize
550
+ autocorr = autocorr / autocorr[0]
551
+
552
+ # Find first peak after lag 0
553
+ # Look for peaks in autocorrelation
554
+ peaks, _ = signal.find_peaks(autocorr, height=0.3)
555
+
556
+ if len(peaks) == 0:
557
+ raise ValidationError("No periodic pattern detected in autocorrelation")
558
+
559
+ # First peak corresponds to period
560
+ period_samples = peaks[0]
561
+ if self.sample_rate is None:
562
+ raise ValidationError("Sample rate not set")
563
+ frequency = self.sample_rate / period_samples
564
+
565
+ return float(frequency)
566
+
567
+ def _pll_track(
568
+ self, trace: NDArray[np.float64], initial_freq: float, bandwidth: float = 0.01
569
+ ) -> NDArray[np.float64]:
570
+ """Software PLL for phase tracking.
571
+
572
+
573
+
574
+ Implements a simple digital PLL for tracking phase and frequency
575
+ variations in the input signal.
576
+
577
+ Args:
578
+ trace: Input data trace.
579
+ initial_freq: Initial frequency estimate in Hz.
580
+ bandwidth: Loop bandwidth (0.0 to 1.0), lower = more filtering.
581
+
582
+ Returns:
583
+ Recovered clock signal.
584
+
585
+ Raises:
586
+ ValidationError: If sample rate is not set.
587
+ """
588
+ n_samples = len(trace)
589
+ clock = np.zeros(n_samples)
590
+
591
+ # PLL state
592
+ phase = 0.0
593
+ freq = initial_freq
594
+ if self.sample_rate is None:
595
+ raise ValidationError("Sample rate not set")
596
+ omega = 2 * np.pi * freq / self.sample_rate
597
+
598
+ # Loop filter gains (proportional + integral)
599
+ kp = 2 * bandwidth # Proportional gain
600
+ ki = bandwidth**2 # Integral gain
601
+
602
+ # Detect edges for phase error calculation
603
+ threshold = (np.max(trace) + np.min(trace)) / 2.0
604
+ prev_sample = trace[0]
605
+
606
+ for i in range(n_samples):
607
+ # Generate clock output
608
+ clock[i] = 1.0 if np.cos(phase) > 0 else 0.0
609
+
610
+ # Detect phase error at edges
611
+ current_sample = trace[i]
612
+ phase_error = 0.0
613
+
614
+ # Simple phase detector: check if edge coincides with clock transition
615
+ if (prev_sample < threshold <= current_sample) or (
616
+ prev_sample > threshold >= current_sample
617
+ ):
618
+ # Edge detected
619
+ clock_value = np.cos(phase)
620
+ # Phase error is sign of clock at edge
621
+ phase_error = np.sign(clock_value) * 0.1
622
+
623
+ # Update frequency and phase with loop filter
624
+ _freq_adjust = kp * phase_error
625
+ omega += ki * phase_error
626
+
627
+ # Update phase
628
+ phase += omega
629
+ phase = phase % (2 * np.pi)
630
+
631
+ prev_sample = current_sample
632
+
633
+ return clock
634
+
635
+ def _detect_edges_simple(self, trace: NDArray[np.float64]) -> NDArray[np.intp]:
636
+ """Detect all edges in trace (both rising and falling).
637
+
638
+ Args:
639
+ trace: Signal trace.
640
+
641
+ Returns:
642
+ Array of edge indices.
643
+ """
644
+ threshold = (np.max(trace) + np.min(trace)) / 2.0
645
+ rising = np.where((trace[:-1] < threshold) & (trace[1:] >= threshold))[0]
646
+ falling = np.where((trace[:-1] > threshold) & (trace[1:] <= threshold))[0]
647
+
648
+ # Combine and sort
649
+ all_edges = np.concatenate([rising, falling])
650
+ all_edges.sort()
651
+
652
+ return all_edges
653
+
654
+ def _detect_edges_by_type(
655
+ self, trace: NDArray[np.float64], edge_type: Literal["rising", "falling"]
656
+ ) -> NDArray[np.intp]:
657
+ """Detect edges of specific type.
658
+
659
+ Args:
660
+ trace: Signal trace.
661
+ edge_type: Type of edge to detect.
662
+
663
+ Returns:
664
+ Array of edge indices.
665
+ """
666
+ threshold = (np.max(trace) + np.min(trace)) / 2.0
667
+
668
+ if edge_type == "rising":
669
+ edges = np.where((trace[:-1] < threshold) & (trace[1:] >= threshold))[0]
670
+ else: # falling
671
+ edges = np.where((trace[:-1] > threshold) & (trace[1:] <= threshold))[0]
672
+
673
+ return edges + 1 # Return index after crossing
674
+
675
+
676
+ # Convenience functions
677
+
678
+
679
+ def detect_clock_frequency(
680
+ trace: NDArray[np.float64],
681
+ sample_rate: float,
682
+ method: Literal["edge", "fft", "autocorr"] = "edge",
683
+ ) -> float:
684
+ """Detect clock frequency from signal.
685
+
686
+
687
+
688
+ Convenience function for detecting clock frequency without creating
689
+ a ClockRecovery instance.
690
+
691
+ Args:
692
+ trace: Signal trace data.
693
+ sample_rate: Sample rate in Hz.
694
+ method: Detection method ('edge', 'fft', or 'autocorr').
695
+
696
+ Returns:
697
+ Detected frequency in Hz.
698
+
699
+ Example:
700
+ >>> freq = detect_clock_frequency(data, sample_rate=1e9, method='edge')
701
+ >>> print(f"Clock: {freq/1e6:.2f} MHz")
702
+ """
703
+ recovery = ClockRecovery(sample_rate)
704
+ return recovery.detect_clock_frequency(trace, method)
705
+
706
+
707
+ def recover_clock(
708
+ data_trace: NDArray[np.float64],
709
+ sample_rate: float,
710
+ method: Literal["edge", "pll", "fft"] = "edge",
711
+ ) -> NDArray[np.float64]:
712
+ """Recover clock signal from data.
713
+
714
+
715
+
716
+ Convenience function for recovering clock signal without creating
717
+ a ClockRecovery instance.
718
+
719
+ Args:
720
+ data_trace: Data signal trace.
721
+ sample_rate: Sample rate in Hz.
722
+ method: Recovery method ('edge', 'pll', or 'fft').
723
+
724
+ Returns:
725
+ Recovered clock trace.
726
+
727
+ Example:
728
+ >>> clock = recover_clock(data, sample_rate=1e9, method='pll')
729
+ """
730
+ recovery = ClockRecovery(sample_rate)
731
+ return recovery.recover_clock(data_trace, method)
732
+
733
+
734
+ def detect_baud_rate(
735
+ trace: Any, sample_rate: float | None = None, candidates: list[int] | None = None
736
+ ) -> int | BaudRateResult:
737
+ """Auto-detect baud rate.
738
+
739
+
740
+
741
+ Convenience function for baud rate detection. Supports both DigitalTrace
742
+ objects (with metadata) and raw numpy arrays (requiring sample_rate).
743
+
744
+ Args:
745
+ trace: Signal trace data (DigitalTrace or numpy array).
746
+ sample_rate: Sample rate in Hz (optional if trace has metadata).
747
+ candidates: List of candidate baud rates. If None, uses standard rates.
748
+
749
+ Returns:
750
+ Detected baud rate as int (for DigitalTrace) or BaudRateResult.
751
+
752
+ Raises:
753
+ ValidationError: If sample_rate is required but not provided.
754
+
755
+ Example:
756
+ >>> baud = detect_baud_rate(digital_trace) # Uses metadata
757
+ >>> result = detect_baud_rate(data_array, sample_rate=1e6) # Explicit rate
758
+ """
759
+ # Check if trace is a DigitalTrace with metadata
760
+ if hasattr(trace, "metadata") and hasattr(trace.metadata, "sample_rate"):
761
+ rate = trace.metadata.sample_rate
762
+ data = np.asarray(trace.data, dtype=np.float64)
763
+ recovery = ClockRecovery(rate)
764
+ result = recovery.detect_baud_rate(data, candidates)
765
+ return result.baud_rate # Return just the baud rate for DigitalTrace
766
+ elif sample_rate is not None:
767
+ data = np.asarray(trace, dtype=np.float64)
768
+ recovery = ClockRecovery(sample_rate)
769
+ return recovery.detect_baud_rate(data, candidates)
770
+ else:
771
+ raise ValidationError("sample_rate required when trace is not a DigitalTrace with metadata")
772
+
773
+
774
+ def measure_clock_jitter(clock_trace: NDArray[np.float64], sample_rate: float) -> ClockMetrics:
775
+ """Measure clock jitter.
776
+
777
+
778
+
779
+ Convenience function for jitter measurement without creating
780
+ a ClockRecovery instance.
781
+
782
+ Args:
783
+ clock_trace: Clock signal trace.
784
+ sample_rate: Sample rate in Hz.
785
+
786
+ Returns:
787
+ ClockMetrics with jitter and quality measurements.
788
+
789
+ Example:
790
+ >>> metrics = measure_clock_jitter(clock, sample_rate=1e9)
791
+ >>> print(f"RMS jitter: {metrics.jitter_rms*1e12:.2f} ps")
792
+ """
793
+ recovery = ClockRecovery(sample_rate)
794
+ return recovery.measure_clock_jitter(clock_trace)
795
+
796
+
797
+ __all__ = [
798
+ "BaudRateResult",
799
+ "ClockMetrics",
800
+ "ClockRecovery",
801
+ "detect_baud_rate",
802
+ "detect_clock_frequency",
803
+ "measure_clock_jitter",
804
+ "recover_clock",
805
+ ]