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,740 @@
1
+ """Ensemble methods for combining multiple analysis algorithms.
2
+
3
+ This module provides robust analysis by combining results from multiple algorithms
4
+ using various aggregation strategies. Ensemble methods reduce individual algorithm
5
+ bias, handle outliers, and provide confidence bounds for more reliable measurements.
6
+
7
+
8
+ Example:
9
+ >>> from oscura.quality.ensemble import EnsembleAggregator, AggregationMethod
10
+ >>> from oscura.quality.ensemble import create_frequency_ensemble
11
+ >>> # Combine multiple frequency measurements
12
+ >>> result = create_frequency_ensemble(signal, sample_rate=1e9)
13
+ >>> print(f"Frequency: {result.value:.2f} Hz ± {result.confidence*100:.1f}%")
14
+ >>> print(f"Methods agree: {result.method_agreement*100:.1f}%")
15
+ >>> # Use custom ensemble
16
+ >>> aggregator = EnsembleAggregator(method=AggregationMethod.WEIGHTED_AVERAGE)
17
+ >>> results = [
18
+ ... {"value": 1000.0, "confidence": 0.9, "method": "fft"},
19
+ ... {"value": 1005.0, "confidence": 0.8, "method": "autocorr"},
20
+ ... {"value": 995.0, "confidence": 0.85, "method": "zero_crossing"},
21
+ ... ]
22
+ >>> ensemble_result = aggregator.aggregate(results)
23
+
24
+ References:
25
+ - Kuncheva, L.I.: "Combining Pattern Classifiers" (2nd Ed), Wiley, 2014
26
+ - Polikar, R.: "Ensemble Learning", Scholarpedia, 2009
27
+ - Dietterich, T.G.: "Ensemble Methods in Machine Learning", 2000
28
+ """
29
+
30
+ from __future__ import annotations
31
+
32
+ import logging
33
+ from collections import Counter
34
+ from dataclasses import dataclass, field
35
+ from enum import Enum
36
+ from typing import TYPE_CHECKING, Any
37
+
38
+ import numpy as np
39
+ from scipy import stats
40
+
41
+ from oscura.quality.scoring import AnalysisQualityScore, combine_quality_scores
42
+
43
+ if TYPE_CHECKING:
44
+ from numpy.typing import NDArray
45
+
46
+ logger = logging.getLogger(__name__)
47
+
48
+
49
+ class AggregationMethod(Enum):
50
+ """Strategy for combining multiple analysis results.
51
+
52
+ Attributes:
53
+ WEIGHTED_AVERAGE: Weight results by confidence (best for numeric values).
54
+ VOTING: Majority voting (best for categorical results).
55
+ MEDIAN: Robust to outliers (best when outliers expected).
56
+ BAYESIAN: Bayesian combination with prior (best when prior knowledge available).
57
+ """
58
+
59
+ WEIGHTED_AVERAGE = "weighted_average"
60
+ VOTING = "voting"
61
+ MEDIAN = "median"
62
+ BAYESIAN = "bayesian"
63
+
64
+
65
+ @dataclass
66
+ class EnsembleResult:
67
+ """Combined result from multiple analysis methods.
68
+
69
+ Attributes:
70
+ value: Aggregated value (numeric or categorical).
71
+ confidence: Overall confidence in combined result (0-1).
72
+ lower_bound: Lower confidence bound (None for categorical).
73
+ upper_bound: Upper confidence bound (None for categorical).
74
+ method_agreement: Agreement between methods (0-1, higher is better).
75
+ individual_results: List of individual method results.
76
+ aggregation_method: Method used for aggregation.
77
+ quality_score: Optional quality score for the ensemble result.
78
+ outlier_methods: Indices of methods producing outlier results.
79
+
80
+ Example:
81
+ >>> if result.method_agreement > 0.8:
82
+ ... print(f"High agreement: {result.value}")
83
+ >>> else:
84
+ ... print(f"Methods disagree, confidence: {result.confidence}")
85
+ """
86
+
87
+ value: Any
88
+ confidence: float
89
+ lower_bound: float | None = None
90
+ upper_bound: float | None = None
91
+ method_agreement: float = 1.0
92
+ individual_results: list[dict[str, Any]] = field(default_factory=list)
93
+ aggregation_method: AggregationMethod = AggregationMethod.WEIGHTED_AVERAGE
94
+ quality_score: AnalysisQualityScore | None = None
95
+ outlier_methods: list[int] = field(default_factory=list)
96
+
97
+ def __post_init__(self) -> None:
98
+ """Validate confidence and agreement values."""
99
+ if not 0 <= self.confidence <= 1:
100
+ raise ValueError(f"Confidence must be in [0, 1], got {self.confidence}")
101
+ if not 0 <= self.method_agreement <= 1:
102
+ raise ValueError(f"Method agreement must be in [0, 1], got {self.method_agreement}")
103
+
104
+ def to_dict(self) -> dict[str, Any]:
105
+ """Convert to dictionary for serialization.
106
+
107
+ Returns:
108
+ Dictionary representation of ensemble result.
109
+ """
110
+ return {
111
+ "value": self.value,
112
+ "confidence": self.confidence,
113
+ "lower_bound": self.lower_bound,
114
+ "upper_bound": self.upper_bound,
115
+ "method_agreement": self.method_agreement,
116
+ "aggregation_method": self.aggregation_method.value,
117
+ "individual_results": self.individual_results,
118
+ "outlier_methods": self.outlier_methods,
119
+ "quality_score": self.quality_score.to_dict() if self.quality_score else None,
120
+ }
121
+
122
+
123
+ class EnsembleAggregator:
124
+ """Combines multiple analysis results for robust estimation.
125
+
126
+ Supports various aggregation strategies optimized for different data types
127
+ and analysis scenarios. Automatically detects and handles outliers, computes
128
+ confidence bounds, and measures inter-method agreement.
129
+
130
+ QUAL-004: Ensemble Methods for Robust Analysis
131
+ QUAL-005: Disagreement Detection and Handling
132
+ QUAL-006: Confidence Bound Estimation
133
+
134
+ Example:
135
+ >>> aggregator = EnsembleAggregator(method=AggregationMethod.WEIGHTED_AVERAGE)
136
+ >>> results = [
137
+ ... {"value": 100.0, "confidence": 0.9},
138
+ ... {"value": 102.0, "confidence": 0.85},
139
+ ... {"value": 98.0, "confidence": 0.8},
140
+ ... ]
141
+ >>> ensemble = aggregator.aggregate(results)
142
+ >>> print(f"Result: {ensemble.value:.2f} ± {ensemble.confidence*100:.1f}%")
143
+ """
144
+
145
+ def __init__(
146
+ self,
147
+ method: AggregationMethod = AggregationMethod.WEIGHTED_AVERAGE,
148
+ outlier_threshold: float = 3.0,
149
+ min_agreement: float = 0.5,
150
+ ):
151
+ """Initialize ensemble aggregator.
152
+
153
+ Args:
154
+ method: Aggregation strategy to use.
155
+ outlier_threshold: Z-score threshold for outlier detection (default 3.0).
156
+ min_agreement: Minimum agreement threshold to warn (default 0.5).
157
+ """
158
+ self.method = method
159
+ self.outlier_threshold = outlier_threshold
160
+ self.min_agreement = min_agreement
161
+
162
+ def aggregate(self, results: list[dict[str, Any]]) -> EnsembleResult:
163
+ """Combine multiple results into one robust estimate.
164
+
165
+ Args:
166
+ results: List of result dictionaries with keys:
167
+ - value: Measured value (numeric or categorical)
168
+ - confidence: Confidence score (0-1)
169
+ - method: Optional method name
170
+ - quality_score: Optional AnalysisQualityScore
171
+
172
+ Returns:
173
+ EnsembleResult with combined value and metadata.
174
+
175
+ Raises:
176
+ ValueError: If results list is empty or invalid.
177
+
178
+ Example:
179
+ >>> results = [
180
+ ... {"value": 1000, "confidence": 0.9, "method": "fft"},
181
+ ... {"value": 1005, "confidence": 0.85, "method": "autocorr"},
182
+ ... ]
183
+ >>> ensemble = aggregator.aggregate(results)
184
+ """
185
+ if not results:
186
+ raise ValueError("Cannot aggregate empty results list")
187
+
188
+ # Extract values and confidences
189
+ values = [r["value"] for r in results]
190
+ confidences = [r.get("confidence", 1.0) for r in results]
191
+
192
+ # Determine if values are numeric or categorical
193
+ is_numeric = all(isinstance(v, int | float | np.number) for v in values)
194
+
195
+ if is_numeric:
196
+ return self.aggregate_numeric(
197
+ [float(v) for v in values],
198
+ confidences,
199
+ original_results=results,
200
+ )
201
+ else:
202
+ return self.aggregate_categorical(
203
+ [str(v) for v in values],
204
+ confidences,
205
+ original_results=results,
206
+ )
207
+
208
+ def aggregate_numeric(
209
+ self,
210
+ values: list[float],
211
+ confidences: list[float],
212
+ original_results: list[dict[str, Any]] | None = None,
213
+ ) -> EnsembleResult:
214
+ """Combine numeric values with confidence weighting.
215
+
216
+ Args:
217
+ values: List of numeric values to combine.
218
+ confidences: Confidence scores for each value (0-1).
219
+ original_results: Optional original result dictionaries.
220
+
221
+ Returns:
222
+ EnsembleResult with aggregated numeric value.
223
+
224
+ Raises:
225
+ ValueError: If values list is empty.
226
+
227
+ Example:
228
+ >>> values = [100.0, 102.0, 98.0, 150.0] # 150 is outlier
229
+ >>> confidences = [0.9, 0.85, 0.8, 0.7]
230
+ >>> result = aggregator.aggregate_numeric(values, confidences)
231
+ >>> # Outlier detected and handled
232
+ """
233
+ if not values:
234
+ raise ValueError("Cannot aggregate empty values list")
235
+
236
+ if original_results is None:
237
+ original_results = [
238
+ {"value": v, "confidence": c} for v, c in zip(values, confidences, strict=False)
239
+ ]
240
+
241
+ values_arr = np.array(values, dtype=np.float64)
242
+ confidences_arr = np.array(confidences, dtype=np.float64)
243
+
244
+ # Detect outliers
245
+ outlier_indices = self.detect_outlier_methods(original_results)
246
+
247
+ # Create mask for non-outlier values
248
+ valid_mask = np.ones(len(values), dtype=bool)
249
+ valid_mask[outlier_indices] = False
250
+
251
+ # Use only non-outliers for aggregation
252
+ valid_values = values_arr[valid_mask]
253
+ valid_confidences = confidences_arr[valid_mask]
254
+
255
+ if len(valid_values) == 0:
256
+ # All values are outliers, use all with warning
257
+ logger.warning("All methods detected as outliers, using all values")
258
+ valid_values = values_arr
259
+ valid_confidences = confidences_arr
260
+ outlier_indices = []
261
+
262
+ # Compute aggregated value based on method
263
+ if self.method == AggregationMethod.WEIGHTED_AVERAGE:
264
+ # Normalize weights
265
+ weights = valid_confidences / np.sum(valid_confidences)
266
+ aggregated_value = float(np.sum(valid_values * weights))
267
+ # Weighted variance
268
+ variance = float(np.sum(weights * (valid_values - aggregated_value) ** 2))
269
+ std_dev = np.sqrt(variance)
270
+
271
+ elif self.method == AggregationMethod.MEDIAN:
272
+ aggregated_value = float(np.median(valid_values))
273
+ # Use MAD (Median Absolute Deviation) for robust std estimate
274
+ mad = float(np.median(np.abs(valid_values - aggregated_value)))
275
+ std_dev = mad * 1.4826 # Scale factor for normal distribution
276
+
277
+ elif self.method == AggregationMethod.BAYESIAN:
278
+ # Bayesian combination with Gaussian likelihood
279
+ # Prior: uniform over range
280
+ # Likelihood: Gaussian with confidence-based variance
281
+ precisions = valid_confidences**2 # Higher confidence = lower variance
282
+ total_precision = np.sum(precisions)
283
+ aggregated_value = float(np.sum(valid_values * precisions) / total_precision)
284
+ # Posterior variance
285
+ variance = 1.0 / total_precision
286
+ std_dev = float(np.sqrt(variance))
287
+
288
+ else:
289
+ # Fallback to simple average
290
+ aggregated_value = float(np.mean(valid_values))
291
+ std_dev = float(np.std(valid_values))
292
+
293
+ # Compute confidence bounds (95% confidence interval)
294
+ if len(valid_values) > 1:
295
+ # Use t-distribution for small samples
296
+ dof = len(valid_values) - 1
297
+ t_value = stats.t.ppf(0.975, dof) # 95% CI
298
+ margin = t_value * std_dev / np.sqrt(len(valid_values))
299
+ lower_bound = aggregated_value - margin
300
+ upper_bound = aggregated_value + margin
301
+ else:
302
+ lower_bound = aggregated_value
303
+ upper_bound = aggregated_value
304
+
305
+ # Compute method agreement (inverse of coefficient of variation)
306
+ if len(valid_values) > 1 and aggregated_value != 0:
307
+ cv = std_dev / abs(aggregated_value)
308
+ method_agreement = float(np.clip(1.0 - cv, 0.0, 1.0))
309
+ else:
310
+ method_agreement = 1.0
311
+
312
+ # Overall confidence (weighted average of individual confidences)
313
+ overall_confidence = float(np.mean(valid_confidences))
314
+
315
+ # Penalize confidence if agreement is low
316
+ if method_agreement < self.min_agreement:
317
+ overall_confidence *= method_agreement
318
+ logger.warning(
319
+ f"Low method agreement ({method_agreement:.2f}), "
320
+ f"reduced confidence to {overall_confidence:.2f}"
321
+ )
322
+
323
+ # Combine quality scores if available
324
+ quality_scores_raw = [
325
+ r.get("quality_score") for r in original_results if "quality_score" in r
326
+ ]
327
+ ensemble_quality = None
328
+ if quality_scores_raw and all(
329
+ isinstance(q, AnalysisQualityScore) for q in quality_scores_raw
330
+ ):
331
+ # Type narrowing - we know all are AnalysisQualityScore at this point
332
+ quality_scores: list[AnalysisQualityScore] = quality_scores_raw # type: ignore[assignment]
333
+ ensemble_quality = combine_quality_scores(
334
+ quality_scores, weights=confidences[: len(quality_scores)]
335
+ )
336
+
337
+ return EnsembleResult(
338
+ value=aggregated_value,
339
+ confidence=overall_confidence,
340
+ lower_bound=lower_bound,
341
+ upper_bound=upper_bound,
342
+ method_agreement=method_agreement,
343
+ individual_results=original_results,
344
+ aggregation_method=self.method,
345
+ quality_score=ensemble_quality,
346
+ outlier_methods=outlier_indices,
347
+ )
348
+
349
+ def aggregate_categorical(
350
+ self,
351
+ values: list[str],
352
+ confidences: list[float],
353
+ original_results: list[dict[str, Any]] | None = None,
354
+ ) -> EnsembleResult:
355
+ """Combine categorical values via weighted voting.
356
+
357
+ Args:
358
+ values: List of categorical values to combine.
359
+ confidences: Confidence scores for each value (0-1).
360
+ original_results: Optional original result dictionaries.
361
+
362
+ Returns:
363
+ EnsembleResult with majority vote value.
364
+
365
+ Raises:
366
+ ValueError: If values list is empty.
367
+
368
+ Example:
369
+ >>> values = ["rising", "rising", "falling", "rising"]
370
+ >>> confidences = [0.9, 0.85, 0.6, 0.8]
371
+ >>> result = aggregator.aggregate_categorical(values, confidences)
372
+ >>> # "rising" wins by weighted vote
373
+ """
374
+ if not values:
375
+ raise ValueError("Cannot aggregate empty values list")
376
+
377
+ if original_results is None:
378
+ original_results = [
379
+ {"value": v, "confidence": c} for v, c in zip(values, confidences, strict=False)
380
+ ]
381
+
382
+ # Weighted voting
383
+ vote_weights: dict[str, float] = {}
384
+ for value, confidence in zip(values, confidences, strict=False):
385
+ vote_weights[value] = vote_weights.get(value, 0.0) + confidence
386
+
387
+ # Get winner
388
+ winner = max(vote_weights.items(), key=lambda x: x[1])
389
+ aggregated_value = winner[0]
390
+ total_weight = sum(vote_weights.values())
391
+
392
+ # Confidence is the fraction of votes for winner
393
+ overall_confidence = winner[1] / total_weight if total_weight > 0 else 0.0
394
+
395
+ # Agreement is measured by vote concentration
396
+ # Higher agreement when votes are concentrated on one option
397
+ vote_counts = Counter(values)
398
+ total_votes = len(values)
399
+ max_count = vote_counts.most_common(1)[0][1]
400
+ method_agreement = max_count / total_votes
401
+
402
+ # Combine quality scores if available
403
+ quality_scores_raw = [
404
+ r.get("quality_score") for r in original_results if "quality_score" in r
405
+ ]
406
+ ensemble_quality = None
407
+ if quality_scores_raw and all(
408
+ isinstance(q, AnalysisQualityScore) for q in quality_scores_raw
409
+ ):
410
+ # Type narrowing - we know all are AnalysisQualityScore at this point
411
+ quality_scores: list[AnalysisQualityScore] = quality_scores_raw # type: ignore[assignment]
412
+ ensemble_quality = combine_quality_scores(
413
+ quality_scores, weights=confidences[: len(quality_scores)]
414
+ )
415
+
416
+ return EnsembleResult(
417
+ value=aggregated_value,
418
+ confidence=overall_confidence,
419
+ lower_bound=None,
420
+ upper_bound=None,
421
+ method_agreement=method_agreement,
422
+ individual_results=original_results,
423
+ aggregation_method=self.method,
424
+ quality_score=ensemble_quality,
425
+ outlier_methods=[], # No outlier detection for categorical
426
+ )
427
+
428
+ def detect_outlier_methods(self, results: list[dict[str, Any]]) -> list[int]:
429
+ """Identify methods producing outlier results.
430
+
431
+ Uses modified Z-score (based on MAD) for robust outlier detection.
432
+
433
+ Args:
434
+ results: List of result dictionaries with "value" key.
435
+
436
+ Returns:
437
+ List of indices corresponding to outlier methods.
438
+
439
+ Example:
440
+ >>> results = [
441
+ ... {"value": 100}, {"value": 102}, {"value": 98}, {"value": 500}
442
+ ... ]
443
+ >>> outliers = aggregator.detect_outlier_methods(results)
444
+ >>> # Returns [3] - the 500 value is an outlier
445
+ """
446
+ values = [r["value"] for r in results]
447
+
448
+ # Only works for numeric values
449
+ if not all(isinstance(v, int | float | np.number) for v in values):
450
+ return []
451
+
452
+ if len(values) < 3:
453
+ # Need at least 3 values for outlier detection
454
+ return []
455
+
456
+ values_arr = np.array(values, dtype=np.float64)
457
+
458
+ # Use modified Z-score based on MAD (robust to outliers)
459
+ median = np.median(values_arr)
460
+ mad = np.median(np.abs(values_arr - median))
461
+
462
+ if mad == 0:
463
+ # All values are identical
464
+ return []
465
+
466
+ # Modified Z-score
467
+ modified_z_scores = 0.6745 * (values_arr - median) / mad
468
+
469
+ # Identify outliers
470
+ outlier_mask = np.abs(modified_z_scores) > self.outlier_threshold
471
+ outlier_indices: list[int] = np.where(outlier_mask)[0].tolist()
472
+
473
+ if outlier_indices:
474
+ logger.info(f"Detected {len(outlier_indices)} outlier method(s): {outlier_indices}")
475
+
476
+ return outlier_indices
477
+
478
+
479
+ # Pre-configured ensembles for common analysis types
480
+ # Weights represent the relative reliability of each method
481
+
482
+ FREQUENCY_ENSEMBLE: list[tuple[str, float]] = [
483
+ ("fft_peak", 0.4), # FFT peak is generally most reliable
484
+ ("zero_crossing", 0.3), # Zero crossing is robust but can be noisy
485
+ ("autocorrelation", 0.3), # Autocorrelation handles noise well
486
+ ]
487
+
488
+ EDGE_DETECTION_ENSEMBLE: list[tuple[str, float]] = [
489
+ ("threshold_crossing", 0.5), # Most direct method
490
+ ("derivative", 0.3), # Good for clean signals
491
+ ("schmitt_trigger", 0.2), # Noise immunity but less precise
492
+ ]
493
+
494
+ AMPLITUDE_ENSEMBLE: list[tuple[str, float]] = [
495
+ ("peak_to_peak", 0.4), # Direct measurement
496
+ ("rms", 0.3), # Robust to noise
497
+ ("percentile_99", 0.3), # Outlier resistant
498
+ ]
499
+
500
+
501
+ def create_frequency_ensemble(
502
+ signal: NDArray[np.float64],
503
+ sample_rate: float,
504
+ method_weights: list[tuple[str, float]] | None = None,
505
+ ) -> EnsembleResult:
506
+ """Run multiple frequency detection methods and combine results.
507
+
508
+ Applies FFT peak detection, zero-crossing rate, and autocorrelation-based
509
+ frequency estimation, then combines using weighted averaging.
510
+
511
+ Args:
512
+ signal: Input signal array.
513
+ sample_rate: Sample rate in Hz.
514
+ method_weights: Optional custom method weights. Defaults to FREQUENCY_ENSEMBLE.
515
+
516
+ Returns:
517
+ EnsembleResult with combined frequency estimate.
518
+
519
+ Raises:
520
+ ValueError: If all frequency detection methods fail.
521
+
522
+ Example:
523
+ >>> import numpy as np
524
+ >>> t = np.linspace(0, 1, 1000)
525
+ >>> signal = np.sin(2 * np.pi * 10 * t) # 10 Hz sine
526
+ >>> result = create_frequency_ensemble(signal, sample_rate=1000)
527
+ >>> print(f"Frequency: {result.value:.2f} Hz")
528
+ >>> print(f"Confidence: {result.confidence:.2%}")
529
+ """
530
+ if method_weights is None:
531
+ method_weights = FREQUENCY_ENSEMBLE
532
+
533
+ results = []
534
+
535
+ # Method 1: FFT peak detection
536
+ try:
537
+ fft_result = np.fft.rfft(signal)
538
+ freqs = np.fft.rfftfreq(len(signal), d=1.0 / sample_rate)
539
+ peak_idx = np.argmax(np.abs(fft_result[1:])) + 1 # Skip DC
540
+ freq_fft = float(freqs[peak_idx])
541
+ # Confidence based on peak prominence
542
+ peak_magnitude = np.abs(fft_result[peak_idx])
543
+ mean_magnitude = np.mean(np.abs(fft_result[1:]))
544
+ confidence_fft = min(1.0, peak_magnitude / (mean_magnitude * 10))
545
+ results.append(
546
+ {
547
+ "value": freq_fft,
548
+ "confidence": confidence_fft * method_weights[0][1],
549
+ "method": "fft_peak",
550
+ }
551
+ )
552
+ except Exception as e:
553
+ logger.debug(f"FFT peak detection failed: {e}")
554
+
555
+ # Method 2: Zero crossing rate
556
+ try:
557
+ zero_crossings = np.where(np.diff(np.sign(signal)))[0]
558
+ if len(zero_crossings) > 1:
559
+ # Average time between zero crossings (half period)
560
+ avg_half_period = np.mean(np.diff(zero_crossings)) / sample_rate
561
+ freq_zc = 1.0 / (2.0 * avg_half_period)
562
+ # Confidence based on regularity of crossings
563
+ std_half_period = np.std(np.diff(zero_crossings)) / sample_rate
564
+ confidence_zc = max(0.0, 1.0 - std_half_period / avg_half_period)
565
+ results.append(
566
+ {
567
+ "value": float(freq_zc),
568
+ "confidence": confidence_zc * method_weights[1][1],
569
+ "method": "zero_crossing",
570
+ }
571
+ )
572
+ except Exception as e:
573
+ logger.debug(f"Zero crossing detection failed: {e}")
574
+
575
+ # Method 3: Autocorrelation
576
+ try:
577
+ # Compute autocorrelation
578
+ autocorr = np.correlate(signal, signal, mode="full")
579
+ autocorr = autocorr[len(autocorr) // 2 :]
580
+ # Find first peak after zero lag (skip DC)
581
+ peaks = []
582
+ for i in range(1, min(len(autocorr) - 1, len(signal) // 2)):
583
+ if autocorr[i] > autocorr[i - 1] and autocorr[i] > autocorr[i + 1]:
584
+ peaks.append(i)
585
+ if peaks:
586
+ first_peak = peaks[0]
587
+ period_samples = first_peak
588
+ freq_ac = sample_rate / period_samples
589
+ # Confidence based on peak strength
590
+ peak_strength = autocorr[first_peak] / autocorr[0]
591
+ confidence_ac = float(np.clip(peak_strength, 0.0, 1.0))
592
+ results.append(
593
+ {
594
+ "value": float(freq_ac),
595
+ "confidence": confidence_ac * method_weights[2][1],
596
+ "method": "autocorrelation",
597
+ }
598
+ )
599
+ except Exception as e:
600
+ logger.debug(f"Autocorrelation detection failed: {e}")
601
+
602
+ if not results:
603
+ raise ValueError("All frequency detection methods failed")
604
+
605
+ # Aggregate results
606
+ aggregator = EnsembleAggregator(method=AggregationMethod.WEIGHTED_AVERAGE)
607
+ return aggregator.aggregate(results)
608
+
609
+
610
+ def create_edge_ensemble(
611
+ signal: NDArray[np.float64],
612
+ sample_rate: float,
613
+ threshold: float | None = None,
614
+ method_weights: list[tuple[str, float]] | None = None,
615
+ ) -> EnsembleResult:
616
+ """Run multiple edge detection methods and combine results.
617
+
618
+ Applies threshold crossing, derivative-based, and Schmitt trigger edge
619
+ detection, then combines results using weighted voting or averaging.
620
+
621
+ Args:
622
+ signal: Input signal array.
623
+ sample_rate: Sample rate in Hz.
624
+ threshold: Detection threshold. If None, uses signal midpoint.
625
+ method_weights: Optional custom method weights. Defaults to EDGE_DETECTION_ENSEMBLE.
626
+
627
+ Returns:
628
+ EnsembleResult with combined edge detection results.
629
+
630
+ Raises:
631
+ ValueError: If all edge detection methods fail.
632
+
633
+ Example:
634
+ >>> signal = np.array([0, 0, 1, 1, 0, 0, 1, 1])
635
+ >>> result = create_edge_ensemble(signal, sample_rate=1000)
636
+ >>> print(f"Edge count: {result.value}")
637
+ >>> print(f"Agreement: {result.method_agreement:.2%}")
638
+ """
639
+ if method_weights is None:
640
+ method_weights = EDGE_DETECTION_ENSEMBLE
641
+
642
+ if threshold is None:
643
+ threshold = float((np.max(signal) + np.min(signal)) / 2.0)
644
+
645
+ results = []
646
+
647
+ # Method 1: Threshold crossing
648
+ try:
649
+ crossings = np.where(np.diff(np.sign(signal - threshold)))[0]
650
+ edge_count_tc = len(crossings)
651
+ # Confidence based on signal quality (SNR proxy)
652
+ signal_range = np.ptp(signal)
653
+ noise_estimate = np.std(np.diff(signal))
654
+ confidence_tc = (
655
+ min(1.0, signal_range / (noise_estimate * 10)) if noise_estimate > 0 else 0.5
656
+ )
657
+ results.append(
658
+ {
659
+ "value": edge_count_tc,
660
+ "confidence": confidence_tc * method_weights[0][1],
661
+ "method": "threshold_crossing",
662
+ }
663
+ )
664
+ except Exception as e:
665
+ logger.debug(f"Threshold crossing detection failed: {e}")
666
+
667
+ # Method 2: Derivative-based
668
+ try:
669
+ derivative = np.diff(signal)
670
+ # Find peaks in absolute derivative
671
+ deriv_std = np.std(derivative)
672
+ deriv_threshold = deriv_std * 2
673
+ edge_indices = np.where(np.abs(derivative) > deriv_threshold)[0]
674
+ # Remove consecutive detections (within 2 samples)
675
+ filtered_edges = []
676
+ for i, idx in enumerate(edge_indices):
677
+ if i == 0 or idx - edge_indices[i - 1] > 2:
678
+ filtered_edges.append(idx)
679
+ edge_count_deriv = len(filtered_edges)
680
+ # Confidence based on peak derivative prominence above threshold
681
+ # Higher max derivative relative to threshold means clearer edges
682
+ max_deriv = np.max(np.abs(derivative)) if len(derivative) > 0 else 0.0
683
+ prominence_ratio = (max_deriv / deriv_threshold) if deriv_threshold > 0 else 0.0
684
+ confidence_deriv = float(
685
+ np.clip(prominence_ratio / 3.0, 0.0, 1.0)
686
+ ) # Normalize: 3x threshold = 100%
687
+ results.append(
688
+ {
689
+ "value": edge_count_deriv,
690
+ "confidence": confidence_deriv * method_weights[1][1],
691
+ "method": "derivative",
692
+ }
693
+ )
694
+ except Exception as e:
695
+ logger.debug(f"Derivative edge detection failed: {e}")
696
+
697
+ # Method 3: Schmitt trigger (hysteresis)
698
+ try:
699
+ hysteresis = float(np.std(signal) * 0.1)
700
+ thresh_high = threshold + hysteresis
701
+ thresh_low = threshold - hysteresis
702
+ state = signal[0] > threshold
703
+ edge_count_schmitt = 0
704
+ for val in signal:
705
+ if not state and val > thresh_high:
706
+ edge_count_schmitt += 1
707
+ state = True
708
+ elif state and val < thresh_low:
709
+ edge_count_schmitt += 1
710
+ state = False
711
+ # Confidence based on hysteresis effectiveness
712
+ confidence_schmitt = 0.7 # Lower base confidence due to hysteresis delay
713
+ results.append(
714
+ {
715
+ "value": edge_count_schmitt,
716
+ "confidence": confidence_schmitt * method_weights[2][1],
717
+ "method": "schmitt_trigger",
718
+ }
719
+ )
720
+ except Exception as e:
721
+ logger.debug(f"Schmitt trigger detection failed: {e}")
722
+
723
+ if not results:
724
+ raise ValueError("All edge detection methods failed")
725
+
726
+ # Aggregate results (use median for integer counts)
727
+ aggregator = EnsembleAggregator(method=AggregationMethod.MEDIAN)
728
+ return aggregator.aggregate(results)
729
+
730
+
731
+ __all__ = [
732
+ "AMPLITUDE_ENSEMBLE",
733
+ "EDGE_DETECTION_ENSEMBLE",
734
+ "FREQUENCY_ENSEMBLE",
735
+ "AggregationMethod",
736
+ "EnsembleAggregator",
737
+ "EnsembleResult",
738
+ "create_edge_ensemble",
739
+ "create_frequency_ensemble",
740
+ ]