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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (465) hide show
  1. oscura/__init__.py +813 -8
  2. oscura/__main__.py +392 -0
  3. oscura/analyzers/__init__.py +37 -0
  4. oscura/analyzers/digital/__init__.py +177 -0
  5. oscura/analyzers/digital/bus.py +691 -0
  6. oscura/analyzers/digital/clock.py +805 -0
  7. oscura/analyzers/digital/correlation.py +720 -0
  8. oscura/analyzers/digital/edges.py +632 -0
  9. oscura/analyzers/digital/extraction.py +413 -0
  10. oscura/analyzers/digital/quality.py +878 -0
  11. oscura/analyzers/digital/signal_quality.py +877 -0
  12. oscura/analyzers/digital/thresholds.py +708 -0
  13. oscura/analyzers/digital/timing.py +1104 -0
  14. oscura/analyzers/eye/__init__.py +46 -0
  15. oscura/analyzers/eye/diagram.py +434 -0
  16. oscura/analyzers/eye/metrics.py +555 -0
  17. oscura/analyzers/jitter/__init__.py +83 -0
  18. oscura/analyzers/jitter/ber.py +333 -0
  19. oscura/analyzers/jitter/decomposition.py +759 -0
  20. oscura/analyzers/jitter/measurements.py +413 -0
  21. oscura/analyzers/jitter/spectrum.py +220 -0
  22. oscura/analyzers/measurements.py +40 -0
  23. oscura/analyzers/packet/__init__.py +171 -0
  24. oscura/analyzers/packet/daq.py +1077 -0
  25. oscura/analyzers/packet/metrics.py +437 -0
  26. oscura/analyzers/packet/parser.py +327 -0
  27. oscura/analyzers/packet/payload.py +2156 -0
  28. oscura/analyzers/packet/payload_analysis.py +1312 -0
  29. oscura/analyzers/packet/payload_extraction.py +236 -0
  30. oscura/analyzers/packet/payload_patterns.py +670 -0
  31. oscura/analyzers/packet/stream.py +359 -0
  32. oscura/analyzers/patterns/__init__.py +266 -0
  33. oscura/analyzers/patterns/clustering.py +1036 -0
  34. oscura/analyzers/patterns/discovery.py +539 -0
  35. oscura/analyzers/patterns/learning.py +797 -0
  36. oscura/analyzers/patterns/matching.py +1091 -0
  37. oscura/analyzers/patterns/periodic.py +650 -0
  38. oscura/analyzers/patterns/sequences.py +767 -0
  39. oscura/analyzers/power/__init__.py +116 -0
  40. oscura/analyzers/power/ac_power.py +391 -0
  41. oscura/analyzers/power/basic.py +383 -0
  42. oscura/analyzers/power/conduction.py +314 -0
  43. oscura/analyzers/power/efficiency.py +297 -0
  44. oscura/analyzers/power/ripple.py +356 -0
  45. oscura/analyzers/power/soa.py +372 -0
  46. oscura/analyzers/power/switching.py +479 -0
  47. oscura/analyzers/protocol/__init__.py +150 -0
  48. oscura/analyzers/protocols/__init__.py +150 -0
  49. oscura/analyzers/protocols/base.py +500 -0
  50. oscura/analyzers/protocols/can.py +620 -0
  51. oscura/analyzers/protocols/can_fd.py +448 -0
  52. oscura/analyzers/protocols/flexray.py +405 -0
  53. oscura/analyzers/protocols/hdlc.py +399 -0
  54. oscura/analyzers/protocols/i2c.py +368 -0
  55. oscura/analyzers/protocols/i2s.py +296 -0
  56. oscura/analyzers/protocols/jtag.py +393 -0
  57. oscura/analyzers/protocols/lin.py +445 -0
  58. oscura/analyzers/protocols/manchester.py +333 -0
  59. oscura/analyzers/protocols/onewire.py +501 -0
  60. oscura/analyzers/protocols/spi.py +334 -0
  61. oscura/analyzers/protocols/swd.py +325 -0
  62. oscura/analyzers/protocols/uart.py +393 -0
  63. oscura/analyzers/protocols/usb.py +495 -0
  64. oscura/analyzers/signal_integrity/__init__.py +63 -0
  65. oscura/analyzers/signal_integrity/embedding.py +294 -0
  66. oscura/analyzers/signal_integrity/equalization.py +370 -0
  67. oscura/analyzers/signal_integrity/sparams.py +484 -0
  68. oscura/analyzers/spectral/__init__.py +53 -0
  69. oscura/analyzers/spectral/chunked.py +273 -0
  70. oscura/analyzers/spectral/chunked_fft.py +571 -0
  71. oscura/analyzers/spectral/chunked_wavelet.py +391 -0
  72. oscura/analyzers/spectral/fft.py +92 -0
  73. oscura/analyzers/statistical/__init__.py +250 -0
  74. oscura/analyzers/statistical/checksum.py +923 -0
  75. oscura/analyzers/statistical/chunked_corr.py +228 -0
  76. oscura/analyzers/statistical/classification.py +778 -0
  77. oscura/analyzers/statistical/entropy.py +1113 -0
  78. oscura/analyzers/statistical/ngrams.py +614 -0
  79. oscura/analyzers/statistics/__init__.py +119 -0
  80. oscura/analyzers/statistics/advanced.py +885 -0
  81. oscura/analyzers/statistics/basic.py +263 -0
  82. oscura/analyzers/statistics/correlation.py +630 -0
  83. oscura/analyzers/statistics/distribution.py +298 -0
  84. oscura/analyzers/statistics/outliers.py +463 -0
  85. oscura/analyzers/statistics/streaming.py +93 -0
  86. oscura/analyzers/statistics/trend.py +520 -0
  87. oscura/analyzers/validation.py +598 -0
  88. oscura/analyzers/waveform/__init__.py +36 -0
  89. oscura/analyzers/waveform/measurements.py +943 -0
  90. oscura/analyzers/waveform/measurements_with_uncertainty.py +371 -0
  91. oscura/analyzers/waveform/spectral.py +1689 -0
  92. oscura/analyzers/waveform/wavelets.py +298 -0
  93. oscura/api/__init__.py +62 -0
  94. oscura/api/dsl.py +538 -0
  95. oscura/api/fluent.py +571 -0
  96. oscura/api/operators.py +498 -0
  97. oscura/api/optimization.py +392 -0
  98. oscura/api/profiling.py +396 -0
  99. oscura/automotive/__init__.py +73 -0
  100. oscura/automotive/can/__init__.py +52 -0
  101. oscura/automotive/can/analysis.py +356 -0
  102. oscura/automotive/can/checksum.py +250 -0
  103. oscura/automotive/can/correlation.py +212 -0
  104. oscura/automotive/can/discovery.py +355 -0
  105. oscura/automotive/can/message_wrapper.py +375 -0
  106. oscura/automotive/can/models.py +385 -0
  107. oscura/automotive/can/patterns.py +381 -0
  108. oscura/automotive/can/session.py +452 -0
  109. oscura/automotive/can/state_machine.py +300 -0
  110. oscura/automotive/can/stimulus_response.py +461 -0
  111. oscura/automotive/dbc/__init__.py +15 -0
  112. oscura/automotive/dbc/generator.py +156 -0
  113. oscura/automotive/dbc/parser.py +146 -0
  114. oscura/automotive/dtc/__init__.py +30 -0
  115. oscura/automotive/dtc/database.py +3036 -0
  116. oscura/automotive/j1939/__init__.py +14 -0
  117. oscura/automotive/j1939/decoder.py +745 -0
  118. oscura/automotive/loaders/__init__.py +35 -0
  119. oscura/automotive/loaders/asc.py +98 -0
  120. oscura/automotive/loaders/blf.py +77 -0
  121. oscura/automotive/loaders/csv_can.py +136 -0
  122. oscura/automotive/loaders/dispatcher.py +136 -0
  123. oscura/automotive/loaders/mdf.py +331 -0
  124. oscura/automotive/loaders/pcap.py +132 -0
  125. oscura/automotive/obd/__init__.py +14 -0
  126. oscura/automotive/obd/decoder.py +707 -0
  127. oscura/automotive/uds/__init__.py +48 -0
  128. oscura/automotive/uds/decoder.py +265 -0
  129. oscura/automotive/uds/models.py +64 -0
  130. oscura/automotive/visualization.py +369 -0
  131. oscura/batch/__init__.py +55 -0
  132. oscura/batch/advanced.py +627 -0
  133. oscura/batch/aggregate.py +300 -0
  134. oscura/batch/analyze.py +139 -0
  135. oscura/batch/logging.py +487 -0
  136. oscura/batch/metrics.py +556 -0
  137. oscura/builders/__init__.py +41 -0
  138. oscura/builders/signal_builder.py +1131 -0
  139. oscura/cli/__init__.py +14 -0
  140. oscura/cli/batch.py +339 -0
  141. oscura/cli/characterize.py +273 -0
  142. oscura/cli/compare.py +775 -0
  143. oscura/cli/decode.py +551 -0
  144. oscura/cli/main.py +247 -0
  145. oscura/cli/shell.py +350 -0
  146. oscura/comparison/__init__.py +66 -0
  147. oscura/comparison/compare.py +397 -0
  148. oscura/comparison/golden.py +487 -0
  149. oscura/comparison/limits.py +391 -0
  150. oscura/comparison/mask.py +434 -0
  151. oscura/comparison/trace_diff.py +30 -0
  152. oscura/comparison/visualization.py +481 -0
  153. oscura/compliance/__init__.py +70 -0
  154. oscura/compliance/advanced.py +756 -0
  155. oscura/compliance/masks.py +363 -0
  156. oscura/compliance/reporting.py +483 -0
  157. oscura/compliance/testing.py +298 -0
  158. oscura/component/__init__.py +38 -0
  159. oscura/component/impedance.py +365 -0
  160. oscura/component/reactive.py +598 -0
  161. oscura/component/transmission_line.py +312 -0
  162. oscura/config/__init__.py +191 -0
  163. oscura/config/defaults.py +254 -0
  164. oscura/config/loader.py +348 -0
  165. oscura/config/memory.py +271 -0
  166. oscura/config/migration.py +458 -0
  167. oscura/config/pipeline.py +1077 -0
  168. oscura/config/preferences.py +530 -0
  169. oscura/config/protocol.py +875 -0
  170. oscura/config/schema.py +713 -0
  171. oscura/config/settings.py +420 -0
  172. oscura/config/thresholds.py +599 -0
  173. oscura/convenience.py +457 -0
  174. oscura/core/__init__.py +299 -0
  175. oscura/core/audit.py +457 -0
  176. oscura/core/backend_selector.py +405 -0
  177. oscura/core/cache.py +590 -0
  178. oscura/core/cancellation.py +439 -0
  179. oscura/core/confidence.py +225 -0
  180. oscura/core/config.py +506 -0
  181. oscura/core/correlation.py +216 -0
  182. oscura/core/cross_domain.py +422 -0
  183. oscura/core/debug.py +301 -0
  184. oscura/core/edge_cases.py +541 -0
  185. oscura/core/exceptions.py +535 -0
  186. oscura/core/gpu_backend.py +523 -0
  187. oscura/core/lazy.py +832 -0
  188. oscura/core/log_query.py +540 -0
  189. oscura/core/logging.py +931 -0
  190. oscura/core/logging_advanced.py +952 -0
  191. oscura/core/memoize.py +171 -0
  192. oscura/core/memory_check.py +274 -0
  193. oscura/core/memory_guard.py +290 -0
  194. oscura/core/memory_limits.py +336 -0
  195. oscura/core/memory_monitor.py +453 -0
  196. oscura/core/memory_progress.py +465 -0
  197. oscura/core/memory_warnings.py +315 -0
  198. oscura/core/numba_backend.py +362 -0
  199. oscura/core/performance.py +352 -0
  200. oscura/core/progress.py +524 -0
  201. oscura/core/provenance.py +358 -0
  202. oscura/core/results.py +331 -0
  203. oscura/core/types.py +504 -0
  204. oscura/core/uncertainty.py +383 -0
  205. oscura/discovery/__init__.py +52 -0
  206. oscura/discovery/anomaly_detector.py +672 -0
  207. oscura/discovery/auto_decoder.py +415 -0
  208. oscura/discovery/comparison.py +497 -0
  209. oscura/discovery/quality_validator.py +528 -0
  210. oscura/discovery/signal_detector.py +769 -0
  211. oscura/dsl/__init__.py +73 -0
  212. oscura/dsl/commands.py +246 -0
  213. oscura/dsl/interpreter.py +455 -0
  214. oscura/dsl/parser.py +689 -0
  215. oscura/dsl/repl.py +172 -0
  216. oscura/exceptions.py +59 -0
  217. oscura/exploratory/__init__.py +111 -0
  218. oscura/exploratory/error_recovery.py +642 -0
  219. oscura/exploratory/fuzzy.py +513 -0
  220. oscura/exploratory/fuzzy_advanced.py +786 -0
  221. oscura/exploratory/legacy.py +831 -0
  222. oscura/exploratory/parse.py +358 -0
  223. oscura/exploratory/recovery.py +275 -0
  224. oscura/exploratory/sync.py +382 -0
  225. oscura/exploratory/unknown.py +707 -0
  226. oscura/export/__init__.py +25 -0
  227. oscura/export/wireshark/README.md +265 -0
  228. oscura/export/wireshark/__init__.py +47 -0
  229. oscura/export/wireshark/generator.py +312 -0
  230. oscura/export/wireshark/lua_builder.py +159 -0
  231. oscura/export/wireshark/templates/dissector.lua.j2 +92 -0
  232. oscura/export/wireshark/type_mapping.py +165 -0
  233. oscura/export/wireshark/validator.py +105 -0
  234. oscura/exporters/__init__.py +94 -0
  235. oscura/exporters/csv.py +303 -0
  236. oscura/exporters/exporters.py +44 -0
  237. oscura/exporters/hdf5.py +219 -0
  238. oscura/exporters/html_export.py +701 -0
  239. oscura/exporters/json_export.py +291 -0
  240. oscura/exporters/markdown_export.py +367 -0
  241. oscura/exporters/matlab_export.py +354 -0
  242. oscura/exporters/npz_export.py +219 -0
  243. oscura/exporters/spice_export.py +210 -0
  244. oscura/extensibility/__init__.py +131 -0
  245. oscura/extensibility/docs.py +752 -0
  246. oscura/extensibility/extensions.py +1125 -0
  247. oscura/extensibility/logging.py +259 -0
  248. oscura/extensibility/measurements.py +485 -0
  249. oscura/extensibility/plugins.py +414 -0
  250. oscura/extensibility/registry.py +346 -0
  251. oscura/extensibility/templates.py +913 -0
  252. oscura/extensibility/validation.py +651 -0
  253. oscura/filtering/__init__.py +89 -0
  254. oscura/filtering/base.py +563 -0
  255. oscura/filtering/convenience.py +564 -0
  256. oscura/filtering/design.py +725 -0
  257. oscura/filtering/filters.py +32 -0
  258. oscura/filtering/introspection.py +605 -0
  259. oscura/guidance/__init__.py +24 -0
  260. oscura/guidance/recommender.py +429 -0
  261. oscura/guidance/wizard.py +518 -0
  262. oscura/inference/__init__.py +251 -0
  263. oscura/inference/active_learning/README.md +153 -0
  264. oscura/inference/active_learning/__init__.py +38 -0
  265. oscura/inference/active_learning/lstar.py +257 -0
  266. oscura/inference/active_learning/observation_table.py +230 -0
  267. oscura/inference/active_learning/oracle.py +78 -0
  268. oscura/inference/active_learning/teachers/__init__.py +15 -0
  269. oscura/inference/active_learning/teachers/simulator.py +192 -0
  270. oscura/inference/adaptive_tuning.py +453 -0
  271. oscura/inference/alignment.py +653 -0
  272. oscura/inference/bayesian.py +943 -0
  273. oscura/inference/binary.py +1016 -0
  274. oscura/inference/crc_reverse.py +711 -0
  275. oscura/inference/logic.py +288 -0
  276. oscura/inference/message_format.py +1305 -0
  277. oscura/inference/protocol.py +417 -0
  278. oscura/inference/protocol_dsl.py +1084 -0
  279. oscura/inference/protocol_library.py +1230 -0
  280. oscura/inference/sequences.py +809 -0
  281. oscura/inference/signal_intelligence.py +1509 -0
  282. oscura/inference/spectral.py +215 -0
  283. oscura/inference/state_machine.py +634 -0
  284. oscura/inference/stream.py +918 -0
  285. oscura/integrations/__init__.py +59 -0
  286. oscura/integrations/llm.py +1827 -0
  287. oscura/jupyter/__init__.py +32 -0
  288. oscura/jupyter/display.py +268 -0
  289. oscura/jupyter/magic.py +334 -0
  290. oscura/loaders/__init__.py +526 -0
  291. oscura/loaders/binary.py +69 -0
  292. oscura/loaders/configurable.py +1255 -0
  293. oscura/loaders/csv.py +26 -0
  294. oscura/loaders/csv_loader.py +473 -0
  295. oscura/loaders/hdf5.py +9 -0
  296. oscura/loaders/hdf5_loader.py +510 -0
  297. oscura/loaders/lazy.py +370 -0
  298. oscura/loaders/mmap_loader.py +583 -0
  299. oscura/loaders/numpy_loader.py +436 -0
  300. oscura/loaders/pcap.py +432 -0
  301. oscura/loaders/preprocessing.py +368 -0
  302. oscura/loaders/rigol.py +287 -0
  303. oscura/loaders/sigrok.py +321 -0
  304. oscura/loaders/tdms.py +367 -0
  305. oscura/loaders/tektronix.py +711 -0
  306. oscura/loaders/validation.py +584 -0
  307. oscura/loaders/vcd.py +464 -0
  308. oscura/loaders/wav.py +233 -0
  309. oscura/math/__init__.py +45 -0
  310. oscura/math/arithmetic.py +824 -0
  311. oscura/math/interpolation.py +413 -0
  312. oscura/onboarding/__init__.py +39 -0
  313. oscura/onboarding/help.py +498 -0
  314. oscura/onboarding/tutorials.py +405 -0
  315. oscura/onboarding/wizard.py +466 -0
  316. oscura/optimization/__init__.py +19 -0
  317. oscura/optimization/parallel.py +440 -0
  318. oscura/optimization/search.py +532 -0
  319. oscura/pipeline/__init__.py +43 -0
  320. oscura/pipeline/base.py +338 -0
  321. oscura/pipeline/composition.py +242 -0
  322. oscura/pipeline/parallel.py +448 -0
  323. oscura/pipeline/pipeline.py +375 -0
  324. oscura/pipeline/reverse_engineering.py +1119 -0
  325. oscura/plugins/__init__.py +122 -0
  326. oscura/plugins/base.py +272 -0
  327. oscura/plugins/cli.py +497 -0
  328. oscura/plugins/discovery.py +411 -0
  329. oscura/plugins/isolation.py +418 -0
  330. oscura/plugins/lifecycle.py +959 -0
  331. oscura/plugins/manager.py +493 -0
  332. oscura/plugins/registry.py +421 -0
  333. oscura/plugins/versioning.py +372 -0
  334. oscura/py.typed +0 -0
  335. oscura/quality/__init__.py +65 -0
  336. oscura/quality/ensemble.py +740 -0
  337. oscura/quality/explainer.py +338 -0
  338. oscura/quality/scoring.py +616 -0
  339. oscura/quality/warnings.py +456 -0
  340. oscura/reporting/__init__.py +248 -0
  341. oscura/reporting/advanced.py +1234 -0
  342. oscura/reporting/analyze.py +448 -0
  343. oscura/reporting/argument_preparer.py +596 -0
  344. oscura/reporting/auto_report.py +507 -0
  345. oscura/reporting/batch.py +615 -0
  346. oscura/reporting/chart_selection.py +223 -0
  347. oscura/reporting/comparison.py +330 -0
  348. oscura/reporting/config.py +615 -0
  349. oscura/reporting/content/__init__.py +39 -0
  350. oscura/reporting/content/executive.py +127 -0
  351. oscura/reporting/content/filtering.py +191 -0
  352. oscura/reporting/content/minimal.py +257 -0
  353. oscura/reporting/content/verbosity.py +162 -0
  354. oscura/reporting/core.py +508 -0
  355. oscura/reporting/core_formats/__init__.py +17 -0
  356. oscura/reporting/core_formats/multi_format.py +210 -0
  357. oscura/reporting/engine.py +836 -0
  358. oscura/reporting/export.py +366 -0
  359. oscura/reporting/formatting/__init__.py +129 -0
  360. oscura/reporting/formatting/emphasis.py +81 -0
  361. oscura/reporting/formatting/numbers.py +403 -0
  362. oscura/reporting/formatting/standards.py +55 -0
  363. oscura/reporting/formatting.py +466 -0
  364. oscura/reporting/html.py +578 -0
  365. oscura/reporting/index.py +590 -0
  366. oscura/reporting/multichannel.py +296 -0
  367. oscura/reporting/output.py +379 -0
  368. oscura/reporting/pdf.py +373 -0
  369. oscura/reporting/plots.py +731 -0
  370. oscura/reporting/pptx_export.py +360 -0
  371. oscura/reporting/renderers/__init__.py +11 -0
  372. oscura/reporting/renderers/pdf.py +94 -0
  373. oscura/reporting/sections.py +471 -0
  374. oscura/reporting/standards.py +680 -0
  375. oscura/reporting/summary_generator.py +368 -0
  376. oscura/reporting/tables.py +397 -0
  377. oscura/reporting/template_system.py +724 -0
  378. oscura/reporting/templates/__init__.py +15 -0
  379. oscura/reporting/templates/definition.py +205 -0
  380. oscura/reporting/templates/index.html +649 -0
  381. oscura/reporting/templates/index.md +173 -0
  382. oscura/schemas/__init__.py +158 -0
  383. oscura/schemas/bus_configuration.json +322 -0
  384. oscura/schemas/device_mapping.json +182 -0
  385. oscura/schemas/packet_format.json +418 -0
  386. oscura/schemas/protocol_definition.json +363 -0
  387. oscura/search/__init__.py +16 -0
  388. oscura/search/anomaly.py +292 -0
  389. oscura/search/context.py +149 -0
  390. oscura/search/pattern.py +160 -0
  391. oscura/session/__init__.py +34 -0
  392. oscura/session/annotations.py +289 -0
  393. oscura/session/history.py +313 -0
  394. oscura/session/session.py +445 -0
  395. oscura/streaming/__init__.py +43 -0
  396. oscura/streaming/chunked.py +611 -0
  397. oscura/streaming/progressive.py +393 -0
  398. oscura/streaming/realtime.py +622 -0
  399. oscura/testing/__init__.py +54 -0
  400. oscura/testing/synthetic.py +808 -0
  401. oscura/triggering/__init__.py +68 -0
  402. oscura/triggering/base.py +229 -0
  403. oscura/triggering/edge.py +353 -0
  404. oscura/triggering/pattern.py +344 -0
  405. oscura/triggering/pulse.py +581 -0
  406. oscura/triggering/window.py +453 -0
  407. oscura/ui/__init__.py +48 -0
  408. oscura/ui/formatters.py +526 -0
  409. oscura/ui/progressive_display.py +340 -0
  410. oscura/utils/__init__.py +99 -0
  411. oscura/utils/autodetect.py +338 -0
  412. oscura/utils/buffer.py +389 -0
  413. oscura/utils/lazy.py +407 -0
  414. oscura/utils/lazy_imports.py +147 -0
  415. oscura/utils/memory.py +836 -0
  416. oscura/utils/memory_advanced.py +1326 -0
  417. oscura/utils/memory_extensions.py +465 -0
  418. oscura/utils/progressive.py +352 -0
  419. oscura/utils/windowing.py +362 -0
  420. oscura/visualization/__init__.py +321 -0
  421. oscura/visualization/accessibility.py +526 -0
  422. oscura/visualization/annotations.py +374 -0
  423. oscura/visualization/axis_scaling.py +305 -0
  424. oscura/visualization/colors.py +453 -0
  425. oscura/visualization/digital.py +337 -0
  426. oscura/visualization/eye.py +420 -0
  427. oscura/visualization/histogram.py +281 -0
  428. oscura/visualization/interactive.py +858 -0
  429. oscura/visualization/jitter.py +702 -0
  430. oscura/visualization/keyboard.py +394 -0
  431. oscura/visualization/layout.py +365 -0
  432. oscura/visualization/optimization.py +1028 -0
  433. oscura/visualization/palettes.py +446 -0
  434. oscura/visualization/plot.py +92 -0
  435. oscura/visualization/power.py +290 -0
  436. oscura/visualization/power_extended.py +626 -0
  437. oscura/visualization/presets.py +467 -0
  438. oscura/visualization/protocols.py +932 -0
  439. oscura/visualization/render.py +207 -0
  440. oscura/visualization/rendering.py +444 -0
  441. oscura/visualization/reverse_engineering.py +791 -0
  442. oscura/visualization/signal_integrity.py +808 -0
  443. oscura/visualization/specialized.py +553 -0
  444. oscura/visualization/spectral.py +811 -0
  445. oscura/visualization/styles.py +381 -0
  446. oscura/visualization/thumbnails.py +311 -0
  447. oscura/visualization/time_axis.py +351 -0
  448. oscura/visualization/waveform.py +367 -0
  449. oscura/workflow/__init__.py +13 -0
  450. oscura/workflow/dag.py +377 -0
  451. oscura/workflows/__init__.py +58 -0
  452. oscura/workflows/compliance.py +280 -0
  453. oscura/workflows/digital.py +272 -0
  454. oscura/workflows/multi_trace.py +502 -0
  455. oscura/workflows/power.py +178 -0
  456. oscura/workflows/protocol.py +492 -0
  457. oscura/workflows/reverse_engineering.py +639 -0
  458. oscura/workflows/signal_integrity.py +227 -0
  459. oscura-0.1.1.dist-info/METADATA +300 -0
  460. oscura-0.1.1.dist-info/RECORD +463 -0
  461. oscura-0.1.1.dist-info/entry_points.txt +2 -0
  462. {oscura-0.0.1.dist-info → oscura-0.1.1.dist-info}/licenses/LICENSE +1 -1
  463. oscura-0.0.1.dist-info/METADATA +0 -63
  464. oscura-0.0.1.dist-info/RECORD +0 -5
  465. {oscura-0.0.1.dist-info → oscura-0.1.1.dist-info}/WHEEL +0 -0
@@ -0,0 +1,596 @@
1
+ """Argument preparation for analysis functions.
2
+
3
+ This module handles automatic argument detection and preparation for analysis functions,
4
+ including data type detection, parameter inference, and intelligent defaults.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import inspect
10
+ import logging
11
+ from pathlib import Path
12
+ from typing import TYPE_CHECKING, Any
13
+
14
+ import numpy as np
15
+
16
+ from oscura.core.types import TraceMetadata, WaveformTrace
17
+
18
+ if TYPE_CHECKING:
19
+ from collections.abc import Callable
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ class ArgumentPreparer:
25
+ """Prepares arguments for analysis functions automatically.
26
+
27
+ This class examines function signatures and prepares appropriate arguments
28
+ from input data, handling type conversions, parameter detection, and
29
+ intelligent defaults.
30
+ """
31
+
32
+ def __init__(self, input_path: Path | None = None, default_sample_rate: float = 1e6):
33
+ """Initialize argument preparer.
34
+
35
+ Args:
36
+ input_path: Path to input file (used for filename-based detection).
37
+ default_sample_rate: Default sample rate when not available in data.
38
+ """
39
+ self._input_path = input_path
40
+ self._default_sample_rate = default_sample_rate
41
+
42
+ def prepare_arguments(
43
+ self, func: Callable[..., Any], data: Any
44
+ ) -> tuple[list[Any] | None, dict[str, Any]]:
45
+ """Prepare arguments for an analysis function.
46
+
47
+ Args:
48
+ func: Function to prepare arguments for.
49
+ data: Input data object.
50
+
51
+ Returns:
52
+ Tuple of (args_list, kwargs_dict), or (None, {}) if not applicable.
53
+ """
54
+ sig = inspect.signature(func)
55
+ params = list(sig.parameters.keys())
56
+
57
+ if not params:
58
+ return [], {}
59
+
60
+ first_param = params[0]
61
+
62
+ # Handle specialized data types
63
+ if self._is_eye_diagram(data):
64
+ return self._handle_eye_diagram(data, first_param, sig)
65
+
66
+ if self._is_sparam_data(data):
67
+ return self._handle_sparam_data(data, first_param, sig)
68
+
69
+ # Check type annotation for special handling
70
+ first_param_info = sig.parameters.get(first_param)
71
+ param_annotation = first_param_info.annotation if first_param_info else None
72
+ annotation_str = str(param_annotation) if param_annotation else ""
73
+
74
+ # Handle packet data
75
+ if "PacketInfo" in annotation_str or first_param == "packets":
76
+ return self._handle_packet_data(data)
77
+
78
+ # Extract raw data and determine if trace
79
+ is_trace, raw_data, sample_rate = self._extract_raw_data(data)
80
+
81
+ if raw_data is None or (hasattr(raw_data, "__len__") and len(raw_data) == 0):
82
+ return None, {}
83
+
84
+ # Build kwargs with auto-detected parameters
85
+ kwargs = self._build_kwargs(params, sig, raw_data, sample_rate)
86
+
87
+ # Handle trace wrapper creation if needed
88
+ if not is_trace and self._needs_trace_wrapper(first_param, annotation_str):
89
+ data, is_trace = self._create_trace_wrapper(raw_data, sample_rate, data)
90
+ if data is None:
91
+ return None, {}
92
+
93
+ # Return appropriate arguments based on first parameter
94
+ return self._build_args(first_param, annotation_str, data, is_trace, raw_data, kwargs, sig)
95
+
96
+ def _is_eye_diagram(self, data: Any) -> bool:
97
+ """Check if data is an EyeDiagram object."""
98
+ return hasattr(data, "samples_per_ui") and hasattr(data, "time_axis")
99
+
100
+ def _is_sparam_data(self, data: Any) -> bool:
101
+ """Check if data is S-parameter data."""
102
+ return hasattr(data, "s_matrix") and hasattr(data, "frequencies")
103
+
104
+ def _handle_eye_diagram(
105
+ self, data: Any, first_param: str, sig: inspect.Signature
106
+ ) -> tuple[list[Any] | None, dict[str, Any]]:
107
+ """Handle EyeDiagram data."""
108
+ if first_param == "eye" or "EyeDiagram" in str(sig.parameters.get(first_param, "")):
109
+ return [data], {}
110
+ return None, {}
111
+
112
+ def _handle_sparam_data(
113
+ self, data: Any, first_param: str, sig: inspect.Signature
114
+ ) -> tuple[list[Any] | None, dict[str, Any]]:
115
+ """Handle S-parameter data."""
116
+ if first_param in ("s_params", "s_param", "s_data", "sparams"):
117
+ return [data], {}
118
+ if "SParameter" in str(sig.parameters.get(first_param, "")):
119
+ return [data], {}
120
+ return None, {}
121
+
122
+ def _handle_packet_data(self, data: Any) -> tuple[list[Any] | None, dict[str, Any]]:
123
+ """Handle packet data - convert to PacketInfo objects if needed."""
124
+ if isinstance(data, list):
125
+ if data and hasattr(data[0], "timestamp"):
126
+ return [data], {}
127
+ elif data and isinstance(data[0], dict):
128
+ try:
129
+ from oscura.analyzers.packet.metrics import PacketInfo
130
+
131
+ packets = [
132
+ PacketInfo(
133
+ timestamp=p.get("timestamp", 0.0),
134
+ size=p.get("size", 0),
135
+ sequence=p.get("sequence"),
136
+ )
137
+ for p in data
138
+ ]
139
+ return [packets], {}
140
+ except Exception as e:
141
+ logger.debug(f"Failed to convert to PacketInfo: {e}")
142
+ return None, {}
143
+ return None, {}
144
+
145
+ def _extract_raw_data(self, data: Any) -> tuple[bool, Any, float]:
146
+ """Extract raw data array, determine if trace, and get sample rate.
147
+
148
+ Returns:
149
+ Tuple of (is_trace, raw_data, sample_rate).
150
+ """
151
+ is_trace = hasattr(data, "data") and hasattr(data, "metadata")
152
+
153
+ if is_trace:
154
+ raw_data = data.data
155
+ sample_rate = self._get_sample_rate_from_data(data)
156
+ elif isinstance(data, np.ndarray):
157
+ raw_data = data
158
+ sample_rate = self._default_sample_rate
159
+ elif isinstance(data, bytes | bytearray):
160
+ raw_data = np.frombuffer(data, dtype=np.uint8)
161
+ sample_rate = 1.0 # Binary data context
162
+ else:
163
+ # Try to convert to array
164
+ try:
165
+ raw_data = np.array(data) if hasattr(data, "__iter__") else None
166
+ except (ValueError, TypeError):
167
+ raw_data = None
168
+ sample_rate = self._default_sample_rate
169
+
170
+ return is_trace, raw_data, sample_rate
171
+
172
+ def _get_sample_rate_from_data(self, data: Any) -> float:
173
+ """Get sample rate from data metadata or use default."""
174
+ if hasattr(data, "metadata") and hasattr(data.metadata, "sample_rate"):
175
+ sample_rate = data.metadata.sample_rate
176
+ if sample_rate is not None and sample_rate > 0:
177
+ return float(sample_rate)
178
+ return self._default_sample_rate
179
+
180
+ def _build_kwargs(
181
+ self,
182
+ params: list[str],
183
+ sig: inspect.Signature,
184
+ raw_data: Any,
185
+ sample_rate: float,
186
+ ) -> dict[str, Any]:
187
+ """Build kwargs dictionary with auto-detected parameters."""
188
+ kwargs: dict[str, Any] = {}
189
+
190
+ # Add sample rate parameters
191
+ kwargs.update(self._add_sample_rate_params(params, sample_rate))
192
+
193
+ # Add digital domain parameters
194
+ kwargs.update(self._add_digital_params(params, sig, raw_data))
195
+
196
+ # Add frequency domain parameters
197
+ kwargs.update(self._add_frequency_params(params, sig, raw_data, sample_rate))
198
+
199
+ # Add noise/threshold parameters
200
+ kwargs.update(self._add_noise_params(params, sig, raw_data))
201
+
202
+ # Add window/width parameters
203
+ kwargs.update(self._add_window_params(params, sig, raw_data, sample_rate))
204
+
205
+ return kwargs
206
+
207
+ def _add_sample_rate_params(self, params: list[str], sample_rate: float) -> dict[str, Any]:
208
+ """Add sample rate related parameters."""
209
+ kwargs = {}
210
+ if "sample_rate" in params:
211
+ kwargs["sample_rate"] = sample_rate
212
+ if "fs" in params:
213
+ kwargs["fs"] = sample_rate
214
+ if "rate" in params:
215
+ kwargs["rate"] = sample_rate
216
+ return kwargs
217
+
218
+ def _add_digital_params(
219
+ self, params: list[str], sig: inspect.Signature, raw_data: Any
220
+ ) -> dict[str, Any]:
221
+ """Add digital domain parameters (baud_rate, logic_family)."""
222
+ kwargs: dict[str, Any] = {}
223
+
224
+ # Baud rate detection
225
+ if "baud_rate" in params:
226
+ param_info = sig.parameters.get("baud_rate")
227
+ has_default = (
228
+ param_info is not None and param_info.default is not inspect.Parameter.empty
229
+ )
230
+ if not has_default or (param_info and param_info.default is None):
231
+ detected_baud = self._detect_baud_rate_from_filename()
232
+ if detected_baud is not None:
233
+ kwargs["baud_rate"] = detected_baud
234
+
235
+ # Logic family detection
236
+ if "logic_family" in params:
237
+ param_info = sig.parameters.get("logic_family")
238
+ has_default = (
239
+ param_info is not None and param_info.default is not inspect.Parameter.empty
240
+ )
241
+ if not has_default or (param_info and param_info.default in (None, "auto")):
242
+ try:
243
+ detected_family: Any = self._detect_logic_family(raw_data)
244
+ kwargs["logic_family"] = detected_family
245
+ except Exception as e:
246
+ logger.debug(f"Could not auto-detect logic family: {e}")
247
+
248
+ return kwargs
249
+
250
+ def _add_frequency_params(
251
+ self, params: list[str], sig: inspect.Signature, raw_data: Any, sample_rate: float
252
+ ) -> dict[str, Any]:
253
+ """Add frequency range parameters."""
254
+ kwargs = {}
255
+
256
+ if "freq_min" in params or "freq_max" in params:
257
+ try:
258
+ freq_range = self._detect_frequency_range(raw_data, sample_rate)
259
+ if freq_range is not None:
260
+ min_freq, max_freq = freq_range
261
+
262
+ if "freq_min" in params:
263
+ if self._param_needs_value(sig, "freq_min"):
264
+ kwargs["freq_min"] = min_freq
265
+
266
+ if "freq_max" in params:
267
+ if self._param_needs_value(sig, "freq_max"):
268
+ kwargs["freq_max"] = max_freq
269
+ except Exception as e:
270
+ logger.debug(f"Could not auto-detect frequency range: {e}")
271
+
272
+ return kwargs
273
+
274
+ def _add_noise_params(
275
+ self, params: list[str], sig: inspect.Signature, raw_data: Any
276
+ ) -> dict[str, Any]:
277
+ """Add noise/threshold parameters."""
278
+ kwargs = {}
279
+
280
+ if "noise_threshold" in params or "snr_threshold" in params:
281
+ try:
282
+ noise_floor = self._detect_noise_floor(raw_data)
283
+ if noise_floor is not None:
284
+ if "noise_threshold" in params and self._param_needs_value(
285
+ sig, "noise_threshold"
286
+ ):
287
+ kwargs["noise_threshold"] = noise_floor * 3.0
288
+
289
+ if "snr_threshold" in params and self._param_needs_value(sig, "snr_threshold"):
290
+ signal_rms = float(np.std(raw_data))
291
+ if noise_floor > 0:
292
+ detected_snr = signal_rms / noise_floor
293
+ kwargs["snr_threshold"] = detected_snr / 2.0
294
+ except Exception as e:
295
+ logger.debug(f"Could not auto-detect noise floor: {e}")
296
+
297
+ # Protocol hints for baud rate
298
+ if "baud_rate" in params and "baud_rate" not in kwargs:
299
+ try:
300
+ protocol_hints = self._detect_protocol_hints(raw_data, self._default_sample_rate)
301
+ if "detected_baud" in protocol_hints:
302
+ if self._param_needs_value(sig, "baud_rate"):
303
+ kwargs["baud_rate"] = protocol_hints["detected_baud"]
304
+ logger.debug(
305
+ f"Using protocol-detected baud rate: {protocol_hints['detected_baud']} bps"
306
+ )
307
+ except Exception as e:
308
+ logger.debug(f"Could not use protocol hints for baud detection: {e}")
309
+
310
+ return kwargs
311
+
312
+ def _add_window_params(
313
+ self, params: list[str], sig: inspect.Signature, raw_data: Any, sample_rate: float
314
+ ) -> dict[str, Any]:
315
+ """Add window size and width parameters."""
316
+ kwargs: dict[str, Any] = {}
317
+ data_length = len(raw_data) if hasattr(raw_data, "__len__") else 0
318
+
319
+ if "window_size" in params:
320
+ if self._param_needs_value(sig, "window_size") and "window_size" not in kwargs:
321
+ window_size: Any = max(10, data_length // 10)
322
+ kwargs["window_size"] = window_size
323
+ logger.debug(f"Using auto-detected window_size: {kwargs['window_size']}")
324
+
325
+ if "min_width" in params:
326
+ if self._param_needs_value(sig, "min_width") and "min_width" not in kwargs:
327
+ min_width: Any = max(1e-9, 10.0 / sample_rate)
328
+ kwargs["min_width"] = min_width
329
+ logger.debug(f"Using auto-detected min_width: {kwargs['min_width']:.2e}s")
330
+
331
+ if "max_width" in params:
332
+ if self._param_needs_value(sig, "max_width") and "max_width" not in kwargs:
333
+ total_duration = data_length / sample_rate if data_length > 0 else 1e-3
334
+ max_width: Any = min(1e-3, total_duration)
335
+ kwargs["max_width"] = max_width
336
+ logger.debug(f"Using auto-detected max_width: {kwargs['max_width']:.2e}s")
337
+
338
+ if "threshold" in params and "threshold" not in kwargs:
339
+ param_info = sig.parameters.get("threshold")
340
+ has_default = (
341
+ param_info is not None and param_info.default is not inspect.Parameter.empty
342
+ )
343
+ if not has_default or (param_info and param_info.default in (None, "auto")):
344
+ try:
345
+ if isinstance(raw_data, np.ndarray) and raw_data.size > 0:
346
+ threshold: Any = float(np.median(raw_data))
347
+ kwargs["threshold"] = threshold
348
+ logger.debug(f"Using auto-detected threshold: {kwargs['threshold']:.3f}")
349
+ except Exception as e:
350
+ logger.debug(f"Could not auto-detect threshold: {e}")
351
+
352
+ if "window_duration" in params:
353
+ if self._param_needs_value(sig, "window_duration") and "window_duration" not in kwargs:
354
+ total_duration = data_length / sample_rate if data_length > 0 else 1.0
355
+ window_duration: Any = min(1.0, total_duration / 10.0)
356
+ kwargs["window_duration"] = window_duration
357
+ logger.debug(
358
+ f"Using auto-detected window_duration: {kwargs['window_duration']:.3f}s"
359
+ )
360
+
361
+ return kwargs
362
+
363
+ def _param_needs_value(self, sig: inspect.Signature, param_name: str) -> bool:
364
+ """Check if parameter needs a value (no default or default is None)."""
365
+ param_info = sig.parameters.get(param_name)
366
+ if param_info is None:
367
+ return False
368
+ has_default = param_info.default is not inspect.Parameter.empty
369
+ return not has_default or param_info.default is None
370
+
371
+ def _needs_trace_wrapper(self, first_param: str, annotation_str: str) -> bool:
372
+ """Check if function needs trace wrapper."""
373
+ return (
374
+ "Trace" in annotation_str or "WaveformTrace" in annotation_str or first_param == "trace"
375
+ )
376
+
377
+ def _create_trace_wrapper(
378
+ self, raw_data: Any, sample_rate: float, original_data: Any
379
+ ) -> tuple[Any, bool]:
380
+ """Create WaveformTrace wrapper for raw array data."""
381
+ try:
382
+ trace_data = np.asarray(raw_data) if isinstance(raw_data, memoryview) else raw_data
383
+ metadata = TraceMetadata(sample_rate=sample_rate)
384
+ data = WaveformTrace(data=trace_data, metadata=metadata)
385
+ logger.debug("Created WaveformTrace wrapper for raw array data")
386
+ return data, True
387
+ except Exception as e:
388
+ logger.debug(f"Could not create trace wrapper: {e}")
389
+ return None, False
390
+
391
+ def _build_args(
392
+ self,
393
+ first_param: str,
394
+ annotation_str: str,
395
+ data: Any,
396
+ is_trace: bool,
397
+ raw_data: Any,
398
+ kwargs: dict[str, Any],
399
+ sig: inspect.Signature,
400
+ ) -> tuple[list[Any] | None, dict[str, Any]]:
401
+ """Build the final args list based on first parameter."""
402
+ # If function expects WaveformTrace and we have a trace
403
+ if is_trace and (
404
+ "Trace" in annotation_str or "WaveformTrace" in annotation_str or first_param == "trace"
405
+ ):
406
+ return [data], kwargs
407
+
408
+ # Common data parameter names
409
+ if first_param in ("data", "signal", "x", "samples", "waveform"):
410
+ return [raw_data], kwargs
411
+
412
+ # Trace expected but not available
413
+ if first_param == "trace" and not is_trace:
414
+ return None, {}
415
+
416
+ # Edge timestamps
417
+ if first_param == "edges":
418
+ return self._extract_edges(data, is_trace, kwargs)
419
+
420
+ # Period measurements
421
+ if first_param == "periods":
422
+ return self._extract_periods(data, is_trace, kwargs)
423
+
424
+ # Bytes data
425
+ if first_param in ("stream", "data") and "bytes" in annotation_str:
426
+ return self._convert_to_bytes(data, raw_data, kwargs)
427
+
428
+ if first_param == "bytes" or (first_param == "data" and "bytes" in str(sig)):
429
+ return self._convert_to_bytes(data, raw_data, kwargs)
430
+
431
+ # Default: pass raw data
432
+ return [raw_data], kwargs
433
+
434
+ def _extract_edges(
435
+ self, data: Any, is_trace: bool, kwargs: dict[str, Any]
436
+ ) -> tuple[list[Any] | None, dict[str, Any]]:
437
+ """Extract edge timestamps from data."""
438
+ try:
439
+ from oscura.analyzers.digital import detect_edges
440
+
441
+ if is_trace:
442
+ edges = detect_edges(data)
443
+ edge_times = edges.tolist() if len(edges) > 0 else []
444
+ if len(edge_times) < 3:
445
+ return None, {}
446
+ return [edge_times], kwargs
447
+ except Exception:
448
+ pass
449
+ return None, {}
450
+
451
+ def _extract_periods(
452
+ self, data: Any, is_trace: bool, kwargs: dict[str, Any]
453
+ ) -> tuple[list[Any] | None, dict[str, Any]]:
454
+ """Extract period measurements from data."""
455
+ try:
456
+ if is_trace:
457
+ from oscura.analyzers.waveform.measurements import period
458
+
459
+ periods_result = period(data, return_all=True)
460
+ if isinstance(periods_result, np.ndarray) and len(periods_result) >= 3:
461
+ return [periods_result], kwargs
462
+ except Exception as e:
463
+ logger.debug(f"Could not compute periods: {e}")
464
+ return None, {}
465
+
466
+ def _convert_to_bytes(
467
+ self, data: Any, raw_data: Any, kwargs: dict[str, Any]
468
+ ) -> tuple[list[Any] | None, dict[str, Any]]:
469
+ """Convert data to bytes."""
470
+ if isinstance(data, bytes | bytearray):
471
+ return [data], kwargs
472
+ elif isinstance(raw_data, np.ndarray) or hasattr(raw_data, "astype"):
473
+ return [raw_data.astype(np.uint8).tobytes()], kwargs
474
+ return None, {}
475
+
476
+ # Detection methods
477
+
478
+ def _detect_baud_rate_from_filename(self) -> float | None:
479
+ """Extract baud rate from filename patterns."""
480
+ if self._input_path is None:
481
+ return None
482
+
483
+ import re
484
+
485
+ patterns = [
486
+ r"(\d+(?:\.\d+)?)[_\s]*[Mm]?baud",
487
+ r"(\d+(?:\.\d+)?)[_\s]*bps",
488
+ r"baud[_-]?(\d+)",
489
+ ]
490
+ filename = self._input_path.stem.lower()
491
+
492
+ for pattern in patterns:
493
+ match = re.search(pattern, filename, re.IGNORECASE)
494
+ if match:
495
+ value = float(match.group(1))
496
+ matched_text = filename[match.start() : match.end()].lower()
497
+ if "m" in matched_text and "baud" in matched_text:
498
+ value *= 1_000_000
499
+ logger.debug(
500
+ f"Detected baud rate from filename '{self._input_path.name}': {value} bps"
501
+ )
502
+ return value
503
+
504
+ return None
505
+
506
+ def _detect_logic_family(self, data: np.ndarray[Any, Any]) -> str:
507
+ """Detect logic family from voltage levels."""
508
+ vmax = float(np.max(data))
509
+ vmin = float(np.min(data))
510
+ voltage_swing = vmax - vmin
511
+
512
+ if voltage_swing < 1.0:
513
+ logic_family = "LVDS"
514
+ elif voltage_swing < 2.0:
515
+ logic_family = "LVCMOS18"
516
+ elif voltage_swing < 3.0:
517
+ logic_family = "LVCMOS25"
518
+ elif voltage_swing < 4.0:
519
+ logic_family = "LVCMOS33"
520
+ else:
521
+ logic_family = "TTL"
522
+
523
+ logger.debug(
524
+ f"Detected logic family from voltage swing {voltage_swing:.2f}V: {logic_family}"
525
+ )
526
+ return logic_family
527
+
528
+ def _detect_frequency_range(
529
+ self, data: np.ndarray[Any, Any], sample_rate: float
530
+ ) -> tuple[float, float] | None:
531
+ """Detect dominant frequency range from FFT analysis."""
532
+ try:
533
+ fft_result = np.fft.rfft(data - np.mean(data))
534
+ freqs = np.fft.rfftfreq(len(data), d=1.0 / sample_rate)
535
+ magnitude = np.abs(fft_result)
536
+
537
+ threshold = 0.1 * np.max(magnitude)
538
+ significant = freqs[magnitude > threshold]
539
+
540
+ if len(significant) > 0:
541
+ min_freq = float(np.min(significant))
542
+ max_freq = float(np.max(significant))
543
+ logger.debug(f"Detected frequency range: {min_freq:.2f} Hz - {max_freq:.2f} Hz")
544
+ return (min_freq, max_freq)
545
+ return None
546
+ except Exception as e:
547
+ logger.debug(f"Frequency range detection failed: {e}")
548
+ return None
549
+
550
+ def _detect_noise_floor(self, data: np.ndarray[Any, Any]) -> float | None:
551
+ """Estimate noise floor using median absolute deviation."""
552
+ try:
553
+ try:
554
+ from scipy import stats
555
+
556
+ mad = stats.median_abs_deviation(data, scale="normal")
557
+ logger.debug(f"Detected noise floor (scipy MAD): {mad:.6f}")
558
+ return float(mad)
559
+ except ImportError:
560
+ median = np.median(data)
561
+ mad = np.median(np.abs(data - median)) * 1.4826
562
+ logger.debug(f"Detected noise floor (numpy MAD): {mad:.6f}")
563
+ return float(mad)
564
+ except Exception as e:
565
+ logger.debug(f"Noise floor detection failed: {e}")
566
+ return None
567
+
568
+ def _detect_protocol_hints(
569
+ self, data: np.ndarray[Any, Any], sample_rate: float
570
+ ) -> dict[str, Any]:
571
+ """Detect hints about potential protocols in the signal."""
572
+ hints: dict[str, Any] = {}
573
+ try:
574
+ zero_crossings = np.where(np.diff(np.sign(data - np.mean(data))))[0]
575
+ if len(zero_crossings) > 10:
576
+ intervals = np.diff(zero_crossings) / sample_rate
577
+ avg_interval = float(np.median(intervals))
578
+
579
+ common_bauds = [300, 1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200]
580
+ for baud in common_bauds:
581
+ expected_interval = 1.0 / baud
582
+ if 0.8 < avg_interval / expected_interval < 1.2:
583
+ hints["detected_baud"] = baud
584
+ logger.debug(f"Protocol hint: detected baud rate {baud} bps")
585
+ break
586
+
587
+ if len(zero_crossings) > 20:
588
+ interval_std = float(np.std(np.diff(zero_crossings)))
589
+ regularity = "high" if interval_std < 2 else "medium" if interval_std < 5 else "low"
590
+ hints["clock_regularity"] = regularity
591
+ logger.debug(f"Protocol hint: clock regularity {regularity}")
592
+
593
+ except Exception as e:
594
+ logger.debug(f"Protocol hints detection failed: {e}")
595
+
596
+ return hints