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,759 @@
1
+ """Jitter decomposition into random and deterministic components.
2
+
3
+ This module implements IEEE 2414-2020 compliant jitter decomposition
4
+ using the dual-Dirac model and spectral analysis techniques.
5
+
6
+
7
+ Example:
8
+ >>> from oscura.analyzers.jitter.decomposition import extract_rj, extract_dj
9
+ >>> rj_result = extract_rj(tie_data)
10
+ >>> print(f"RJ RMS: {rj_result.rj_rms * 1e12:.2f} ps")
11
+
12
+ References:
13
+ IEEE 2414-2020: Standard for Jitter and Phase Noise
14
+ Dual-Dirac Model: JEDEC JESD65C
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from dataclasses import dataclass
20
+ from typing import TYPE_CHECKING, Literal
21
+
22
+ import numpy as np
23
+ from scipy import stats
24
+
25
+ from oscura.core.exceptions import InsufficientDataError
26
+
27
+ if TYPE_CHECKING:
28
+ from numpy.typing import NDArray
29
+
30
+
31
+ @dataclass
32
+ class RandomJitterResult:
33
+ """Result of random jitter extraction.
34
+
35
+ Attributes:
36
+ rj_rms: Random jitter RMS value in seconds.
37
+ method: Method used for extraction ("tail_fit" or "q_scale").
38
+ confidence: Confidence score (0.0 to 1.0).
39
+ sigma: Gaussian sigma parameter in seconds.
40
+ mu: Gaussian mean offset in seconds.
41
+ n_samples: Number of TIE samples used.
42
+ """
43
+
44
+ rj_rms: float
45
+ method: str
46
+ confidence: float
47
+ sigma: float
48
+ mu: float
49
+ n_samples: int
50
+
51
+
52
+ @dataclass
53
+ class DeterministicJitterResult:
54
+ """Result of deterministic jitter extraction.
55
+
56
+ Attributes:
57
+ dj_pp: Peak-to-peak deterministic jitter in seconds.
58
+ dj_delta: Dual-Dirac delta (half-width) in seconds.
59
+ method: Method used for extraction.
60
+ confidence: Confidence score (0.0 to 1.0).
61
+ histogram: Histogram counts for analysis.
62
+ bin_centers: Bin centers for histogram.
63
+ """
64
+
65
+ dj_pp: float
66
+ dj_delta: float
67
+ method: str
68
+ confidence: float
69
+ histogram: NDArray[np.float64] | None = None
70
+ bin_centers: NDArray[np.float64] | None = None
71
+
72
+
73
+ @dataclass
74
+ class PeriodicJitterResult:
75
+ """Result of periodic jitter extraction.
76
+
77
+ Attributes:
78
+ components: List of (frequency_hz, amplitude_seconds) tuples.
79
+ pj_pp: Total periodic jitter peak-to-peak in seconds.
80
+ dominant_frequency: Most significant PJ frequency in Hz.
81
+ dominant_amplitude: Amplitude at dominant frequency in seconds.
82
+ """
83
+
84
+ components: list[tuple[float, float]]
85
+ pj_pp: float
86
+ dominant_frequency: float | None
87
+ dominant_amplitude: float | None
88
+
89
+
90
+ @dataclass
91
+ class DataDependentJitterResult:
92
+ """Result of data-dependent jitter extraction.
93
+
94
+ Attributes:
95
+ ddj_pp: Peak-to-peak DDJ in seconds.
96
+ pattern_histogram: Jitter vs bit pattern histogram.
97
+ pattern_length: Length of bit patterns analyzed.
98
+ isi_coefficient: ISI correlation coefficient.
99
+ """
100
+
101
+ ddj_pp: float
102
+ pattern_histogram: dict[str, float]
103
+ pattern_length: int
104
+ isi_coefficient: float
105
+
106
+
107
+ @dataclass
108
+ class JitterDecomposition:
109
+ """Complete jitter decomposition result.
110
+
111
+ Attributes:
112
+ rj: Random jitter component.
113
+ dj: Deterministic jitter component.
114
+ pj: Periodic jitter component (optional).
115
+ ddj: Data-dependent jitter component (optional).
116
+ tj_pp: Total jitter peak-to-peak at measured BER.
117
+ ber_measured: BER at which TJ was measured.
118
+ """
119
+
120
+ rj: RandomJitterResult
121
+ dj: DeterministicJitterResult
122
+ pj: PeriodicJitterResult | None = None
123
+ ddj: DataDependentJitterResult | None = None
124
+ tj_pp: float | None = None
125
+ ber_measured: float | None = None
126
+
127
+ @property
128
+ def rj_rms(self) -> float:
129
+ """Convenience property for random jitter RMS."""
130
+ return self.rj.rj_rms
131
+
132
+ @property
133
+ def dj_pp(self) -> float:
134
+ """Convenience property for deterministic jitter peak-to-peak."""
135
+ return self.dj.dj_pp
136
+
137
+
138
+ def extract_rj(
139
+ tie_data: NDArray[np.float64],
140
+ *,
141
+ method: Literal["tail_fit", "q_scale", "auto"] = "auto",
142
+ min_samples: int = 1000,
143
+ ) -> RandomJitterResult:
144
+ """Extract random jitter component from TIE data.
145
+
146
+ Uses the dual-Dirac model to separate random (Gaussian) jitter
147
+ from the total jitter distribution. RJ is the unbounded random
148
+ component typically caused by thermal noise.
149
+
150
+ Args:
151
+ tie_data: Time Interval Error data array in seconds.
152
+ method: Extraction method:
153
+ - "tail_fit": Fit Gaussian to distribution tails
154
+ - "q_scale": Q-scale (probabilistic) analysis
155
+ - "auto": Automatically select best method
156
+ min_samples: Minimum samples required for analysis.
157
+
158
+ Returns:
159
+ RandomJitterResult with RJ_rms and analysis details.
160
+
161
+ Raises:
162
+ InsufficientDataError: If tie_data has fewer than min_samples.
163
+ ValueError: If method is not recognized.
164
+
165
+ Example:
166
+ >>> rj = extract_rj(tie_data)
167
+ >>> print(f"RJ: {rj.rj_rms * 1e12:.2f} ps RMS")
168
+
169
+ References:
170
+ IEEE 2414-2020 Section 6.2
171
+ """
172
+ if len(tie_data) < min_samples:
173
+ raise InsufficientDataError(
174
+ f"RJ extraction requires at least {min_samples} samples",
175
+ required=min_samples,
176
+ available=len(tie_data),
177
+ analysis_type="random_jitter_extraction",
178
+ )
179
+
180
+ # Remove NaN values
181
+ valid_data = tie_data[~np.isnan(tie_data)]
182
+
183
+ if len(valid_data) < min_samples:
184
+ raise InsufficientDataError(
185
+ f"RJ extraction requires at least {min_samples} valid samples",
186
+ required=min_samples,
187
+ available=len(valid_data),
188
+ analysis_type="random_jitter_extraction",
189
+ )
190
+
191
+ # Select method
192
+ if method == "auto":
193
+ # Use Q-scale for large datasets, tail_fit for smaller
194
+ method = "q_scale" if len(valid_data) > 10000 else "tail_fit"
195
+
196
+ if method == "tail_fit":
197
+ return _extract_rj_tail_fit(valid_data)
198
+ elif method == "q_scale":
199
+ return _extract_rj_q_scale(valid_data)
200
+ else:
201
+ raise ValueError(f"Unknown method: {method}")
202
+
203
+
204
+ def _extract_rj_tail_fit(tie_data: NDArray[np.float64]) -> RandomJitterResult:
205
+ """Extract RJ using Gaussian tail fitting.
206
+
207
+ Fits a Gaussian distribution to the outer tails of the TIE histogram
208
+ where deterministic jitter has minimal effect.
209
+
210
+ Args:
211
+ tie_data: Time Interval Error data array in seconds.
212
+
213
+ Returns:
214
+ RandomJitterResult with RJ_rms and analysis details.
215
+ """
216
+ # For pure Gaussian data, the tails should follow a Gaussian perfectly.
217
+ # The key insight is to use Q-Q plot analysis on the extreme tails.
218
+
219
+ sorted_data = np.sort(tie_data)
220
+ n = len(sorted_data)
221
+
222
+ # Use percentiles to estimate Gaussian parameters
223
+ # For a Gaussian: P16 = μ - σ, P50 = μ, P84 = μ + σ # noqa: RUF003
224
+ p16 = np.percentile(sorted_data, 16)
225
+ p50 = np.percentile(sorted_data, 50)
226
+ p84 = np.percentile(sorted_data, 84)
227
+
228
+ # Estimate sigma from the 68% confidence interval
229
+ sigma_estimate = (p84 - p16) / 2
230
+ mu_estimate = p50
231
+
232
+ # Refine estimate using tail data (beyond ±2 sigma)
233
+ # For pure Gaussian, fit Q-Q plot in the tails
234
+ tail_fraction = 0.025 # Use outer 2.5% on each side (beyond ~2 sigma)
235
+ lower_tail_idx = int(n * tail_fraction)
236
+ upper_tail_idx = int(n * (1 - tail_fraction))
237
+
238
+ # Get tail indices
239
+ tail_indices = np.concatenate([np.arange(0, lower_tail_idx), np.arange(upper_tail_idx, n)])
240
+
241
+ if len(tail_indices) >= 10:
242
+ # Q-Q plot analysis on tails
243
+ tail_data = sorted_data[tail_indices]
244
+ tail_probabilities = np.array(
245
+ [i / (n - 1) if i < lower_tail_idx else (i + 1) / (n - 1) for i in tail_indices]
246
+ )
247
+
248
+ # Get theoretical quantiles
249
+ theoretical_quantiles = stats.norm.ppf(tail_probabilities)
250
+ valid_mask = np.isfinite(theoretical_quantiles)
251
+
252
+ if np.sum(valid_mask) >= 10:
253
+ # Linear regression: data = sigma * theoretical + mu
254
+ slope, intercept, r_value, _, _ = stats.linregress(
255
+ theoretical_quantiles[valid_mask], tail_data[valid_mask]
256
+ )
257
+
258
+ sigma = abs(slope)
259
+ mu = intercept
260
+ confidence = max(0.0, min(1.0, r_value**2))
261
+ else:
262
+ sigma = sigma_estimate
263
+ mu = mu_estimate
264
+ confidence = 0.6
265
+ else:
266
+ sigma = sigma_estimate
267
+ mu = mu_estimate
268
+ confidence = 0.6
269
+
270
+ return RandomJitterResult(
271
+ rj_rms=sigma,
272
+ method="tail_fit",
273
+ confidence=confidence,
274
+ sigma=sigma,
275
+ mu=mu,
276
+ n_samples=len(tie_data),
277
+ )
278
+
279
+
280
+ def _extract_rj_q_scale(tie_data: NDArray[np.float64]) -> RandomJitterResult:
281
+ """Extract RJ using Q-scale (probability plot) analysis.
282
+
283
+ Uses quantile-quantile analysis to separate Gaussian (random)
284
+ from non-Gaussian (deterministic) components.
285
+
286
+ Args:
287
+ tie_data: Time Interval Error data array in seconds.
288
+
289
+ Returns:
290
+ RandomJitterResult with RJ_rms and analysis details.
291
+ """
292
+ n = len(tie_data)
293
+ sorted_data = np.sort(tie_data)
294
+
295
+ # Calculate theoretical Gaussian quantiles
296
+ probabilities = (np.arange(1, n + 1) - 0.5) / n
297
+ theoretical_quantiles = stats.norm.ppf(probabilities)
298
+
299
+ # Remove infinities from edges
300
+ valid_mask = np.isfinite(theoretical_quantiles)
301
+ theoretical_quantiles = theoretical_quantiles[valid_mask]
302
+ sorted_data = sorted_data[valid_mask]
303
+
304
+ # Focus on the linear (Gaussian) region in the tails
305
+ # Use outer 30% of data for slope estimation
306
+ n_valid = len(sorted_data)
307
+ tail_frac = 0.15
308
+
309
+ lower_idx = int(n_valid * tail_frac)
310
+ upper_idx = int(n_valid * (1 - tail_frac))
311
+
312
+ # Combine tail indices
313
+ tail_indices = np.concatenate([np.arange(0, lower_idx), np.arange(upper_idx, n_valid)])
314
+
315
+ if len(tail_indices) < 10:
316
+ # Fall back to simple estimation
317
+ sigma = np.std(sorted_data)
318
+ mu = np.mean(sorted_data)
319
+ confidence = 0.3
320
+ else:
321
+ # Linear regression on Q-Q tail data
322
+ x_tail = theoretical_quantiles[tail_indices]
323
+ y_tail = sorted_data[tail_indices]
324
+
325
+ slope, intercept, r_value, _p_value, _std_err = stats.linregress(x_tail, y_tail)
326
+
327
+ # Slope of Q-Q plot is sigma, intercept is mu
328
+ sigma = abs(slope)
329
+ mu = intercept
330
+ confidence = min(1.0, max(0.0, r_value**2))
331
+
332
+ return RandomJitterResult(
333
+ rj_rms=sigma,
334
+ method="q_scale",
335
+ confidence=confidence,
336
+ sigma=sigma,
337
+ mu=mu,
338
+ n_samples=n,
339
+ )
340
+
341
+
342
+ def extract_dj(
343
+ tie_data: NDArray[np.float64],
344
+ rj_result: RandomJitterResult | None = None,
345
+ *,
346
+ min_samples: int = 1000,
347
+ ) -> DeterministicJitterResult:
348
+ """Extract deterministic jitter component from TIE data.
349
+
350
+ DJ is the bounded, repeatable component of jitter. It is calculated
351
+ as TJ - RJ contribution using the dual-Dirac model.
352
+
353
+ Args:
354
+ tie_data: Time Interval Error data array in seconds.
355
+ rj_result: Pre-computed RJ result (computed if None).
356
+ min_samples: Minimum samples required.
357
+
358
+ Returns:
359
+ DeterministicJitterResult with DJ_pp value.
360
+
361
+ Raises:
362
+ InsufficientDataError: If insufficient samples.
363
+
364
+ Example:
365
+ >>> dj = extract_dj(tie_data)
366
+ >>> print(f"DJ: {dj.dj_pp * 1e12:.2f} ps peak-to-peak")
367
+
368
+ References:
369
+ IEEE 2414-2020 Section 6.3
370
+ """
371
+ if len(tie_data) < min_samples:
372
+ raise InsufficientDataError(
373
+ f"DJ extraction requires at least {min_samples} samples",
374
+ required=min_samples,
375
+ available=len(tie_data),
376
+ analysis_type="deterministic_jitter_extraction",
377
+ )
378
+
379
+ valid_data = tie_data[~np.isnan(tie_data)]
380
+
381
+ # Get RJ if not provided - use tail fitting for better RJ isolation
382
+ if rj_result is None:
383
+ rj_result = extract_rj(valid_data, method="tail_fit", min_samples=min_samples)
384
+
385
+ rj_rms = rj_result.rj_rms
386
+
387
+ # Create histogram for analysis
388
+ n_bins = min(100, len(valid_data) // 50)
389
+ hist, bin_edges = np.histogram(valid_data, bins=n_bins, density=False)
390
+ bin_centers = (bin_edges[:-1] + bin_edges[1:]) / 2
391
+
392
+ # DJ extraction using dual-Dirac model:
393
+ # The dual-Dirac model assumes DJ creates two impulses at ±δ,
394
+ # each convolved with Gaussian RJ.
395
+ # Total distribution is: 0.5*N(μ-δ, σ) + 0.5*N(μ+δ, σ) # noqa: RUF003
396
+
397
+ sorted_data = np.sort(valid_data)
398
+ n = len(sorted_data)
399
+
400
+ # Try to find DJ by detecting bimodal peaks in histogram
401
+ from scipy.ndimage import gaussian_filter1d
402
+ from scipy.signal import find_peaks
403
+
404
+ dj_pp = 0.0
405
+ peak_separation_dj = None
406
+
407
+ # Smooth histogram for peak detection
408
+ if len(hist) >= 5:
409
+ hist_smooth = gaussian_filter1d(hist.astype(float), sigma=2)
410
+ peaks, properties = find_peaks(hist_smooth, prominence=np.max(hist_smooth) * 0.1)
411
+
412
+ # If we find 2 clear peaks, DJ is their separation
413
+ if len(peaks) >= 2:
414
+ # Sort peaks by prominence and take top 2
415
+ prominences = properties.get("prominences", np.ones(len(peaks)))
416
+ sorted_peak_idx = np.argsort(prominences)[::-1][:2]
417
+ top_peaks = peaks[sorted_peak_idx]
418
+ top_peaks = np.sort(top_peaks)
419
+
420
+ # Peak separation in the histogram
421
+ peak_positions = bin_centers[top_peaks]
422
+ peak_separation_dj = abs(peak_positions[1] - peak_positions[0])
423
+
424
+ # If we found peaks, use that as DJ
425
+ if peak_separation_dj is not None and peak_separation_dj > 2 * rj_rms:
426
+ dj_pp = peak_separation_dj
427
+ else:
428
+ # Fallback: use quantile-based method
429
+ # At BER = 1e-4 (Q ≈ 3.72), we're well into the tails
430
+ lower_idx = max(0, int(n * 0.0001))
431
+ upper_idx = min(n - 1, int(n * 0.9999))
432
+
433
+ tj_at_ber = sorted_data[upper_idx] - sorted_data[lower_idx]
434
+
435
+ # For dual-Dirac + RJ: TJ = 2*Q*RJ + DJ
436
+ # Using Q for BER = 1e-4
437
+ q_factor = 3.72
438
+ rj_contribution = 2 * q_factor * rj_rms
439
+
440
+ # DJ is what remains after removing RJ contribution
441
+ dj_pp = max(0.0, tj_at_ber - rj_contribution)
442
+
443
+ # Delta is half the DJ peak-to-peak (dual-Dirac separation)
444
+ dj_delta = dj_pp / 2
445
+
446
+ # Confidence based on whether we found clear DJ
447
+ if peak_separation_dj is not None:
448
+ # Found bimodal peaks
449
+ confidence = 0.9 if len(peaks) == 2 else 0.7
450
+ elif dj_pp > 2 * rj_rms:
451
+ # Significant DJ from quantile method
452
+ confidence = 0.5
453
+ else:
454
+ # Little or no DJ detected
455
+ confidence = 0.2
456
+
457
+ return DeterministicJitterResult(
458
+ dj_pp=dj_pp,
459
+ dj_delta=dj_delta,
460
+ method="dual_dirac",
461
+ confidence=confidence,
462
+ histogram=hist.astype(np.float64),
463
+ bin_centers=bin_centers,
464
+ )
465
+
466
+
467
+ def extract_pj(
468
+ tie_data: NDArray[np.float64],
469
+ sample_rate: float,
470
+ *,
471
+ min_frequency: float = 1.0,
472
+ max_frequency: float | None = None,
473
+ n_components: int = 5,
474
+ ) -> PeriodicJitterResult:
475
+ """Extract periodic jitter components via spectral analysis.
476
+
477
+ Uses FFT of TIE data to identify sinusoidal jitter components,
478
+ typically caused by power supply noise or EMI.
479
+
480
+ Args:
481
+ tie_data: Time Interval Error data array in seconds.
482
+ sample_rate: Sample rate of edge events (edges per second).
483
+ min_frequency: Minimum PJ frequency to detect (Hz).
484
+ max_frequency: Maximum PJ frequency (default: Nyquist).
485
+ n_components: Number of periodic components to extract.
486
+
487
+ Returns:
488
+ PeriodicJitterResult with frequency/amplitude pairs.
489
+
490
+ Example:
491
+ >>> pj = extract_pj(tie_data, sample_rate=1e6)
492
+ >>> for freq, amp in pj.components:
493
+ ... print(f"{freq/1e3:.1f} kHz: {amp*1e12:.2f} ps")
494
+
495
+ References:
496
+ IEEE 2414-2020 Section 6.4
497
+ """
498
+ valid_data = tie_data[~np.isnan(tie_data)]
499
+ n = len(valid_data)
500
+
501
+ if n < 32:
502
+ return PeriodicJitterResult(
503
+ components=[],
504
+ pj_pp=0.0,
505
+ dominant_frequency=None,
506
+ dominant_amplitude=None,
507
+ )
508
+
509
+ # Remove DC offset (mean)
510
+ data_centered = valid_data - np.mean(valid_data)
511
+
512
+ # Apply window to reduce spectral leakage
513
+ window = np.hanning(n)
514
+ data_windowed = data_centered * window
515
+
516
+ # Compute FFT
517
+ nfft = int(2 ** np.ceil(np.log2(n)))
518
+ spectrum = np.fft.rfft(data_windowed, n=nfft)
519
+ frequencies = np.fft.rfftfreq(nfft, d=1.0 / sample_rate)
520
+ magnitudes = np.abs(spectrum) * 2 / n # Scale for amplitude
521
+
522
+ # Set frequency range
523
+ if max_frequency is None:
524
+ max_frequency = sample_rate / 2
525
+
526
+ # Find peaks in valid frequency range
527
+ freq_mask = (frequencies >= min_frequency) & (frequencies <= max_frequency)
528
+ valid_freqs = frequencies[freq_mask]
529
+ valid_mags = magnitudes[freq_mask]
530
+
531
+ if len(valid_mags) < 3:
532
+ return PeriodicJitterResult(
533
+ components=[],
534
+ pj_pp=0.0,
535
+ dominant_frequency=None,
536
+ dominant_amplitude=None,
537
+ )
538
+
539
+ # Find peaks (local maxima)
540
+ from scipy.signal import find_peaks
541
+
542
+ # Threshold: peaks must be 3x the median magnitude
543
+ threshold = 3 * np.median(valid_mags)
544
+ peak_indices, _properties = find_peaks(
545
+ valid_mags,
546
+ height=threshold,
547
+ distance=3, # Minimum separation between peaks
548
+ )
549
+
550
+ # Sort by amplitude and take top n_components
551
+ if len(peak_indices) > 0:
552
+ peak_heights = valid_mags[peak_indices]
553
+ sorted_indices = np.argsort(peak_heights)[::-1][:n_components]
554
+ top_peaks = peak_indices[sorted_indices]
555
+
556
+ components = [(float(valid_freqs[idx]), float(valid_mags[idx])) for idx in top_peaks]
557
+
558
+ # Calculate total PJ as sum of component amplitudes (peak-to-peak)
559
+ pj_pp = 2 * sum(amp for _, amp in components)
560
+
561
+ dominant_frequency = components[0][0] if components else None
562
+ dominant_amplitude = components[0][1] if components else None
563
+ else:
564
+ components = []
565
+ pj_pp = 0.0
566
+ dominant_frequency = None
567
+ dominant_amplitude = None
568
+
569
+ return PeriodicJitterResult(
570
+ components=components,
571
+ pj_pp=pj_pp,
572
+ dominant_frequency=dominant_frequency,
573
+ dominant_amplitude=dominant_amplitude,
574
+ )
575
+
576
+
577
+ def extract_ddj(
578
+ tie_data: NDArray[np.float64],
579
+ bit_pattern: NDArray[np.int_] | None = None,
580
+ *,
581
+ pattern_length: int = 3,
582
+ ) -> DataDependentJitterResult:
583
+ """Extract data-dependent jitter caused by ISI effects.
584
+
585
+ Analyzes correlation between jitter and preceding bit patterns
586
+ to identify inter-symbol interference (ISI) induced timing variations.
587
+
588
+ Args:
589
+ tie_data: Time Interval Error data array in seconds.
590
+ bit_pattern: Associated bit pattern for each TIE sample.
591
+ pattern_length: Number of preceding bits to correlate.
592
+
593
+ Returns:
594
+ DataDependentJitterResult with pattern-correlated jitter.
595
+
596
+ Raises:
597
+ ValueError: If bit_pattern length does not match tie_data length.
598
+
599
+ Example:
600
+ >>> ddj = extract_ddj(tie_data, bit_pattern=bits, pattern_length=3)
601
+ >>> print(f"DDJ: {ddj.ddj_pp * 1e12:.2f} ps")
602
+
603
+ References:
604
+ IEEE 2414-2020 Section 6.5
605
+ """
606
+ valid_data = tie_data[~np.isnan(tie_data)]
607
+ n = len(valid_data)
608
+
609
+ if bit_pattern is None:
610
+ # Without bit pattern data, estimate from TIE distribution
611
+ # Use alternating pattern assumption
612
+ pattern_histogram: dict[str, float] = {}
613
+
614
+ # Simple estimation: look for bimodality in TIE
615
+ median = np.median(valid_data)
616
+ above_median = valid_data > median
617
+ below_median = ~above_median
618
+
619
+ pattern_histogram["above_median"] = float(np.mean(valid_data[above_median]))
620
+ pattern_histogram["below_median"] = float(np.mean(valid_data[below_median]))
621
+
622
+ ddj_pp = abs(pattern_histogram["above_median"] - pattern_histogram["below_median"])
623
+
624
+ return DataDependentJitterResult(
625
+ ddj_pp=ddj_pp,
626
+ pattern_histogram=pattern_histogram,
627
+ pattern_length=pattern_length,
628
+ isi_coefficient=0.0, # Unknown without pattern
629
+ )
630
+
631
+ # With bit pattern available
632
+ if len(bit_pattern) != n:
633
+ raise ValueError("bit_pattern length must match tie_data length")
634
+
635
+ pattern_histogram = {}
636
+ 2**pattern_length
637
+
638
+ # Create pattern strings and accumulate TIE values
639
+ for i in range(pattern_length - 1, n):
640
+ pattern_bits = bit_pattern[i - pattern_length + 1 : i + 1]
641
+ pattern_str = "".join(str(int(b)) for b in pattern_bits)
642
+
643
+ if pattern_str not in pattern_histogram:
644
+ pattern_histogram[pattern_str] = [] # type: ignore[assignment]
645
+ pattern_histogram[pattern_str].append(valid_data[i]) # type: ignore[attr-defined]
646
+
647
+ # Calculate mean TIE for each pattern
648
+ pattern_means: dict[str, float] = {}
649
+ for pattern, values in pattern_histogram.items():
650
+ if len(values) > 0: # type: ignore[arg-type]
651
+ pattern_means[pattern] = float(np.mean(values))
652
+
653
+ # DDJ is the range of pattern-dependent means
654
+ if len(pattern_means) > 1:
655
+ mean_values = list(pattern_means.values())
656
+ ddj_pp = max(mean_values) - min(mean_values)
657
+ else:
658
+ ddj_pp = 0.0
659
+
660
+ # Calculate ISI correlation coefficient
661
+ # Correlation between previous bit and current TIE
662
+ if n > 1:
663
+ prev_bits = bit_pattern[:-1].astype(float)
664
+ curr_tie = valid_data[1:]
665
+ correlation = np.corrcoef(prev_bits, curr_tie)[0, 1]
666
+ isi_coefficient = correlation if np.isfinite(correlation) else 0.0
667
+ else:
668
+ isi_coefficient = 0.0
669
+
670
+ return DataDependentJitterResult(
671
+ ddj_pp=ddj_pp,
672
+ pattern_histogram=pattern_means,
673
+ pattern_length=pattern_length,
674
+ isi_coefficient=isi_coefficient,
675
+ )
676
+
677
+
678
+ def decompose_jitter(
679
+ tie_data: NDArray[np.float64],
680
+ *,
681
+ edge_rate: float | None = None,
682
+ include_pj: bool = True,
683
+ include_ddj: bool = False,
684
+ bit_pattern: NDArray[np.int_] | None = None,
685
+ target_ber: float = 1e-12,
686
+ ) -> JitterDecomposition:
687
+ """Perform complete jitter decomposition.
688
+
689
+ Decomposes total jitter into its constituent components:
690
+ - Random Jitter (RJ): Unbounded Gaussian component
691
+ - Deterministic Jitter (DJ): Bounded, repeatable component
692
+ - Periodic Jitter (PJ): Sinusoidal components (optional)
693
+ - Data-Dependent Jitter (DDJ): ISI-related component (optional)
694
+
695
+ Args:
696
+ tie_data: Time Interval Error data array in seconds.
697
+ edge_rate: Rate of edges in Hz (required for PJ analysis).
698
+ include_pj: Include periodic jitter analysis.
699
+ include_ddj: Include data-dependent jitter analysis.
700
+ bit_pattern: Bit pattern for DDJ analysis.
701
+ target_ber: Target bit error rate for TJ calculation (default: 1e-12).
702
+
703
+ Returns:
704
+ JitterDecomposition with all component results and calculated TJ.
705
+
706
+ Example:
707
+ >>> decomp = decompose_jitter(tie_data, edge_rate=1e9)
708
+ >>> print(f"RJ: {decomp.rj.rj_rms * 1e12:.2f} ps")
709
+ >>> print(f"DJ: {decomp.dj.dj_pp * 1e12:.2f} ps")
710
+ >>> print(f"TJ: {decomp.tj_pp * 1e12:.2f} ps")
711
+
712
+ References:
713
+ IEEE 2414-2020 Section 6
714
+ Dual-Dirac model: TJ = DJ + 2*Q*RJ where Q = norm.ppf(1 - BER/2)
715
+ """
716
+ # Extract RJ first
717
+ rj_result = extract_rj(tie_data)
718
+
719
+ # Extract DJ using RJ result
720
+ dj_result = extract_dj(tie_data, rj_result)
721
+
722
+ # Optional: Extract PJ
723
+ pj_result = None
724
+ if include_pj and edge_rate is not None:
725
+ pj_result = extract_pj(tie_data, edge_rate)
726
+
727
+ # Optional: Extract DDJ
728
+ ddj_result = None
729
+ if include_ddj:
730
+ ddj_result = extract_ddj(tie_data, bit_pattern)
731
+
732
+ # Calculate Total Jitter using dual-Dirac model
733
+ # TJ = DJ + 2 * Q * RJ
734
+ # where Q is the Q-factor for the target BER
735
+ q_factor = stats.norm.ppf(1 - target_ber / 2)
736
+ tj_pp = dj_result.dj_pp + 2 * q_factor * rj_result.rj_rms
737
+
738
+ return JitterDecomposition(
739
+ rj=rj_result,
740
+ dj=dj_result,
741
+ pj=pj_result,
742
+ ddj=ddj_result,
743
+ tj_pp=tj_pp,
744
+ ber_measured=target_ber,
745
+ )
746
+
747
+
748
+ __all__ = [
749
+ "DataDependentJitterResult",
750
+ "DeterministicJitterResult",
751
+ "JitterDecomposition",
752
+ "PeriodicJitterResult",
753
+ "RandomJitterResult",
754
+ "decompose_jitter",
755
+ "extract_ddj",
756
+ "extract_dj",
757
+ "extract_pj",
758
+ "extract_rj",
759
+ ]