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,708 @@
1
+ """Time-varying and multi-level threshold support for digital signal analysis.
2
+
3
+ - RE-THR-001: Time-Varying Threshold Support
4
+ - RE-THR-002: Multi-Level Logic Support
5
+
6
+ This module provides adaptive thresholding for signals with varying DC offset
7
+ or amplitude, and support for multi-level logic standards beyond simple
8
+ high/low states.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from dataclasses import dataclass, field
14
+ from typing import TYPE_CHECKING, Literal
15
+
16
+ import numpy as np
17
+
18
+ if TYPE_CHECKING:
19
+ from numpy.typing import NDArray
20
+
21
+
22
+ @dataclass
23
+ class ThresholdConfig:
24
+ """Configuration for threshold detection.
25
+
26
+ Implements RE-THR-001, RE-THR-002: Threshold configuration.
27
+
28
+ Attributes:
29
+ threshold_type: Type of thresholding ('fixed', 'adaptive', 'multi_level').
30
+ fixed_threshold: Fixed threshold value (for 'fixed' type).
31
+ window_size: Window size for adaptive thresholding.
32
+ percentile: Percentile for adaptive threshold calculation.
33
+ levels: Voltage levels for multi-level logic.
34
+ hysteresis: Hysteresis margin to prevent oscillation.
35
+ """
36
+
37
+ threshold_type: Literal["fixed", "adaptive", "multi_level"] = "fixed"
38
+ fixed_threshold: float = 0.5
39
+ window_size: int = 1024
40
+ percentile: float = 50.0
41
+ levels: list[float] = field(default_factory=lambda: [0.0, 1.0])
42
+ hysteresis: float = 0.05
43
+
44
+
45
+ @dataclass
46
+ class AdaptiveThresholdResult:
47
+ """Result of adaptive threshold calculation.
48
+
49
+ Implements RE-THR-001: Time-varying threshold result.
50
+
51
+ Attributes:
52
+ thresholds: Array of threshold values at each sample.
53
+ binary_output: Digitized signal.
54
+ crossings: Indices of threshold crossings.
55
+ dc_offset: Estimated DC offset over time.
56
+ amplitude: Estimated signal amplitude over time.
57
+ """
58
+
59
+ thresholds: NDArray[np.float64]
60
+ binary_output: NDArray[np.uint8]
61
+ crossings: list[int]
62
+ dc_offset: NDArray[np.float64]
63
+ amplitude: NDArray[np.float64]
64
+
65
+
66
+ @dataclass
67
+ class MultiLevelResult:
68
+ """Result of multi-level logic detection.
69
+
70
+ Implements RE-THR-002: Multi-level detection result.
71
+
72
+ Attributes:
73
+ levels: Detected logic levels at each sample.
74
+ level_values: Voltage levels used.
75
+ transitions: List of (index, from_level, to_level) transitions.
76
+ level_histogram: Count of samples at each level.
77
+ eye_heights: Eye height for each transition.
78
+ """
79
+
80
+ levels: NDArray[np.int32]
81
+ level_values: list[float]
82
+ transitions: list[tuple[int, int, int]]
83
+ level_histogram: dict[int, int]
84
+ eye_heights: list[float]
85
+
86
+
87
+ class AdaptiveThresholder:
88
+ """Apply time-varying thresholds to signals.
89
+
90
+ Implements RE-THR-001: Time-Varying Threshold Support.
91
+
92
+ Tracks DC offset and amplitude changes to maintain accurate
93
+ thresholding despite signal drift.
94
+
95
+ Example:
96
+ >>> thresholder = AdaptiveThresholder(window_size=1000)
97
+ >>> result = thresholder.apply(analog_signal)
98
+ >>> digital = result.binary_output
99
+ """
100
+
101
+ def __init__(
102
+ self,
103
+ window_size: int = 1024,
104
+ percentile: float = 50.0,
105
+ method: Literal["median", "mean", "envelope", "otsu"] = "median",
106
+ hysteresis: float = 0.05,
107
+ ) -> None:
108
+ """Initialize adaptive thresholder.
109
+
110
+ Args:
111
+ window_size: Size of sliding window for adaptation.
112
+ percentile: Percentile for threshold calculation.
113
+ method: Thresholding method.
114
+ hysteresis: Hysteresis margin. This is used as an absolute value
115
+ when amplitude-relative calculation would be too small. For
116
+ signals with small amplitude variations (e.g., oscillating
117
+ around a threshold), this value is applied directly.
118
+ """
119
+ self.window_size = window_size
120
+ self.percentile = percentile
121
+ self.method = method
122
+ self.hysteresis = hysteresis
123
+
124
+ def apply(self, signal: NDArray[np.float64]) -> AdaptiveThresholdResult:
125
+ """Apply adaptive thresholding to signal.
126
+
127
+ Implements RE-THR-001: Adaptive threshold application.
128
+
129
+ Args:
130
+ signal: Input analog signal.
131
+
132
+ Returns:
133
+ AdaptiveThresholdResult with thresholds and digitized output.
134
+
135
+ Example:
136
+ >>> result = thresholder.apply(analog_waveform)
137
+ >>> plt.plot(result.binary_output)
138
+ """
139
+ n_samples = len(signal)
140
+
141
+ # Estimate DC offset and amplitude over time
142
+ dc_offset = np.zeros(n_samples)
143
+ amplitude = np.zeros(n_samples)
144
+ thresholds = np.zeros(n_samples)
145
+
146
+ half_window = self.window_size // 2
147
+
148
+ for i in range(n_samples):
149
+ # Window bounds
150
+ start = max(0, i - half_window)
151
+ end = min(n_samples, i + half_window)
152
+ window = signal[start:end]
153
+
154
+ if self.method == "median":
155
+ dc_offset[i] = np.median(window)
156
+ amplitude[i] = np.percentile(window, 95) - np.percentile(window, 5)
157
+ thresholds[i] = dc_offset[i]
158
+
159
+ elif self.method == "mean":
160
+ dc_offset[i] = np.mean(window)
161
+ amplitude[i] = np.std(window) * 4 # Approximate peak-to-peak
162
+ thresholds[i] = dc_offset[i]
163
+
164
+ elif self.method == "envelope":
165
+ # Use min/max envelope
166
+ high = np.max(window)
167
+ low = np.min(window)
168
+ dc_offset[i] = (high + low) / 2
169
+ amplitude[i] = high - low
170
+ thresholds[i] = dc_offset[i]
171
+
172
+ elif self.method == "otsu":
173
+ # Simplified Otsu's method
174
+ threshold = self._otsu_threshold(window)
175
+ thresholds[i] = threshold
176
+ dc_offset[i] = threshold
177
+ amplitude[i] = np.max(window) - np.min(window)
178
+
179
+ # Apply hysteresis
180
+ binary_output, crossings = self._apply_with_hysteresis(signal, thresholds, amplitude)
181
+
182
+ return AdaptiveThresholdResult(
183
+ thresholds=thresholds,
184
+ binary_output=binary_output,
185
+ crossings=crossings,
186
+ dc_offset=dc_offset,
187
+ amplitude=amplitude,
188
+ )
189
+
190
+ def calculate_threshold_profile(self, signal: NDArray[np.float64]) -> NDArray[np.float64]:
191
+ """Calculate threshold values without applying.
192
+
193
+ Implements RE-THR-001: Threshold profile calculation.
194
+
195
+ Args:
196
+ signal: Input signal.
197
+
198
+ Returns:
199
+ Array of threshold values.
200
+ """
201
+ result = self.apply(signal)
202
+ return result.thresholds
203
+
204
+ def _apply_with_hysteresis(
205
+ self,
206
+ signal: NDArray[np.float64],
207
+ thresholds: NDArray[np.float64],
208
+ amplitude: NDArray[np.float64],
209
+ ) -> tuple[NDArray[np.uint8], list[int]]:
210
+ """Apply thresholding with hysteresis.
211
+
212
+ The hysteresis prevents rapid oscillation when the signal hovers near
213
+ the threshold. The margin is calculated as:
214
+ - If amplitude is significant: hyst_margin = amplitude * hysteresis
215
+ - If amplitude is small: hyst_margin = hysteresis (used as absolute value)
216
+
217
+ This ensures hysteresis remains effective even for signals with very
218
+ small amplitude variations.
219
+
220
+ Args:
221
+ signal: Input signal.
222
+ thresholds: Threshold values.
223
+ amplitude: Signal amplitude at each point.
224
+
225
+ Returns:
226
+ Tuple of (binary_output, crossings).
227
+ """
228
+ n_samples = len(signal)
229
+ binary = np.zeros(n_samples, dtype=np.uint8)
230
+ crossings = []
231
+
232
+ # Initial state
233
+ current_state = 1 if signal[0] > thresholds[0] else 0
234
+ binary[0] = current_state
235
+
236
+ for i in range(1, n_samples):
237
+ threshold = thresholds[i]
238
+ amp = amplitude[i]
239
+
240
+ # Calculate hysteresis margin:
241
+ # - Use amplitude-relative margin for signals with significant amplitude
242
+ # - Use absolute hysteresis value when amplitude is small
243
+ # This prevents oscillation for signals hovering around the threshold
244
+ amplitude_relative_margin = amp * self.hysteresis
245
+ absolute_margin = self.hysteresis
246
+
247
+ # Use the larger of the two to ensure effective hysteresis
248
+ # When amplitude is large (e.g., > 1.0), amplitude-relative dominates
249
+ # When amplitude is small (e.g., 0.02), absolute hysteresis dominates
250
+ hyst_margin = max(amplitude_relative_margin, absolute_margin)
251
+
252
+ if current_state == 0:
253
+ # Currently low, need signal above threshold + hysteresis to go high
254
+ if signal[i] > threshold + hyst_margin:
255
+ current_state = 1
256
+ crossings.append(i)
257
+ else:
258
+ # Currently high, need signal below threshold - hysteresis to go low
259
+ if signal[i] < threshold - hyst_margin:
260
+ current_state = 0
261
+ crossings.append(i)
262
+
263
+ binary[i] = current_state
264
+
265
+ return binary, crossings
266
+
267
+ def _otsu_threshold(self, data: NDArray[np.float64]) -> float:
268
+ """Calculate Otsu's threshold.
269
+
270
+ Args:
271
+ data: Data window.
272
+
273
+ Returns:
274
+ Optimal threshold value.
275
+ """
276
+ # Simplified Otsu's method
277
+ hist, bin_edges = np.histogram(data, bins=50)
278
+ bin_centers = (bin_edges[:-1] + bin_edges[1:]) / 2
279
+
280
+ total = hist.sum()
281
+ if total == 0:
282
+ return float(np.mean(data))
283
+
284
+ current_max = 0
285
+ threshold = bin_centers[0]
286
+
287
+ sum_total = np.sum(bin_centers * hist)
288
+ sum_background = 0
289
+ weight_background = 0
290
+
291
+ for i in range(len(hist)):
292
+ weight_background += hist[i]
293
+ if weight_background == 0:
294
+ continue
295
+
296
+ weight_foreground = total - weight_background
297
+ if weight_foreground == 0:
298
+ break
299
+
300
+ sum_background += bin_centers[i] * hist[i]
301
+
302
+ mean_background = sum_background / weight_background
303
+ mean_foreground = (sum_total - sum_background) / weight_foreground
304
+
305
+ variance_between = (
306
+ weight_background * weight_foreground * (mean_background - mean_foreground) ** 2
307
+ )
308
+
309
+ if variance_between > current_max:
310
+ current_max = variance_between
311
+ threshold = bin_centers[i]
312
+
313
+ return float(threshold)
314
+
315
+
316
+ class MultiLevelDetector:
317
+ """Detect multi-level logic signals.
318
+
319
+ Implements RE-THR-002: Multi-Level Logic Support.
320
+
321
+ Supports PAM-2, PAM-4, PAM-8, and custom multi-level signaling
322
+ where signals encode multiple bits per symbol.
323
+
324
+ Example:
325
+ >>> detector = MultiLevelDetector(levels=4) # PAM-4
326
+ >>> result = detector.detect(signal)
327
+ >>> symbols = result.levels
328
+ """
329
+
330
+ def __init__(
331
+ self,
332
+ levels: int | list[float] = 2,
333
+ auto_detect_levels: bool = True,
334
+ hysteresis: float = 0.1,
335
+ ) -> None:
336
+ """Initialize multi-level detector.
337
+
338
+ Args:
339
+ levels: Number of levels or explicit voltage levels.
340
+ auto_detect_levels: Automatically detect level voltages.
341
+ hysteresis: Hysteresis fraction between levels.
342
+ """
343
+ if isinstance(levels, int):
344
+ self.n_levels = levels
345
+ self.level_values = None
346
+ else:
347
+ self.n_levels = len(levels)
348
+ self.level_values = sorted(levels)
349
+
350
+ self.auto_detect_levels = auto_detect_levels
351
+ self.hysteresis = hysteresis
352
+
353
+ def detect(self, signal: NDArray[np.float64]) -> MultiLevelResult:
354
+ """Detect multi-level logic in signal.
355
+
356
+ Implements RE-THR-002: Multi-level detection workflow.
357
+
358
+ Args:
359
+ signal: Input analog signal.
360
+
361
+ Returns:
362
+ MultiLevelResult with detected levels.
363
+
364
+ Example:
365
+ >>> result = detector.detect(pam4_signal)
366
+ >>> print(f"Detected {len(result.level_values)} levels")
367
+ """
368
+ # Auto-detect level values if needed
369
+ if self.level_values is None or self.auto_detect_levels:
370
+ level_values = self._detect_levels(signal)
371
+ else:
372
+ level_values = self.level_values
373
+
374
+ # Calculate decision thresholds between levels
375
+ thresholds = [
376
+ (level_values[i] + level_values[i + 1]) / 2 for i in range(len(level_values) - 1)
377
+ ]
378
+
379
+ # Apply hysteresis-aware level detection
380
+ levels, transitions = self._detect_with_hysteresis(signal, level_values, thresholds)
381
+
382
+ # Calculate level histogram
383
+ level_histogram = {}
384
+ for level in range(len(level_values)):
385
+ level_histogram[level] = int(np.sum(levels == level))
386
+
387
+ # Calculate eye heights
388
+ eye_heights = self._calculate_eye_heights(signal, level_values)
389
+
390
+ return MultiLevelResult(
391
+ levels=levels,
392
+ level_values=level_values,
393
+ transitions=transitions,
394
+ level_histogram=level_histogram,
395
+ eye_heights=eye_heights,
396
+ )
397
+
398
+ def detect_levels_from_histogram(
399
+ self, signal: NDArray[np.float64], n_levels: int | None = None
400
+ ) -> list[float]:
401
+ """Detect logic levels from signal histogram.
402
+
403
+ Implements RE-THR-002: Level detection.
404
+
405
+ Args:
406
+ signal: Input signal.
407
+ n_levels: Expected number of levels (auto-detect if None).
408
+
409
+ Returns:
410
+ List of detected voltage levels.
411
+ """
412
+ if n_levels is None:
413
+ n_levels = self.n_levels
414
+
415
+ return self._detect_levels(signal, n_levels)
416
+
417
+ def calculate_eye_diagram(
418
+ self,
419
+ signal: NDArray[np.float64],
420
+ samples_per_symbol: int,
421
+ n_symbols: int = 100,
422
+ ) -> NDArray[np.float64]:
423
+ """Calculate eye diagram data for multi-level signal.
424
+
425
+ Implements RE-THR-002: Eye diagram support.
426
+
427
+ Args:
428
+ signal: Input signal.
429
+ samples_per_symbol: Samples per symbol period.
430
+ n_symbols: Number of symbols to overlay.
431
+
432
+ Returns:
433
+ 2D array of overlaid symbol waveforms.
434
+ """
435
+ n_available = len(signal) // samples_per_symbol
436
+ n_symbols = min(n_symbols, n_available)
437
+
438
+ # Create 2D array with overlaid symbols
439
+ eye_data = np.zeros((n_symbols, samples_per_symbol * 2))
440
+
441
+ for i in range(n_symbols):
442
+ start = i * samples_per_symbol
443
+ end = start + samples_per_symbol * 2
444
+
445
+ if end <= len(signal):
446
+ eye_data[i] = signal[start:end]
447
+
448
+ return eye_data
449
+
450
+ def _detect_levels(
451
+ self, signal: NDArray[np.float64], n_levels: int | None = None
452
+ ) -> list[float]:
453
+ """Detect voltage levels using clustering.
454
+
455
+ Args:
456
+ signal: Input signal.
457
+ n_levels: Expected number of levels.
458
+
459
+ Returns:
460
+ List of level voltage values.
461
+ """
462
+ if n_levels is None:
463
+ n_levels = self.n_levels
464
+
465
+ # Use histogram-based clustering
466
+ hist, bin_edges = np.histogram(signal, bins=100)
467
+ bin_centers = (bin_edges[:-1] + bin_edges[1:]) / 2
468
+
469
+ # Find peaks in histogram
470
+ peaks = []
471
+ for i in range(1, len(hist) - 1):
472
+ if hist[i] > hist[i - 1] and hist[i] > hist[i + 1]:
473
+ peaks.append((hist[i], bin_centers[i]))
474
+
475
+ # Sort by frequency and take top n_levels
476
+ peaks.sort(reverse=True)
477
+ level_values = sorted([p[1] for p in peaks[:n_levels]])
478
+
479
+ # If not enough peaks found, use evenly spaced levels
480
+ if len(level_values) < n_levels:
481
+ min_val = np.min(signal)
482
+ max_val = np.max(signal)
483
+ level_values = list(np.linspace(min_val, max_val, n_levels))
484
+
485
+ return level_values
486
+
487
+ def _detect_with_hysteresis(
488
+ self,
489
+ signal: NDArray[np.float64],
490
+ level_values: list[float],
491
+ thresholds: list[float],
492
+ ) -> tuple[NDArray[np.int32], list[tuple[int, int, int]]]:
493
+ """Detect levels with hysteresis.
494
+
495
+ Args:
496
+ signal: Input signal.
497
+ level_values: Voltage levels.
498
+ thresholds: Decision thresholds.
499
+
500
+ Returns:
501
+ Tuple of (level_array, transitions).
502
+ """
503
+ n_samples = len(signal)
504
+ levels = np.zeros(n_samples, dtype=np.int32)
505
+ transitions = []
506
+
507
+ # Calculate hysteresis margins
508
+ margins = []
509
+ for i in range(len(level_values) - 1):
510
+ margin = (level_values[i + 1] - level_values[i]) * self.hysteresis
511
+ margins.append(margin)
512
+
513
+ # Initial level
514
+ current_level = self._find_closest_level(signal[0], level_values)
515
+ levels[0] = current_level
516
+
517
+ for i in range(1, n_samples):
518
+ new_level = current_level
519
+
520
+ # Check for transitions
521
+ if current_level < len(level_values) - 1:
522
+ # Can go up
523
+ upper_threshold = thresholds[current_level] + margins[current_level]
524
+ if signal[i] > upper_threshold:
525
+ new_level = current_level + 1
526
+
527
+ if current_level > 0:
528
+ # Can go down
529
+ lower_threshold = thresholds[current_level - 1] - margins[current_level - 1]
530
+ if signal[i] < lower_threshold:
531
+ new_level = current_level - 1
532
+
533
+ if new_level != current_level:
534
+ transitions.append((i, current_level, new_level))
535
+ current_level = new_level
536
+
537
+ levels[i] = current_level
538
+
539
+ return levels, transitions
540
+
541
+ def _find_closest_level(self, value: float, level_values: list[float]) -> int:
542
+ """Find closest level to value.
543
+
544
+ Args:
545
+ value: Sample value.
546
+ level_values: Level voltages.
547
+
548
+ Returns:
549
+ Level index.
550
+ """
551
+ distances = [abs(value - lv) for lv in level_values]
552
+ return int(np.argmin(distances))
553
+
554
+ def _calculate_eye_heights(
555
+ self, signal: NDArray[np.float64], level_values: list[float]
556
+ ) -> list[float]:
557
+ """Calculate eye heights between levels.
558
+
559
+ Args:
560
+ signal: Input signal.
561
+ level_values: Level voltages.
562
+
563
+ Returns:
564
+ List of eye heights for each level transition.
565
+ """
566
+ eye_heights = []
567
+
568
+ for i in range(len(level_values) - 1):
569
+ lower = level_values[i]
570
+ upper = level_values[i + 1]
571
+
572
+ # Find samples near each level
573
+ lower_samples = signal[np.abs(signal - lower) < (upper - lower) * 0.2]
574
+ upper_samples = signal[np.abs(signal - upper) < (upper - lower) * 0.2]
575
+
576
+ if len(lower_samples) > 0 and len(upper_samples) > 0:
577
+ # Eye height is gap between worst cases
578
+ worst_low = np.max(lower_samples)
579
+ worst_high = np.min(upper_samples)
580
+ eye_height = worst_high - worst_low
581
+ else:
582
+ eye_height = upper - lower
583
+
584
+ eye_heights.append(max(0, eye_height))
585
+
586
+ return eye_heights
587
+
588
+
589
+ # =============================================================================
590
+ # Convenience functions
591
+ # =============================================================================
592
+
593
+
594
+ def apply_adaptive_threshold(
595
+ signal: NDArray[np.float64],
596
+ window_size: int = 1024,
597
+ method: Literal["median", "mean", "envelope", "otsu"] = "median",
598
+ hysteresis: float = 0.05,
599
+ ) -> AdaptiveThresholdResult:
600
+ """Apply adaptive thresholding to a signal.
601
+
602
+ Implements RE-THR-001: Time-Varying Threshold Support.
603
+
604
+ Args:
605
+ signal: Input analog signal.
606
+ window_size: Adaptive window size.
607
+ method: Thresholding method.
608
+ hysteresis: Hysteresis margin.
609
+
610
+ Returns:
611
+ AdaptiveThresholdResult with thresholds and binary output.
612
+
613
+ Example:
614
+ >>> result = apply_adaptive_threshold(noisy_signal)
615
+ >>> digital = result.binary_output
616
+ """
617
+ thresholder = AdaptiveThresholder(
618
+ window_size=window_size,
619
+ method=method,
620
+ hysteresis=hysteresis,
621
+ )
622
+ return thresholder.apply(signal)
623
+
624
+
625
+ def detect_multi_level(
626
+ signal: NDArray[np.float64],
627
+ n_levels: int = 4,
628
+ auto_detect: bool = True,
629
+ hysteresis: float = 0.1,
630
+ ) -> MultiLevelResult:
631
+ """Detect multi-level logic in signal.
632
+
633
+ Implements RE-THR-002: Multi-Level Logic Support.
634
+
635
+ Args:
636
+ signal: Input analog signal.
637
+ n_levels: Expected number of levels.
638
+ auto_detect: Automatically detect level voltages.
639
+ hysteresis: Hysteresis between levels.
640
+
641
+ Returns:
642
+ MultiLevelResult with detected levels.
643
+
644
+ Example:
645
+ >>> result = detect_multi_level(pam4_signal, n_levels=4)
646
+ >>> symbols = result.levels
647
+ """
648
+ detector = MultiLevelDetector(
649
+ levels=n_levels,
650
+ auto_detect_levels=auto_detect,
651
+ hysteresis=hysteresis,
652
+ )
653
+ return detector.detect(signal)
654
+
655
+
656
+ def calculate_threshold_snr(
657
+ signal: NDArray[np.float64],
658
+ threshold: float | NDArray[np.float64],
659
+ ) -> float:
660
+ """Calculate signal-to-noise ratio at threshold.
661
+
662
+ Implements RE-THR-001: Threshold quality metric.
663
+
664
+ Args:
665
+ signal: Input signal.
666
+ threshold: Threshold value(s).
667
+
668
+ Returns:
669
+ Estimated SNR in dB.
670
+ """
671
+ if isinstance(threshold, np.ndarray):
672
+ threshold = float(np.mean(threshold))
673
+
674
+ # Separate high and low samples
675
+ high_samples = signal[signal > threshold]
676
+ low_samples = signal[signal <= threshold]
677
+
678
+ if len(high_samples) == 0 or len(low_samples) == 0:
679
+ return 0.0
680
+
681
+ # Calculate signal power (difference between means)
682
+ signal_power = (np.mean(high_samples) - np.mean(low_samples)) ** 2
683
+
684
+ # Calculate noise power (variance around means)
685
+ noise_power = (np.var(high_samples) + np.var(low_samples)) / 2
686
+
687
+ if noise_power == 0:
688
+ return 100.0 # Very high SNR
689
+
690
+ snr_linear = signal_power / noise_power
691
+ snr_db = 10 * np.log10(snr_linear)
692
+
693
+ return float(snr_db)
694
+
695
+
696
+ __all__ = [
697
+ "AdaptiveThresholdResult",
698
+ # Classes
699
+ "AdaptiveThresholder",
700
+ "MultiLevelDetector",
701
+ "MultiLevelResult",
702
+ # Data classes
703
+ "ThresholdConfig",
704
+ # Functions
705
+ "apply_adaptive_threshold",
706
+ "calculate_threshold_snr",
707
+ "detect_multi_level",
708
+ ]