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,769 @@
1
+ """Automatic signal characterization and type detection.
2
+
3
+ This module provides intelligent signal type detection, extracting
4
+ characteristics without requiring user expertise.
5
+
6
+
7
+ Example:
8
+ >>> from oscura.discovery import characterize_signal
9
+ >>> result = characterize_signal(trace)
10
+ >>> print(f"{result.signal_type}: {result.confidence:.2f}")
11
+ UART: 0.94
12
+
13
+ References:
14
+ IEEE 181-2011: Transitional Waveform Definitions
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from dataclasses import dataclass, field
20
+ from typing import TYPE_CHECKING, Any, Literal
21
+
22
+ import numpy as np
23
+
24
+ from oscura.analyzers.statistics.basic import basic_stats
25
+ from oscura.core.types import DigitalTrace, WaveformTrace
26
+
27
+ if TYPE_CHECKING:
28
+ from numpy.typing import NDArray
29
+
30
+ SignalType = Literal["digital", "analog", "pwm", "uart", "spi", "i2c", "unknown"]
31
+
32
+
33
+ @dataclass
34
+ class SignalCharacterization:
35
+ """Result of automatic signal characterization.
36
+
37
+ Contains detected signal type, confidence score, and extracted parameters.
38
+
39
+ Attributes:
40
+ signal_type: Detected signal type.
41
+ confidence: Confidence score (0.0-1.0).
42
+ voltage_low: Low voltage level in volts.
43
+ voltage_high: High voltage level in volts.
44
+ frequency_hz: Dominant frequency in Hz.
45
+ parameters: Additional signal-specific parameters.
46
+ quality_metrics: Signal quality measurements.
47
+ alternatives: Alternative signal type suggestions.
48
+
49
+ Example:
50
+ >>> result = characterize_signal(trace)
51
+ >>> if result.confidence >= 0.8:
52
+ ... print(f"High confidence: {result.signal_type}")
53
+ """
54
+
55
+ signal_type: SignalType
56
+ confidence: float
57
+ voltage_low: float
58
+ voltage_high: float
59
+ frequency_hz: float
60
+ parameters: dict[str, Any] = field(default_factory=dict)
61
+ quality_metrics: dict[str, float] = field(default_factory=dict)
62
+ alternatives: list[tuple[SignalType, float]] = field(default_factory=list)
63
+
64
+
65
+ def characterize_signal(
66
+ trace: WaveformTrace | DigitalTrace,
67
+ *,
68
+ confidence_threshold: float = 0.6,
69
+ include_alternatives: bool = False,
70
+ min_alternatives: int = 3,
71
+ ) -> SignalCharacterization:
72
+ """Automatically characterize signal type and properties.
73
+
74
+ Analyzes waveform to detect signal type (digital, analog, PWM, UART, SPI, I2C)
75
+ and extract key parameters without requiring manual configuration.
76
+
77
+ Args:
78
+ trace: Input waveform or digital trace.
79
+ confidence_threshold: Minimum confidence for primary detection (0.0-1.0).
80
+ include_alternatives: Whether to include alternative suggestions.
81
+ min_alternatives: Minimum number of alternatives when confidence is low.
82
+
83
+ Returns:
84
+ SignalCharacterization with detected type and parameters.
85
+
86
+ Raises:
87
+ ValueError: If trace is empty or invalid.
88
+
89
+ Example:
90
+ >>> result = characterize_signal(trace, confidence_threshold=0.8)
91
+ >>> print(f"Signal: {result.signal_type}")
92
+ >>> print(f"Confidence: {result.confidence:.2f}")
93
+ >>> print(f"Voltage: {result.voltage_low:.2f}V to {result.voltage_high:.2f}V")
94
+ Signal: UART
95
+ Confidence: 0.94
96
+ Voltage: 0.02V to 3.28V
97
+
98
+ References:
99
+ DISC-001: Automatic Signal Characterization
100
+ """
101
+ # Validate input
102
+ if len(trace) == 0:
103
+ raise ValueError("Cannot characterize empty trace")
104
+
105
+ # Get signal data
106
+ if isinstance(trace, WaveformTrace):
107
+ data = trace.data
108
+ sample_rate = trace.metadata.sample_rate
109
+ is_analog = True
110
+ else:
111
+ data = trace.data.astype(np.float64)
112
+ sample_rate = trace.metadata.sample_rate
113
+ is_analog = False
114
+
115
+ # Compute basic statistics
116
+ stats = basic_stats(data)
117
+
118
+ # Determine voltage levels using percentiles to be robust to noise
119
+ # Use 5th and 95th percentiles to ignore outliers from noise
120
+ voltage_low = float(np.percentile(data, 5))
121
+ voltage_high = float(np.percentile(data, 95))
122
+ voltage_swing = voltage_high - voltage_low
123
+
124
+ # Analyze signal characteristics
125
+ candidates: dict[SignalType, float] = {}
126
+
127
+ # Check for digital signal (bimodal distribution)
128
+ digital_confidence = _detect_digital(data, voltage_swing)
129
+ candidates["digital"] = digital_confidence
130
+
131
+ # Check for analog signal (continuous distribution)
132
+ analog_confidence = _detect_analog(data, voltage_swing, is_analog)
133
+ candidates["analog"] = analog_confidence
134
+
135
+ # Check for PWM (periodic square wave with varying duty cycle)
136
+ pwm_confidence = _detect_pwm(data, sample_rate, voltage_swing)
137
+ candidates["pwm"] = pwm_confidence
138
+
139
+ # Check for UART (asynchronous serial with start/stop bits)
140
+ uart_confidence = _detect_uart(data, sample_rate, voltage_swing)
141
+ candidates["uart"] = uart_confidence
142
+
143
+ # Check for SPI (synchronous with clock and data)
144
+ spi_confidence = _detect_spi(data, sample_rate, voltage_swing)
145
+ candidates["spi"] = spi_confidence
146
+
147
+ # Check for I2C (two-wire with specific patterns)
148
+ i2c_confidence = _detect_i2c(data, sample_rate, voltage_swing)
149
+ candidates["i2c"] = i2c_confidence
150
+
151
+ # Select best match
152
+ sorted_candidates = sorted(candidates.items(), key=lambda x: x[1], reverse=True)
153
+ best_type, best_confidence = sorted_candidates[0]
154
+
155
+ # If confidence is too low, mark as unknown
156
+ if best_confidence < 0.5:
157
+ best_type = "unknown"
158
+
159
+ # If analog won but digital score is meaningful, prefer digital/unknown
160
+ # This handles noisy digital signals that look analog-ish
161
+ if best_type == "analog":
162
+ # Check if any protocol detector had reasonable confidence
163
+ protocol_confidence = max(
164
+ candidates.get("uart", 0),
165
+ candidates.get("spi", 0),
166
+ candidates.get("pwm", 0),
167
+ )
168
+
169
+ # If digital or protocol detectors have some confidence, don't call it purely analog
170
+ if digital_confidence > 0.3 or protocol_confidence > 0.2:
171
+ # Signal has digital characteristics - don't call it analog
172
+ if protocol_confidence > 0.3:
173
+ best_type = "unknown" # Too noisy/ambiguous to classify as specific protocol
174
+ best_confidence = protocol_confidence
175
+ elif digital_confidence > 0.4:
176
+ best_type = "digital" # Generic digital signal
177
+ best_confidence = digital_confidence
178
+ else:
179
+ best_type = "unknown" # Too ambiguous
180
+ best_confidence = max(digital_confidence, protocol_confidence, analog_confidence)
181
+
182
+ # Estimate dominant frequency
183
+ frequency_hz = _estimate_frequency(data, sample_rate)
184
+
185
+ # Extract type-specific parameters
186
+ parameters = _extract_parameters(best_type, data, sample_rate, voltage_low, voltage_high)
187
+
188
+ # Calculate quality metrics with improved noise estimation
189
+ noise_level = _estimate_noise_level(data, voltage_low, voltage_high, digital_confidence)
190
+ quality_metrics = {
191
+ "snr_db": _estimate_snr(data, stats),
192
+ "jitter_ns": _estimate_jitter(data, sample_rate) * 1e9,
193
+ "noise_level": noise_level,
194
+ }
195
+
196
+ # Prepare alternatives
197
+ alternatives: list[tuple[SignalType, float]] = []
198
+ if include_alternatives or best_confidence < confidence_threshold:
199
+ # Include top alternatives (excluding the winner)
200
+ for sig_type, conf in sorted_candidates[1:]:
201
+ if len(alternatives) >= min_alternatives:
202
+ break
203
+ if conf >= 0.3: # Only include reasonable alternatives
204
+ alternatives.append((sig_type, conf))
205
+
206
+ return SignalCharacterization(
207
+ signal_type=best_type,
208
+ confidence=round(best_confidence, 2),
209
+ voltage_low=voltage_low,
210
+ voltage_high=voltage_high,
211
+ frequency_hz=frequency_hz,
212
+ parameters=parameters,
213
+ quality_metrics=quality_metrics,
214
+ alternatives=alternatives,
215
+ )
216
+
217
+
218
+ def _estimate_noise_level(
219
+ data: NDArray[np.floating[Any]],
220
+ voltage_low: float,
221
+ voltage_high: float,
222
+ digital_confidence: float,
223
+ ) -> float:
224
+ """Estimate noise level in signal.
225
+
226
+ For digital signals, measures deviation from ideal logic levels.
227
+ For analog signals, uses normalized std.
228
+
229
+ Args:
230
+ data: Signal data array.
231
+ voltage_low: Low voltage level.
232
+ voltage_high: High voltage level.
233
+ digital_confidence: Confidence that signal is digital.
234
+
235
+ Returns:
236
+ Noise level as fraction of voltage swing (0.0-1.0).
237
+ """
238
+ voltage_swing = voltage_high - voltage_low
239
+ if voltage_swing == 0:
240
+ return 0.0
241
+
242
+ # For digital signals, estimate noise from deviation around logic levels
243
+ if digital_confidence >= 0.5:
244
+ threshold = (voltage_high + voltage_low) / 2
245
+ low_samples = data[data < threshold]
246
+ high_samples = data[data >= threshold]
247
+
248
+ noise_estimates = []
249
+ if len(low_samples) > 0:
250
+ # Deviation from the low level
251
+ low_level = np.min(data)
252
+ low_noise = np.std(low_samples - low_level)
253
+ noise_estimates.append(low_noise)
254
+ if len(high_samples) > 0:
255
+ # Deviation from the high level
256
+ high_level = np.max(data)
257
+ high_noise = np.std(high_samples - high_level)
258
+ noise_estimates.append(high_noise)
259
+
260
+ if noise_estimates:
261
+ avg_noise = np.mean(noise_estimates)
262
+ return float(avg_noise / voltage_swing)
263
+
264
+ # For analog signals, use std as fraction of range
265
+ # But cap it at 0.5 to indicate high variability, not noise
266
+ std_noise = float(np.std(data) / voltage_swing)
267
+ return min(0.5, std_noise)
268
+
269
+
270
+ def _detect_digital(data: NDArray[np.floating[Any]], voltage_swing: float) -> float:
271
+ """Detect digital signal characteristics.
272
+
273
+ Args:
274
+ data: Signal data array.
275
+ voltage_swing: Peak-to-peak voltage swing.
276
+
277
+ Returns:
278
+ Confidence score (0.0-1.0).
279
+ """
280
+ if voltage_swing == 0:
281
+ return 0.0
282
+
283
+ # Check for bimodal distribution (two distinct levels)
284
+ hist, bin_edges = np.histogram(data, bins=50)
285
+
286
+ # Normalize histogram
287
+ hist = hist / np.sum(hist)
288
+
289
+ # Find peaks in histogram (should have 2 for digital)
290
+ peak_threshold = np.max(hist) * 0.3
291
+ peaks = np.where(hist > peak_threshold)[0]
292
+
293
+ if len(peaks) < 2:
294
+ return 0.3 # Low confidence
295
+
296
+ # Check if peaks are well separated
297
+ peak_separation = (bin_edges[peaks[-1]] - bin_edges[peaks[0]]) / voltage_swing
298
+
299
+ # Digital signals spend most time at rails
300
+ edge_bins = hist[:5].sum() + hist[-5:].sum()
301
+
302
+ # Combine factors
303
+ bimodal_score = min(1.0, len(peaks) / 2.0) # Closer to 2 peaks is better
304
+ separation_score = min(1.0, peak_separation)
305
+ rail_score = min(1.0, edge_bins * 2) # More time at rails is better
306
+
307
+ confidence = bimodal_score * 0.4 + separation_score * 0.3 + rail_score * 0.3
308
+ return min(0.95, confidence) # type: ignore[no-any-return]
309
+
310
+
311
+ def _detect_analog(data: NDArray[np.floating[Any]], voltage_swing: float, is_analog: bool) -> float:
312
+ """Detect analog signal characteristics.
313
+
314
+ Args:
315
+ data: Signal data array.
316
+ voltage_swing: Peak-to-peak voltage swing.
317
+ is_analog: Whether input is from analog trace.
318
+
319
+ Returns:
320
+ Confidence score (0.0-1.0).
321
+ """
322
+ if voltage_swing == 0:
323
+ return 0.0
324
+
325
+ # Check if signal has strong digital characteristics first
326
+ # If it does, this is NOT analog - reduce confidence significantly
327
+ digital_confidence = _detect_digital(data, voltage_swing)
328
+ if digital_confidence >= 0.6:
329
+ # Strong digital signal - very low analog confidence
330
+ return max(0.0, 0.4 - digital_confidence * 0.3)
331
+
332
+ # Analog signals have continuous distribution
333
+ hist, _ = np.histogram(data, bins=50)
334
+ hist = hist / np.sum(hist)
335
+
336
+ # Check for uniform or Gaussian-like distribution
337
+ uniform_score = 1.0 - np.std(hist)
338
+
339
+ # Check for smooth transitions (not many abrupt changes)
340
+ diff = np.diff(data)
341
+ smooth_score = 1.0 - min(1.0, np.mean(np.abs(diff)) / voltage_swing)
342
+
343
+ # Analog traces get boost
344
+ source_score = 1.0 if is_analog else 0.5 # Reduced from 0.7
345
+
346
+ confidence = uniform_score * 0.4 + smooth_score * 0.3 + source_score * 0.3
347
+
348
+ # Further reduce if there's any digital characteristics
349
+ if digital_confidence > 0.3:
350
+ confidence *= 1.0 - digital_confidence * 0.5
351
+
352
+ return min(0.9, confidence) # type: ignore[no-any-return]
353
+
354
+
355
+ def _detect_pwm(
356
+ data: NDArray[np.floating[Any]],
357
+ sample_rate: float,
358
+ voltage_swing: float,
359
+ ) -> float:
360
+ """Detect PWM signal characteristics.
361
+
362
+ Args:
363
+ data: Signal data array.
364
+ sample_rate: Sample rate in Hz.
365
+ voltage_swing: Peak-to-peak voltage swing.
366
+
367
+ Returns:
368
+ Confidence score (0.0-1.0).
369
+ """
370
+ if voltage_swing == 0 or len(data) < 100:
371
+ return 0.0
372
+
373
+ # PWM should have digital levels
374
+ digital_score = _detect_digital(data, voltage_swing)
375
+
376
+ if digital_score < 0.5:
377
+ return 0.0
378
+
379
+ # Threshold signal
380
+ threshold = (np.max(data) + np.min(data)) / 2
381
+ digital = data > threshold
382
+
383
+ # Find transitions
384
+ transitions = np.diff(digital.astype(int))
385
+ rising = np.where(transitions > 0)[0]
386
+ falling = np.where(transitions < 0)[0]
387
+
388
+ if len(rising) < 3 or len(falling) < 3:
389
+ return 0.0
390
+
391
+ # Check for periodic transitions
392
+ rising_periods = np.diff(rising)
393
+ period_std = np.std(rising_periods) if len(rising_periods) > 0 else 0
394
+ mean_period = np.mean(rising_periods) if len(rising_periods) > 0 else 1
395
+
396
+ periodicity_score = 1.0 - min(1.0, period_std / (mean_period + 1e-10))
397
+
398
+ # PWM should have varying duty cycle
399
+ duty_cycles = []
400
+ for i in range(min(len(rising), len(falling))):
401
+ if i < len(falling) and falling[i] > rising[i]:
402
+ duty = (falling[i] - rising[i]) / (mean_period + 1e-10)
403
+ duty_cycles.append(duty)
404
+
405
+ duty_variation = np.std(duty_cycles) if len(duty_cycles) > 1 else 0
406
+ variation_score = min(1.0, duty_variation * 5) # Some variation expected
407
+
408
+ confidence = digital_score * 0.3 + periodicity_score * 0.5 + variation_score * 0.2
409
+
410
+ # Boost if strong periodicity with variation (classic PWM signature)
411
+ if periodicity_score > 0.7 and variation_score > 0.3:
412
+ confidence = min(0.94, confidence * 1.1)
413
+
414
+ return min(0.94, confidence)
415
+
416
+
417
+ def _detect_uart(
418
+ data: NDArray[np.floating[Any]], sample_rate: float, voltage_swing: float
419
+ ) -> float:
420
+ """Detect UART signal characteristics.
421
+
422
+ Args:
423
+ data: Signal data array.
424
+ sample_rate: Sample rate in Hz.
425
+ voltage_swing: Peak-to-peak voltage swing.
426
+
427
+ Returns:
428
+ Confidence score (0.0-1.0).
429
+ """
430
+ if voltage_swing == 0 or len(data) < 200:
431
+ return 0.0
432
+
433
+ # UART should be digital
434
+ digital_score = _detect_digital(data, voltage_swing)
435
+ if digital_score < 0.7: # Strict threshold for UART
436
+ return 0.0
437
+
438
+ # Check for bimodal (two-level) distribution
439
+ # UART should have primarily two voltage levels, not continuous values
440
+ hist, _ = np.histogram(data, bins=50)
441
+ hist = hist / np.sum(hist)
442
+
443
+ # Count significant histogram bins (>5% of samples)
444
+ significant_bins = np.sum(hist > 0.05)
445
+
446
+ # UART should have at most 2-4 significant bins (low and high with some noise)
447
+ # Sine wave will have many bins
448
+ if significant_bins > 6:
449
+ return 0.0
450
+
451
+ # Threshold signal
452
+ threshold = (np.max(data) + np.min(data)) / 2
453
+ digital = data > threshold
454
+
455
+ # Find edges
456
+ transitions = np.diff(digital.astype(int))
457
+ edges = np.where(np.abs(transitions) > 0)[0]
458
+
459
+ if len(edges) < 10:
460
+ return 0.0
461
+
462
+ # UART has consistent bit timing
463
+ edge_intervals = np.diff(edges)
464
+
465
+ # Look for common baud rates
466
+ common_bauds = [9600, 19200, 38400, 57600, 115200]
467
+ baud_scores = []
468
+
469
+ for baud in common_bauds:
470
+ bit_period_samples = sample_rate / baud
471
+ # Count edges that align with this baud rate (stricter alignment)
472
+ aligned = np.sum(np.abs(edge_intervals % bit_period_samples) < bit_period_samples * 0.15)
473
+ baud_scores.append(aligned / len(edge_intervals))
474
+
475
+ timing_score = max(baud_scores) if baud_scores else 0.0
476
+
477
+ # UART requires strong timing alignment
478
+ if timing_score < 0.4:
479
+ return 0.0
480
+
481
+ # UART idles high typically
482
+ idle_score = np.mean(digital[-100:])
483
+
484
+ confidence = digital_score * 0.3 + timing_score * 0.6 + idle_score * 0.1
485
+
486
+ # Boost confidence if timing alignment is strong
487
+ if timing_score > 0.7:
488
+ confidence = min(0.96, confidence * 1.1)
489
+
490
+ return min(0.96, confidence) # type: ignore[no-any-return]
491
+
492
+
493
+ def _detect_spi(
494
+ data: NDArray[np.floating[Any]],
495
+ sample_rate: float,
496
+ voltage_swing: float,
497
+ ) -> float:
498
+ """Detect SPI signal characteristics.
499
+
500
+ Args:
501
+ data: Signal data array.
502
+ sample_rate: Sample rate in Hz.
503
+ voltage_swing: Peak-to-peak voltage swing.
504
+
505
+ Returns:
506
+ Confidence score (0.0-1.0).
507
+ """
508
+ if voltage_swing == 0 or len(data) < 200:
509
+ return 0.0
510
+
511
+ # SPI should be digital
512
+ digital_score = _detect_digital(data, voltage_swing)
513
+ if digital_score < 0.6:
514
+ return 0.0
515
+
516
+ # Threshold signal
517
+ threshold = (np.max(data) + np.min(data)) / 2
518
+ digital = data > threshold
519
+
520
+ # Find edges
521
+ transitions = np.diff(digital.astype(int))
522
+ edges = np.where(np.abs(transitions) > 0)[0]
523
+
524
+ if len(edges) < 20:
525
+ return 0.0
526
+
527
+ # SPI typically has bursts of regular clock transitions
528
+ edge_intervals = np.diff(edges)
529
+
530
+ # Check for consistent clock period
531
+ median_interval = np.median(edge_intervals)
532
+ interval_std = np.std(edge_intervals)
533
+ consistency_score = 1.0 - min(1.0, interval_std / (median_interval + 1e-10))
534
+
535
+ # SPI has many transitions (clock toggling)
536
+ transition_density = len(edges) / len(data)
537
+ density_score = min(1.0, transition_density * 20)
538
+
539
+ confidence = digital_score * 0.3 + consistency_score * 0.5 + density_score * 0.2
540
+
541
+ # Boost confidence if consistency is very high (strong clock signal)
542
+ if consistency_score > 0.8 and density_score > 0.5:
543
+ confidence = min(0.95, confidence * 1.15)
544
+
545
+ return min(0.95, confidence) # type: ignore[no-any-return]
546
+
547
+
548
+ def _detect_i2c(
549
+ data: NDArray[np.floating[Any]],
550
+ sample_rate: float,
551
+ voltage_swing: float,
552
+ ) -> float:
553
+ """Detect I2C signal characteristics.
554
+
555
+ Args:
556
+ data: Signal data array.
557
+ sample_rate: Sample rate in Hz.
558
+ voltage_swing: Peak-to-peak voltage swing.
559
+
560
+ Returns:
561
+ Confidence score (0.0-1.0).
562
+ """
563
+ # I2C detection requires both SDA and SCL, single channel is limited
564
+ # This is a placeholder that gives low confidence
565
+ digital_score = _detect_digital(data, voltage_swing)
566
+ return min(0.6, digital_score * 0.5)
567
+
568
+
569
+ def _estimate_frequency(data: NDArray[np.floating[Any]], sample_rate: float) -> float:
570
+ """Estimate dominant frequency in signal.
571
+
572
+ Args:
573
+ data: Signal data array.
574
+ sample_rate: Sample rate in Hz.
575
+
576
+ Returns:
577
+ Dominant frequency in Hz.
578
+ """
579
+ if len(data) < 10:
580
+ return 0.0
581
+
582
+ # Simple zero-crossing based frequency estimate
583
+ mean_val = np.mean(data)
584
+ crossings = np.where(np.diff(np.sign(data - mean_val)) != 0)[0]
585
+
586
+ if len(crossings) < 2:
587
+ return 0.0
588
+
589
+ # Average period between crossings (half periods)
590
+ avg_half_period = np.mean(np.diff(crossings))
591
+ period_samples = avg_half_period * 2
592
+
593
+ frequency = sample_rate / period_samples if period_samples > 0 else 0.0
594
+ return frequency
595
+
596
+
597
+ def _estimate_snr(data: NDArray[np.floating[Any]], stats: dict[str, float]) -> float:
598
+ """Estimate signal-to-noise ratio.
599
+
600
+ Args:
601
+ data: Signal data array.
602
+ stats: Basic statistics dictionary.
603
+
604
+ Returns:
605
+ Estimated SNR in dB.
606
+ """
607
+ signal_power = stats["mean"] ** 2
608
+ noise_power = stats["variance"]
609
+
610
+ if noise_power == 0:
611
+ return 100.0 # Very high SNR
612
+
613
+ snr = signal_power / noise_power
614
+ snr_db = 10 * np.log10(snr) if snr > 0 else 0.0
615
+
616
+ return max(0.0, min(100.0, snr_db))
617
+
618
+
619
+ def _estimate_jitter(data: NDArray[np.floating[Any]], sample_rate: float) -> float:
620
+ """Estimate timing jitter.
621
+
622
+ Args:
623
+ data: Signal data array.
624
+ sample_rate: Sample rate in Hz.
625
+
626
+ Returns:
627
+ Estimated jitter in seconds.
628
+ """
629
+ # Simple edge-to-edge jitter estimate
630
+ threshold = (np.max(data) + np.min(data)) / 2
631
+ digital = data > threshold
632
+ edges = np.where(np.diff(digital.astype(int)) != 0)[0]
633
+
634
+ if len(edges) < 3:
635
+ return 0.0
636
+
637
+ edge_intervals = np.diff(edges)
638
+ jitter_samples = np.std(edge_intervals)
639
+ jitter_seconds = jitter_samples / sample_rate
640
+
641
+ return jitter_seconds # type: ignore[no-any-return]
642
+
643
+
644
+ def _extract_parameters(
645
+ signal_type: SignalType,
646
+ data: NDArray[np.floating[Any]],
647
+ sample_rate: float,
648
+ voltage_low: float,
649
+ voltage_high: float,
650
+ ) -> dict[str, Any]:
651
+ """Extract signal-specific parameters.
652
+
653
+ Args:
654
+ signal_type: Detected signal type.
655
+ data: Signal data array.
656
+ sample_rate: Sample rate in Hz.
657
+ voltage_low: Low voltage level.
658
+ voltage_high: High voltage level.
659
+
660
+ Returns:
661
+ Dictionary of parameters specific to signal type.
662
+ """
663
+ params: dict[str, Any] = {}
664
+
665
+ if signal_type in ("digital", "uart", "spi", "i2c"):
666
+ # Add logic level parameters
667
+ logic_family = _guess_logic_family(voltage_low, voltage_high)
668
+ if logic_family != "Unknown":
669
+ params["logic_family"] = logic_family
670
+
671
+ if signal_type == "pwm":
672
+ # Calculate duty cycle
673
+ threshold = (voltage_high + voltage_low) / 2
674
+ digital = data > threshold
675
+ duty_cycle = np.mean(digital)
676
+ params["duty_cycle"] = round(duty_cycle, 3)
677
+
678
+ if signal_type == "uart":
679
+ # Estimate baud rate
680
+ params["estimated_baud"] = _estimate_baud_rate(data, sample_rate)
681
+
682
+ return params
683
+
684
+
685
+ def _guess_logic_family(voltage_low: float, voltage_high: float) -> str:
686
+ """Guess logic family from voltage levels.
687
+
688
+ Args:
689
+ voltage_low: Low voltage level in volts.
690
+ voltage_high: High voltage level in volts.
691
+
692
+ Returns:
693
+ Logic family name.
694
+ """
695
+ voltage_swing = voltage_high - voltage_low
696
+
697
+ # Match to closest standard voltage level
698
+ # This handles noise better than fixed ranges
699
+ standard_levels = [
700
+ (1.8, "1.8V LVCMOS"),
701
+ (3.3, "3.3V LVCMOS"),
702
+ (5.0, "5V TTL/CMOS"),
703
+ ]
704
+
705
+ # Find closest match
706
+ closest_diff = float("inf")
707
+ second_closest_diff = float("inf")
708
+ closest_family = "Unknown"
709
+ closest_level = 0.0
710
+
711
+ for level, family in standard_levels:
712
+ diff = abs(voltage_swing - level)
713
+ if diff < closest_diff:
714
+ second_closest_diff = closest_diff
715
+ closest_diff = diff
716
+ closest_family = family
717
+ closest_level = level
718
+ elif diff < second_closest_diff:
719
+ second_closest_diff = diff
720
+
721
+ # Only return a match if:
722
+ # 1. Closest match is within 50% tolerance
723
+ # 2. AND it's significantly closer than second-best (not ambiguous)
724
+ if closest_diff == float("inf") or closest_diff > closest_level * 0.5:
725
+ return "Unknown"
726
+
727
+ # Check if ambiguous (second closest is also pretty close)
728
+ # If second-best is within 20% more distance, it's too ambiguous
729
+ if second_closest_diff < closest_diff * 1.2:
730
+ return "Unknown" # Too ambiguous
731
+
732
+ return closest_family
733
+
734
+
735
+ def _estimate_baud_rate(data: NDArray[np.floating[Any]], sample_rate: float) -> int:
736
+ """Estimate UART baud rate.
737
+
738
+ Args:
739
+ data: Signal data array.
740
+ sample_rate: Sample rate in Hz.
741
+
742
+ Returns:
743
+ Estimated baud rate in bps.
744
+ """
745
+ # Find bit period from edge intervals
746
+ threshold = (np.max(data) + np.min(data)) / 2
747
+ digital = data > threshold
748
+ edges = np.where(np.diff(digital.astype(int)) != 0)[0]
749
+
750
+ if len(edges) < 10:
751
+ return 9600 # Default fallback
752
+
753
+ edge_intervals = np.diff(edges)
754
+ # Use median to be robust to outliers
755
+ median_interval = np.median(edge_intervals)
756
+ estimated_baud = int(sample_rate / median_interval)
757
+
758
+ # Snap to common baud rates
759
+ common_bauds = [9600, 19200, 38400, 57600, 115200, 230400, 460800, 921600]
760
+ closest_baud = min(common_bauds, key=lambda x: abs(x - estimated_baud))
761
+
762
+ return closest_baud
763
+
764
+
765
+ __all__ = [
766
+ "SignalCharacterization",
767
+ "SignalType",
768
+ "characterize_signal",
769
+ ]