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
@@ -18,7 +18,7 @@ References:
18
18
  from __future__ import annotations
19
19
 
20
20
  from dataclasses import dataclass
21
- from typing import TYPE_CHECKING, Literal
21
+ from typing import TYPE_CHECKING, Any, Literal
22
22
 
23
23
  import numpy as np
24
24
 
@@ -325,11 +325,12 @@ def slew_rate(
325
325
  Args:
326
326
  trace: Input waveform trace.
327
327
  ref_levels: Reference levels as fractions (default 20%-80%).
328
- edge_type: Type of edges to measure.
328
+ edge_type: Type of edges to measure ("rising", "falling", or "both").
329
329
  return_all: If True, return array of all slew rates. If False, return mean.
330
330
 
331
331
  Returns:
332
332
  Slew rate in V/s (positive for rising, negative for falling).
333
+ Returns NaN if no transitions found or amplitude is zero.
333
334
 
334
335
  Example:
335
336
  >>> sr = slew_rate(trace)
@@ -339,69 +340,107 @@ def slew_rate(
339
340
  IEEE 181-2011 Section 5.2
340
341
  """
341
342
  if len(trace.data) < 3:
342
- if return_all:
343
- return np.array([], dtype=np.float64)
344
- return np.nan
343
+ return np.array([], dtype=np.float64) if return_all else np.nan
345
344
 
346
345
  data = trace.data
347
346
  sample_period = trace.metadata.time_base
348
347
 
349
- # Find signal levels
348
+ # Find signal levels and validate
350
349
  low, high = _find_levels(data)
351
350
  amplitude = high - low
352
351
 
353
352
  if amplitude <= 0:
354
- if return_all:
355
- return np.array([], dtype=np.float64)
356
- return np.nan
353
+ return np.array([], dtype=np.float64) if return_all else np.nan
357
354
 
358
355
  # Calculate reference voltages
359
356
  v_low = low + ref_levels[0] * amplitude
360
357
  v_high = low + ref_levels[1] * amplitude
361
358
  dv = v_high - v_low
362
359
 
360
+ # Measure slew rates for requested edge types
363
361
  slew_rates: list[float] = []
364
362
 
365
363
  if edge_type in ("rising", "both"):
366
- # Find rising transitions
367
- rising_start = np.where((data[:-1] < v_low) & (data[1:] >= v_low))[0]
364
+ slew_rates.extend(_measure_rising_slew_rates(data, v_low, v_high, dv, sample_period))
368
365
 
369
- for start_idx in rising_start:
370
- # Find where signal reaches v_high
371
- remaining = data[start_idx:]
372
- above_high = remaining >= v_high
366
+ if edge_type in ("falling", "both"):
367
+ slew_rates.extend(_measure_falling_slew_rates(data, v_low, v_high, dv, sample_period))
373
368
 
374
- if np.any(above_high):
375
- end_offset = np.argmax(above_high)
376
- dt = end_offset * sample_period
377
- if dt > 0:
378
- slew_rates.append(float(dv / dt))
369
+ if len(slew_rates) == 0:
370
+ return np.array([], dtype=np.float64) if return_all else np.nan
379
371
 
380
- if edge_type in ("falling", "both"):
381
- # Find falling transitions
382
- falling_start = np.where((data[:-1] > v_high) & (data[1:] <= v_high))[0]
372
+ result = np.array(slew_rates, dtype=np.float64)
373
+ return result if return_all else float(np.mean(result))
383
374
 
384
- for start_idx in falling_start:
385
- # Find where signal reaches v_low
386
- remaining = data[start_idx:]
387
- below_low = remaining <= v_low
388
375
 
389
- if np.any(below_low):
390
- end_offset = np.argmax(below_low)
391
- dt = end_offset * sample_period
392
- if dt > 0:
393
- slew_rates.append(float(-dv / dt)) # Negative for falling
376
+ def _measure_rising_slew_rates(
377
+ data: NDArray[np.float64],
378
+ v_low: float,
379
+ v_high: float,
380
+ dv: float,
381
+ sample_period: float,
382
+ ) -> list[float]:
383
+ """Measure slew rates for rising edges.
394
384
 
395
- if len(slew_rates) == 0:
396
- if return_all:
397
- return np.array([], dtype=np.float64)
398
- return np.nan
385
+ Args:
386
+ data: Signal data.
387
+ v_low: Low reference voltage.
388
+ v_high: High reference voltage.
389
+ dv: Voltage difference between reference levels.
390
+ sample_period: Time between samples.
399
391
 
400
- result = np.array(slew_rates, dtype=np.float64)
392
+ Returns:
393
+ List of rising slew rates (V/s).
394
+ """
395
+ slew_rates: list[float] = []
396
+ rising_start = np.where((data[:-1] < v_low) & (data[1:] >= v_low))[0]
401
397
 
402
- if return_all:
403
- return result
404
- return float(np.mean(result))
398
+ for start_idx in rising_start:
399
+ remaining = data[start_idx:]
400
+ above_high = remaining >= v_high
401
+
402
+ if np.any(above_high):
403
+ end_offset = np.argmax(above_high)
404
+ dt = end_offset * sample_period
405
+ if dt > 0:
406
+ slew_rates.append(float(dv / dt))
407
+
408
+ return slew_rates
409
+
410
+
411
+ def _measure_falling_slew_rates(
412
+ data: NDArray[np.float64],
413
+ v_low: float,
414
+ v_high: float,
415
+ dv: float,
416
+ sample_period: float,
417
+ ) -> list[float]:
418
+ """Measure slew rates for falling edges.
419
+
420
+ Args:
421
+ data: Signal data.
422
+ v_low: Low reference voltage.
423
+ v_high: High reference voltage.
424
+ dv: Voltage difference between reference levels.
425
+ sample_period: Time between samples.
426
+
427
+ Returns:
428
+ List of falling slew rates (negative V/s).
429
+ """
430
+ slew_rates: list[float] = []
431
+ falling_start = np.where((data[:-1] > v_high) & (data[1:] <= v_high))[0]
432
+
433
+ for start_idx in falling_start:
434
+ remaining = data[start_idx:]
435
+ below_low = remaining <= v_low
436
+
437
+ if np.any(below_low):
438
+ end_offset = np.argmax(below_low)
439
+ dt = end_offset * sample_period
440
+ if dt > 0:
441
+ slew_rates.append(float(-dv / dt)) # Negative for falling
442
+
443
+ return slew_rates
405
444
 
406
445
 
407
446
  def phase(
@@ -456,13 +495,13 @@ def _phase_edge(
456
495
  edges2 = _get_edge_timestamps(trace2, "rising", 0.5)
457
496
 
458
497
  if len(edges1) < 2 or len(edges2) < 2:
459
- return np.nan # type: ignore[no-any-return]
498
+ return np.nan
460
499
 
461
500
  # Calculate period from first signal
462
501
  period1 = np.mean(np.diff(edges1))
463
502
 
464
503
  if period1 <= 0:
465
- return np.nan # type: ignore[no-any-return]
504
+ return np.nan
466
505
 
467
506
  # Calculate phase from edge differences
468
507
  phase_times: list[float] = []
@@ -475,7 +514,7 @@ def _phase_edge(
475
514
  phase_times.append(diffs[idx])
476
515
 
477
516
  if len(phase_times) == 0:
478
- return np.nan # type: ignore[no-any-return]
517
+ return np.nan
479
518
 
480
519
  mean_phase_time = np.mean(phase_times)
481
520
 
@@ -505,7 +544,7 @@ def _phase_fft(
505
544
  data2 = data2[:n]
506
545
 
507
546
  if n < 16:
508
- return np.nan # type: ignore[no-any-return]
547
+ return np.nan
509
548
 
510
549
  # Compute FFTs
511
550
  fft1 = np.fft.rfft(data1)
@@ -543,12 +582,7 @@ def skew(
543
582
  edge_type: Type of edges to use for comparison.
544
583
 
545
584
  Returns:
546
- Dictionary with skew statistics:
547
- - skew_values: Array of skew for each non-reference trace
548
- - min: Minimum skew
549
- - max: Maximum skew
550
- - mean: Mean skew
551
- - range: Max - min (total skew spread)
585
+ Dictionary with skew statistics (skew_values, min, max, mean, range).
552
586
 
553
587
  Raises:
554
588
  ValueError: If fewer than 2 traces or reference_idx out of range.
@@ -562,52 +596,103 @@ def skew(
562
596
  """
563
597
  if len(traces) < 2:
564
598
  raise ValueError("Need at least 2 traces for skew measurement")
565
-
566
599
  if reference_idx >= len(traces):
567
600
  raise ValueError(f"reference_idx {reference_idx} out of range")
568
601
 
569
- # Get reference edges
570
- ref_trace = traces[reference_idx]
571
- ref_edges = _get_edge_timestamps(ref_trace, edge_type, 0.5)
602
+ ref_edges = _get_edge_timestamps(traces[reference_idx], edge_type, 0.5)
572
603
 
573
604
  if len(ref_edges) == 0:
574
- return {
575
- "skew_values": np.array([], dtype=np.float64),
576
- "min": float(np.nan),
577
- "max": float(np.nan),
578
- "mean": float(np.nan),
579
- "range": float(np.nan),
580
- }
605
+ return _empty_skew_result()
606
+
607
+ all_skews, skew_values = _compute_all_skews(traces, reference_idx, ref_edges, edge_type)
608
+
609
+ return _build_skew_result(skew_values, all_skews)
610
+
611
+
612
+ def _empty_skew_result() -> dict[str, float | NDArray[np.float64]]:
613
+ """Return empty skew result dictionary.
614
+
615
+ Returns:
616
+ Dictionary with empty/NaN skew values.
617
+ """
618
+ return {
619
+ "skew_values": np.array([], dtype=np.float64),
620
+ "min": float(np.nan),
621
+ "max": float(np.nan),
622
+ "mean": float(np.nan),
623
+ "range": float(np.nan),
624
+ }
625
+
626
+
627
+ def _compute_all_skews(
628
+ traces: list[WaveformTrace | DigitalTrace],
629
+ reference_idx: int,
630
+ ref_edges: NDArray[np.float64],
631
+ edge_type: Literal["rising", "falling"],
632
+ ) -> tuple[list[float], list[float]]:
633
+ """Compute skew values for all traces.
634
+
635
+ Args:
636
+ traces: List of traces to analyze.
637
+ reference_idx: Index of reference trace.
638
+ ref_edges: Reference edge timestamps.
639
+ edge_type: Edge type to analyze.
581
640
 
582
- # Compute skew for all traces (including reference which has 0 skew)
641
+ Returns:
642
+ Tuple of (all_skews including reference, skew_values excluding reference).
643
+ """
583
644
  all_skews: list[float] = []
584
645
  skew_values: list[float] = []
585
646
 
586
647
  for i, trace in enumerate(traces):
587
648
  if i == reference_idx:
588
- # Reference has zero skew by definition
589
649
  all_skews.append(0.0)
590
650
  continue
591
651
 
592
652
  trace_edges = _get_edge_timestamps(trace, edge_type, 0.5)
593
-
594
- if len(trace_edges) == 0:
595
- skew_val = np.nan
596
- else:
597
- # Match edges and compute skew
598
- edge_skews = []
599
- for ref_edge in ref_edges:
600
- # Find nearest edge in this trace
601
- diffs = np.abs(trace_edges - ref_edge)
602
- nearest_idx = np.argmin(diffs)
603
- skew_val_edge = trace_edges[nearest_idx] - ref_edge
604
- edge_skews.append(skew_val_edge)
605
-
606
- skew_val = float(np.mean(edge_skews)) if len(edge_skews) > 0 else np.nan
653
+ skew_val = _compute_trace_skew(trace_edges, ref_edges)
607
654
 
608
655
  skew_values.append(skew_val)
609
656
  all_skews.append(skew_val)
610
657
 
658
+ return all_skews, skew_values
659
+
660
+
661
+ def _compute_trace_skew(trace_edges: NDArray[np.float64], ref_edges: NDArray[np.float64]) -> float:
662
+ """Compute skew for a single trace relative to reference.
663
+
664
+ Args:
665
+ trace_edges: Edge timestamps for trace.
666
+ ref_edges: Reference edge timestamps.
667
+
668
+ Returns:
669
+ Mean skew value or NaN if no edges.
670
+ """
671
+ if len(trace_edges) == 0:
672
+ return float(np.nan)
673
+
674
+ edge_skews = []
675
+ for ref_edge in ref_edges:
676
+ diffs = np.abs(trace_edges - ref_edge)
677
+ nearest_idx = np.argmin(diffs)
678
+ skew_val_edge = trace_edges[nearest_idx] - ref_edge
679
+ edge_skews.append(skew_val_edge)
680
+
681
+ return float(np.mean(edge_skews)) if len(edge_skews) > 0 else float(np.nan)
682
+
683
+
684
+ def _build_skew_result(
685
+ skew_values: list[float], all_skews: list[float]
686
+ ) -> dict[str, float | NDArray[np.float64]]:
687
+ """Build final skew result dictionary.
688
+
689
+ Args:
690
+ skew_values: Skew values excluding reference.
691
+ all_skews: Skew values including reference.
692
+
693
+ Returns:
694
+ Dictionary with skew statistics.
695
+ """
611
696
  skew_arr = np.array(skew_values, dtype=np.float64)
612
697
  all_skews_arr = np.array(all_skews, dtype=np.float64)
613
698
  valid_all_skews = all_skews_arr[~np.isnan(all_skews_arr)]
@@ -615,13 +700,12 @@ def skew(
615
700
  if len(valid_all_skews) == 0:
616
701
  return {
617
702
  "skew_values": skew_arr,
618
- "min": np.nan,
619
- "max": np.nan,
620
- "mean": np.nan,
621
- "range": np.nan,
703
+ "min": float(np.nan),
704
+ "max": float(np.nan),
705
+ "mean": float(np.nan),
706
+ "range": float(np.nan),
622
707
  }
623
708
 
624
- # Compute statistics across ALL traces (including reference)
625
709
  return {
626
710
  "skew_values": skew_arr,
627
711
  "min": float(np.min(valid_all_skews)),
@@ -673,73 +757,141 @@ def recover_clock_fft(
673
757
  References:
674
758
  IEEE 1241-2010 Section 4.1
675
759
  """
760
+ # Prepare data and validate
676
761
  data = trace.data.astype(np.float64) if isinstance(trace, DigitalTrace) else trace.data
677
-
678
- n = len(data)
679
762
  sample_rate = trace.metadata.sample_rate
763
+ _validate_fft_requirements(len(data))
764
+
765
+ # Set frequency range
766
+ min_freq_val, max_freq_val = _determine_frequency_range(min_freq, max_freq, sample_rate)
767
+
768
+ # Compute FFT spectrum
769
+ freq, magnitude = _compute_fft_spectrum(data, sample_rate)
770
+
771
+ # Find peak frequency
772
+ peak_freq, peak_mag, valid_indices = _find_peak_frequency(
773
+ freq, magnitude, min_freq_val, max_freq_val
774
+ )
680
775
 
681
- # FFT requires sufficient samples for reliable frequency resolution
682
- # Rule of thumb: At least 4-5 cycles of the signal for accurate peak detection
683
- # With typical bit rates, this means ~100-200 samples minimum
684
- min_samples = 64 # Increased from 16 for better frequency resolution
685
- if n < min_samples:
776
+ # Calculate confidence
777
+ confidence = _calculate_fft_confidence(magnitude, peak_mag, valid_indices)
778
+
779
+ # Refine frequency with interpolation
780
+ peak_freq_refined = _refine_peak_frequency(peak_freq, magnitude, freq, sample_rate, len(data))
781
+
782
+ # Warn if low confidence
783
+ _check_confidence_and_warn(confidence, peak_freq_refined)
784
+
785
+ period = 1.0 / peak_freq_refined if peak_freq_refined > 0 else np.nan
786
+
787
+ return ClockRecoveryResult(
788
+ frequency=float(peak_freq_refined),
789
+ period=float(period),
790
+ method="fft",
791
+ confidence=float(confidence),
792
+ )
793
+
794
+
795
+ def _validate_fft_requirements(n_samples: int) -> None:
796
+ """Validate trace has enough samples for FFT."""
797
+ min_samples = 64
798
+ if n_samples < min_samples:
686
799
  raise InsufficientDataError(
687
800
  f"FFT clock recovery requires at least {min_samples} samples for reliable frequency detection",
688
801
  required=min_samples,
689
- available=n,
802
+ available=n_samples,
690
803
  analysis_type="clock_recovery_fft",
691
804
  fix_hint="Use edge-based clock recovery for short signals or acquire more data",
692
805
  )
693
806
 
694
- # Set frequency range defaults
695
- if min_freq is None:
696
- min_freq = sample_rate / 1000
697
- if max_freq is None:
698
- max_freq = sample_rate / 2
699
807
 
700
- # Remove DC and compute FFT
808
+ def _determine_frequency_range(
809
+ min_freq: float | None,
810
+ max_freq: float | None,
811
+ sample_rate: float,
812
+ ) -> tuple[float, float]:
813
+ """Determine frequency range for FFT analysis."""
814
+ min_freq_val = min_freq if min_freq is not None else sample_rate / 1000
815
+ max_freq_val = max_freq if max_freq is not None else sample_rate / 2
816
+ return min_freq_val, max_freq_val
817
+
818
+
819
+ def _compute_fft_spectrum(
820
+ data: NDArray[Any],
821
+ sample_rate: float,
822
+ ) -> tuple[NDArray[Any], NDArray[Any]]:
823
+ """Compute FFT spectrum of signal."""
824
+ n = len(data)
701
825
  data_centered = data - np.mean(data)
702
826
  nfft = int(2 ** np.ceil(np.log2(n)))
703
827
  spectrum = np.fft.rfft(data_centered, n=nfft)
704
828
  freq = np.fft.rfftfreq(nfft, d=1.0 / sample_rate)
705
829
  magnitude = np.abs(spectrum)
830
+ return freq, magnitude
831
+
706
832
 
707
- # Apply frequency range mask
833
+ def _find_peak_frequency(
834
+ freq: NDArray[Any],
835
+ magnitude: NDArray[Any],
836
+ min_freq: float,
837
+ max_freq: float,
838
+ ) -> tuple[float, float, NDArray[Any]]:
839
+ """Find peak frequency in specified range."""
708
840
  mask = (freq >= min_freq) & (freq <= max_freq)
709
841
  valid_indices = np.where(mask)[0]
710
842
 
711
843
  if len(valid_indices) == 0:
712
- # No valid frequencies in range - signal may be DC or out of range
713
844
  raise ValueError(
714
845
  f"No frequency components found in range [{min_freq:.0f} Hz, {max_freq:.0f} Hz]. "
715
846
  f"Signal may be constant (DC) or frequency is outside specified range. "
716
847
  f"Adjust min_freq/max_freq or check signal integrity."
717
848
  )
718
849
 
719
- # Find peak in valid range
720
850
  local_peak_idx = np.argmax(magnitude[valid_indices])
721
851
  peak_idx = valid_indices[local_peak_idx]
722
852
  peak_freq = freq[peak_idx]
723
853
  peak_mag = magnitude[peak_idx]
724
854
 
725
- # Calculate confidence (ratio of peak to RMS of spectrum)
855
+ return peak_freq, peak_mag, valid_indices
856
+
857
+
858
+ def _calculate_fft_confidence(
859
+ magnitude: NDArray[Any],
860
+ peak_mag: float,
861
+ valid_indices: NDArray[Any],
862
+ ) -> float:
863
+ """Calculate confidence score for FFT peak."""
726
864
  rms_mag = np.sqrt(np.mean(magnitude[valid_indices] ** 2))
727
- confidence = min(1.0, (peak_mag / rms_mag - 1) / 10) if rms_mag > 0 else 0.0
865
+ return min(1.0, (peak_mag / rms_mag - 1) / 10) if rms_mag > 0 else 0.0
866
+
867
+
868
+ def _refine_peak_frequency(
869
+ peak_freq: float,
870
+ magnitude: NDArray[Any],
871
+ freq: NDArray[Any],
872
+ sample_rate: float,
873
+ n_data: int,
874
+ ) -> float:
875
+ """Refine peak frequency using parabolic interpolation."""
876
+ peak_idx = np.argmin(np.abs(freq - peak_freq))
728
877
 
729
- # Parabolic interpolation for more accurate frequency
730
878
  if 0 < peak_idx < len(magnitude) - 1:
731
879
  alpha = magnitude[peak_idx - 1]
732
880
  beta = magnitude[peak_idx]
733
881
  gamma = magnitude[peak_idx + 1]
734
882
 
735
883
  if beta > alpha and beta > gamma:
884
+ nfft = int(2 ** np.ceil(np.log2(n_data)))
736
885
  freq_resolution = sample_rate / nfft
737
886
  delta = 0.5 * (alpha - gamma) / (alpha - 2 * beta + gamma + 1e-12)
738
- peak_freq = peak_freq + delta * freq_resolution
887
+ refined: float = float(peak_freq + delta * freq_resolution)
888
+ return refined
739
889
 
740
- period = 1.0 / peak_freq if peak_freq > 0 else np.nan
890
+ return peak_freq
741
891
 
742
- # Warn on low confidence results (may be unreliable)
892
+
893
+ def _check_confidence_and_warn(confidence: float, peak_freq: float) -> None:
894
+ """Warn if confidence is low."""
743
895
  if confidence < 0.5:
744
896
  import warnings
745
897
 
@@ -748,16 +900,9 @@ def recover_clock_fft(
748
900
  f"Detected frequency: {peak_freq / 1e6:.3f} MHz. "
749
901
  f"Consider using longer signal, edge-based recovery, or verifying signal periodicity.",
750
902
  UserWarning,
751
- stacklevel=2,
903
+ stacklevel=3,
752
904
  )
753
905
 
754
- return ClockRecoveryResult(
755
- frequency=float(peak_freq),
756
- period=float(period),
757
- method="fft",
758
- confidence=float(confidence),
759
- )
760
-
761
906
 
762
907
  def recover_clock_edge(
763
908
  trace: WaveformTrace | DigitalTrace,
@@ -1038,13 +1183,13 @@ def peak_to_peak_jitter(
1038
1183
  edges = _get_edge_timestamps(trace, edge_type, threshold)
1039
1184
 
1040
1185
  if len(edges) < 3:
1041
- return np.nan # type: ignore[no-any-return]
1186
+ return np.nan
1042
1187
 
1043
1188
  # Calculate periods
1044
1189
  periods = np.diff(edges)
1045
1190
 
1046
1191
  if len(periods) < 2:
1047
- return np.nan # type: ignore[no-any-return]
1192
+ return np.nan
1048
1193
 
1049
1194
  # Pk-Pk jitter is the range of period variations
1050
1195
  jitter_pp = float(np.max(periods) - np.min(periods))