oscura 0.5.1__py3-none-any.whl → 0.7.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (497) hide show
  1. oscura/__init__.py +169 -167
  2. oscura/analyzers/__init__.py +3 -0
  3. oscura/analyzers/classification.py +659 -0
  4. oscura/analyzers/digital/edges.py +325 -65
  5. oscura/analyzers/digital/quality.py +293 -166
  6. oscura/analyzers/digital/timing.py +260 -115
  7. oscura/analyzers/digital/timing_numba.py +334 -0
  8. oscura/analyzers/entropy.py +605 -0
  9. oscura/analyzers/eye/diagram.py +176 -109
  10. oscura/analyzers/eye/metrics.py +5 -5
  11. oscura/analyzers/jitter/__init__.py +6 -4
  12. oscura/analyzers/jitter/ber.py +52 -52
  13. oscura/analyzers/jitter/classification.py +156 -0
  14. oscura/analyzers/jitter/decomposition.py +163 -113
  15. oscura/analyzers/jitter/spectrum.py +80 -64
  16. oscura/analyzers/ml/__init__.py +39 -0
  17. oscura/analyzers/ml/features.py +600 -0
  18. oscura/analyzers/ml/signal_classifier.py +604 -0
  19. oscura/analyzers/packet/daq.py +246 -158
  20. oscura/analyzers/packet/parser.py +12 -1
  21. oscura/analyzers/packet/payload.py +50 -2110
  22. oscura/analyzers/packet/payload_analysis.py +361 -181
  23. oscura/analyzers/packet/payload_patterns.py +133 -70
  24. oscura/analyzers/packet/stream.py +84 -23
  25. oscura/analyzers/patterns/__init__.py +26 -5
  26. oscura/analyzers/patterns/anomaly_detection.py +908 -0
  27. oscura/analyzers/patterns/clustering.py +169 -108
  28. oscura/analyzers/patterns/clustering_optimized.py +227 -0
  29. oscura/analyzers/patterns/discovery.py +1 -1
  30. oscura/analyzers/patterns/matching.py +581 -197
  31. oscura/analyzers/patterns/pattern_mining.py +778 -0
  32. oscura/analyzers/patterns/periodic.py +121 -38
  33. oscura/analyzers/patterns/sequences.py +175 -78
  34. oscura/analyzers/power/conduction.py +1 -1
  35. oscura/analyzers/power/soa.py +6 -6
  36. oscura/analyzers/power/switching.py +250 -110
  37. oscura/analyzers/protocol/__init__.py +17 -1
  38. oscura/analyzers/protocols/base.py +6 -6
  39. oscura/analyzers/protocols/ble/__init__.py +38 -0
  40. oscura/analyzers/protocols/ble/analyzer.py +809 -0
  41. oscura/analyzers/protocols/ble/uuids.py +288 -0
  42. oscura/analyzers/protocols/can.py +257 -127
  43. oscura/analyzers/protocols/can_fd.py +107 -80
  44. oscura/analyzers/protocols/flexray.py +139 -80
  45. oscura/analyzers/protocols/hdlc.py +93 -58
  46. oscura/analyzers/protocols/i2c.py +247 -106
  47. oscura/analyzers/protocols/i2s.py +138 -86
  48. oscura/analyzers/protocols/industrial/__init__.py +40 -0
  49. oscura/analyzers/protocols/industrial/bacnet/__init__.py +33 -0
  50. oscura/analyzers/protocols/industrial/bacnet/analyzer.py +708 -0
  51. oscura/analyzers/protocols/industrial/bacnet/encoding.py +412 -0
  52. oscura/analyzers/protocols/industrial/bacnet/services.py +622 -0
  53. oscura/analyzers/protocols/industrial/ethercat/__init__.py +30 -0
  54. oscura/analyzers/protocols/industrial/ethercat/analyzer.py +474 -0
  55. oscura/analyzers/protocols/industrial/ethercat/mailbox.py +339 -0
  56. oscura/analyzers/protocols/industrial/ethercat/topology.py +166 -0
  57. oscura/analyzers/protocols/industrial/modbus/__init__.py +31 -0
  58. oscura/analyzers/protocols/industrial/modbus/analyzer.py +525 -0
  59. oscura/analyzers/protocols/industrial/modbus/crc.py +79 -0
  60. oscura/analyzers/protocols/industrial/modbus/functions.py +436 -0
  61. oscura/analyzers/protocols/industrial/opcua/__init__.py +21 -0
  62. oscura/analyzers/protocols/industrial/opcua/analyzer.py +552 -0
  63. oscura/analyzers/protocols/industrial/opcua/datatypes.py +446 -0
  64. oscura/analyzers/protocols/industrial/opcua/services.py +264 -0
  65. oscura/analyzers/protocols/industrial/profinet/__init__.py +23 -0
  66. oscura/analyzers/protocols/industrial/profinet/analyzer.py +441 -0
  67. oscura/analyzers/protocols/industrial/profinet/dcp.py +263 -0
  68. oscura/analyzers/protocols/industrial/profinet/ptcp.py +200 -0
  69. oscura/analyzers/protocols/jtag.py +180 -98
  70. oscura/analyzers/protocols/lin.py +219 -114
  71. oscura/analyzers/protocols/manchester.py +4 -4
  72. oscura/analyzers/protocols/onewire.py +253 -149
  73. oscura/analyzers/protocols/parallel_bus/__init__.py +20 -0
  74. oscura/analyzers/protocols/parallel_bus/centronics.py +92 -0
  75. oscura/analyzers/protocols/parallel_bus/gpib.py +137 -0
  76. oscura/analyzers/protocols/spi.py +192 -95
  77. oscura/analyzers/protocols/swd.py +321 -167
  78. oscura/analyzers/protocols/uart.py +267 -125
  79. oscura/analyzers/protocols/usb.py +235 -131
  80. oscura/analyzers/side_channel/power.py +17 -12
  81. oscura/analyzers/signal/__init__.py +15 -0
  82. oscura/analyzers/signal/timing_analysis.py +1086 -0
  83. oscura/analyzers/signal_integrity/__init__.py +4 -1
  84. oscura/analyzers/signal_integrity/sparams.py +2 -19
  85. oscura/analyzers/spectral/chunked.py +129 -60
  86. oscura/analyzers/spectral/chunked_fft.py +300 -94
  87. oscura/analyzers/spectral/chunked_wavelet.py +100 -80
  88. oscura/analyzers/statistical/checksum.py +376 -217
  89. oscura/analyzers/statistical/classification.py +229 -107
  90. oscura/analyzers/statistical/entropy.py +78 -53
  91. oscura/analyzers/statistics/correlation.py +407 -211
  92. oscura/analyzers/statistics/outliers.py +2 -2
  93. oscura/analyzers/statistics/streaming.py +30 -5
  94. oscura/analyzers/validation.py +216 -101
  95. oscura/analyzers/waveform/measurements.py +9 -0
  96. oscura/analyzers/waveform/measurements_with_uncertainty.py +31 -15
  97. oscura/analyzers/waveform/spectral.py +500 -228
  98. oscura/api/__init__.py +31 -5
  99. oscura/api/dsl/__init__.py +582 -0
  100. oscura/{dsl → api/dsl}/commands.py +43 -76
  101. oscura/{dsl → api/dsl}/interpreter.py +26 -51
  102. oscura/{dsl → api/dsl}/parser.py +107 -77
  103. oscura/{dsl → api/dsl}/repl.py +2 -2
  104. oscura/api/dsl.py +1 -1
  105. oscura/{integrations → api/integrations}/__init__.py +1 -1
  106. oscura/{integrations → api/integrations}/llm.py +201 -102
  107. oscura/api/operators.py +3 -3
  108. oscura/api/optimization.py +144 -30
  109. oscura/api/rest_server.py +921 -0
  110. oscura/api/server/__init__.py +17 -0
  111. oscura/api/server/dashboard.py +850 -0
  112. oscura/api/server/static/README.md +34 -0
  113. oscura/api/server/templates/base.html +181 -0
  114. oscura/api/server/templates/export.html +120 -0
  115. oscura/api/server/templates/home.html +284 -0
  116. oscura/api/server/templates/protocols.html +58 -0
  117. oscura/api/server/templates/reports.html +43 -0
  118. oscura/api/server/templates/session_detail.html +89 -0
  119. oscura/api/server/templates/sessions.html +83 -0
  120. oscura/api/server/templates/waveforms.html +73 -0
  121. oscura/automotive/__init__.py +8 -1
  122. oscura/automotive/can/__init__.py +10 -0
  123. oscura/automotive/can/checksum.py +3 -1
  124. oscura/automotive/can/dbc_generator.py +590 -0
  125. oscura/automotive/can/message_wrapper.py +121 -74
  126. oscura/automotive/can/patterns.py +98 -21
  127. oscura/automotive/can/session.py +292 -56
  128. oscura/automotive/can/state_machine.py +6 -3
  129. oscura/automotive/can/stimulus_response.py +97 -75
  130. oscura/automotive/dbc/__init__.py +10 -2
  131. oscura/automotive/dbc/generator.py +84 -56
  132. oscura/automotive/dbc/parser.py +6 -6
  133. oscura/automotive/dtc/data.json +17 -102
  134. oscura/automotive/dtc/database.py +2 -2
  135. oscura/automotive/flexray/__init__.py +31 -0
  136. oscura/automotive/flexray/analyzer.py +504 -0
  137. oscura/automotive/flexray/crc.py +185 -0
  138. oscura/automotive/flexray/fibex.py +449 -0
  139. oscura/automotive/j1939/__init__.py +45 -8
  140. oscura/automotive/j1939/analyzer.py +605 -0
  141. oscura/automotive/j1939/spns.py +326 -0
  142. oscura/automotive/j1939/transport.py +306 -0
  143. oscura/automotive/lin/__init__.py +47 -0
  144. oscura/automotive/lin/analyzer.py +612 -0
  145. oscura/automotive/loaders/blf.py +13 -2
  146. oscura/automotive/loaders/csv_can.py +143 -72
  147. oscura/automotive/loaders/dispatcher.py +50 -2
  148. oscura/automotive/loaders/mdf.py +86 -45
  149. oscura/automotive/loaders/pcap.py +111 -61
  150. oscura/automotive/uds/__init__.py +4 -0
  151. oscura/automotive/uds/analyzer.py +725 -0
  152. oscura/automotive/uds/decoder.py +140 -58
  153. oscura/automotive/uds/models.py +7 -1
  154. oscura/automotive/visualization.py +1 -1
  155. oscura/cli/analyze.py +348 -0
  156. oscura/cli/batch.py +142 -122
  157. oscura/cli/benchmark.py +275 -0
  158. oscura/cli/characterize.py +137 -82
  159. oscura/cli/compare.py +224 -131
  160. oscura/cli/completion.py +250 -0
  161. oscura/cli/config_cmd.py +361 -0
  162. oscura/cli/decode.py +164 -87
  163. oscura/cli/export.py +286 -0
  164. oscura/cli/main.py +115 -31
  165. oscura/{onboarding → cli/onboarding}/__init__.py +3 -3
  166. oscura/{onboarding → cli/onboarding}/help.py +80 -58
  167. oscura/{onboarding → cli/onboarding}/tutorials.py +97 -72
  168. oscura/{onboarding → cli/onboarding}/wizard.py +55 -36
  169. oscura/cli/progress.py +147 -0
  170. oscura/cli/shell.py +157 -135
  171. oscura/cli/validate_cmd.py +204 -0
  172. oscura/cli/visualize.py +158 -0
  173. oscura/convenience.py +125 -79
  174. oscura/core/__init__.py +4 -2
  175. oscura/core/backend_selector.py +3 -3
  176. oscura/core/cache.py +126 -15
  177. oscura/core/cancellation.py +1 -1
  178. oscura/{config → core/config}/__init__.py +20 -11
  179. oscura/{config → core/config}/defaults.py +1 -1
  180. oscura/{config → core/config}/loader.py +7 -5
  181. oscura/{config → core/config}/memory.py +5 -5
  182. oscura/{config → core/config}/migration.py +1 -1
  183. oscura/{config → core/config}/pipeline.py +99 -23
  184. oscura/{config → core/config}/preferences.py +1 -1
  185. oscura/{config → core/config}/protocol.py +3 -3
  186. oscura/{config → core/config}/schema.py +426 -272
  187. oscura/{config → core/config}/settings.py +1 -1
  188. oscura/{config → core/config}/thresholds.py +195 -153
  189. oscura/core/correlation.py +5 -6
  190. oscura/core/cross_domain.py +0 -2
  191. oscura/core/debug.py +9 -5
  192. oscura/{extensibility → core/extensibility}/docs.py +158 -70
  193. oscura/{extensibility → core/extensibility}/extensions.py +160 -76
  194. oscura/{extensibility → core/extensibility}/logging.py +1 -1
  195. oscura/{extensibility → core/extensibility}/measurements.py +1 -1
  196. oscura/{extensibility → core/extensibility}/plugins.py +1 -1
  197. oscura/{extensibility → core/extensibility}/templates.py +73 -3
  198. oscura/{extensibility → core/extensibility}/validation.py +1 -1
  199. oscura/core/gpu_backend.py +11 -7
  200. oscura/core/log_query.py +101 -11
  201. oscura/core/logging.py +126 -54
  202. oscura/core/logging_advanced.py +5 -5
  203. oscura/core/memory_limits.py +108 -70
  204. oscura/core/memory_monitor.py +2 -2
  205. oscura/core/memory_progress.py +7 -7
  206. oscura/core/memory_warnings.py +1 -1
  207. oscura/core/numba_backend.py +13 -13
  208. oscura/{plugins → core/plugins}/__init__.py +9 -9
  209. oscura/{plugins → core/plugins}/base.py +7 -7
  210. oscura/{plugins → core/plugins}/cli.py +3 -3
  211. oscura/{plugins → core/plugins}/discovery.py +186 -106
  212. oscura/{plugins → core/plugins}/lifecycle.py +1 -1
  213. oscura/{plugins → core/plugins}/manager.py +7 -7
  214. oscura/{plugins → core/plugins}/registry.py +3 -3
  215. oscura/{plugins → core/plugins}/versioning.py +1 -1
  216. oscura/core/progress.py +16 -1
  217. oscura/core/provenance.py +8 -2
  218. oscura/{schemas → core/schemas}/__init__.py +2 -2
  219. oscura/{schemas → core/schemas}/device_mapping.json +2 -8
  220. oscura/{schemas → core/schemas}/packet_format.json +4 -24
  221. oscura/{schemas → core/schemas}/protocol_definition.json +2 -12
  222. oscura/core/types.py +4 -0
  223. oscura/core/uncertainty.py +3 -3
  224. oscura/correlation/__init__.py +52 -0
  225. oscura/correlation/multi_protocol.py +811 -0
  226. oscura/discovery/auto_decoder.py +117 -35
  227. oscura/discovery/comparison.py +191 -86
  228. oscura/discovery/quality_validator.py +155 -68
  229. oscura/discovery/signal_detector.py +196 -79
  230. oscura/export/__init__.py +18 -8
  231. oscura/export/kaitai_struct.py +513 -0
  232. oscura/export/scapy_layer.py +801 -0
  233. oscura/export/wireshark/generator.py +1 -1
  234. oscura/export/wireshark/templates/dissector.lua.j2 +2 -2
  235. oscura/export/wireshark_dissector.py +746 -0
  236. oscura/guidance/wizard.py +207 -111
  237. oscura/hardware/__init__.py +19 -0
  238. oscura/{acquisition → hardware/acquisition}/__init__.py +4 -4
  239. oscura/{acquisition → hardware/acquisition}/file.py +2 -2
  240. oscura/{acquisition → hardware/acquisition}/hardware.py +7 -7
  241. oscura/{acquisition → hardware/acquisition}/saleae.py +15 -12
  242. oscura/{acquisition → hardware/acquisition}/socketcan.py +1 -1
  243. oscura/{acquisition → hardware/acquisition}/streaming.py +2 -2
  244. oscura/{acquisition → hardware/acquisition}/synthetic.py +3 -3
  245. oscura/{acquisition → hardware/acquisition}/visa.py +33 -11
  246. oscura/hardware/firmware/__init__.py +29 -0
  247. oscura/hardware/firmware/pattern_recognition.py +874 -0
  248. oscura/hardware/hal_detector.py +736 -0
  249. oscura/hardware/security/__init__.py +37 -0
  250. oscura/hardware/security/side_channel_detector.py +1126 -0
  251. oscura/inference/__init__.py +4 -0
  252. oscura/inference/active_learning/observation_table.py +4 -1
  253. oscura/inference/alignment.py +216 -123
  254. oscura/inference/bayesian.py +113 -33
  255. oscura/inference/crc_reverse.py +101 -55
  256. oscura/inference/logic.py +6 -2
  257. oscura/inference/message_format.py +342 -183
  258. oscura/inference/protocol.py +95 -44
  259. oscura/inference/protocol_dsl.py +180 -82
  260. oscura/inference/signal_intelligence.py +1439 -706
  261. oscura/inference/spectral.py +99 -57
  262. oscura/inference/state_machine.py +810 -158
  263. oscura/inference/stream.py +270 -110
  264. oscura/iot/__init__.py +34 -0
  265. oscura/iot/coap/__init__.py +32 -0
  266. oscura/iot/coap/analyzer.py +668 -0
  267. oscura/iot/coap/options.py +212 -0
  268. oscura/iot/lorawan/__init__.py +21 -0
  269. oscura/iot/lorawan/crypto.py +206 -0
  270. oscura/iot/lorawan/decoder.py +801 -0
  271. oscura/iot/lorawan/mac_commands.py +341 -0
  272. oscura/iot/mqtt/__init__.py +27 -0
  273. oscura/iot/mqtt/analyzer.py +999 -0
  274. oscura/iot/mqtt/properties.py +315 -0
  275. oscura/iot/zigbee/__init__.py +31 -0
  276. oscura/iot/zigbee/analyzer.py +615 -0
  277. oscura/iot/zigbee/security.py +153 -0
  278. oscura/iot/zigbee/zcl.py +349 -0
  279. oscura/jupyter/display.py +125 -45
  280. oscura/{exploratory → jupyter/exploratory}/__init__.py +8 -8
  281. oscura/{exploratory → jupyter/exploratory}/error_recovery.py +298 -141
  282. oscura/jupyter/exploratory/fuzzy.py +746 -0
  283. oscura/{exploratory → jupyter/exploratory}/fuzzy_advanced.py +258 -100
  284. oscura/{exploratory → jupyter/exploratory}/legacy.py +464 -242
  285. oscura/{exploratory → jupyter/exploratory}/parse.py +167 -145
  286. oscura/{exploratory → jupyter/exploratory}/recovery.py +119 -87
  287. oscura/jupyter/exploratory/sync.py +612 -0
  288. oscura/{exploratory → jupyter/exploratory}/unknown.py +299 -176
  289. oscura/jupyter/magic.py +4 -4
  290. oscura/{ui → jupyter/ui}/__init__.py +2 -2
  291. oscura/{ui → jupyter/ui}/formatters.py +3 -3
  292. oscura/{ui → jupyter/ui}/progressive_display.py +153 -82
  293. oscura/loaders/__init__.py +183 -67
  294. oscura/loaders/binary.py +88 -1
  295. oscura/loaders/chipwhisperer.py +153 -137
  296. oscura/loaders/configurable.py +208 -86
  297. oscura/loaders/csv_loader.py +458 -215
  298. oscura/loaders/hdf5_loader.py +278 -119
  299. oscura/loaders/lazy.py +87 -54
  300. oscura/loaders/mmap_loader.py +1 -1
  301. oscura/loaders/numpy_loader.py +253 -116
  302. oscura/loaders/pcap.py +226 -151
  303. oscura/loaders/rigol.py +110 -49
  304. oscura/loaders/sigrok.py +201 -78
  305. oscura/loaders/tdms.py +81 -58
  306. oscura/loaders/tektronix.py +291 -174
  307. oscura/loaders/touchstone.py +182 -87
  308. oscura/loaders/tss.py +456 -0
  309. oscura/loaders/vcd.py +215 -117
  310. oscura/loaders/wav.py +155 -68
  311. oscura/reporting/__init__.py +9 -0
  312. oscura/reporting/analyze.py +352 -146
  313. oscura/reporting/argument_preparer.py +69 -14
  314. oscura/reporting/auto_report.py +97 -61
  315. oscura/reporting/batch.py +131 -58
  316. oscura/reporting/chart_selection.py +57 -45
  317. oscura/reporting/comparison.py +63 -17
  318. oscura/reporting/content/executive.py +76 -24
  319. oscura/reporting/core_formats/multi_format.py +11 -8
  320. oscura/reporting/engine.py +312 -158
  321. oscura/reporting/enhanced_reports.py +949 -0
  322. oscura/reporting/export.py +86 -43
  323. oscura/reporting/formatting/numbers.py +69 -42
  324. oscura/reporting/html.py +139 -58
  325. oscura/reporting/index.py +137 -65
  326. oscura/reporting/output.py +158 -67
  327. oscura/reporting/pdf.py +67 -102
  328. oscura/reporting/plots.py +191 -112
  329. oscura/reporting/sections.py +88 -47
  330. oscura/reporting/standards.py +104 -61
  331. oscura/reporting/summary_generator.py +75 -55
  332. oscura/reporting/tables.py +138 -54
  333. oscura/reporting/templates/enhanced/protocol_re.html +525 -0
  334. oscura/sessions/__init__.py +14 -23
  335. oscura/sessions/base.py +3 -3
  336. oscura/sessions/blackbox.py +106 -10
  337. oscura/sessions/generic.py +2 -2
  338. oscura/sessions/legacy.py +783 -0
  339. oscura/side_channel/__init__.py +63 -0
  340. oscura/side_channel/dpa.py +1025 -0
  341. oscura/utils/__init__.py +15 -1
  342. oscura/utils/bitwise.py +118 -0
  343. oscura/{builders → utils/builders}/__init__.py +1 -1
  344. oscura/{comparison → utils/comparison}/__init__.py +6 -6
  345. oscura/{comparison → utils/comparison}/compare.py +202 -101
  346. oscura/{comparison → utils/comparison}/golden.py +83 -63
  347. oscura/{comparison → utils/comparison}/limits.py +313 -89
  348. oscura/{comparison → utils/comparison}/mask.py +151 -45
  349. oscura/{comparison → utils/comparison}/trace_diff.py +1 -1
  350. oscura/{comparison → utils/comparison}/visualization.py +147 -89
  351. oscura/{component → utils/component}/__init__.py +3 -3
  352. oscura/{component → utils/component}/impedance.py +122 -58
  353. oscura/{component → utils/component}/reactive.py +165 -168
  354. oscura/{component → utils/component}/transmission_line.py +3 -3
  355. oscura/{filtering → utils/filtering}/__init__.py +6 -6
  356. oscura/{filtering → utils/filtering}/base.py +1 -1
  357. oscura/{filtering → utils/filtering}/convenience.py +2 -2
  358. oscura/{filtering → utils/filtering}/design.py +169 -93
  359. oscura/{filtering → utils/filtering}/filters.py +2 -2
  360. oscura/{filtering → utils/filtering}/introspection.py +2 -2
  361. oscura/utils/geometry.py +31 -0
  362. oscura/utils/imports.py +184 -0
  363. oscura/utils/lazy.py +1 -1
  364. oscura/{math → utils/math}/__init__.py +2 -2
  365. oscura/{math → utils/math}/arithmetic.py +114 -48
  366. oscura/{math → utils/math}/interpolation.py +139 -106
  367. oscura/utils/memory.py +129 -66
  368. oscura/utils/memory_advanced.py +92 -9
  369. oscura/utils/memory_extensions.py +10 -8
  370. oscura/{optimization → utils/optimization}/__init__.py +1 -1
  371. oscura/{optimization → utils/optimization}/search.py +2 -2
  372. oscura/utils/performance/__init__.py +58 -0
  373. oscura/utils/performance/caching.py +889 -0
  374. oscura/utils/performance/lsh_clustering.py +333 -0
  375. oscura/utils/performance/memory_optimizer.py +699 -0
  376. oscura/utils/performance/optimizations.py +675 -0
  377. oscura/utils/performance/parallel.py +654 -0
  378. oscura/utils/performance/profiling.py +661 -0
  379. oscura/{pipeline → utils/pipeline}/base.py +1 -1
  380. oscura/{pipeline → utils/pipeline}/composition.py +1 -1
  381. oscura/{pipeline → utils/pipeline}/parallel.py +3 -2
  382. oscura/{pipeline → utils/pipeline}/pipeline.py +1 -1
  383. oscura/{pipeline → utils/pipeline}/reverse_engineering.py +412 -221
  384. oscura/{search → utils/search}/__init__.py +3 -3
  385. oscura/{search → utils/search}/anomaly.py +188 -58
  386. oscura/utils/search/context.py +294 -0
  387. oscura/{search → utils/search}/pattern.py +138 -10
  388. oscura/utils/serial.py +51 -0
  389. oscura/utils/storage/__init__.py +61 -0
  390. oscura/utils/storage/database.py +1166 -0
  391. oscura/{streaming → utils/streaming}/chunked.py +302 -143
  392. oscura/{streaming → utils/streaming}/progressive.py +1 -1
  393. oscura/{streaming → utils/streaming}/realtime.py +3 -2
  394. oscura/{triggering → utils/triggering}/__init__.py +6 -6
  395. oscura/{triggering → utils/triggering}/base.py +6 -6
  396. oscura/{triggering → utils/triggering}/edge.py +2 -2
  397. oscura/{triggering → utils/triggering}/pattern.py +2 -2
  398. oscura/{triggering → utils/triggering}/pulse.py +115 -74
  399. oscura/{triggering → utils/triggering}/window.py +2 -2
  400. oscura/utils/validation.py +32 -0
  401. oscura/validation/__init__.py +121 -0
  402. oscura/{compliance → validation/compliance}/__init__.py +5 -5
  403. oscura/{compliance → validation/compliance}/advanced.py +5 -5
  404. oscura/{compliance → validation/compliance}/masks.py +1 -1
  405. oscura/{compliance → validation/compliance}/reporting.py +127 -53
  406. oscura/{compliance → validation/compliance}/testing.py +114 -52
  407. oscura/validation/compliance_tests.py +915 -0
  408. oscura/validation/fuzzer.py +990 -0
  409. oscura/validation/grammar_tests.py +596 -0
  410. oscura/validation/grammar_validator.py +904 -0
  411. oscura/validation/hil_testing.py +977 -0
  412. oscura/{quality → validation/quality}/__init__.py +4 -4
  413. oscura/{quality → validation/quality}/ensemble.py +251 -171
  414. oscura/{quality → validation/quality}/explainer.py +3 -3
  415. oscura/{quality → validation/quality}/scoring.py +1 -1
  416. oscura/{quality → validation/quality}/warnings.py +4 -4
  417. oscura/validation/regression_suite.py +808 -0
  418. oscura/validation/replay.py +788 -0
  419. oscura/{testing → validation/testing}/__init__.py +2 -2
  420. oscura/{testing → validation/testing}/synthetic.py +5 -5
  421. oscura/visualization/__init__.py +9 -0
  422. oscura/visualization/accessibility.py +1 -1
  423. oscura/visualization/annotations.py +64 -67
  424. oscura/visualization/colors.py +7 -7
  425. oscura/visualization/digital.py +180 -81
  426. oscura/visualization/eye.py +236 -85
  427. oscura/visualization/interactive.py +320 -143
  428. oscura/visualization/jitter.py +587 -247
  429. oscura/visualization/layout.py +169 -134
  430. oscura/visualization/optimization.py +103 -52
  431. oscura/visualization/palettes.py +1 -1
  432. oscura/visualization/power.py +427 -211
  433. oscura/visualization/power_extended.py +626 -297
  434. oscura/visualization/presets.py +2 -0
  435. oscura/visualization/protocols.py +495 -181
  436. oscura/visualization/render.py +79 -63
  437. oscura/visualization/reverse_engineering.py +171 -124
  438. oscura/visualization/signal_integrity.py +460 -279
  439. oscura/visualization/specialized.py +190 -100
  440. oscura/visualization/spectral.py +670 -255
  441. oscura/visualization/thumbnails.py +166 -137
  442. oscura/visualization/waveform.py +150 -63
  443. oscura/workflows/__init__.py +3 -0
  444. oscura/{batch → workflows/batch}/__init__.py +5 -5
  445. oscura/{batch → workflows/batch}/advanced.py +150 -75
  446. oscura/workflows/batch/aggregate.py +531 -0
  447. oscura/workflows/batch/analyze.py +236 -0
  448. oscura/{batch → workflows/batch}/logging.py +2 -2
  449. oscura/{batch → workflows/batch}/metrics.py +1 -1
  450. oscura/workflows/complete_re.py +1144 -0
  451. oscura/workflows/compliance.py +44 -54
  452. oscura/workflows/digital.py +197 -51
  453. oscura/workflows/legacy/__init__.py +12 -0
  454. oscura/{workflow → workflows/legacy}/dag.py +4 -1
  455. oscura/workflows/multi_trace.py +9 -9
  456. oscura/workflows/power.py +42 -62
  457. oscura/workflows/protocol.py +82 -49
  458. oscura/workflows/reverse_engineering.py +351 -150
  459. oscura/workflows/signal_integrity.py +157 -82
  460. oscura-0.7.0.dist-info/METADATA +661 -0
  461. oscura-0.7.0.dist-info/RECORD +591 -0
  462. oscura/batch/aggregate.py +0 -300
  463. oscura/batch/analyze.py +0 -139
  464. oscura/dsl/__init__.py +0 -73
  465. oscura/exceptions.py +0 -59
  466. oscura/exploratory/fuzzy.py +0 -513
  467. oscura/exploratory/sync.py +0 -384
  468. oscura/exporters/__init__.py +0 -94
  469. oscura/exporters/csv.py +0 -303
  470. oscura/exporters/exporters.py +0 -44
  471. oscura/exporters/hdf5.py +0 -217
  472. oscura/exporters/html_export.py +0 -701
  473. oscura/exporters/json_export.py +0 -291
  474. oscura/exporters/markdown_export.py +0 -367
  475. oscura/exporters/matlab_export.py +0 -354
  476. oscura/exporters/npz_export.py +0 -219
  477. oscura/exporters/spice_export.py +0 -210
  478. oscura/search/context.py +0 -149
  479. oscura/session/__init__.py +0 -34
  480. oscura/session/annotations.py +0 -289
  481. oscura/session/history.py +0 -313
  482. oscura/session/session.py +0 -520
  483. oscura/workflow/__init__.py +0 -13
  484. oscura-0.5.1.dist-info/METADATA +0 -583
  485. oscura-0.5.1.dist-info/RECORD +0 -481
  486. /oscura/core/{config.py → config/legacy.py} +0 -0
  487. /oscura/{extensibility → core/extensibility}/__init__.py +0 -0
  488. /oscura/{extensibility → core/extensibility}/registry.py +0 -0
  489. /oscura/{plugins → core/plugins}/isolation.py +0 -0
  490. /oscura/{schemas → core/schemas}/bus_configuration.json +0 -0
  491. /oscura/{builders → utils/builders}/signal_builder.py +0 -0
  492. /oscura/{optimization → utils/optimization}/parallel.py +0 -0
  493. /oscura/{pipeline → utils/pipeline}/__init__.py +0 -0
  494. /oscura/{streaming → utils/streaming}/__init__.py +0 -0
  495. {oscura-0.5.1.dist-info → oscura-0.7.0.dist-info}/WHEEL +0 -0
  496. {oscura-0.5.1.dist-info → oscura-0.7.0.dist-info}/entry_points.txt +0 -0
  497. {oscura-0.5.1.dist-info → oscura-0.7.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,6 +1,6 @@
1
1
  """Signal quality analysis module for Oscura."""
2
2
 
3
- from oscura.quality.ensemble import (
3
+ from oscura.validation.quality.ensemble import (
4
4
  AMPLITUDE_ENSEMBLE,
5
5
  EDGE_DETECTION_ENSEMBLE,
6
6
  FREQUENCY_ENSEMBLE,
@@ -10,12 +10,12 @@ from oscura.quality.ensemble import (
10
10
  create_edge_ensemble,
11
11
  create_frequency_ensemble,
12
12
  )
13
- from oscura.quality.explainer import (
13
+ from oscura.validation.quality.explainer import (
14
14
  ResultExplainer,
15
15
  ResultExplanation,
16
16
  explain_result,
17
17
  )
18
- from oscura.quality.scoring import (
18
+ from oscura.validation.quality.scoring import (
19
19
  AnalysisQualityScore,
20
20
  DataQualityMetrics,
21
21
  ReliabilityCategory,
@@ -24,7 +24,7 @@ from oscura.quality.scoring import (
24
24
  combine_quality_scores,
25
25
  score_analysis_result,
26
26
  )
27
- from oscura.quality.warnings import (
27
+ from oscura.validation.quality.warnings import (
28
28
  QualityWarning,
29
29
  SignalQualityAnalyzer,
30
30
  check_clipping,
@@ -6,8 +6,8 @@ bias, handle outliers, and provide confidence bounds for more reliable measureme
6
6
 
7
7
 
8
8
  Example:
9
- >>> from oscura.quality.ensemble import EnsembleAggregator, AggregationMethod
10
- >>> from oscura.quality.ensemble import create_frequency_ensemble
9
+ >>> from oscura.validation.quality.ensemble import EnsembleAggregator, AggregationMethod
10
+ >>> from oscura.validation.quality.ensemble import create_frequency_ensemble
11
11
  >>> # Combine multiple frequency measurements
12
12
  >>> result = create_frequency_ensemble(signal, sample_rate=1e9)
13
13
  >>> print(f"Frequency: {result.value:.2f} Hz ± {result.confidence*100:.1f}%")
@@ -38,7 +38,7 @@ from typing import TYPE_CHECKING, Any
38
38
  import numpy as np
39
39
  from scipy import stats
40
40
 
41
- from oscura.quality.scoring import AnalysisQualityScore, combine_quality_scores
41
+ from oscura.validation.quality.scoring import AnalysisQualityScore, combine_quality_scores
42
42
 
43
43
  if TYPE_CHECKING:
44
44
  from numpy.typing import NDArray
@@ -233,118 +233,143 @@ class EnsembleAggregator:
233
233
  if not values:
234
234
  raise ValueError("Cannot aggregate empty values list")
235
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
- ]
236
+ original_results = original_results or [
237
+ {"value": v, "confidence": c} for v, c in zip(values, confidences, strict=False)
238
+ ]
239
+
240
+ # Filter outliers
241
+ valid_values, valid_confidences, outlier_indices = self._filter_outliers(
242
+ values, confidences, original_results
243
+ )
244
+
245
+ # Compute aggregation
246
+ aggregated_value, std_dev = self._compute_aggregated_value(valid_values, valid_confidences)
247
+
248
+ # Compute confidence bounds and agreement
249
+ lower_bound, upper_bound = self._compute_confidence_bounds(
250
+ aggregated_value, std_dev, len(valid_values)
251
+ )
252
+ method_agreement = self._compute_method_agreement(valid_values, aggregated_value, std_dev)
253
+
254
+ # Overall confidence
255
+ overall_confidence = self._compute_overall_confidence(valid_confidences, method_agreement)
256
+
257
+ # Quality scores
258
+ ensemble_quality = self._combine_quality_scores(original_results, confidences)
259
+
260
+ return EnsembleResult(
261
+ value=aggregated_value,
262
+ confidence=overall_confidence,
263
+ lower_bound=lower_bound,
264
+ upper_bound=upper_bound,
265
+ method_agreement=method_agreement,
266
+ individual_results=original_results,
267
+ aggregation_method=self.method,
268
+ quality_score=ensemble_quality,
269
+ outlier_methods=outlier_indices,
270
+ )
240
271
 
272
+ def _filter_outliers(
273
+ self,
274
+ values: list[float],
275
+ confidences: list[float],
276
+ original_results: list[dict[str, Any]],
277
+ ) -> tuple[NDArray[np.float64], NDArray[np.float64], list[int]]:
278
+ """Filter outliers from values and return valid subsets."""
241
279
  values_arr = np.array(values, dtype=np.float64)
242
280
  confidences_arr = np.array(confidences, dtype=np.float64)
243
281
 
244
- # Detect outliers
245
282
  outlier_indices = self.detect_outlier_methods(original_results)
246
283
 
247
- # Create mask for non-outlier values
248
284
  valid_mask = np.ones(len(values), dtype=bool)
249
285
  valid_mask[outlier_indices] = False
250
286
 
251
- # Use only non-outliers for aggregation
252
287
  valid_values = values_arr[valid_mask]
253
288
  valid_confidences = confidences_arr[valid_mask]
254
289
 
255
290
  if len(valid_values) == 0:
256
- # All values are outliers, use all with warning
257
291
  logger.warning("All methods detected as outliers, using all values")
258
- valid_values = values_arr
259
- valid_confidences = confidences_arr
260
- outlier_indices = []
292
+ return values_arr, confidences_arr, []
293
+
294
+ return valid_values, valid_confidences, outlier_indices
261
295
 
262
- # Compute aggregated value based on method
296
+ def _compute_aggregated_value(
297
+ self, valid_values: NDArray[np.float64], valid_confidences: NDArray[np.float64]
298
+ ) -> tuple[float, float]:
299
+ """Compute aggregated value and standard deviation."""
263
300
  if self.method == AggregationMethod.WEIGHTED_AVERAGE:
264
- # Normalize weights
265
301
  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)
302
+ value = float(np.sum(valid_values * weights))
303
+ variance = float(np.sum(weights * (valid_values - value) ** 2))
304
+ return value, float(np.sqrt(variance))
270
305
 
271
306
  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
307
+ value = float(np.median(valid_values))
308
+ mad = float(np.median(np.abs(valid_values - value)))
309
+ return value, mad * 1.4826
276
310
 
277
311
  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
312
+ precisions = valid_confidences**2
282
313
  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
314
+ value = float(np.sum(valid_values * precisions) / total_precision)
315
+ std_dev = float(np.sqrt(1.0 / total_precision))
316
+ return value, std_dev
304
317
 
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
318
  else:
310
- method_agreement = 1.0
311
-
312
- # Overall confidence (weighted average of individual confidences)
313
- overall_confidence = float(np.mean(valid_confidences))
319
+ return float(np.mean(valid_values)), float(np.std(valid_values))
320
+
321
+ def _compute_confidence_bounds(
322
+ self, value: float, std_dev: float, n_values: int
323
+ ) -> tuple[float, float]:
324
+ """Compute 95% confidence interval bounds."""
325
+ if n_values <= 1:
326
+ return value, value
327
+
328
+ dof = n_values - 1
329
+ t_value = stats.t.ppf(0.975, dof)
330
+ margin = t_value * std_dev / np.sqrt(n_values)
331
+ return value - margin, value + margin
332
+
333
+ def _compute_method_agreement(
334
+ self, valid_values: NDArray[np.float64], value: float, std_dev: float
335
+ ) -> float:
336
+ """Compute method agreement from coefficient of variation."""
337
+ if len(valid_values) <= 1 or value == 0:
338
+ return 1.0
339
+
340
+ cv = std_dev / abs(value)
341
+ return float(np.clip(1.0 - cv, 0.0, 1.0))
342
+
343
+ def _compute_overall_confidence(
344
+ self, valid_confidences: NDArray[np.float64], method_agreement: float
345
+ ) -> float:
346
+ """Compute overall confidence with agreement penalty."""
347
+ confidence = float(np.mean(valid_confidences))
314
348
 
315
- # Penalize confidence if agreement is low
316
349
  if method_agreement < self.min_agreement:
317
- overall_confidence *= method_agreement
350
+ confidence *= method_agreement
318
351
  logger.warning(
319
352
  f"Low method agreement ({method_agreement:.2f}), "
320
- f"reduced confidence to {overall_confidence:.2f}"
353
+ f"reduced confidence to {confidence:.2f}"
321
354
  )
322
355
 
323
- # Combine quality scores if available
356
+ return confidence
357
+
358
+ def _combine_quality_scores(
359
+ self, original_results: list[dict[str, Any]], confidences: list[float]
360
+ ) -> AnalysisQualityScore | None:
361
+ """Combine quality scores if available."""
324
362
  quality_scores_raw = [
325
363
  r.get("quality_score") for r in original_results if "quality_score" in r
326
364
  ]
327
- ensemble_quality = None
328
- if quality_scores_raw and all(
365
+
366
+ if not quality_scores_raw or not all(
329
367
  isinstance(q, AnalysisQualityScore) for q in quality_scores_raw
330
368
  ):
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
- )
369
+ return None
336
370
 
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
- )
371
+ quality_scores: list[AnalysisQualityScore] = quality_scores_raw # type: ignore[assignment]
372
+ return combine_quality_scores(quality_scores, weights=confidences[: len(quality_scores)])
348
373
 
349
374
  def aggregate_categorical(
350
375
  self,
@@ -517,7 +542,7 @@ def create_frequency_ensemble(
517
542
  EnsembleResult with combined frequency estimate.
518
543
 
519
544
  Raises:
520
- ValueError: If all frequency detection methods fail.
545
+ ValueError: If all frequency detection methods failed.
521
546
 
522
547
  Example:
523
548
  >>> import numpy as np
@@ -531,80 +556,96 @@ def create_frequency_ensemble(
531
556
  method_weights = FREQUENCY_ENSEMBLE
532
557
 
533
558
  results = []
559
+ results.extend(_try_fft_frequency(signal, sample_rate, method_weights[0][1]))
560
+ results.extend(_try_zero_crossing_frequency(signal, sample_rate, method_weights[1][1]))
561
+ results.extend(_try_autocorr_frequency(signal, sample_rate, method_weights[2][1]))
562
+
563
+ if not results:
564
+ raise ValueError("All frequency detection methods failed")
565
+
566
+ aggregator = EnsembleAggregator(method=AggregationMethod.WEIGHTED_AVERAGE)
567
+ return aggregator.aggregate(results)
534
568
 
535
- # Method 1: FFT peak detection
569
+
570
+ def _try_fft_frequency(
571
+ signal: NDArray[np.float64], sample_rate: float, weight: float
572
+ ) -> list[dict[str, Any]]:
573
+ """Try FFT peak detection for frequency estimation."""
536
574
  try:
537
575
  fft_result = np.fft.rfft(signal)
538
576
  freqs = np.fft.rfftfreq(len(signal), d=1.0 / sample_rate)
539
577
  peak_idx = np.argmax(np.abs(fft_result[1:])) + 1 # Skip DC
540
578
  freq_fft = float(freqs[peak_idx])
579
+
541
580
  # Confidence based on peak prominence
542
581
  peak_magnitude = np.abs(fft_result[peak_idx])
543
582
  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
- )
583
+ confidence = min(1.0, peak_magnitude / (mean_magnitude * 10))
584
+
585
+ return [{"value": freq_fft, "confidence": confidence * weight, "method": "fft_peak"}]
552
586
  except Exception as e:
553
587
  logger.debug(f"FFT peak detection failed: {e}")
588
+ return []
554
589
 
555
- # Method 2: Zero crossing rate
590
+
591
+ def _try_zero_crossing_frequency(
592
+ signal: NDArray[np.float64], sample_rate: float, weight: float
593
+ ) -> list[dict[str, Any]]:
594
+ """Try zero crossing rate for frequency estimation."""
556
595
  try:
557
596
  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
- )
597
+ if len(zero_crossings) <= 1:
598
+ return []
599
+
600
+ # Average time between zero crossings (half period)
601
+ avg_half_period = np.mean(np.diff(zero_crossings)) / sample_rate
602
+ freq_zc = 1.0 / (2.0 * avg_half_period)
603
+
604
+ # Confidence based on regularity of crossings
605
+ std_half_period = np.std(np.diff(zero_crossings)) / sample_rate
606
+ confidence = max(0.0, 1.0 - std_half_period / avg_half_period)
607
+
608
+ return [
609
+ {"value": float(freq_zc), "confidence": confidence * weight, "method": "zero_crossing"}
610
+ ]
572
611
  except Exception as e:
573
612
  logger.debug(f"Zero crossing detection failed: {e}")
613
+ return []
574
614
 
575
- # Method 3: Autocorrelation
615
+
616
+ def _try_autocorr_frequency(
617
+ signal: NDArray[np.float64], sample_rate: float, weight: float
618
+ ) -> list[dict[str, Any]]:
619
+ """Try autocorrelation for frequency estimation."""
576
620
  try:
577
- # Compute autocorrelation
578
621
  autocorr = np.correlate(signal, signal, mode="full")
579
622
  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
623
 
602
- if not results:
603
- raise ValueError("All frequency detection methods failed")
624
+ # Find first peak after zero lag
625
+ peaks = [
626
+ i
627
+ for i in range(1, min(len(autocorr) - 1, len(signal) // 2))
628
+ if autocorr[i] > autocorr[i - 1] and autocorr[i] > autocorr[i + 1]
629
+ ]
604
630
 
605
- # Aggregate results
606
- aggregator = EnsembleAggregator(method=AggregationMethod.WEIGHTED_AVERAGE)
607
- return aggregator.aggregate(results)
631
+ if not peaks:
632
+ return []
633
+
634
+ first_peak = peaks[0]
635
+ freq_ac = sample_rate / first_peak
636
+ peak_strength = autocorr[first_peak] / autocorr[0]
637
+ confidence = float(np.clip(peak_strength, 0.0, 1.0))
638
+
639
+ return [
640
+ {
641
+ "value": float(freq_ac),
642
+ "confidence": confidence * weight,
643
+ "method": "autocorrelation",
644
+ }
645
+ ]
646
+ except Exception as e:
647
+ logger.debug(f"Autocorrelation detection failed: {e}")
648
+ return []
608
649
 
609
650
 
610
651
  def create_edge_ensemble(
@@ -643,89 +684,128 @@ def create_edge_ensemble(
643
684
  threshold = float((np.max(signal) + np.min(signal)) / 2.0)
644
685
 
645
686
  results = []
687
+ results.extend(_detect_threshold_crossing(signal, threshold, method_weights[0][1]))
688
+ results.extend(_detect_derivative_edges(signal, method_weights[1][1]))
689
+ results.extend(_detect_schmitt_trigger(signal, threshold, method_weights[2][1]))
646
690
 
647
- # Method 1: Threshold crossing
691
+ if not results:
692
+ raise ValueError("All edge detection methods failed")
693
+
694
+ aggregator = EnsembleAggregator(method=AggregationMethod.MEDIAN)
695
+ return aggregator.aggregate(results)
696
+
697
+
698
+ def _detect_threshold_crossing(
699
+ signal: NDArray[np.float64], threshold: float, weight: float
700
+ ) -> list[dict[str, Any]]:
701
+ """Detect edges via threshold crossing.
702
+
703
+ Args:
704
+ signal: Input signal.
705
+ threshold: Detection threshold.
706
+ weight: Method weight for confidence.
707
+
708
+ Returns:
709
+ List with single result dict or empty list if failed.
710
+ """
648
711
  try:
649
712
  crossings = np.where(np.diff(np.sign(signal - threshold)))[0]
650
- edge_count_tc = len(crossings)
651
- # Confidence based on signal quality (SNR proxy)
713
+ edge_count = len(crossings)
714
+
652
715
  signal_range = np.ptp(signal)
653
716
  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(
717
+ confidence = min(1.0, signal_range / (noise_estimate * 10)) if noise_estimate > 0 else 0.5
718
+
719
+ return [
658
720
  {
659
- "value": edge_count_tc,
660
- "confidence": confidence_tc * method_weights[0][1],
721
+ "value": edge_count,
722
+ "confidence": confidence * weight,
661
723
  "method": "threshold_crossing",
662
724
  }
663
- )
725
+ ]
664
726
  except Exception as e:
665
727
  logger.debug(f"Threshold crossing detection failed: {e}")
728
+ return []
729
+
730
+
731
+ def _detect_derivative_edges(signal: NDArray[np.float64], weight: float) -> list[dict[str, Any]]:
732
+ """Detect edges via derivative peak detection.
733
+
734
+ Args:
735
+ signal: Input signal.
736
+ weight: Method weight for confidence.
666
737
 
667
- # Method 2: Derivative-based
738
+ Returns:
739
+ List with single result dict or empty list if failed.
740
+ """
668
741
  try:
669
742
  derivative = np.diff(signal)
670
- # Find peaks in absolute derivative
671
743
  deriv_std = np.std(derivative)
672
744
  deriv_threshold = deriv_std * 2
673
745
  edge_indices = np.where(np.abs(derivative) > deriv_threshold)[0]
746
+
674
747
  # Remove consecutive detections (within 2 samples)
675
748
  filtered_edges = []
676
749
  for i, idx in enumerate(edge_indices):
677
750
  if i == 0 or idx - edge_indices[i - 1] > 2:
678
751
  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
752
+
682
753
  max_deriv = np.max(np.abs(derivative)) if len(derivative) > 0 else 0.0
683
754
  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(
755
+ confidence = float(np.clip(prominence_ratio / 3.0, 0.0, 1.0))
756
+
757
+ return [
688
758
  {
689
- "value": edge_count_deriv,
690
- "confidence": confidence_deriv * method_weights[1][1],
759
+ "value": len(filtered_edges),
760
+ "confidence": confidence * weight,
691
761
  "method": "derivative",
692
762
  }
693
- )
763
+ ]
694
764
  except Exception as e:
695
765
  logger.debug(f"Derivative edge detection failed: {e}")
766
+ return []
767
+
696
768
 
697
- # Method 3: Schmitt trigger (hysteresis)
769
+ def _detect_schmitt_trigger(
770
+ signal: NDArray[np.float64], threshold: float, weight: float
771
+ ) -> list[dict[str, Any]]:
772
+ """Detect edges via Schmitt trigger with hysteresis.
773
+
774
+ Args:
775
+ signal: Input signal.
776
+ threshold: Base threshold.
777
+ weight: Method weight for confidence.
778
+
779
+ Returns:
780
+ List with single result dict or empty list if failed.
781
+ """
698
782
  try:
699
783
  hysteresis = float(np.std(signal) * 0.1)
700
784
  thresh_high = threshold + hysteresis
701
785
  thresh_low = threshold - hysteresis
702
786
  state = signal[0] > threshold
703
- edge_count_schmitt = 0
787
+ edge_count = 0
788
+
704
789
  for val in signal:
705
790
  if not state and val > thresh_high:
706
- edge_count_schmitt += 1
791
+ edge_count += 1
707
792
  state = True
708
793
  elif state and val < thresh_low:
709
- edge_count_schmitt += 1
794
+ edge_count += 1
710
795
  state = False
711
- # Confidence based on hysteresis effectiveness
712
- confidence_schmitt = 0.7 # Lower base confidence due to hysteresis delay
713
- results.append(
796
+
797
+ confidence = 0.7 # Lower base confidence due to hysteresis delay
798
+
799
+ return [
714
800
  {
715
- "value": edge_count_schmitt,
716
- "confidence": confidence_schmitt * method_weights[2][1],
801
+ "value": edge_count,
802
+ "confidence": confidence * weight,
717
803
  "method": "schmitt_trigger",
718
804
  }
719
- )
805
+ ]
720
806
  except Exception as e:
721
807
  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)
808
+ return []
729
809
 
730
810
 
731
811
  __all__ = [
@@ -4,8 +4,8 @@ Generates human-readable explanations for why analysis results
4
4
  are reliable or unreliable.
5
5
 
6
6
  Example:
7
- >>> from oscura.quality.explainer import explain_result
8
- >>> from oscura.quality.scoring import calculate_quality_score
7
+ >>> from oscura.validation.quality.explainer import explain_result
8
+ >>> from oscura.validation.quality.scoring import calculate_quality_score
9
9
  >>> score = calculate_quality_score(0.9, 0.8, 0.85)
10
10
  >>> explanation = explain_result("frequency", 10.5e6, score, "fft")
11
11
  >>> print(explanation)
@@ -20,7 +20,7 @@ from dataclasses import dataclass, field
20
20
  from typing import TYPE_CHECKING, Any, ClassVar
21
21
 
22
22
  if TYPE_CHECKING:
23
- from oscura.quality.scoring import AnalysisQualityScore
23
+ from oscura.validation.quality.scoring import AnalysisQualityScore
24
24
 
25
25
 
26
26
  @dataclass
@@ -5,7 +5,7 @@ analysis results, enabling users to assess confidence in automated findings.
5
5
 
6
6
 
7
7
  Example:
8
- >>> from oscura.quality.scoring import AnalysisQualityScore, ReliabilityCategory
8
+ >>> from oscura.validation.quality.scoring import AnalysisQualityScore, ReliabilityCategory
9
9
  >>> score = AnalysisQualityScore(
10
10
  ... confidence=0.85,
11
11
  ... category=ReliabilityCategory.HIGH,
@@ -5,7 +5,7 @@ including clipping, noise, saturation, and undersampling.
5
5
 
6
6
 
7
7
  Example:
8
- >>> from oscura.quality.warnings import SignalQualityAnalyzer
8
+ >>> from oscura.validation.quality.warnings import SignalQualityAnalyzer
9
9
  >>> analyzer = SignalQualityAnalyzer()
10
10
  >>> warnings = analyzer.analyze(trace)
11
11
  >>> for warning in warnings:
@@ -91,7 +91,7 @@ class SignalQualityAnalyzer:
91
91
  nyquist_factor: Factor for Nyquist frequency check (default: 2.0)
92
92
 
93
93
  Example:
94
- >>> from oscura.quality.warnings import SignalQualityAnalyzer
94
+ >>> from oscura.validation.quality.warnings import SignalQualityAnalyzer
95
95
  >>> analyzer = SignalQualityAnalyzer(clip_threshold=0.95)
96
96
  >>> warnings = analyzer.analyze(trace)
97
97
  >>> if warnings:
@@ -152,9 +152,9 @@ class SignalQualityAnalyzer:
152
152
  """
153
153
  # Extract data and sample rate
154
154
  if hasattr(trace, "data"):
155
- data = trace.data # type: ignore[ignore-without-code]
155
+ data = trace.data
156
156
  if sample_rate is None and hasattr(trace, "metadata"):
157
- sample_rate = trace.metadata.sample_rate # type: ignore[ignore-without-code]
157
+ sample_rate = trace.metadata.sample_rate
158
158
  else:
159
159
  data = trace # type: ignore[assignment]
160
160