oscura 0.5.0__py3-none-any.whl → 0.6.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 (513) 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/__init__.py +0 -48
  5. oscura/analyzers/digital/edges.py +325 -65
  6. oscura/analyzers/digital/extraction.py +0 -195
  7. oscura/analyzers/digital/quality.py +293 -166
  8. oscura/analyzers/digital/timing.py +260 -115
  9. oscura/analyzers/digital/timing_numba.py +334 -0
  10. oscura/analyzers/entropy.py +605 -0
  11. oscura/analyzers/eye/diagram.py +176 -109
  12. oscura/analyzers/eye/metrics.py +5 -5
  13. oscura/analyzers/jitter/__init__.py +6 -4
  14. oscura/analyzers/jitter/ber.py +52 -52
  15. oscura/analyzers/jitter/classification.py +156 -0
  16. oscura/analyzers/jitter/decomposition.py +163 -113
  17. oscura/analyzers/jitter/spectrum.py +80 -64
  18. oscura/analyzers/ml/__init__.py +39 -0
  19. oscura/analyzers/ml/features.py +600 -0
  20. oscura/analyzers/ml/signal_classifier.py +604 -0
  21. oscura/analyzers/packet/daq.py +246 -158
  22. oscura/analyzers/packet/parser.py +12 -1
  23. oscura/analyzers/packet/payload.py +50 -2110
  24. oscura/analyzers/packet/payload_analysis.py +361 -181
  25. oscura/analyzers/packet/payload_patterns.py +133 -70
  26. oscura/analyzers/packet/stream.py +84 -23
  27. oscura/analyzers/patterns/__init__.py +26 -5
  28. oscura/analyzers/patterns/anomaly_detection.py +908 -0
  29. oscura/analyzers/patterns/clustering.py +169 -108
  30. oscura/analyzers/patterns/clustering_optimized.py +227 -0
  31. oscura/analyzers/patterns/discovery.py +1 -1
  32. oscura/analyzers/patterns/matching.py +581 -197
  33. oscura/analyzers/patterns/pattern_mining.py +778 -0
  34. oscura/analyzers/patterns/periodic.py +121 -38
  35. oscura/analyzers/patterns/sequences.py +175 -78
  36. oscura/analyzers/power/conduction.py +1 -1
  37. oscura/analyzers/power/soa.py +6 -6
  38. oscura/analyzers/power/switching.py +250 -110
  39. oscura/analyzers/protocol/__init__.py +17 -1
  40. oscura/analyzers/protocols/__init__.py +1 -22
  41. oscura/analyzers/protocols/base.py +6 -6
  42. oscura/analyzers/protocols/ble/__init__.py +38 -0
  43. oscura/analyzers/protocols/ble/analyzer.py +809 -0
  44. oscura/analyzers/protocols/ble/uuids.py +288 -0
  45. oscura/analyzers/protocols/can.py +257 -127
  46. oscura/analyzers/protocols/can_fd.py +107 -80
  47. oscura/analyzers/protocols/flexray.py +139 -80
  48. oscura/analyzers/protocols/hdlc.py +93 -58
  49. oscura/analyzers/protocols/i2c.py +247 -106
  50. oscura/analyzers/protocols/i2s.py +138 -86
  51. oscura/analyzers/protocols/industrial/__init__.py +40 -0
  52. oscura/analyzers/protocols/industrial/bacnet/__init__.py +33 -0
  53. oscura/analyzers/protocols/industrial/bacnet/analyzer.py +708 -0
  54. oscura/analyzers/protocols/industrial/bacnet/encoding.py +412 -0
  55. oscura/analyzers/protocols/industrial/bacnet/services.py +622 -0
  56. oscura/analyzers/protocols/industrial/ethercat/__init__.py +30 -0
  57. oscura/analyzers/protocols/industrial/ethercat/analyzer.py +474 -0
  58. oscura/analyzers/protocols/industrial/ethercat/mailbox.py +339 -0
  59. oscura/analyzers/protocols/industrial/ethercat/topology.py +166 -0
  60. oscura/analyzers/protocols/industrial/modbus/__init__.py +31 -0
  61. oscura/analyzers/protocols/industrial/modbus/analyzer.py +525 -0
  62. oscura/analyzers/protocols/industrial/modbus/crc.py +79 -0
  63. oscura/analyzers/protocols/industrial/modbus/functions.py +436 -0
  64. oscura/analyzers/protocols/industrial/opcua/__init__.py +21 -0
  65. oscura/analyzers/protocols/industrial/opcua/analyzer.py +552 -0
  66. oscura/analyzers/protocols/industrial/opcua/datatypes.py +446 -0
  67. oscura/analyzers/protocols/industrial/opcua/services.py +264 -0
  68. oscura/analyzers/protocols/industrial/profinet/__init__.py +23 -0
  69. oscura/analyzers/protocols/industrial/profinet/analyzer.py +441 -0
  70. oscura/analyzers/protocols/industrial/profinet/dcp.py +263 -0
  71. oscura/analyzers/protocols/industrial/profinet/ptcp.py +200 -0
  72. oscura/analyzers/protocols/jtag.py +180 -98
  73. oscura/analyzers/protocols/lin.py +219 -114
  74. oscura/analyzers/protocols/manchester.py +4 -4
  75. oscura/analyzers/protocols/onewire.py +253 -149
  76. oscura/analyzers/protocols/parallel_bus/__init__.py +20 -0
  77. oscura/analyzers/protocols/parallel_bus/centronics.py +92 -0
  78. oscura/analyzers/protocols/parallel_bus/gpib.py +137 -0
  79. oscura/analyzers/protocols/spi.py +192 -95
  80. oscura/analyzers/protocols/swd.py +321 -167
  81. oscura/analyzers/protocols/uart.py +267 -125
  82. oscura/analyzers/protocols/usb.py +235 -131
  83. oscura/analyzers/side_channel/power.py +17 -12
  84. oscura/analyzers/signal/__init__.py +15 -0
  85. oscura/analyzers/signal/timing_analysis.py +1086 -0
  86. oscura/analyzers/signal_integrity/__init__.py +4 -1
  87. oscura/analyzers/signal_integrity/sparams.py +2 -19
  88. oscura/analyzers/spectral/chunked.py +129 -60
  89. oscura/analyzers/spectral/chunked_fft.py +300 -94
  90. oscura/analyzers/spectral/chunked_wavelet.py +100 -80
  91. oscura/analyzers/statistical/checksum.py +376 -217
  92. oscura/analyzers/statistical/classification.py +229 -107
  93. oscura/analyzers/statistical/entropy.py +78 -53
  94. oscura/analyzers/statistics/correlation.py +407 -211
  95. oscura/analyzers/statistics/outliers.py +2 -2
  96. oscura/analyzers/statistics/streaming.py +30 -5
  97. oscura/analyzers/validation.py +216 -101
  98. oscura/analyzers/waveform/measurements.py +9 -0
  99. oscura/analyzers/waveform/measurements_with_uncertainty.py +31 -15
  100. oscura/analyzers/waveform/spectral.py +500 -228
  101. oscura/api/__init__.py +31 -5
  102. oscura/api/dsl/__init__.py +582 -0
  103. oscura/{dsl → api/dsl}/commands.py +43 -76
  104. oscura/{dsl → api/dsl}/interpreter.py +26 -51
  105. oscura/{dsl → api/dsl}/parser.py +107 -77
  106. oscura/{dsl → api/dsl}/repl.py +2 -2
  107. oscura/api/dsl.py +1 -1
  108. oscura/{integrations → api/integrations}/__init__.py +1 -1
  109. oscura/{integrations → api/integrations}/llm.py +201 -102
  110. oscura/api/operators.py +3 -3
  111. oscura/api/optimization.py +144 -30
  112. oscura/api/rest_server.py +921 -0
  113. oscura/api/server/__init__.py +17 -0
  114. oscura/api/server/dashboard.py +850 -0
  115. oscura/api/server/static/README.md +34 -0
  116. oscura/api/server/templates/base.html +181 -0
  117. oscura/api/server/templates/export.html +120 -0
  118. oscura/api/server/templates/home.html +284 -0
  119. oscura/api/server/templates/protocols.html +58 -0
  120. oscura/api/server/templates/reports.html +43 -0
  121. oscura/api/server/templates/session_detail.html +89 -0
  122. oscura/api/server/templates/sessions.html +83 -0
  123. oscura/api/server/templates/waveforms.html +73 -0
  124. oscura/automotive/__init__.py +8 -1
  125. oscura/automotive/can/__init__.py +10 -0
  126. oscura/automotive/can/checksum.py +3 -1
  127. oscura/automotive/can/dbc_generator.py +590 -0
  128. oscura/automotive/can/message_wrapper.py +121 -74
  129. oscura/automotive/can/patterns.py +98 -21
  130. oscura/automotive/can/session.py +292 -56
  131. oscura/automotive/can/state_machine.py +6 -3
  132. oscura/automotive/can/stimulus_response.py +97 -75
  133. oscura/automotive/dbc/__init__.py +10 -2
  134. oscura/automotive/dbc/generator.py +84 -56
  135. oscura/automotive/dbc/parser.py +6 -6
  136. oscura/automotive/dtc/data.json +2763 -0
  137. oscura/automotive/dtc/database.py +2 -2
  138. oscura/automotive/flexray/__init__.py +31 -0
  139. oscura/automotive/flexray/analyzer.py +504 -0
  140. oscura/automotive/flexray/crc.py +185 -0
  141. oscura/automotive/flexray/fibex.py +449 -0
  142. oscura/automotive/j1939/__init__.py +45 -8
  143. oscura/automotive/j1939/analyzer.py +605 -0
  144. oscura/automotive/j1939/spns.py +326 -0
  145. oscura/automotive/j1939/transport.py +306 -0
  146. oscura/automotive/lin/__init__.py +47 -0
  147. oscura/automotive/lin/analyzer.py +612 -0
  148. oscura/automotive/loaders/blf.py +13 -2
  149. oscura/automotive/loaders/csv_can.py +143 -72
  150. oscura/automotive/loaders/dispatcher.py +50 -2
  151. oscura/automotive/loaders/mdf.py +86 -45
  152. oscura/automotive/loaders/pcap.py +111 -61
  153. oscura/automotive/uds/__init__.py +4 -0
  154. oscura/automotive/uds/analyzer.py +725 -0
  155. oscura/automotive/uds/decoder.py +140 -58
  156. oscura/automotive/uds/models.py +7 -1
  157. oscura/automotive/visualization.py +1 -1
  158. oscura/cli/analyze.py +348 -0
  159. oscura/cli/batch.py +142 -122
  160. oscura/cli/benchmark.py +275 -0
  161. oscura/cli/characterize.py +137 -82
  162. oscura/cli/compare.py +224 -131
  163. oscura/cli/completion.py +250 -0
  164. oscura/cli/config_cmd.py +361 -0
  165. oscura/cli/decode.py +164 -87
  166. oscura/cli/export.py +286 -0
  167. oscura/cli/main.py +115 -31
  168. oscura/{onboarding → cli/onboarding}/__init__.py +3 -3
  169. oscura/{onboarding → cli/onboarding}/help.py +80 -58
  170. oscura/{onboarding → cli/onboarding}/tutorials.py +97 -72
  171. oscura/{onboarding → cli/onboarding}/wizard.py +55 -36
  172. oscura/cli/progress.py +147 -0
  173. oscura/cli/shell.py +157 -135
  174. oscura/cli/validate_cmd.py +204 -0
  175. oscura/cli/visualize.py +158 -0
  176. oscura/convenience.py +125 -79
  177. oscura/core/__init__.py +4 -2
  178. oscura/core/backend_selector.py +3 -3
  179. oscura/core/cache.py +126 -15
  180. oscura/core/cancellation.py +1 -1
  181. oscura/{config → core/config}/__init__.py +20 -11
  182. oscura/{config → core/config}/defaults.py +1 -1
  183. oscura/{config → core/config}/loader.py +7 -5
  184. oscura/{config → core/config}/memory.py +5 -5
  185. oscura/{config → core/config}/migration.py +1 -1
  186. oscura/{config → core/config}/pipeline.py +99 -23
  187. oscura/{config → core/config}/preferences.py +1 -1
  188. oscura/{config → core/config}/protocol.py +3 -3
  189. oscura/{config → core/config}/schema.py +426 -272
  190. oscura/{config → core/config}/settings.py +1 -1
  191. oscura/{config → core/config}/thresholds.py +195 -153
  192. oscura/core/correlation.py +5 -6
  193. oscura/core/cross_domain.py +0 -2
  194. oscura/core/debug.py +9 -5
  195. oscura/{extensibility → core/extensibility}/docs.py +158 -70
  196. oscura/{extensibility → core/extensibility}/extensions.py +160 -76
  197. oscura/{extensibility → core/extensibility}/logging.py +1 -1
  198. oscura/{extensibility → core/extensibility}/measurements.py +1 -1
  199. oscura/{extensibility → core/extensibility}/plugins.py +1 -1
  200. oscura/{extensibility → core/extensibility}/templates.py +73 -3
  201. oscura/{extensibility → core/extensibility}/validation.py +1 -1
  202. oscura/core/gpu_backend.py +11 -7
  203. oscura/core/log_query.py +101 -11
  204. oscura/core/logging.py +126 -54
  205. oscura/core/logging_advanced.py +5 -5
  206. oscura/core/memory_limits.py +108 -70
  207. oscura/core/memory_monitor.py +2 -2
  208. oscura/core/memory_progress.py +7 -7
  209. oscura/core/memory_warnings.py +1 -1
  210. oscura/core/numba_backend.py +13 -13
  211. oscura/{plugins → core/plugins}/__init__.py +9 -9
  212. oscura/{plugins → core/plugins}/base.py +7 -7
  213. oscura/{plugins → core/plugins}/cli.py +3 -3
  214. oscura/{plugins → core/plugins}/discovery.py +186 -106
  215. oscura/{plugins → core/plugins}/lifecycle.py +1 -1
  216. oscura/{plugins → core/plugins}/manager.py +7 -7
  217. oscura/{plugins → core/plugins}/registry.py +3 -3
  218. oscura/{plugins → core/plugins}/versioning.py +1 -1
  219. oscura/core/progress.py +16 -1
  220. oscura/core/provenance.py +8 -2
  221. oscura/{schemas → core/schemas}/__init__.py +2 -2
  222. oscura/core/schemas/bus_configuration.json +322 -0
  223. oscura/core/schemas/device_mapping.json +182 -0
  224. oscura/core/schemas/packet_format.json +418 -0
  225. oscura/core/schemas/protocol_definition.json +363 -0
  226. oscura/core/types.py +4 -0
  227. oscura/core/uncertainty.py +3 -3
  228. oscura/correlation/__init__.py +52 -0
  229. oscura/correlation/multi_protocol.py +811 -0
  230. oscura/discovery/auto_decoder.py +117 -35
  231. oscura/discovery/comparison.py +191 -86
  232. oscura/discovery/quality_validator.py +155 -68
  233. oscura/discovery/signal_detector.py +196 -79
  234. oscura/export/__init__.py +18 -20
  235. oscura/export/kaitai_struct.py +513 -0
  236. oscura/export/scapy_layer.py +801 -0
  237. oscura/export/wireshark/README.md +15 -15
  238. oscura/export/wireshark/generator.py +1 -1
  239. oscura/export/wireshark/templates/dissector.lua.j2 +2 -2
  240. oscura/export/wireshark_dissector.py +746 -0
  241. oscura/guidance/wizard.py +207 -111
  242. oscura/hardware/__init__.py +19 -0
  243. oscura/{acquisition → hardware/acquisition}/__init__.py +4 -4
  244. oscura/{acquisition → hardware/acquisition}/file.py +2 -2
  245. oscura/{acquisition → hardware/acquisition}/hardware.py +7 -7
  246. oscura/{acquisition → hardware/acquisition}/saleae.py +15 -12
  247. oscura/{acquisition → hardware/acquisition}/socketcan.py +1 -1
  248. oscura/{acquisition → hardware/acquisition}/streaming.py +2 -2
  249. oscura/{acquisition → hardware/acquisition}/synthetic.py +3 -3
  250. oscura/{acquisition → hardware/acquisition}/visa.py +33 -11
  251. oscura/hardware/firmware/__init__.py +29 -0
  252. oscura/hardware/firmware/pattern_recognition.py +874 -0
  253. oscura/hardware/hal_detector.py +736 -0
  254. oscura/hardware/security/__init__.py +37 -0
  255. oscura/hardware/security/side_channel_detector.py +1126 -0
  256. oscura/inference/__init__.py +4 -0
  257. oscura/inference/active_learning/README.md +7 -7
  258. oscura/inference/active_learning/observation_table.py +4 -1
  259. oscura/inference/alignment.py +216 -123
  260. oscura/inference/bayesian.py +113 -33
  261. oscura/inference/crc_reverse.py +101 -55
  262. oscura/inference/logic.py +6 -2
  263. oscura/inference/message_format.py +342 -183
  264. oscura/inference/protocol.py +95 -44
  265. oscura/inference/protocol_dsl.py +180 -82
  266. oscura/inference/signal_intelligence.py +1439 -706
  267. oscura/inference/spectral.py +99 -57
  268. oscura/inference/state_machine.py +810 -158
  269. oscura/inference/stream.py +270 -110
  270. oscura/iot/__init__.py +34 -0
  271. oscura/iot/coap/__init__.py +32 -0
  272. oscura/iot/coap/analyzer.py +668 -0
  273. oscura/iot/coap/options.py +212 -0
  274. oscura/iot/lorawan/__init__.py +21 -0
  275. oscura/iot/lorawan/crypto.py +206 -0
  276. oscura/iot/lorawan/decoder.py +801 -0
  277. oscura/iot/lorawan/mac_commands.py +341 -0
  278. oscura/iot/mqtt/__init__.py +27 -0
  279. oscura/iot/mqtt/analyzer.py +999 -0
  280. oscura/iot/mqtt/properties.py +315 -0
  281. oscura/iot/zigbee/__init__.py +31 -0
  282. oscura/iot/zigbee/analyzer.py +615 -0
  283. oscura/iot/zigbee/security.py +153 -0
  284. oscura/iot/zigbee/zcl.py +349 -0
  285. oscura/jupyter/display.py +125 -45
  286. oscura/{exploratory → jupyter/exploratory}/__init__.py +8 -8
  287. oscura/{exploratory → jupyter/exploratory}/error_recovery.py +298 -141
  288. oscura/jupyter/exploratory/fuzzy.py +746 -0
  289. oscura/{exploratory → jupyter/exploratory}/fuzzy_advanced.py +258 -100
  290. oscura/{exploratory → jupyter/exploratory}/legacy.py +464 -242
  291. oscura/{exploratory → jupyter/exploratory}/parse.py +167 -145
  292. oscura/{exploratory → jupyter/exploratory}/recovery.py +119 -87
  293. oscura/jupyter/exploratory/sync.py +612 -0
  294. oscura/{exploratory → jupyter/exploratory}/unknown.py +299 -176
  295. oscura/jupyter/magic.py +4 -4
  296. oscura/{ui → jupyter/ui}/__init__.py +2 -2
  297. oscura/{ui → jupyter/ui}/formatters.py +3 -3
  298. oscura/{ui → jupyter/ui}/progressive_display.py +153 -82
  299. oscura/loaders/__init__.py +171 -63
  300. oscura/loaders/binary.py +88 -1
  301. oscura/loaders/chipwhisperer.py +153 -137
  302. oscura/loaders/configurable.py +208 -86
  303. oscura/loaders/csv_loader.py +458 -215
  304. oscura/loaders/hdf5_loader.py +278 -119
  305. oscura/loaders/lazy.py +87 -54
  306. oscura/loaders/mmap_loader.py +1 -1
  307. oscura/loaders/numpy_loader.py +253 -116
  308. oscura/loaders/pcap.py +226 -151
  309. oscura/loaders/rigol.py +110 -49
  310. oscura/loaders/sigrok.py +201 -78
  311. oscura/loaders/tdms.py +81 -58
  312. oscura/loaders/tektronix.py +291 -174
  313. oscura/loaders/touchstone.py +182 -87
  314. oscura/loaders/vcd.py +215 -117
  315. oscura/loaders/wav.py +155 -68
  316. oscura/reporting/__init__.py +9 -7
  317. oscura/reporting/analyze.py +352 -146
  318. oscura/reporting/argument_preparer.py +69 -14
  319. oscura/reporting/auto_report.py +97 -61
  320. oscura/reporting/batch.py +131 -58
  321. oscura/reporting/chart_selection.py +57 -45
  322. oscura/reporting/comparison.py +63 -17
  323. oscura/reporting/content/executive.py +76 -24
  324. oscura/reporting/core_formats/multi_format.py +11 -8
  325. oscura/reporting/engine.py +312 -158
  326. oscura/reporting/enhanced_reports.py +949 -0
  327. oscura/reporting/export.py +86 -43
  328. oscura/reporting/formatting/numbers.py +69 -42
  329. oscura/reporting/html.py +139 -58
  330. oscura/reporting/index.py +137 -65
  331. oscura/reporting/output.py +158 -67
  332. oscura/reporting/pdf.py +67 -102
  333. oscura/reporting/plots.py +191 -112
  334. oscura/reporting/sections.py +88 -47
  335. oscura/reporting/standards.py +104 -61
  336. oscura/reporting/summary_generator.py +75 -55
  337. oscura/reporting/tables.py +138 -54
  338. oscura/reporting/templates/enhanced/protocol_re.html +525 -0
  339. oscura/reporting/templates/index.md +13 -13
  340. oscura/sessions/__init__.py +14 -23
  341. oscura/sessions/base.py +3 -3
  342. oscura/sessions/blackbox.py +106 -10
  343. oscura/sessions/generic.py +2 -2
  344. oscura/sessions/legacy.py +783 -0
  345. oscura/side_channel/__init__.py +63 -0
  346. oscura/side_channel/dpa.py +1025 -0
  347. oscura/utils/__init__.py +15 -1
  348. oscura/utils/autodetect.py +1 -5
  349. oscura/utils/bitwise.py +118 -0
  350. oscura/{builders → utils/builders}/__init__.py +1 -1
  351. oscura/{comparison → utils/comparison}/__init__.py +6 -6
  352. oscura/{comparison → utils/comparison}/compare.py +202 -101
  353. oscura/{comparison → utils/comparison}/golden.py +83 -63
  354. oscura/{comparison → utils/comparison}/limits.py +313 -89
  355. oscura/{comparison → utils/comparison}/mask.py +151 -45
  356. oscura/{comparison → utils/comparison}/trace_diff.py +1 -1
  357. oscura/{comparison → utils/comparison}/visualization.py +147 -89
  358. oscura/{component → utils/component}/__init__.py +3 -3
  359. oscura/{component → utils/component}/impedance.py +122 -58
  360. oscura/{component → utils/component}/reactive.py +165 -168
  361. oscura/{component → utils/component}/transmission_line.py +3 -3
  362. oscura/{filtering → utils/filtering}/__init__.py +6 -6
  363. oscura/{filtering → utils/filtering}/base.py +1 -1
  364. oscura/{filtering → utils/filtering}/convenience.py +2 -2
  365. oscura/{filtering → utils/filtering}/design.py +169 -93
  366. oscura/{filtering → utils/filtering}/filters.py +2 -2
  367. oscura/{filtering → utils/filtering}/introspection.py +2 -2
  368. oscura/utils/geometry.py +31 -0
  369. oscura/utils/imports.py +184 -0
  370. oscura/utils/lazy.py +1 -1
  371. oscura/{math → utils/math}/__init__.py +2 -2
  372. oscura/{math → utils/math}/arithmetic.py +114 -48
  373. oscura/{math → utils/math}/interpolation.py +139 -106
  374. oscura/utils/memory.py +129 -66
  375. oscura/utils/memory_advanced.py +92 -9
  376. oscura/utils/memory_extensions.py +10 -8
  377. oscura/{optimization → utils/optimization}/__init__.py +1 -1
  378. oscura/{optimization → utils/optimization}/search.py +2 -2
  379. oscura/utils/performance/__init__.py +58 -0
  380. oscura/utils/performance/caching.py +889 -0
  381. oscura/utils/performance/lsh_clustering.py +333 -0
  382. oscura/utils/performance/memory_optimizer.py +699 -0
  383. oscura/utils/performance/optimizations.py +675 -0
  384. oscura/utils/performance/parallel.py +654 -0
  385. oscura/utils/performance/profiling.py +661 -0
  386. oscura/{pipeline → utils/pipeline}/base.py +1 -1
  387. oscura/{pipeline → utils/pipeline}/composition.py +11 -3
  388. oscura/{pipeline → utils/pipeline}/parallel.py +3 -2
  389. oscura/{pipeline → utils/pipeline}/pipeline.py +1 -1
  390. oscura/{pipeline → utils/pipeline}/reverse_engineering.py +412 -221
  391. oscura/{search → utils/search}/__init__.py +3 -3
  392. oscura/{search → utils/search}/anomaly.py +188 -58
  393. oscura/utils/search/context.py +294 -0
  394. oscura/{search → utils/search}/pattern.py +138 -10
  395. oscura/utils/serial.py +51 -0
  396. oscura/utils/storage/__init__.py +61 -0
  397. oscura/utils/storage/database.py +1166 -0
  398. oscura/{streaming → utils/streaming}/chunked.py +302 -143
  399. oscura/{streaming → utils/streaming}/progressive.py +1 -1
  400. oscura/{streaming → utils/streaming}/realtime.py +3 -2
  401. oscura/{triggering → utils/triggering}/__init__.py +6 -6
  402. oscura/{triggering → utils/triggering}/base.py +6 -6
  403. oscura/{triggering → utils/triggering}/edge.py +2 -2
  404. oscura/{triggering → utils/triggering}/pattern.py +2 -2
  405. oscura/{triggering → utils/triggering}/pulse.py +115 -74
  406. oscura/{triggering → utils/triggering}/window.py +2 -2
  407. oscura/utils/validation.py +32 -0
  408. oscura/validation/__init__.py +121 -0
  409. oscura/{compliance → validation/compliance}/__init__.py +5 -5
  410. oscura/{compliance → validation/compliance}/advanced.py +5 -5
  411. oscura/{compliance → validation/compliance}/masks.py +1 -1
  412. oscura/{compliance → validation/compliance}/reporting.py +127 -53
  413. oscura/{compliance → validation/compliance}/testing.py +114 -52
  414. oscura/validation/compliance_tests.py +915 -0
  415. oscura/validation/fuzzer.py +990 -0
  416. oscura/validation/grammar_tests.py +596 -0
  417. oscura/validation/grammar_validator.py +904 -0
  418. oscura/validation/hil_testing.py +977 -0
  419. oscura/{quality → validation/quality}/__init__.py +4 -4
  420. oscura/{quality → validation/quality}/ensemble.py +251 -171
  421. oscura/{quality → validation/quality}/explainer.py +3 -3
  422. oscura/{quality → validation/quality}/scoring.py +1 -1
  423. oscura/{quality → validation/quality}/warnings.py +4 -4
  424. oscura/validation/regression_suite.py +808 -0
  425. oscura/validation/replay.py +788 -0
  426. oscura/{testing → validation/testing}/__init__.py +2 -2
  427. oscura/{testing → validation/testing}/synthetic.py +5 -5
  428. oscura/visualization/__init__.py +9 -0
  429. oscura/visualization/accessibility.py +1 -1
  430. oscura/visualization/annotations.py +64 -67
  431. oscura/visualization/colors.py +7 -7
  432. oscura/visualization/digital.py +180 -81
  433. oscura/visualization/eye.py +236 -85
  434. oscura/visualization/interactive.py +320 -143
  435. oscura/visualization/jitter.py +587 -247
  436. oscura/visualization/layout.py +169 -134
  437. oscura/visualization/optimization.py +103 -52
  438. oscura/visualization/palettes.py +1 -1
  439. oscura/visualization/power.py +427 -211
  440. oscura/visualization/power_extended.py +626 -297
  441. oscura/visualization/presets.py +2 -0
  442. oscura/visualization/protocols.py +495 -181
  443. oscura/visualization/render.py +79 -63
  444. oscura/visualization/reverse_engineering.py +171 -124
  445. oscura/visualization/signal_integrity.py +460 -279
  446. oscura/visualization/specialized.py +190 -100
  447. oscura/visualization/spectral.py +670 -255
  448. oscura/visualization/thumbnails.py +166 -137
  449. oscura/visualization/waveform.py +150 -63
  450. oscura/workflows/__init__.py +3 -0
  451. oscura/{batch → workflows/batch}/__init__.py +5 -5
  452. oscura/{batch → workflows/batch}/advanced.py +150 -75
  453. oscura/workflows/batch/aggregate.py +531 -0
  454. oscura/workflows/batch/analyze.py +236 -0
  455. oscura/{batch → workflows/batch}/logging.py +2 -2
  456. oscura/{batch → workflows/batch}/metrics.py +1 -1
  457. oscura/workflows/complete_re.py +1144 -0
  458. oscura/workflows/compliance.py +44 -54
  459. oscura/workflows/digital.py +197 -51
  460. oscura/workflows/legacy/__init__.py +12 -0
  461. oscura/{workflow → workflows/legacy}/dag.py +4 -1
  462. oscura/workflows/multi_trace.py +9 -9
  463. oscura/workflows/power.py +42 -62
  464. oscura/workflows/protocol.py +82 -49
  465. oscura/workflows/reverse_engineering.py +351 -150
  466. oscura/workflows/signal_integrity.py +157 -82
  467. oscura-0.6.0.dist-info/METADATA +643 -0
  468. oscura-0.6.0.dist-info/RECORD +590 -0
  469. oscura/analyzers/digital/ic_database.py +0 -498
  470. oscura/analyzers/digital/timing_paths.py +0 -339
  471. oscura/analyzers/digital/vintage.py +0 -377
  472. oscura/analyzers/digital/vintage_result.py +0 -148
  473. oscura/analyzers/protocols/parallel_bus.py +0 -449
  474. oscura/batch/aggregate.py +0 -300
  475. oscura/batch/analyze.py +0 -139
  476. oscura/dsl/__init__.py +0 -73
  477. oscura/exceptions.py +0 -59
  478. oscura/exploratory/fuzzy.py +0 -513
  479. oscura/exploratory/sync.py +0 -384
  480. oscura/export/wavedrom.py +0 -430
  481. oscura/exporters/__init__.py +0 -94
  482. oscura/exporters/csv.py +0 -303
  483. oscura/exporters/exporters.py +0 -44
  484. oscura/exporters/hdf5.py +0 -217
  485. oscura/exporters/html_export.py +0 -701
  486. oscura/exporters/json_export.py +0 -338
  487. oscura/exporters/markdown_export.py +0 -367
  488. oscura/exporters/matlab_export.py +0 -354
  489. oscura/exporters/npz_export.py +0 -219
  490. oscura/exporters/spice_export.py +0 -210
  491. oscura/exporters/vintage_logic_csv.py +0 -247
  492. oscura/reporting/vintage_logic_report.py +0 -523
  493. oscura/search/context.py +0 -149
  494. oscura/session/__init__.py +0 -34
  495. oscura/session/annotations.py +0 -289
  496. oscura/session/history.py +0 -313
  497. oscura/session/session.py +0 -520
  498. oscura/visualization/digital_advanced.py +0 -718
  499. oscura/visualization/figure_manager.py +0 -156
  500. oscura/workflow/__init__.py +0 -13
  501. oscura-0.5.0.dist-info/METADATA +0 -407
  502. oscura-0.5.0.dist-info/RECORD +0 -486
  503. /oscura/core/{config.py → config/legacy.py} +0 -0
  504. /oscura/{extensibility → core/extensibility}/__init__.py +0 -0
  505. /oscura/{extensibility → core/extensibility}/registry.py +0 -0
  506. /oscura/{plugins → core/plugins}/isolation.py +0 -0
  507. /oscura/{builders → utils/builders}/signal_builder.py +0 -0
  508. /oscura/{optimization → utils/optimization}/parallel.py +0 -0
  509. /oscura/{pipeline → utils/pipeline}/__init__.py +0 -0
  510. /oscura/{streaming → utils/streaming}/__init__.py +0 -0
  511. {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/WHEEL +0 -0
  512. {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/entry_points.txt +0 -0
  513. {oscura-0.5.0.dist-info → oscura-0.6.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))