oscura 0.0.1__py3-none-any.whl → 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (465) hide show
  1. oscura/__init__.py +813 -8
  2. oscura/__main__.py +392 -0
  3. oscura/analyzers/__init__.py +37 -0
  4. oscura/analyzers/digital/__init__.py +177 -0
  5. oscura/analyzers/digital/bus.py +691 -0
  6. oscura/analyzers/digital/clock.py +805 -0
  7. oscura/analyzers/digital/correlation.py +720 -0
  8. oscura/analyzers/digital/edges.py +632 -0
  9. oscura/analyzers/digital/extraction.py +413 -0
  10. oscura/analyzers/digital/quality.py +878 -0
  11. oscura/analyzers/digital/signal_quality.py +877 -0
  12. oscura/analyzers/digital/thresholds.py +708 -0
  13. oscura/analyzers/digital/timing.py +1104 -0
  14. oscura/analyzers/eye/__init__.py +46 -0
  15. oscura/analyzers/eye/diagram.py +434 -0
  16. oscura/analyzers/eye/metrics.py +555 -0
  17. oscura/analyzers/jitter/__init__.py +83 -0
  18. oscura/analyzers/jitter/ber.py +333 -0
  19. oscura/analyzers/jitter/decomposition.py +759 -0
  20. oscura/analyzers/jitter/measurements.py +413 -0
  21. oscura/analyzers/jitter/spectrum.py +220 -0
  22. oscura/analyzers/measurements.py +40 -0
  23. oscura/analyzers/packet/__init__.py +171 -0
  24. oscura/analyzers/packet/daq.py +1077 -0
  25. oscura/analyzers/packet/metrics.py +437 -0
  26. oscura/analyzers/packet/parser.py +327 -0
  27. oscura/analyzers/packet/payload.py +2156 -0
  28. oscura/analyzers/packet/payload_analysis.py +1312 -0
  29. oscura/analyzers/packet/payload_extraction.py +236 -0
  30. oscura/analyzers/packet/payload_patterns.py +670 -0
  31. oscura/analyzers/packet/stream.py +359 -0
  32. oscura/analyzers/patterns/__init__.py +266 -0
  33. oscura/analyzers/patterns/clustering.py +1036 -0
  34. oscura/analyzers/patterns/discovery.py +539 -0
  35. oscura/analyzers/patterns/learning.py +797 -0
  36. oscura/analyzers/patterns/matching.py +1091 -0
  37. oscura/analyzers/patterns/periodic.py +650 -0
  38. oscura/analyzers/patterns/sequences.py +767 -0
  39. oscura/analyzers/power/__init__.py +116 -0
  40. oscura/analyzers/power/ac_power.py +391 -0
  41. oscura/analyzers/power/basic.py +383 -0
  42. oscura/analyzers/power/conduction.py +314 -0
  43. oscura/analyzers/power/efficiency.py +297 -0
  44. oscura/analyzers/power/ripple.py +356 -0
  45. oscura/analyzers/power/soa.py +372 -0
  46. oscura/analyzers/power/switching.py +479 -0
  47. oscura/analyzers/protocol/__init__.py +150 -0
  48. oscura/analyzers/protocols/__init__.py +150 -0
  49. oscura/analyzers/protocols/base.py +500 -0
  50. oscura/analyzers/protocols/can.py +620 -0
  51. oscura/analyzers/protocols/can_fd.py +448 -0
  52. oscura/analyzers/protocols/flexray.py +405 -0
  53. oscura/analyzers/protocols/hdlc.py +399 -0
  54. oscura/analyzers/protocols/i2c.py +368 -0
  55. oscura/analyzers/protocols/i2s.py +296 -0
  56. oscura/analyzers/protocols/jtag.py +393 -0
  57. oscura/analyzers/protocols/lin.py +445 -0
  58. oscura/analyzers/protocols/manchester.py +333 -0
  59. oscura/analyzers/protocols/onewire.py +501 -0
  60. oscura/analyzers/protocols/spi.py +334 -0
  61. oscura/analyzers/protocols/swd.py +325 -0
  62. oscura/analyzers/protocols/uart.py +393 -0
  63. oscura/analyzers/protocols/usb.py +495 -0
  64. oscura/analyzers/signal_integrity/__init__.py +63 -0
  65. oscura/analyzers/signal_integrity/embedding.py +294 -0
  66. oscura/analyzers/signal_integrity/equalization.py +370 -0
  67. oscura/analyzers/signal_integrity/sparams.py +484 -0
  68. oscura/analyzers/spectral/__init__.py +53 -0
  69. oscura/analyzers/spectral/chunked.py +273 -0
  70. oscura/analyzers/spectral/chunked_fft.py +571 -0
  71. oscura/analyzers/spectral/chunked_wavelet.py +391 -0
  72. oscura/analyzers/spectral/fft.py +92 -0
  73. oscura/analyzers/statistical/__init__.py +250 -0
  74. oscura/analyzers/statistical/checksum.py +923 -0
  75. oscura/analyzers/statistical/chunked_corr.py +228 -0
  76. oscura/analyzers/statistical/classification.py +778 -0
  77. oscura/analyzers/statistical/entropy.py +1113 -0
  78. oscura/analyzers/statistical/ngrams.py +614 -0
  79. oscura/analyzers/statistics/__init__.py +119 -0
  80. oscura/analyzers/statistics/advanced.py +885 -0
  81. oscura/analyzers/statistics/basic.py +263 -0
  82. oscura/analyzers/statistics/correlation.py +630 -0
  83. oscura/analyzers/statistics/distribution.py +298 -0
  84. oscura/analyzers/statistics/outliers.py +463 -0
  85. oscura/analyzers/statistics/streaming.py +93 -0
  86. oscura/analyzers/statistics/trend.py +520 -0
  87. oscura/analyzers/validation.py +598 -0
  88. oscura/analyzers/waveform/__init__.py +36 -0
  89. oscura/analyzers/waveform/measurements.py +943 -0
  90. oscura/analyzers/waveform/measurements_with_uncertainty.py +371 -0
  91. oscura/analyzers/waveform/spectral.py +1689 -0
  92. oscura/analyzers/waveform/wavelets.py +298 -0
  93. oscura/api/__init__.py +62 -0
  94. oscura/api/dsl.py +538 -0
  95. oscura/api/fluent.py +571 -0
  96. oscura/api/operators.py +498 -0
  97. oscura/api/optimization.py +392 -0
  98. oscura/api/profiling.py +396 -0
  99. oscura/automotive/__init__.py +73 -0
  100. oscura/automotive/can/__init__.py +52 -0
  101. oscura/automotive/can/analysis.py +356 -0
  102. oscura/automotive/can/checksum.py +250 -0
  103. oscura/automotive/can/correlation.py +212 -0
  104. oscura/automotive/can/discovery.py +355 -0
  105. oscura/automotive/can/message_wrapper.py +375 -0
  106. oscura/automotive/can/models.py +385 -0
  107. oscura/automotive/can/patterns.py +381 -0
  108. oscura/automotive/can/session.py +452 -0
  109. oscura/automotive/can/state_machine.py +300 -0
  110. oscura/automotive/can/stimulus_response.py +461 -0
  111. oscura/automotive/dbc/__init__.py +15 -0
  112. oscura/automotive/dbc/generator.py +156 -0
  113. oscura/automotive/dbc/parser.py +146 -0
  114. oscura/automotive/dtc/__init__.py +30 -0
  115. oscura/automotive/dtc/database.py +3036 -0
  116. oscura/automotive/j1939/__init__.py +14 -0
  117. oscura/automotive/j1939/decoder.py +745 -0
  118. oscura/automotive/loaders/__init__.py +35 -0
  119. oscura/automotive/loaders/asc.py +98 -0
  120. oscura/automotive/loaders/blf.py +77 -0
  121. oscura/automotive/loaders/csv_can.py +136 -0
  122. oscura/automotive/loaders/dispatcher.py +136 -0
  123. oscura/automotive/loaders/mdf.py +331 -0
  124. oscura/automotive/loaders/pcap.py +132 -0
  125. oscura/automotive/obd/__init__.py +14 -0
  126. oscura/automotive/obd/decoder.py +707 -0
  127. oscura/automotive/uds/__init__.py +48 -0
  128. oscura/automotive/uds/decoder.py +265 -0
  129. oscura/automotive/uds/models.py +64 -0
  130. oscura/automotive/visualization.py +369 -0
  131. oscura/batch/__init__.py +55 -0
  132. oscura/batch/advanced.py +627 -0
  133. oscura/batch/aggregate.py +300 -0
  134. oscura/batch/analyze.py +139 -0
  135. oscura/batch/logging.py +487 -0
  136. oscura/batch/metrics.py +556 -0
  137. oscura/builders/__init__.py +41 -0
  138. oscura/builders/signal_builder.py +1131 -0
  139. oscura/cli/__init__.py +14 -0
  140. oscura/cli/batch.py +339 -0
  141. oscura/cli/characterize.py +273 -0
  142. oscura/cli/compare.py +775 -0
  143. oscura/cli/decode.py +551 -0
  144. oscura/cli/main.py +247 -0
  145. oscura/cli/shell.py +350 -0
  146. oscura/comparison/__init__.py +66 -0
  147. oscura/comparison/compare.py +397 -0
  148. oscura/comparison/golden.py +487 -0
  149. oscura/comparison/limits.py +391 -0
  150. oscura/comparison/mask.py +434 -0
  151. oscura/comparison/trace_diff.py +30 -0
  152. oscura/comparison/visualization.py +481 -0
  153. oscura/compliance/__init__.py +70 -0
  154. oscura/compliance/advanced.py +756 -0
  155. oscura/compliance/masks.py +363 -0
  156. oscura/compliance/reporting.py +483 -0
  157. oscura/compliance/testing.py +298 -0
  158. oscura/component/__init__.py +38 -0
  159. oscura/component/impedance.py +365 -0
  160. oscura/component/reactive.py +598 -0
  161. oscura/component/transmission_line.py +312 -0
  162. oscura/config/__init__.py +191 -0
  163. oscura/config/defaults.py +254 -0
  164. oscura/config/loader.py +348 -0
  165. oscura/config/memory.py +271 -0
  166. oscura/config/migration.py +458 -0
  167. oscura/config/pipeline.py +1077 -0
  168. oscura/config/preferences.py +530 -0
  169. oscura/config/protocol.py +875 -0
  170. oscura/config/schema.py +713 -0
  171. oscura/config/settings.py +420 -0
  172. oscura/config/thresholds.py +599 -0
  173. oscura/convenience.py +457 -0
  174. oscura/core/__init__.py +299 -0
  175. oscura/core/audit.py +457 -0
  176. oscura/core/backend_selector.py +405 -0
  177. oscura/core/cache.py +590 -0
  178. oscura/core/cancellation.py +439 -0
  179. oscura/core/confidence.py +225 -0
  180. oscura/core/config.py +506 -0
  181. oscura/core/correlation.py +216 -0
  182. oscura/core/cross_domain.py +422 -0
  183. oscura/core/debug.py +301 -0
  184. oscura/core/edge_cases.py +541 -0
  185. oscura/core/exceptions.py +535 -0
  186. oscura/core/gpu_backend.py +523 -0
  187. oscura/core/lazy.py +832 -0
  188. oscura/core/log_query.py +540 -0
  189. oscura/core/logging.py +931 -0
  190. oscura/core/logging_advanced.py +952 -0
  191. oscura/core/memoize.py +171 -0
  192. oscura/core/memory_check.py +274 -0
  193. oscura/core/memory_guard.py +290 -0
  194. oscura/core/memory_limits.py +336 -0
  195. oscura/core/memory_monitor.py +453 -0
  196. oscura/core/memory_progress.py +465 -0
  197. oscura/core/memory_warnings.py +315 -0
  198. oscura/core/numba_backend.py +362 -0
  199. oscura/core/performance.py +352 -0
  200. oscura/core/progress.py +524 -0
  201. oscura/core/provenance.py +358 -0
  202. oscura/core/results.py +331 -0
  203. oscura/core/types.py +504 -0
  204. oscura/core/uncertainty.py +383 -0
  205. oscura/discovery/__init__.py +52 -0
  206. oscura/discovery/anomaly_detector.py +672 -0
  207. oscura/discovery/auto_decoder.py +415 -0
  208. oscura/discovery/comparison.py +497 -0
  209. oscura/discovery/quality_validator.py +528 -0
  210. oscura/discovery/signal_detector.py +769 -0
  211. oscura/dsl/__init__.py +73 -0
  212. oscura/dsl/commands.py +246 -0
  213. oscura/dsl/interpreter.py +455 -0
  214. oscura/dsl/parser.py +689 -0
  215. oscura/dsl/repl.py +172 -0
  216. oscura/exceptions.py +59 -0
  217. oscura/exploratory/__init__.py +111 -0
  218. oscura/exploratory/error_recovery.py +642 -0
  219. oscura/exploratory/fuzzy.py +513 -0
  220. oscura/exploratory/fuzzy_advanced.py +786 -0
  221. oscura/exploratory/legacy.py +831 -0
  222. oscura/exploratory/parse.py +358 -0
  223. oscura/exploratory/recovery.py +275 -0
  224. oscura/exploratory/sync.py +382 -0
  225. oscura/exploratory/unknown.py +707 -0
  226. oscura/export/__init__.py +25 -0
  227. oscura/export/wireshark/README.md +265 -0
  228. oscura/export/wireshark/__init__.py +47 -0
  229. oscura/export/wireshark/generator.py +312 -0
  230. oscura/export/wireshark/lua_builder.py +159 -0
  231. oscura/export/wireshark/templates/dissector.lua.j2 +92 -0
  232. oscura/export/wireshark/type_mapping.py +165 -0
  233. oscura/export/wireshark/validator.py +105 -0
  234. oscura/exporters/__init__.py +94 -0
  235. oscura/exporters/csv.py +303 -0
  236. oscura/exporters/exporters.py +44 -0
  237. oscura/exporters/hdf5.py +219 -0
  238. oscura/exporters/html_export.py +701 -0
  239. oscura/exporters/json_export.py +291 -0
  240. oscura/exporters/markdown_export.py +367 -0
  241. oscura/exporters/matlab_export.py +354 -0
  242. oscura/exporters/npz_export.py +219 -0
  243. oscura/exporters/spice_export.py +210 -0
  244. oscura/extensibility/__init__.py +131 -0
  245. oscura/extensibility/docs.py +752 -0
  246. oscura/extensibility/extensions.py +1125 -0
  247. oscura/extensibility/logging.py +259 -0
  248. oscura/extensibility/measurements.py +485 -0
  249. oscura/extensibility/plugins.py +414 -0
  250. oscura/extensibility/registry.py +346 -0
  251. oscura/extensibility/templates.py +913 -0
  252. oscura/extensibility/validation.py +651 -0
  253. oscura/filtering/__init__.py +89 -0
  254. oscura/filtering/base.py +563 -0
  255. oscura/filtering/convenience.py +564 -0
  256. oscura/filtering/design.py +725 -0
  257. oscura/filtering/filters.py +32 -0
  258. oscura/filtering/introspection.py +605 -0
  259. oscura/guidance/__init__.py +24 -0
  260. oscura/guidance/recommender.py +429 -0
  261. oscura/guidance/wizard.py +518 -0
  262. oscura/inference/__init__.py +251 -0
  263. oscura/inference/active_learning/README.md +153 -0
  264. oscura/inference/active_learning/__init__.py +38 -0
  265. oscura/inference/active_learning/lstar.py +257 -0
  266. oscura/inference/active_learning/observation_table.py +230 -0
  267. oscura/inference/active_learning/oracle.py +78 -0
  268. oscura/inference/active_learning/teachers/__init__.py +15 -0
  269. oscura/inference/active_learning/teachers/simulator.py +192 -0
  270. oscura/inference/adaptive_tuning.py +453 -0
  271. oscura/inference/alignment.py +653 -0
  272. oscura/inference/bayesian.py +943 -0
  273. oscura/inference/binary.py +1016 -0
  274. oscura/inference/crc_reverse.py +711 -0
  275. oscura/inference/logic.py +288 -0
  276. oscura/inference/message_format.py +1305 -0
  277. oscura/inference/protocol.py +417 -0
  278. oscura/inference/protocol_dsl.py +1084 -0
  279. oscura/inference/protocol_library.py +1230 -0
  280. oscura/inference/sequences.py +809 -0
  281. oscura/inference/signal_intelligence.py +1509 -0
  282. oscura/inference/spectral.py +215 -0
  283. oscura/inference/state_machine.py +634 -0
  284. oscura/inference/stream.py +918 -0
  285. oscura/integrations/__init__.py +59 -0
  286. oscura/integrations/llm.py +1827 -0
  287. oscura/jupyter/__init__.py +32 -0
  288. oscura/jupyter/display.py +268 -0
  289. oscura/jupyter/magic.py +334 -0
  290. oscura/loaders/__init__.py +526 -0
  291. oscura/loaders/binary.py +69 -0
  292. oscura/loaders/configurable.py +1255 -0
  293. oscura/loaders/csv.py +26 -0
  294. oscura/loaders/csv_loader.py +473 -0
  295. oscura/loaders/hdf5.py +9 -0
  296. oscura/loaders/hdf5_loader.py +510 -0
  297. oscura/loaders/lazy.py +370 -0
  298. oscura/loaders/mmap_loader.py +583 -0
  299. oscura/loaders/numpy_loader.py +436 -0
  300. oscura/loaders/pcap.py +432 -0
  301. oscura/loaders/preprocessing.py +368 -0
  302. oscura/loaders/rigol.py +287 -0
  303. oscura/loaders/sigrok.py +321 -0
  304. oscura/loaders/tdms.py +367 -0
  305. oscura/loaders/tektronix.py +711 -0
  306. oscura/loaders/validation.py +584 -0
  307. oscura/loaders/vcd.py +464 -0
  308. oscura/loaders/wav.py +233 -0
  309. oscura/math/__init__.py +45 -0
  310. oscura/math/arithmetic.py +824 -0
  311. oscura/math/interpolation.py +413 -0
  312. oscura/onboarding/__init__.py +39 -0
  313. oscura/onboarding/help.py +498 -0
  314. oscura/onboarding/tutorials.py +405 -0
  315. oscura/onboarding/wizard.py +466 -0
  316. oscura/optimization/__init__.py +19 -0
  317. oscura/optimization/parallel.py +440 -0
  318. oscura/optimization/search.py +532 -0
  319. oscura/pipeline/__init__.py +43 -0
  320. oscura/pipeline/base.py +338 -0
  321. oscura/pipeline/composition.py +242 -0
  322. oscura/pipeline/parallel.py +448 -0
  323. oscura/pipeline/pipeline.py +375 -0
  324. oscura/pipeline/reverse_engineering.py +1119 -0
  325. oscura/plugins/__init__.py +122 -0
  326. oscura/plugins/base.py +272 -0
  327. oscura/plugins/cli.py +497 -0
  328. oscura/plugins/discovery.py +411 -0
  329. oscura/plugins/isolation.py +418 -0
  330. oscura/plugins/lifecycle.py +959 -0
  331. oscura/plugins/manager.py +493 -0
  332. oscura/plugins/registry.py +421 -0
  333. oscura/plugins/versioning.py +372 -0
  334. oscura/py.typed +0 -0
  335. oscura/quality/__init__.py +65 -0
  336. oscura/quality/ensemble.py +740 -0
  337. oscura/quality/explainer.py +338 -0
  338. oscura/quality/scoring.py +616 -0
  339. oscura/quality/warnings.py +456 -0
  340. oscura/reporting/__init__.py +248 -0
  341. oscura/reporting/advanced.py +1234 -0
  342. oscura/reporting/analyze.py +448 -0
  343. oscura/reporting/argument_preparer.py +596 -0
  344. oscura/reporting/auto_report.py +507 -0
  345. oscura/reporting/batch.py +615 -0
  346. oscura/reporting/chart_selection.py +223 -0
  347. oscura/reporting/comparison.py +330 -0
  348. oscura/reporting/config.py +615 -0
  349. oscura/reporting/content/__init__.py +39 -0
  350. oscura/reporting/content/executive.py +127 -0
  351. oscura/reporting/content/filtering.py +191 -0
  352. oscura/reporting/content/minimal.py +257 -0
  353. oscura/reporting/content/verbosity.py +162 -0
  354. oscura/reporting/core.py +508 -0
  355. oscura/reporting/core_formats/__init__.py +17 -0
  356. oscura/reporting/core_formats/multi_format.py +210 -0
  357. oscura/reporting/engine.py +836 -0
  358. oscura/reporting/export.py +366 -0
  359. oscura/reporting/formatting/__init__.py +129 -0
  360. oscura/reporting/formatting/emphasis.py +81 -0
  361. oscura/reporting/formatting/numbers.py +403 -0
  362. oscura/reporting/formatting/standards.py +55 -0
  363. oscura/reporting/formatting.py +466 -0
  364. oscura/reporting/html.py +578 -0
  365. oscura/reporting/index.py +590 -0
  366. oscura/reporting/multichannel.py +296 -0
  367. oscura/reporting/output.py +379 -0
  368. oscura/reporting/pdf.py +373 -0
  369. oscura/reporting/plots.py +731 -0
  370. oscura/reporting/pptx_export.py +360 -0
  371. oscura/reporting/renderers/__init__.py +11 -0
  372. oscura/reporting/renderers/pdf.py +94 -0
  373. oscura/reporting/sections.py +471 -0
  374. oscura/reporting/standards.py +680 -0
  375. oscura/reporting/summary_generator.py +368 -0
  376. oscura/reporting/tables.py +397 -0
  377. oscura/reporting/template_system.py +724 -0
  378. oscura/reporting/templates/__init__.py +15 -0
  379. oscura/reporting/templates/definition.py +205 -0
  380. oscura/reporting/templates/index.html +649 -0
  381. oscura/reporting/templates/index.md +173 -0
  382. oscura/schemas/__init__.py +158 -0
  383. oscura/schemas/bus_configuration.json +322 -0
  384. oscura/schemas/device_mapping.json +182 -0
  385. oscura/schemas/packet_format.json +418 -0
  386. oscura/schemas/protocol_definition.json +363 -0
  387. oscura/search/__init__.py +16 -0
  388. oscura/search/anomaly.py +292 -0
  389. oscura/search/context.py +149 -0
  390. oscura/search/pattern.py +160 -0
  391. oscura/session/__init__.py +34 -0
  392. oscura/session/annotations.py +289 -0
  393. oscura/session/history.py +313 -0
  394. oscura/session/session.py +445 -0
  395. oscura/streaming/__init__.py +43 -0
  396. oscura/streaming/chunked.py +611 -0
  397. oscura/streaming/progressive.py +393 -0
  398. oscura/streaming/realtime.py +622 -0
  399. oscura/testing/__init__.py +54 -0
  400. oscura/testing/synthetic.py +808 -0
  401. oscura/triggering/__init__.py +68 -0
  402. oscura/triggering/base.py +229 -0
  403. oscura/triggering/edge.py +353 -0
  404. oscura/triggering/pattern.py +344 -0
  405. oscura/triggering/pulse.py +581 -0
  406. oscura/triggering/window.py +453 -0
  407. oscura/ui/__init__.py +48 -0
  408. oscura/ui/formatters.py +526 -0
  409. oscura/ui/progressive_display.py +340 -0
  410. oscura/utils/__init__.py +99 -0
  411. oscura/utils/autodetect.py +338 -0
  412. oscura/utils/buffer.py +389 -0
  413. oscura/utils/lazy.py +407 -0
  414. oscura/utils/lazy_imports.py +147 -0
  415. oscura/utils/memory.py +836 -0
  416. oscura/utils/memory_advanced.py +1326 -0
  417. oscura/utils/memory_extensions.py +465 -0
  418. oscura/utils/progressive.py +352 -0
  419. oscura/utils/windowing.py +362 -0
  420. oscura/visualization/__init__.py +321 -0
  421. oscura/visualization/accessibility.py +526 -0
  422. oscura/visualization/annotations.py +374 -0
  423. oscura/visualization/axis_scaling.py +305 -0
  424. oscura/visualization/colors.py +453 -0
  425. oscura/visualization/digital.py +337 -0
  426. oscura/visualization/eye.py +420 -0
  427. oscura/visualization/histogram.py +281 -0
  428. oscura/visualization/interactive.py +858 -0
  429. oscura/visualization/jitter.py +702 -0
  430. oscura/visualization/keyboard.py +394 -0
  431. oscura/visualization/layout.py +365 -0
  432. oscura/visualization/optimization.py +1028 -0
  433. oscura/visualization/palettes.py +446 -0
  434. oscura/visualization/plot.py +92 -0
  435. oscura/visualization/power.py +290 -0
  436. oscura/visualization/power_extended.py +626 -0
  437. oscura/visualization/presets.py +467 -0
  438. oscura/visualization/protocols.py +932 -0
  439. oscura/visualization/render.py +207 -0
  440. oscura/visualization/rendering.py +444 -0
  441. oscura/visualization/reverse_engineering.py +791 -0
  442. oscura/visualization/signal_integrity.py +808 -0
  443. oscura/visualization/specialized.py +553 -0
  444. oscura/visualization/spectral.py +811 -0
  445. oscura/visualization/styles.py +381 -0
  446. oscura/visualization/thumbnails.py +311 -0
  447. oscura/visualization/time_axis.py +351 -0
  448. oscura/visualization/waveform.py +367 -0
  449. oscura/workflow/__init__.py +13 -0
  450. oscura/workflow/dag.py +377 -0
  451. oscura/workflows/__init__.py +58 -0
  452. oscura/workflows/compliance.py +280 -0
  453. oscura/workflows/digital.py +272 -0
  454. oscura/workflows/multi_trace.py +502 -0
  455. oscura/workflows/power.py +178 -0
  456. oscura/workflows/protocol.py +492 -0
  457. oscura/workflows/reverse_engineering.py +639 -0
  458. oscura/workflows/signal_integrity.py +227 -0
  459. oscura-0.1.0.dist-info/METADATA +300 -0
  460. oscura-0.1.0.dist-info/RECORD +463 -0
  461. oscura-0.1.0.dist-info/entry_points.txt +2 -0
  462. {oscura-0.0.1.dist-info → oscura-0.1.0.dist-info}/licenses/LICENSE +1 -1
  463. oscura-0.0.1.dist-info/METADATA +0 -63
  464. oscura-0.0.1.dist-info/RECORD +0 -5
  465. {oscura-0.0.1.dist-info → oscura-0.1.0.dist-info}/WHEEL +0 -0
@@ -0,0 +1,831 @@
1
+ """Legacy system signal analysis.
2
+
3
+ This module provides analysis tools for legacy RTL/TTL systems with
4
+ mixed logic families and multi-voltage domains.
5
+
6
+
7
+ Example:
8
+ >>> from oscura.exploratory.legacy import detect_logic_families_multi_channel
9
+ >>> families = detect_logic_families_multi_channel(channels)
10
+ >>> for ch, result in families.items():
11
+ ... print(f"Channel {ch}: {result['family']} (confidence={result['confidence']:.2f})")
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from dataclasses import dataclass
17
+ from typing import TYPE_CHECKING, Any, Literal
18
+
19
+ import numpy as np
20
+
21
+ if TYPE_CHECKING:
22
+ from numpy.typing import NDArray
23
+
24
+ from oscura.core.types import WaveformTrace
25
+
26
+ # Logic family specifications per IEEE/JEDEC standards
27
+ LOGIC_FAMILY_SPECS = {
28
+ "TTL": {
29
+ "vil_max": 0.8,
30
+ "vih_min": 2.0,
31
+ "vol_max": 0.4,
32
+ "voh_min": 2.4,
33
+ "vcc": 5.0,
34
+ },
35
+ "CMOS_5V": {
36
+ "vil_max": 1.5,
37
+ "vih_min": 3.5,
38
+ "vol_max": 0.5,
39
+ "voh_min": 4.5,
40
+ "vcc": 5.0,
41
+ },
42
+ "LVTTL": {
43
+ "vil_max": 0.8,
44
+ "vih_min": 2.0,
45
+ "vol_max": 0.4,
46
+ "voh_min": 2.4,
47
+ "vcc": 3.3,
48
+ },
49
+ "LVCMOS_3V3": {
50
+ "vil_max": 0.8,
51
+ "vih_min": 2.0,
52
+ "vol_max": 0.4,
53
+ "voh_min": 2.4,
54
+ "vcc": 3.3,
55
+ },
56
+ "LVCMOS_2V5": {
57
+ "vil_max": 0.7,
58
+ "vih_min": 1.7,
59
+ "vol_max": 0.4,
60
+ "voh_min": 2.0,
61
+ "vcc": 2.5,
62
+ },
63
+ "LVCMOS_1V8": {
64
+ "vil_max": 0.35 * 1.8,
65
+ "vih_min": 0.65 * 1.8,
66
+ "vol_max": 0.4,
67
+ "voh_min": 1.4,
68
+ "vcc": 1.8,
69
+ },
70
+ "ECL": {
71
+ "vil_max": -1.475,
72
+ "vih_min": -1.105,
73
+ "vol_max": -1.65,
74
+ "voh_min": -0.98,
75
+ "vcc": -5.2,
76
+ },
77
+ "PECL": {
78
+ "vil_max": 3.4,
79
+ "vih_min": 4.0,
80
+ "vol_max": 3.2,
81
+ "voh_min": 4.4,
82
+ "vcc": 5.0,
83
+ },
84
+ "OPEN_COLLECTOR": {
85
+ "vil_max": 0.8,
86
+ "vih_min": 2.0,
87
+ "vol_max": 0.4,
88
+ "voh_min": None, # Depends on pullup
89
+ "vcc": 5.0,
90
+ },
91
+ }
92
+
93
+
94
+ @dataclass
95
+ class LogicFamilyResult:
96
+ """Result of logic family detection.
97
+
98
+ Attributes:
99
+ family: Detected logic family name.
100
+ confidence: Confidence score (0.0 to 1.0).
101
+ v_low: Measured low voltage level.
102
+ v_high: Measured high voltage level.
103
+ alternatives: List of alternative candidates with confidence.
104
+ degradation_warning: Optional warning about signal degradation.
105
+ deviation_pct: Deviation from spec as percentage.
106
+ """
107
+
108
+ family: str
109
+ confidence: float
110
+ v_low: float
111
+ v_high: float
112
+ alternatives: list[tuple[str, float]]
113
+ degradation_warning: str | None = None
114
+ deviation_pct: float = 0.0
115
+
116
+
117
+ def detect_logic_families_multi_channel(
118
+ channels: list[WaveformTrace] | dict[int, WaveformTrace],
119
+ *,
120
+ confidence_thresholds: dict[str, float] | None = None,
121
+ warn_on_degradation: bool = True,
122
+ voltage_tolerance: float = 0.20,
123
+ min_edges_for_detection: int = 10,
124
+ ) -> dict[int, LogicFamilyResult]:
125
+ """Detect logic family for each channel independently.
126
+
127
+ Analyzes voltage distribution per channel and maps to logic family specs.
128
+
129
+ Args:
130
+ channels: List or dict of WaveformTrace objects.
131
+ confidence_thresholds: Thresholds for high/medium confidence.
132
+ Default: {'high': 0.9, 'medium': 0.7}
133
+ warn_on_degradation: If True, warn on degraded signals.
134
+ voltage_tolerance: Tolerance for spec matching (default 20%).
135
+ min_edges_for_detection: Minimum edges required per channel.
136
+
137
+ Returns:
138
+ Dictionary mapping channel ID to LogicFamilyResult.
139
+
140
+ Example:
141
+ >>> channels = [trace.get_channel(i) for i in range(8)]
142
+ >>> families = detect_logic_families_multi_channel(channels)
143
+ >>> for ch_id, result in families.items():
144
+ ... print(f"Channel {ch_id}: {result.family} (confidence={result.confidence:.2f})")
145
+
146
+ References:
147
+ LEGACY-001: Multi-Channel Logic Family Auto-Detection
148
+ IEEE 1164: Standard for Logic Families
149
+ JEDEC: Logic Family Specifications
150
+ """
151
+ if confidence_thresholds is None:
152
+ confidence_thresholds = {"high": 0.9, "medium": 0.7}
153
+
154
+ # Convert list to dict if needed
155
+ if isinstance(channels, list):
156
+ channels = dict(enumerate(channels))
157
+
158
+ results = {}
159
+
160
+ for ch_id, trace in channels.items():
161
+ data = trace.data
162
+
163
+ # Extract voltage percentiles
164
+ p10 = np.percentile(data, 10)
165
+ np.percentile(data, 50)
166
+ p90 = np.percentile(data, 90)
167
+
168
+ # Estimate low and high levels
169
+ v_low = p10
170
+ v_high = p90
171
+ v_high - v_low
172
+
173
+ # Count edges for confidence
174
+ threshold = (v_low + v_high) / 2
175
+ edges = np.sum(np.abs(np.diff(data > threshold)))
176
+
177
+ # Score each logic family
178
+ candidates = []
179
+
180
+ for family_name, specs in LOGIC_FAMILY_SPECS.items():
181
+ score = _score_logic_family(v_low, v_high, specs, voltage_tolerance) # type: ignore[arg-type]
182
+ if score > 0:
183
+ candidates.append((family_name, score))
184
+
185
+ # Sort by score descending
186
+ candidates.sort(key=lambda x: x[1], reverse=True)
187
+
188
+ if not candidates:
189
+ # No match found
190
+ result = LogicFamilyResult(
191
+ family="UNKNOWN",
192
+ confidence=0.0,
193
+ v_low=v_low,
194
+ v_high=v_high,
195
+ alternatives=[],
196
+ degradation_warning="No matching logic family found",
197
+ )
198
+ else:
199
+ best_family, best_score = candidates[0]
200
+ confidence = min(1.0, best_score)
201
+
202
+ # Reduce confidence if insufficient edges
203
+ if edges < min_edges_for_detection:
204
+ confidence *= 0.5
205
+
206
+ # Check for ambiguity (multiple families close in score)
207
+ alternatives = [
208
+ (name, score) for name, score in candidates[1:4] if best_score - score < 0.2
209
+ ]
210
+
211
+ # Check for degradation
212
+ degradation_warning = None
213
+ deviation_pct = 0.0
214
+
215
+ if warn_on_degradation:
216
+ specs = LOGIC_FAMILY_SPECS[best_family]
217
+ if specs["voh_min"] is not None: # type: ignore[index]
218
+ expected_voh = specs["voh_min"] # type: ignore[index]
219
+ if v_high < expected_voh:
220
+ deviation_pct = 100 * (expected_voh - v_high) / expected_voh
221
+ if deviation_pct > 10:
222
+ degradation_warning = (
223
+ f"V_high below spec (expected >= {expected_voh:.3f}V)"
224
+ )
225
+
226
+ result = LogicFamilyResult(
227
+ family=best_family,
228
+ confidence=confidence,
229
+ v_low=v_low,
230
+ v_high=v_high,
231
+ alternatives=alternatives,
232
+ degradation_warning=degradation_warning,
233
+ deviation_pct=deviation_pct,
234
+ )
235
+
236
+ results[ch_id] = result
237
+
238
+ return results
239
+
240
+
241
+ def _score_logic_family(
242
+ v_low: float,
243
+ v_high: float,
244
+ specs: dict[str, float | None],
245
+ tolerance: float,
246
+ ) -> float:
247
+ """Score how well voltage levels match a logic family.
248
+
249
+ Args:
250
+ v_low: Measured low voltage.
251
+ v_high: Measured high voltage.
252
+ specs: Logic family specifications.
253
+ tolerance: Tolerance for matching.
254
+
255
+ Returns:
256
+ Score from 0.0 to 1.0.
257
+ """
258
+ score = 1.0
259
+
260
+ # Check VOL (output low)
261
+ vol_max = specs["vol_max"]
262
+ if vol_max is not None:
263
+ if v_low <= vol_max:
264
+ score *= 1.0 # Exact match
265
+ elif v_low <= vol_max * (1 + tolerance):
266
+ score *= 0.85 # Within tolerance
267
+ else:
268
+ score *= 0.0 # Outside tolerance
269
+
270
+ # Check VOH (output high)
271
+ voh_min = specs["voh_min"]
272
+ if voh_min is not None:
273
+ if v_high >= voh_min:
274
+ score *= 1.0
275
+ elif v_high >= voh_min * (1 - tolerance):
276
+ score *= 0.85
277
+ else:
278
+ score *= 0.0
279
+
280
+ return score
281
+
282
+
283
+ @dataclass
284
+ class CrossCorrelationResult:
285
+ """Result of multi-reference cross-correlation.
286
+
287
+ Attributes:
288
+ correlation: Pearson correlation coefficient.
289
+ confidence: Overall confidence in result.
290
+ ref_offset_mv: Reference voltage offset in mV.
291
+ offset_uncertainty_mv: Uncertainty in offset measurement.
292
+ lag_samples: Time lag in samples.
293
+ lag_ns: Time lag in nanoseconds.
294
+ drift_detected: True if reference drift detected.
295
+ drift_rate: Drift rate in V/ms if detected.
296
+ normalized_signal1: Normalized first signal.
297
+ normalized_signal2: Normalized second signal.
298
+ """
299
+
300
+ correlation: float
301
+ confidence: float
302
+ ref_offset_mv: float
303
+ offset_uncertainty_mv: float
304
+ lag_samples: int
305
+ lag_ns: float
306
+ drift_detected: bool = False
307
+ drift_rate: float | None = None
308
+ normalized_signal1: NDArray[np.float64] | None = None
309
+ normalized_signal2: NDArray[np.float64] | None = None
310
+
311
+
312
+ def cross_correlate_multi_reference(
313
+ signal1: WaveformTrace,
314
+ signal2: WaveformTrace,
315
+ *,
316
+ detect_drift: bool = False,
317
+ drift_window_ms: float = 10.0,
318
+ ) -> CrossCorrelationResult:
319
+ """Correlate signals with different voltage references.
320
+
321
+ Normalizes signals to [0, 1] using per-signal logic levels before
322
+ computing correlation, enabling comparison of signals with different
323
+ ground references.
324
+
325
+ Args:
326
+ signal1: First signal trace.
327
+ signal2: Second signal trace.
328
+ detect_drift: If True, detect time-varying reference drift.
329
+ drift_window_ms: Window size for drift detection in ms.
330
+
331
+ Returns:
332
+ CrossCorrelationResult with correlation and offset information.
333
+
334
+ Example:
335
+ >>> ttl = trace.get_channel(0) # 5V TTL
336
+ >>> cmos = trace.get_channel(1) # 3.3V CMOS
337
+ >>> result = cross_correlate_multi_reference(ttl, cmos)
338
+ >>> print(f"Correlation: {result.correlation:.3f}")
339
+ >>> print(f"Reference offset: {result.ref_offset_mv:.1f} mV")
340
+
341
+ References:
342
+ LEGACY-002: Multi-Reference Voltage Signal Correlation
343
+ """
344
+ data1 = signal1.data
345
+ data2 = signal2.data
346
+
347
+ # Normalize each signal to [0, 1]
348
+ norm1 = _normalize_to_logic_levels(data1)
349
+ norm2 = _normalize_to_logic_levels(data2)
350
+
351
+ # Estimate DC offset between normalized signals
352
+ dc_offset = np.mean(norm1) - np.mean(norm2)
353
+
354
+ # Apply offset correction
355
+ norm2_corrected = norm2 + dc_offset
356
+
357
+ # Compute cross-correlation
358
+ min_len = min(len(norm1), len(norm2_corrected))
359
+ norm1 = norm1[:min_len]
360
+ norm2_corrected = norm2_corrected[:min_len]
361
+
362
+ correlation = np.corrcoef(norm1, norm2_corrected)[0, 1]
363
+
364
+ # Find lag using cross-correlation
365
+ xcorr = np.correlate(
366
+ norm1 - np.mean(norm1), norm2_corrected - np.mean(norm2_corrected), mode="full"
367
+ )
368
+ lag_samples = xcorr.argmax() - (len(norm1) - 1)
369
+
370
+ # Convert lag to nanoseconds
371
+ sample_rate = signal1.metadata.sample_rate
372
+ lag_ns = lag_samples / sample_rate * 1e9
373
+
374
+ # Estimate reference voltage offset
375
+ # Reference offset is how much signal2's ground differs from signal1's ground
376
+ v1_min = np.min(data1)
377
+ v2_min = np.min(data2)
378
+
379
+ # Reference offset is difference in ground levels (signal2 relative to signal1)
380
+ ref_offset_mv = (v2_min - v1_min) * 1000
381
+
382
+ # Confidence calculation
383
+ offset_uncertainty_mv = abs(ref_offset_mv) * 0.1 # 10% uncertainty
384
+ confidence = abs(correlation) * (1 - min(abs(ref_offset_mv) / 1000, 1.0))
385
+
386
+ # Drift detection
387
+ drift_detected = False
388
+ drift_rate = None
389
+
390
+ if detect_drift:
391
+ # Calculate offset in windows
392
+ window_samples = int(drift_window_ms * 1e-3 * sample_rate)
393
+ n_windows = min_len // window_samples
394
+
395
+ if n_windows >= 2:
396
+ offsets = []
397
+ for i in range(n_windows):
398
+ start = i * window_samples
399
+ end = start + window_samples
400
+ win_offset = np.mean(data1[start:end]) - np.mean(data2[start:end])
401
+ offsets.append(win_offset)
402
+
403
+ # Check for drift
404
+ offset_change = abs(offsets[-1] - offsets[0])
405
+ drift_rate_val = offset_change / (n_windows * drift_window_ms)
406
+
407
+ if drift_rate_val > 0.1: # V/ms threshold
408
+ drift_detected = True
409
+ drift_rate = drift_rate_val
410
+
411
+ return CrossCorrelationResult(
412
+ correlation=float(correlation),
413
+ confidence=float(confidence),
414
+ ref_offset_mv=float(ref_offset_mv),
415
+ offset_uncertainty_mv=float(offset_uncertainty_mv),
416
+ lag_samples=int(lag_samples),
417
+ lag_ns=float(lag_ns),
418
+ drift_detected=drift_detected,
419
+ drift_rate=drift_rate,
420
+ normalized_signal1=norm1,
421
+ normalized_signal2=norm2_corrected,
422
+ )
423
+
424
+
425
+ def _normalize_to_logic_levels(data: NDArray[np.float64]) -> NDArray[np.float64]:
426
+ """Normalize signal to [0, 1] based on logic levels.
427
+
428
+ Args:
429
+ data: Signal data.
430
+
431
+ Returns:
432
+ Normalized signal.
433
+ """
434
+ v_min = float(np.percentile(data, 5))
435
+ v_max = float(np.percentile(data, 95))
436
+ v_range = v_max - v_min
437
+
438
+ if v_range < 1e-6:
439
+ return np.zeros_like(data)
440
+
441
+ return (data - v_min) / v_range
442
+
443
+
444
+ @dataclass
445
+ class SignalQualityResult:
446
+ """Result of signal quality assessment.
447
+
448
+ Attributes:
449
+ status: 'OK', 'WARNING', or 'CRITICAL'.
450
+ violation_count: Number of spec violations.
451
+ total_samples: Total samples analyzed.
452
+ min_margin_mv: Minimum margin to spec in mV.
453
+ violations: List of violation details.
454
+ vil_violations: Count of VIL violations.
455
+ vih_violations: Count of VIH violations.
456
+ vol_violations: Count of VOL violations.
457
+ voh_violations: Count of VOH violations.
458
+ failure_diagnosis: Suggested failure mode.
459
+ time_to_failure_s: Estimated time to failure.
460
+ drift_rate_mv_per_s: Voltage drift rate.
461
+ """
462
+
463
+ status: Literal["OK", "WARNING", "CRITICAL"]
464
+ violation_count: int
465
+ total_samples: int
466
+ min_margin_mv: float
467
+ violations: list[dict[str, Any]]
468
+ vil_violations: int = 0
469
+ vih_violations: int = 0
470
+ vol_violations: int = 0
471
+ voh_violations: int = 0
472
+ vil_rate: float = 0.0
473
+ vih_rate: float = 0.0
474
+ vol_rate: float = 0.0
475
+ voh_rate: float = 0.0
476
+ failure_diagnosis: str | None = None
477
+ time_to_failure_s: float | None = None
478
+ drift_rate_mv_per_s: float | None = None
479
+
480
+
481
+ def assess_signal_quality(
482
+ signal: WaveformTrace,
483
+ logic_family: str,
484
+ *,
485
+ check_aging: bool = False,
486
+ time_window_s: float = 1.0,
487
+ ) -> SignalQualityResult:
488
+ """Assess signal quality against logic family specs.
489
+
490
+ Checks voltage compliance with specifications and detects degraded
491
+ signal levels that may indicate aging or failing components.
492
+
493
+ Args:
494
+ signal: Signal trace to assess.
495
+ logic_family: Logic family name (e.g., 'TTL', 'CMOS_5V').
496
+ check_aging: If True, analyze for aging/degradation.
497
+ time_window_s: Window for drift analysis.
498
+
499
+ Returns:
500
+ SignalQualityResult with compliance status and violations.
501
+
502
+ Example:
503
+ >>> result = assess_signal_quality(signal, logic_family='TTL')
504
+ >>> print(f"Status: {result.status}")
505
+ >>> print(f"Violations: {result.violation_count}")
506
+
507
+ References:
508
+ LEGACY-003: Logic Level Compliance Checking
509
+ JEDEC Standard No. 8C
510
+ """
511
+ if logic_family not in LOGIC_FAMILY_SPECS:
512
+ logic_family = "TTL" # Default fallback
513
+
514
+ specs = LOGIC_FAMILY_SPECS[logic_family]
515
+ data = signal.data
516
+ sample_rate = signal.metadata.sample_rate
517
+ n_samples = len(data)
518
+
519
+ # Threshold for high/low classification
520
+ threshold = (specs["vil_max"] + specs["vih_min"]) / 2 # type: ignore[index]
521
+
522
+ # Classify samples
523
+ is_high = data > threshold
524
+ is_low = ~is_high
525
+
526
+ # Count violations
527
+ high_samples = data[is_high]
528
+ low_samples = data[is_low]
529
+
530
+ voh_min = specs["voh_min"] # type: ignore[index]
531
+ vol_max = specs["vol_max"] # type: ignore[index]
532
+
533
+ voh_violations = 0
534
+ vol_violations = 0
535
+ violations = []
536
+
537
+ # Check VOH violations (high samples below spec)
538
+ if voh_min is not None and len(high_samples) > 0:
539
+ voh_mask = high_samples < voh_min
540
+ voh_violations = np.sum(voh_mask)
541
+ if voh_violations > 0:
542
+ violation_indices = np.where(is_high)[0][voh_mask]
543
+ for idx in violation_indices[:10]: # First 10 violations
544
+ violations.append(
545
+ {
546
+ "timestamp_us": idx / sample_rate * 1e6,
547
+ "type": "VOH",
548
+ "voltage": data[idx],
549
+ "spec_limit": voh_min,
550
+ }
551
+ )
552
+
553
+ # Check VOL violations (low samples above spec)
554
+ if vol_max is not None and len(low_samples) > 0:
555
+ vol_mask = low_samples > vol_max
556
+ vol_violations = np.sum(vol_mask)
557
+ if vol_violations > 0:
558
+ violation_indices = np.where(is_low)[0][vol_mask]
559
+ for idx in violation_indices[:10]:
560
+ violations.append(
561
+ {
562
+ "timestamp_us": idx / sample_rate * 1e6,
563
+ "type": "VOL",
564
+ "voltage": data[idx],
565
+ "spec_limit": vol_max,
566
+ }
567
+ )
568
+
569
+ total_violations = voh_violations + vol_violations
570
+
571
+ # Calculate margins
572
+ margins = []
573
+ if len(high_samples) > 0 and voh_min is not None:
574
+ margins.extend((high_samples - voh_min) * 1000) # Convert to mV
575
+ if len(low_samples) > 0 and vol_max is not None:
576
+ margins.extend((vol_max - low_samples) * 1000)
577
+
578
+ min_margin_mv = min(margins) if margins else 0.0
579
+
580
+ # Determine status
581
+ if min_margin_mv < 100:
582
+ status: Literal["OK", "WARNING", "CRITICAL"] = "CRITICAL"
583
+ elif min_margin_mv < 200:
584
+ status = "WARNING"
585
+ else:
586
+ status = "OK"
587
+
588
+ # Calculate rates
589
+ n_high = len(high_samples)
590
+ n_low = len(low_samples)
591
+ voh_rate = voh_violations / n_high if n_high > 0 else 0.0
592
+ vol_rate = vol_violations / n_low if n_low > 0 else 0.0
593
+
594
+ # Aging analysis
595
+ failure_diagnosis = None
596
+ time_to_failure_s = None
597
+ drift_rate_mv_per_s = None
598
+
599
+ if check_aging and n_samples > 1000:
600
+ # Calculate drift over time
601
+ window_samples = int(time_window_s * sample_rate)
602
+ n_windows = n_samples // window_samples
603
+
604
+ if n_windows >= 2:
605
+ window_means = [
606
+ np.mean(data[i * window_samples : (i + 1) * window_samples])
607
+ for i in range(n_windows)
608
+ ]
609
+
610
+ drift = window_means[-1] - window_means[0]
611
+ drift_rate_mv_per_s = drift * 1000 / (n_windows * time_window_s)
612
+
613
+ if abs(drift_rate_mv_per_s) > 0.1: # Significant drift
614
+ # Estimate time to failure
615
+ if voh_min is not None and drift_rate_mv_per_s < 0:
616
+ current_margin = np.mean(high_samples) - voh_min
617
+ if current_margin > 0:
618
+ time_to_failure_s = current_margin * 1000 / abs(drift_rate_mv_per_s)
619
+
620
+ # Diagnose failure mode
621
+ if voh_violations > vol_violations:
622
+ failure_diagnosis = "Degraded output driver (weak high)"
623
+ elif vol_violations > voh_violations:
624
+ failure_diagnosis = "Degraded output driver (weak low)"
625
+ else:
626
+ failure_diagnosis = "General signal degradation"
627
+
628
+ return SignalQualityResult(
629
+ status=status,
630
+ violation_count=total_violations,
631
+ total_samples=n_samples,
632
+ min_margin_mv=min_margin_mv,
633
+ violations=violations,
634
+ voh_violations=voh_violations,
635
+ vol_violations=vol_violations,
636
+ voh_rate=voh_rate,
637
+ vol_rate=vol_rate,
638
+ failure_diagnosis=failure_diagnosis,
639
+ time_to_failure_s=time_to_failure_s,
640
+ drift_rate_mv_per_s=drift_rate_mv_per_s,
641
+ )
642
+
643
+
644
+ @dataclass
645
+ class TestPointCharacterization:
646
+ """Characterization of a single test point.
647
+
648
+ Attributes:
649
+ channel_id: Channel identifier.
650
+ v_low: Low voltage level.
651
+ v_high: High voltage level.
652
+ v_swing: Voltage swing.
653
+ logic_family: Detected logic family.
654
+ confidence: Detection confidence.
655
+ is_digital: True if signal appears digital.
656
+ is_clock: True if signal appears to be a clock.
657
+ frequency: Estimated frequency if periodic.
658
+ """
659
+
660
+ channel_id: int
661
+ v_low: float
662
+ v_high: float
663
+ v_swing: float
664
+ logic_family: str
665
+ confidence: float
666
+ is_digital: bool
667
+ is_clock: bool
668
+ frequency: float | None
669
+
670
+
671
+ def characterize_test_points(
672
+ channels: list[WaveformTrace] | dict[int, WaveformTrace],
673
+ *,
674
+ sample_rate: float | None = None,
675
+ ) -> dict[int, TestPointCharacterization]:
676
+ """Batch characterize multiple test points.
677
+
678
+ Analyzes 8-16 test points to build a voltage level map of an
679
+ unknown board.
680
+
681
+ Args:
682
+ channels: List or dict of WaveformTrace objects.
683
+ sample_rate: Sample rate in Hz (uses metadata if not specified).
684
+
685
+ Returns:
686
+ Dictionary mapping channel ID to TestPointCharacterization.
687
+
688
+ Example:
689
+ >>> channels = [trace.get_channel(i) for i in range(8)]
690
+ >>> chars = characterize_test_points(channels)
691
+ >>> for ch_id, char in chars.items():
692
+ ... print(f"CH{ch_id}: {char.logic_family} ({char.v_low:.2f}V - {char.v_high:.2f}V)")
693
+
694
+ References:
695
+ LEGACY-004: Multi-Channel Voltage Characterization
696
+ """
697
+ if isinstance(channels, list):
698
+ channels = dict(enumerate(channels))
699
+
700
+ # First detect logic families
701
+ families = detect_logic_families_multi_channel(channels)
702
+
703
+ results = {}
704
+
705
+ for ch_id, trace in channels.items():
706
+ data = trace.data
707
+ sr = sample_rate or trace.metadata.sample_rate
708
+
709
+ # Voltage statistics
710
+ v_low = float(np.percentile(data, 10))
711
+ v_high = float(np.percentile(data, 90))
712
+ v_swing = v_high - v_low
713
+
714
+ # Get logic family result
715
+ family_result = families.get(
716
+ ch_id,
717
+ LogicFamilyResult(
718
+ family="UNKNOWN",
719
+ confidence=0.0,
720
+ v_low=v_low,
721
+ v_high=v_high,
722
+ alternatives=[],
723
+ ),
724
+ )
725
+
726
+ # Determine if digital (bimodal distribution)
727
+ is_digital = v_swing > 0.5 and _is_bimodal(data)
728
+
729
+ # Check for clock signal
730
+ is_clock = False
731
+ frequency = None
732
+
733
+ if is_digital and sr is not None:
734
+ # Check for periodic signal via FFT
735
+ from scipy import signal as sp_signal
736
+
737
+ f, psd = sp_signal.welch(data, fs=sr, nperseg=min(1024, len(data)))
738
+ peak_idx = np.argmax(psd[1:]) + 1 # Skip DC
739
+ if psd[peak_idx] > 10 * np.mean(psd): # Strong peak
740
+ frequency = f[peak_idx]
741
+ # Check duty cycle for clock
742
+ threshold = (v_low + v_high) / 2
743
+ high_ratio = np.mean(data > threshold)
744
+ if 0.4 <= high_ratio <= 0.6:
745
+ is_clock = True
746
+
747
+ results[ch_id] = TestPointCharacterization(
748
+ channel_id=ch_id,
749
+ v_low=v_low,
750
+ v_high=v_high,
751
+ v_swing=v_swing,
752
+ logic_family=family_result.family,
753
+ confidence=family_result.confidence,
754
+ is_digital=is_digital,
755
+ is_clock=is_clock,
756
+ frequency=frequency,
757
+ )
758
+
759
+ return results
760
+
761
+
762
+ def _is_bimodal(data: NDArray[np.float64], bins: int = 50) -> bool:
763
+ """Check if data has bimodal distribution.
764
+
765
+ Args:
766
+ data: Signal data.
767
+ bins: Number of histogram bins.
768
+
769
+ Returns:
770
+ True if distribution appears bimodal (digital signal).
771
+ False for analog signals (sine waves have many peaks).
772
+ """
773
+ hist, bin_edges = np.histogram(data, bins=bins)
774
+ centers = (bin_edges[:-1] + bin_edges[1:]) / 2
775
+
776
+ # Find peaks (including edge bins for perfect bimodal signals)
777
+ threshold = 0.1 * np.max(hist)
778
+ peaks = []
779
+
780
+ # Check first bin (only needs to be > right neighbor)
781
+ if len(hist) > 1 and hist[0] > hist[1] and hist[0] > threshold:
782
+ peaks.append((0, hist[0], centers[0]))
783
+
784
+ # Check middle bins (need to be > both neighbors)
785
+ for i in range(1, len(hist) - 1):
786
+ if hist[i] > hist[i - 1] and hist[i] > hist[i + 1] and hist[i] > threshold:
787
+ peaks.append((i, hist[i], centers[i]))
788
+
789
+ # Check last bin (only needs to be > left neighbor)
790
+ if len(hist) > 1 and hist[-1] > hist[-2] and hist[-1] > threshold:
791
+ peaks.append((len(hist) - 1, hist[-1], centers[-1]))
792
+
793
+ # Too many peaks suggests analog signal (e.g., sine wave)
794
+ if len(peaks) >= 4:
795
+ return False
796
+
797
+ # Bimodal if exactly 2-3 significant peaks that are well-separated
798
+ if len(peaks) == 2 or len(peaks) == 3:
799
+ peaks.sort(key=lambda x: x[1], reverse=True)
800
+
801
+ # Check if peaks are well-separated (digital signals have peaks at extremes)
802
+ v_min, v_max = np.min(data), np.max(data)
803
+ v_range = v_max - v_min
804
+ if v_range == 0:
805
+ return False
806
+
807
+ # Normalize peak positions
808
+ peak_positions = [(p[2] - v_min) / v_range for p in peaks[:2]]
809
+
810
+ # Digital signals have one peak < 0.4 and one peak > 0.6
811
+ has_low_peak = any(p < 0.4 for p in peak_positions)
812
+ has_high_peak = any(p > 0.6 for p in peak_positions)
813
+
814
+ # Second peak should be significant
815
+ if has_low_peak and has_high_peak and peaks[1][1] > 0.3 * peaks[0][1]:
816
+ return True
817
+
818
+ return False
819
+
820
+
821
+ __all__ = [
822
+ "LOGIC_FAMILY_SPECS",
823
+ "CrossCorrelationResult",
824
+ "LogicFamilyResult",
825
+ "SignalQualityResult",
826
+ "TestPointCharacterization",
827
+ "assess_signal_quality",
828
+ "characterize_test_points",
829
+ "cross_correlate_multi_reference",
830
+ "detect_logic_families_multi_channel",
831
+ ]