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,877 @@
1
+ """Signal quality and integrity analysis.
2
+
3
+ This module provides comprehensive signal integrity analysis for digital signals,
4
+ including noise margin measurements, transition characterization, overshoot/
5
+ undershoot detection, and ringing analysis.
6
+
7
+
8
+ Example:
9
+ >>> import numpy as np
10
+ >>> from oscura.analyzers.digital.signal_quality import SignalQualityAnalyzer
11
+ >>> # Generate test signal
12
+ >>> signal = np.concatenate([np.zeros(100), np.ones(100)])
13
+ >>> analyzer = SignalQualityAnalyzer(sample_rate=100e6, logic_family='TTL')
14
+ >>> report = analyzer.analyze(signal)
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from dataclasses import dataclass
20
+ from typing import TYPE_CHECKING, Any, Literal
21
+
22
+ import numpy as np
23
+ from scipy import signal as scipy_signal
24
+
25
+ if TYPE_CHECKING:
26
+ from numpy.typing import NDArray
27
+
28
+
29
+ # Logic family thresholds (from existing extraction.py)
30
+ LOGIC_THRESHOLDS = {
31
+ "ttl": {"VIL": 0.8, "VIH": 2.0, "VOL": 0.4, "VOH": 2.4, "VCC": 5.0},
32
+ "cmos": {"VIL": 1.5, "VIH": 3.5, "VOL": 0.1, "VOH": 4.9, "VCC": 5.0},
33
+ "lvttl": {"VIL": 0.8, "VIH": 1.5, "VOL": 0.4, "VOH": 2.4, "VCC": 3.3},
34
+ "lvcmos": {"VIL": 0.99, "VIH": 2.31, "VOL": 0.1, "VOH": 3.2, "VCC": 3.3},
35
+ }
36
+
37
+
38
+ @dataclass
39
+ class NoiseMargins:
40
+ """Noise margins for digital signal.
41
+
42
+ Attributes:
43
+ high_margin: Distance from threshold to logic high level (V).
44
+ low_margin: Distance from threshold to logic low level (V).
45
+ high_mean: Mean high level voltage.
46
+ low_mean: Mean low level voltage.
47
+ high_std: Standard deviation of high level (noise).
48
+ low_std: Standard deviation of low level (noise).
49
+ threshold: Detection threshold used.
50
+ """
51
+
52
+ high_margin: float # Distance from threshold to logic high
53
+ low_margin: float # Distance from threshold to logic low
54
+ high_mean: float # Mean high level
55
+ low_mean: float # Mean low level
56
+ high_std: float # High level noise
57
+ low_std: float # Low level noise
58
+ threshold: float # Detection threshold
59
+
60
+
61
+ @dataclass
62
+ class TransitionMetrics:
63
+ """Metrics for signal transitions.
64
+
65
+ Attributes:
66
+ rise_time: 10-90% rise time in seconds.
67
+ fall_time: 90-10% fall time in seconds.
68
+ slew_rate_rising: Rising edge slew rate (V/s).
69
+ slew_rate_falling: Falling edge slew rate (V/s).
70
+ overshoot: Overshoot as percentage of signal swing.
71
+ undershoot: Undershoot as percentage of signal swing.
72
+ ringing_frequency: Ringing frequency in Hz (None if no ringing).
73
+ ringing_amplitude: Ringing amplitude in volts (None if no ringing).
74
+ settling_time: Time to settle within tolerance (None if not measured).
75
+ """
76
+
77
+ rise_time: float # 10-90% rise time
78
+ fall_time: float # 90-10% fall time
79
+ slew_rate_rising: float
80
+ slew_rate_falling: float
81
+ overshoot: float # Percentage overshoot
82
+ undershoot: float # Percentage undershoot
83
+ ringing_frequency: float | None = None
84
+ ringing_amplitude: float | None = None
85
+ settling_time: float | None = None
86
+
87
+
88
+ @dataclass
89
+ class SignalIntegrityReport:
90
+ """Complete signal integrity report.
91
+
92
+ Attributes:
93
+ noise_margins: Noise margin measurements.
94
+ transitions: Transition quality metrics.
95
+ snr_db: Signal-to-noise ratio in dB.
96
+ signal_quality: Overall quality assessment.
97
+ issues: List of detected issues.
98
+ recommendations: List of recommendations for improvement.
99
+ """
100
+
101
+ noise_margins: NoiseMargins
102
+ transitions: TransitionMetrics
103
+ snr_db: float
104
+ signal_quality: Literal["excellent", "good", "fair", "poor"]
105
+ issues: list[str]
106
+ recommendations: list[str]
107
+
108
+
109
+ @dataclass
110
+ class SimpleQualityMetrics:
111
+ """Simplified quality metrics for test compatibility.
112
+
113
+ Provides a flat interface with direct attribute access for common metrics.
114
+
115
+ Attributes:
116
+ noise_margin_low: Low-side noise margin in volts.
117
+ noise_margin_high: High-side noise margin in volts.
118
+ rise_time: Rise time in samples (or seconds depending on context).
119
+ fall_time: Fall time in samples (or seconds depending on context).
120
+ has_overshoot: Whether overshoot was detected.
121
+ max_overshoot: Maximum overshoot value in volts.
122
+ duty_cycle: Signal duty cycle (0.0 to 1.0).
123
+ """
124
+
125
+ noise_margin_low: float
126
+ noise_margin_high: float
127
+ rise_time: float
128
+ fall_time: float
129
+ has_overshoot: bool
130
+ max_overshoot: float
131
+ duty_cycle: float
132
+
133
+
134
+ class SignalQualityAnalyzer:
135
+ """Analyze digital signal quality and integrity.
136
+
137
+ Provides comprehensive signal integrity analysis including noise margins,
138
+ transition metrics, overshoot/undershoot, and ringing detection.
139
+
140
+ Supports two initialization modes:
141
+ 1. Full mode: SignalQualityAnalyzer(sample_rate=1e9, logic_family='TTL')
142
+ 2. Simple mode: SignalQualityAnalyzer(v_il=0.8, v_ih=2.0) - for test compatibility
143
+
144
+ Attributes:
145
+ sample_rate: Sample rate of input signals in Hz.
146
+ logic_family: Logic family for threshold determination.
147
+ v_il: Input low threshold voltage.
148
+ v_ih: Input high threshold voltage.
149
+ vdd: Supply voltage for overshoot reference.
150
+
151
+ Example:
152
+ >>> analyzer = SignalQualityAnalyzer(sample_rate=1e9, logic_family='TTL')
153
+ >>> report = analyzer.analyze(signal_trace)
154
+ """
155
+
156
+ def __init__(
157
+ self,
158
+ sample_rate: float | None = None,
159
+ logic_family: str = "auto",
160
+ v_il: float | None = None,
161
+ v_ih: float | None = None,
162
+ vdd: float | None = None,
163
+ ):
164
+ """Initialize analyzer.
165
+
166
+ Args:
167
+ sample_rate: Sample rate in Hz (optional for simple mode).
168
+ logic_family: Logic family ('TTL', 'CMOS', 'LVTTL', 'LVCMOS', 'auto').
169
+ v_il: Input low threshold voltage (for simple mode).
170
+ v_ih: Input high threshold voltage (for simple mode).
171
+ vdd: Supply voltage for overshoot reference (for simple mode).
172
+
173
+ Raises:
174
+ ValueError: If sample rate is invalid (when provided).
175
+ """
176
+ # Simple mode: thresholds provided directly
177
+ self.v_il = v_il
178
+ self.v_ih = v_ih
179
+ self.vdd = vdd
180
+
181
+ # Full mode: sample rate and logic family
182
+ if sample_rate is not None:
183
+ if sample_rate <= 0:
184
+ raise ValueError(f"Sample rate must be positive, got {sample_rate}")
185
+ self.sample_rate = sample_rate
186
+ self._time_base = 1.0 / sample_rate
187
+ else:
188
+ # Default sample rate for simple mode (samples per second = 1)
189
+ self.sample_rate = 1.0
190
+ self._time_base = 1.0
191
+
192
+ self.logic_family = logic_family.lower() if logic_family else "auto"
193
+
194
+ # If thresholds provided, use them to determine logic family settings
195
+ self._threshold: float | None
196
+ if v_il is not None and v_ih is not None:
197
+ self._threshold = (v_il + v_ih) / 2.0
198
+ else:
199
+ self._threshold = None
200
+
201
+ def analyze(
202
+ self, trace: NDArray[np.float64], clock_trace: NDArray[np.float64] | None = None
203
+ ) -> Any:
204
+ """Perform complete signal integrity analysis.
205
+
206
+ Returns SimpleQualityMetrics in simple mode (when v_il/v_ih provided),
207
+ or SignalIntegrityReport in full mode.
208
+
209
+ Args:
210
+ trace: Input signal trace (analog voltage values).
211
+ clock_trace: Optional clock signal for synchronized analysis.
212
+
213
+ Returns:
214
+ SimpleQualityMetrics or SignalIntegrityReport with analysis results.
215
+
216
+ Example:
217
+ >>> report = analyzer.analyze(signal_trace)
218
+ >>> print(f"Signal quality: {report.signal_quality}")
219
+ """
220
+ trace = np.asarray(trace, dtype=np.float64)
221
+
222
+ # Simple mode: return SimpleQualityMetrics
223
+ if self.v_il is not None or self.v_ih is not None or self.vdd is not None:
224
+ return self._analyze_simple(trace)
225
+
226
+ # Full mode: return SignalIntegrityReport
227
+ return self._analyze_full(trace, clock_trace)
228
+
229
+ def _analyze_simple(self, trace: NDArray[np.float64]) -> SimpleQualityMetrics:
230
+ """Simple analysis mode returning flat metrics.
231
+
232
+ Args:
233
+ trace: Input signal trace.
234
+
235
+ Returns:
236
+ SimpleQualityMetrics with measured values.
237
+ """
238
+ # Determine threshold
239
+ threshold: float
240
+ if self._threshold is not None:
241
+ threshold = self._threshold
242
+ else:
243
+ threshold = float((np.max(trace) + np.min(trace)) / 2.0)
244
+
245
+ # Separate high and low samples
246
+ high_samples = trace[trace > threshold]
247
+ low_samples = trace[trace <= threshold]
248
+
249
+ # Calculate noise margins
250
+ if len(high_samples) > 0:
251
+ high_mean = np.mean(high_samples)
252
+ if self.v_ih is not None:
253
+ noise_margin_high = high_mean - self.v_ih
254
+ else:
255
+ noise_margin_high = high_mean - threshold
256
+ else:
257
+ noise_margin_high = 0.0
258
+
259
+ if len(low_samples) > 0:
260
+ low_mean = np.mean(low_samples)
261
+ if self.v_il is not None:
262
+ noise_margin_low = self.v_il - low_mean
263
+ else:
264
+ noise_margin_low = threshold - low_mean
265
+ else:
266
+ noise_margin_low = 0.0
267
+
268
+ # Measure rise/fall times in samples
269
+ rise_time, fall_time = self._measure_rise_fall_samples(trace, threshold)
270
+
271
+ # Detect overshoot
272
+ has_overshoot, max_overshoot = self._detect_overshoot_simple(trace)
273
+
274
+ # Calculate duty cycle
275
+ duty_cycle = self._calculate_duty_cycle(trace, threshold)
276
+
277
+ return SimpleQualityMetrics(
278
+ noise_margin_low=float(noise_margin_low),
279
+ noise_margin_high=float(noise_margin_high),
280
+ rise_time=float(rise_time),
281
+ fall_time=float(fall_time),
282
+ has_overshoot=has_overshoot,
283
+ max_overshoot=float(max_overshoot),
284
+ duty_cycle=float(duty_cycle),
285
+ )
286
+
287
+ def _measure_rise_fall_samples(
288
+ self, trace: NDArray[np.float64], threshold: float
289
+ ) -> tuple[float, float]:
290
+ """Measure rise and fall times in samples.
291
+
292
+ Args:
293
+ trace: Input signal trace.
294
+ threshold: Detection threshold.
295
+
296
+ Returns:
297
+ Tuple of (rise_time_samples, fall_time_samples).
298
+ """
299
+ # Detect edges
300
+ crossings = np.diff((trace > threshold).astype(int))
301
+ rising_edges = np.where(crossings > 0)[0]
302
+ falling_edges = np.where(crossings < 0)[0]
303
+
304
+ # Measure rise times
305
+ rise_times = []
306
+ for edge_idx in rising_edges:
307
+ window_size = min(10, edge_idx, len(trace) - edge_idx - 1)
308
+ if window_size < 2:
309
+ continue
310
+
311
+ window = trace[edge_idx - window_size : edge_idx + window_size + 1]
312
+ v_min = np.min(window)
313
+ v_max = np.max(window)
314
+
315
+ if v_max - v_min < 1e-6:
316
+ continue
317
+
318
+ # Find 10% and 90% points
319
+ v_10 = v_min + 0.1 * (v_max - v_min)
320
+ v_90 = v_min + 0.9 * (v_max - v_min)
321
+
322
+ idx_10 = np.where(window >= v_10)[0]
323
+ idx_90 = np.where(window >= v_90)[0]
324
+
325
+ if len(idx_10) > 0 and len(idx_90) > 0:
326
+ rise_time = idx_90[0] - idx_10[0]
327
+ if rise_time > 0:
328
+ rise_times.append(rise_time)
329
+
330
+ # Measure fall times
331
+ fall_times = []
332
+ for edge_idx in falling_edges:
333
+ window_size = min(10, edge_idx, len(trace) - edge_idx - 1)
334
+ if window_size < 2:
335
+ continue
336
+
337
+ window = trace[edge_idx - window_size : edge_idx + window_size + 1]
338
+ v_min = np.min(window)
339
+ v_max = np.max(window)
340
+
341
+ if v_max - v_min < 1e-6:
342
+ continue
343
+
344
+ v_90 = v_min + 0.9 * (v_max - v_min)
345
+ v_10 = v_min + 0.1 * (v_max - v_min)
346
+
347
+ idx_90 = np.where(window <= v_90)[0]
348
+ idx_10 = np.where(window <= v_10)[0]
349
+
350
+ if len(idx_90) > 0 and len(idx_10) > 0:
351
+ fall_time = idx_10[-1] - idx_90[0]
352
+ if fall_time > 0:
353
+ fall_times.append(fall_time)
354
+
355
+ rise_time = np.mean(rise_times) if rise_times else 0.0
356
+ fall_time = np.mean(fall_times) if fall_times else 0.0
357
+
358
+ return rise_time, fall_time
359
+
360
+ def _detect_overshoot_simple(self, trace: NDArray[np.float64]) -> tuple[bool, float]:
361
+ """Detect overshoot in simple mode.
362
+
363
+ Args:
364
+ trace: Input signal trace.
365
+
366
+ Returns:
367
+ Tuple of (has_overshoot, max_overshoot_value).
368
+ """
369
+ threshold = self._threshold or (np.max(trace) + np.min(trace)) / 2.0
370
+ high_samples = trace[trace > threshold]
371
+
372
+ if len(high_samples) == 0:
373
+ return False, 0.0
374
+
375
+ high_median = np.median(high_samples)
376
+ max_val = np.max(trace)
377
+
378
+ # Check if max exceeds expected high level
379
+ if self.vdd is not None:
380
+ # Check against VDD
381
+ overshoot = float(max_val - self.vdd)
382
+ has_overshoot = overshoot > 0.05 # 50mV threshold
383
+ else:
384
+ # Check against median high level
385
+ overshoot = float(max_val - high_median)
386
+ # Only count as overshoot if significantly above stable level
387
+ _high_level = high_median
388
+ signal_swing = high_median - np.min(trace)
389
+ has_overshoot = overshoot > float(signal_swing * 0.05) # 5% threshold
390
+
391
+ return bool(has_overshoot), max(0.0, overshoot)
392
+
393
+ def _calculate_duty_cycle(self, trace: NDArray[np.float64], threshold: float) -> float:
394
+ """Calculate signal duty cycle.
395
+
396
+ Args:
397
+ trace: Input signal trace.
398
+ threshold: Detection threshold.
399
+
400
+ Returns:
401
+ Duty cycle as ratio (0.0 to 1.0).
402
+ """
403
+ if len(trace) == 0:
404
+ return 0.0
405
+
406
+ # Handle boolean trace
407
+ if trace.dtype == np.bool_:
408
+ high_count = np.sum(trace)
409
+ else:
410
+ high_count = np.sum(trace > threshold)
411
+
412
+ return float(high_count) / float(len(trace))
413
+
414
+ def _analyze_full(
415
+ self, trace: NDArray[np.float64], clock_trace: NDArray[np.float64] | None = None
416
+ ) -> SignalIntegrityReport:
417
+ """Full analysis mode returning comprehensive report.
418
+
419
+ Args:
420
+ trace: Input signal trace.
421
+ clock_trace: Optional clock signal.
422
+
423
+ Returns:
424
+ SignalIntegrityReport with complete analysis.
425
+ """
426
+ # Measure noise margins
427
+ logic_fam: Literal["ttl", "cmos", "lvttl", "lvcmos", "auto"]
428
+ if self.logic_family in ("ttl", "cmos", "lvttl", "lvcmos", "auto"):
429
+ logic_fam = self.logic_family # type: ignore[assignment]
430
+ else:
431
+ logic_fam = "auto"
432
+ noise_margins = self.measure_noise_margins(trace, logic_fam)
433
+
434
+ # Measure transitions
435
+ transitions = self.measure_transitions(trace)
436
+
437
+ # Calculate SNR
438
+ snr_db = self.calculate_snr(trace)
439
+
440
+ # Assess overall quality and identify issues
441
+ issues = []
442
+ recommendations = []
443
+
444
+ # Check noise margins
445
+ if noise_margins.high_margin < 0.4:
446
+ issues.append("Insufficient high-level noise margin")
447
+ recommendations.append("Increase signal high level or reduce noise")
448
+
449
+ if noise_margins.low_margin < 0.4:
450
+ issues.append("Insufficient low-level noise margin")
451
+ recommendations.append("Decrease signal low level or reduce noise")
452
+
453
+ # Check transitions
454
+ if transitions.overshoot > 20:
455
+ issues.append(f"Excessive overshoot: {transitions.overshoot:.1f}%")
456
+ recommendations.append("Add series termination or reduce capacitance")
457
+
458
+ if transitions.undershoot > 20:
459
+ issues.append(f"Excessive undershoot: {transitions.undershoot:.1f}%")
460
+ recommendations.append("Check ground connections and reduce inductance")
461
+
462
+ if transitions.ringing_amplitude and transitions.ringing_amplitude > 0.2:
463
+ issues.append("Significant ringing detected")
464
+ recommendations.append("Add damping resistor or improve impedance matching")
465
+
466
+ # Check SNR
467
+ if snr_db < 20:
468
+ issues.append(f"Low SNR: {snr_db:.1f} dB")
469
+ recommendations.append("Reduce noise sources or improve shielding")
470
+
471
+ # Determine overall quality
472
+ quality: Literal["excellent", "good", "fair", "poor"]
473
+ if len(issues) == 0 and snr_db > 40:
474
+ quality = "excellent"
475
+ elif len(issues) <= 1 and snr_db > 30:
476
+ quality = "good"
477
+ elif len(issues) <= 2 and snr_db > 20:
478
+ quality = "fair"
479
+ else:
480
+ quality = "poor"
481
+
482
+ return SignalIntegrityReport(
483
+ noise_margins=noise_margins,
484
+ transitions=transitions,
485
+ snr_db=snr_db,
486
+ signal_quality=quality,
487
+ issues=issues,
488
+ recommendations=recommendations,
489
+ )
490
+
491
+ def measure_noise_margins(
492
+ self,
493
+ trace: NDArray[np.float64],
494
+ logic_family: Literal["ttl", "cmos", "lvttl", "lvcmos", "auto"] = "auto",
495
+ ) -> NoiseMargins:
496
+ """Measure noise margins for high and low states.
497
+
498
+ Args:
499
+ trace: Input signal trace (analog voltage values).
500
+ logic_family: Logic family for threshold determination.
501
+
502
+ Returns:
503
+ NoiseMargins object with measured margins.
504
+
505
+ Example:
506
+ >>> margins = analyzer.measure_noise_margins(trace, logic_family='TTL')
507
+ """
508
+ trace = np.asarray(trace)
509
+
510
+ # Determine threshold
511
+ if logic_family == "auto":
512
+ # Auto-detect based on signal range
513
+ signal_range = np.max(trace) - np.min(trace)
514
+ if signal_range > 4.0:
515
+ logic_family = "ttl" # 5V logic
516
+ elif signal_range > 2.5:
517
+ logic_family = "lvttl" # 3.3V logic
518
+ else:
519
+ logic_family = "lvcmos" # Low voltage
520
+
521
+ # Get thresholds for logic family
522
+ thresholds = LOGIC_THRESHOLDS.get(logic_family, LOGIC_THRESHOLDS["ttl"])
523
+ threshold = (thresholds["VIL"] + thresholds["VIH"]) / 2.0
524
+
525
+ # Separate high and low samples
526
+ high_samples = trace[trace > threshold]
527
+ low_samples = trace[trace <= threshold]
528
+
529
+ # Calculate statistics
530
+ if len(high_samples) > 0:
531
+ high_mean = np.mean(high_samples)
532
+ high_std = np.std(high_samples)
533
+ high_margin = high_mean - threshold
534
+ else:
535
+ high_mean = 0.0
536
+ high_std = 0.0
537
+ high_margin = 0.0
538
+
539
+ if len(low_samples) > 0:
540
+ low_mean = np.mean(low_samples)
541
+ low_std = np.std(low_samples)
542
+ low_margin = threshold - low_mean
543
+ else:
544
+ low_mean = 0.0
545
+ low_std = 0.0
546
+ low_margin = 0.0
547
+
548
+ return NoiseMargins(
549
+ high_margin=float(high_margin),
550
+ low_margin=float(low_margin),
551
+ high_mean=float(high_mean),
552
+ low_mean=float(low_mean),
553
+ high_std=float(high_std),
554
+ low_std=float(low_std),
555
+ threshold=float(threshold),
556
+ )
557
+
558
+ def measure_transitions(self, trace: NDArray[np.float64]) -> TransitionMetrics:
559
+ """Measure transition characteristics.
560
+
561
+ Analyzes rising and falling edges to measure rise/fall times,
562
+ slew rates, overshoot, undershoot, and ringing.
563
+
564
+ Args:
565
+ trace: Input signal trace (analog voltage values).
566
+
567
+ Returns:
568
+ TransitionMetrics object with transition measurements.
569
+
570
+ Example:
571
+ >>> metrics = analyzer.measure_transitions(trace)
572
+ """
573
+ trace = np.asarray(trace)
574
+
575
+ # Find threshold crossings
576
+ threshold = (np.max(trace) + np.min(trace)) / 2.0
577
+ signal_range = np.max(trace) - np.min(trace)
578
+
579
+ # Detect edges (simple threshold crossing)
580
+ crossings = np.diff((trace > threshold).astype(int))
581
+ rising_edges = np.where(crossings > 0)[0]
582
+ falling_edges = np.where(crossings < 0)[0]
583
+
584
+ # Measure rise time (10-90%)
585
+ rise_times = []
586
+ for edge_idx in rising_edges:
587
+ if edge_idx > 10 and edge_idx < len(trace) - 10:
588
+ # Get window around edge
589
+ window = trace[edge_idx - 10 : edge_idx + 10]
590
+ v_min = np.min(window)
591
+ v_max = np.max(window)
592
+
593
+ # Find 10% and 90% points
594
+ v_10 = v_min + 0.1 * (v_max - v_min)
595
+ v_90 = v_min + 0.9 * (v_max - v_min)
596
+
597
+ # Find sample indices
598
+ idx_10 = np.where(window >= v_10)[0]
599
+ idx_90 = np.where(window >= v_90)[0]
600
+
601
+ if len(idx_10) > 0 and len(idx_90) > 0:
602
+ rise_time = (idx_90[0] - idx_10[0]) * self._time_base
603
+ rise_times.append(rise_time)
604
+
605
+ # Measure fall time (90-10%)
606
+ fall_times = []
607
+ for edge_idx in falling_edges:
608
+ if edge_idx > 10 and edge_idx < len(trace) - 10:
609
+ window = trace[edge_idx - 10 : edge_idx + 10]
610
+ v_min = np.min(window)
611
+ v_max = np.max(window)
612
+
613
+ v_90 = v_min + 0.9 * (v_max - v_min)
614
+ v_10 = v_min + 0.1 * (v_max - v_min)
615
+
616
+ idx_90 = np.where(window <= v_90)[0]
617
+ idx_10 = np.where(window <= v_10)[0]
618
+
619
+ if len(idx_90) > 0 and len(idx_10) > 0:
620
+ fall_time = (idx_10[-1] - idx_90[0]) * self._time_base
621
+ fall_times.append(fall_time)
622
+
623
+ # Calculate average times
624
+ rise_time = np.mean(rise_times) if rise_times else 0.0
625
+ fall_time = np.mean(fall_times) if fall_times else 0.0
626
+
627
+ # Calculate slew rates
628
+ slew_rate_rising = (0.8 * signal_range / rise_time) if rise_time > 0 else 0.0
629
+ slew_rate_falling = (0.8 * signal_range / fall_time) if fall_time > 0 else 0.0
630
+
631
+ # Detect overshoot and undershoot
632
+ overshoot_pct, undershoot_pct = self.detect_overshoot(trace)
633
+
634
+ # Detect ringing
635
+ ringing = self.detect_ringing(trace)
636
+ if ringing:
637
+ ringing_freq, ringing_amp = ringing
638
+ else:
639
+ ringing_freq, ringing_amp = None, None
640
+
641
+ return TransitionMetrics(
642
+ rise_time=float(rise_time),
643
+ fall_time=float(fall_time),
644
+ slew_rate_rising=float(slew_rate_rising),
645
+ slew_rate_falling=float(slew_rate_falling),
646
+ overshoot=float(overshoot_pct),
647
+ undershoot=float(undershoot_pct),
648
+ ringing_frequency=ringing_freq,
649
+ ringing_amplitude=ringing_amp,
650
+ )
651
+
652
+ def detect_overshoot(
653
+ self, trace: NDArray[np.float64], edges: list[Any] | None = None
654
+ ) -> tuple[float, float]:
655
+ """Detect and measure overshoot and undershoot.
656
+
657
+ Args:
658
+ trace: Input signal trace.
659
+ edges: Optional list of edge objects (not used in this implementation).
660
+
661
+ Returns:
662
+ Tuple of (overshoot_percent, undershoot_percent).
663
+
664
+ Example:
665
+ >>> overshoot, undershoot = analyzer.detect_overshoot(trace)
666
+ """
667
+ trace = np.asarray(trace)
668
+
669
+ # Determine signal levels
670
+ threshold = (np.max(trace) + np.min(trace)) / 2.0
671
+ high_samples = trace[trace > threshold]
672
+ low_samples = trace[trace <= threshold]
673
+
674
+ if len(high_samples) == 0 or len(low_samples) == 0:
675
+ return 0.0, 0.0
676
+
677
+ # Expected levels (mean of stable regions)
678
+ high_level = np.median(high_samples)
679
+ low_level = np.median(low_samples)
680
+ signal_swing = high_level - low_level
681
+
682
+ if signal_swing < 1e-6:
683
+ return 0.0, 0.0
684
+
685
+ # Overshoot: how much signal exceeds high level
686
+ max_val = np.max(trace)
687
+ overshoot = max_val - high_level
688
+ overshoot_pct = (overshoot / signal_swing) * 100.0
689
+
690
+ # Undershoot: how much signal goes below low level
691
+ min_val = np.min(trace)
692
+ undershoot = low_level - min_val
693
+ undershoot_pct = (undershoot / signal_swing) * 100.0
694
+
695
+ return max(0.0, overshoot_pct), max(0.0, undershoot_pct)
696
+
697
+ def detect_ringing(self, trace: NDArray[np.float64]) -> tuple[float, float] | None:
698
+ """Detect and characterize ringing (frequency, amplitude).
699
+
700
+ Uses FFT analysis to detect oscillations after edges that indicate ringing.
701
+
702
+ Args:
703
+ trace: Input signal trace.
704
+
705
+ Returns:
706
+ Tuple of (frequency_hz, amplitude_volts) if ringing detected, None otherwise.
707
+
708
+ Example:
709
+ >>> ringing = analyzer.detect_ringing(trace)
710
+ >>> if ringing:
711
+ ... freq, amp = ringing
712
+ """
713
+ trace = np.asarray(trace)
714
+
715
+ if len(trace) < 32:
716
+ return None
717
+
718
+ # Detrend to remove DC offset
719
+ detrended = trace - np.mean(trace)
720
+
721
+ # Apply FFT to detect high-frequency oscillations
722
+ fft = np.fft.rfft(detrended)
723
+ freqs = np.fft.rfftfreq(len(trace), self._time_base)
724
+ power = np.abs(fft) ** 2
725
+
726
+ # Look for peaks in high-frequency range (above 1 MHz or 1% of sample rate)
727
+ min_freq = max(1e6, self.sample_rate * 0.01)
728
+ max_freq = self.sample_rate / 4.0 # Below Nyquist/2 for safety
729
+
730
+ freq_mask = (freqs > min_freq) & (freqs < max_freq)
731
+
732
+ if not np.any(freq_mask):
733
+ return None
734
+
735
+ # Find dominant frequency in ringing range
736
+ masked_power = power.copy()
737
+ masked_power[~freq_mask] = 0
738
+
739
+ if np.max(masked_power) < np.max(power) * 0.1:
740
+ # No significant high-frequency content
741
+ return None
742
+
743
+ peak_idx = np.argmax(masked_power)
744
+ ringing_freq = freqs[peak_idx]
745
+
746
+ # Estimate amplitude of ringing (very simplified)
747
+ # Band-pass filter around detected frequency
748
+ try:
749
+ # Design bandpass filter
750
+ bandwidth = ringing_freq * 0.2 # 20% bandwidth
751
+ low = max(ringing_freq - bandwidth, 1.0)
752
+ high = min(ringing_freq + bandwidth, self.sample_rate / 2.0 - 1.0)
753
+
754
+ if high > low:
755
+ sos = scipy_signal.butter(4, [low, high], "band", fs=self.sample_rate, output="sos")
756
+ filtered = scipy_signal.sosfilt(sos, detrended)
757
+ ringing_amp = np.std(filtered) * 2.0 # Peak-to-peak estimate
758
+ else:
759
+ ringing_amp = 0.0
760
+ except Exception:
761
+ # If filtering fails, use simple estimate
762
+ ringing_amp = np.std(detrended) * 0.5
763
+
764
+ # Only report if amplitude is significant
765
+ if ringing_amp < np.std(trace) * 0.1:
766
+ return None
767
+
768
+ return float(ringing_freq), float(ringing_amp)
769
+
770
+ def calculate_snr(self, trace: NDArray[np.float64]) -> float:
771
+ """Calculate signal-to-noise ratio.
772
+
773
+ Computes SNR by separating signal from noise in stable regions.
774
+
775
+ Args:
776
+ trace: Input signal trace.
777
+
778
+ Returns:
779
+ SNR in decibels.
780
+
781
+ Example:
782
+ >>> snr = analyzer.calculate_snr(trace)
783
+ """
784
+ trace = np.asarray(trace)
785
+
786
+ # Separate into high and low regions
787
+ threshold = (np.max(trace) + np.min(trace)) / 2.0
788
+ high_samples = trace[trace > threshold]
789
+ low_samples = trace[trace <= threshold]
790
+
791
+ if len(high_samples) == 0 or len(low_samples) == 0:
792
+ return 0.0
793
+
794
+ # Signal power: difference between high and low levels
795
+ signal_level = abs(np.mean(high_samples) - np.mean(low_samples))
796
+
797
+ # Noise power: standard deviation in stable regions
798
+ noise_high = np.std(high_samples)
799
+ noise_low = np.std(low_samples)
800
+ noise_level = (noise_high + noise_low) / 2.0
801
+
802
+ if noise_level < 1e-10:
803
+ return 100.0 # Very high SNR
804
+
805
+ # SNR in dB
806
+ snr = 20 * np.log10(signal_level / noise_level)
807
+
808
+ return float(snr)
809
+
810
+
811
+ # Convenience functions
812
+
813
+
814
+ def measure_noise_margins(trace: NDArray[np.float64], logic_family: str = "auto") -> NoiseMargins:
815
+ """Measure noise margins.
816
+
817
+ Convenience function for quick noise margin measurement.
818
+
819
+ Args:
820
+ trace: Input signal trace.
821
+ logic_family: Logic family ('TTL', 'CMOS', 'LVTTL', 'LVCMOS', 'auto').
822
+
823
+ Returns:
824
+ NoiseMargins object.
825
+
826
+ Example:
827
+ >>> margins = measure_noise_margins(trace, 'TTL')
828
+ """
829
+ # Use a default sample rate for convenience
830
+ sample_rate = 1e9 # 1 GHz default
831
+ analyzer = SignalQualityAnalyzer(sample_rate, logic_family)
832
+ logic_fam: Literal["ttl", "cmos", "lvttl", "lvcmos", "auto"]
833
+ logic_family_lower = logic_family.lower()
834
+ if logic_family_lower in ("ttl", "cmos", "lvttl", "lvcmos", "auto"):
835
+ logic_fam = logic_family_lower # type: ignore[assignment]
836
+ else:
837
+ logic_fam = "auto"
838
+ return analyzer.measure_noise_margins(trace, logic_fam)
839
+
840
+
841
+ def analyze_signal_integrity(
842
+ trace: NDArray[np.float64],
843
+ sample_rate: float,
844
+ clock_trace: NDArray[np.float64] | None = None,
845
+ ) -> SignalIntegrityReport:
846
+ """Complete signal integrity analysis.
847
+
848
+ Convenience function for complete signal integrity analysis.
849
+
850
+ Args:
851
+ trace: Input signal trace.
852
+ sample_rate: Sample rate in Hz.
853
+ clock_trace: Optional clock signal.
854
+
855
+ Returns:
856
+ SignalIntegrityReport with complete analysis.
857
+
858
+ Example:
859
+ >>> report = analyze_signal_integrity(trace, 100e6)
860
+ """
861
+ analyzer = SignalQualityAnalyzer(sample_rate, logic_family="auto")
862
+ result = analyzer.analyze(trace, clock_trace)
863
+ # In full mode (no v_il/v_ih/vdd), this always returns SignalIntegrityReport
864
+ assert isinstance(result, SignalIntegrityReport)
865
+ return result
866
+
867
+
868
+ __all__ = [
869
+ "LOGIC_THRESHOLDS",
870
+ "NoiseMargins",
871
+ "SignalIntegrityReport",
872
+ "SignalQualityAnalyzer",
873
+ "SimpleQualityMetrics",
874
+ "TransitionMetrics",
875
+ "analyze_signal_integrity",
876
+ "measure_noise_margins",
877
+ ]