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
@@ -8,12 +8,12 @@ accumulator pattern for rolling statistics.
8
8
  from __future__ import annotations
9
9
 
10
10
  from pathlib import Path
11
- from typing import TYPE_CHECKING, Any, cast
11
+ from typing import TYPE_CHECKING, Any
12
12
 
13
13
  import numpy as np
14
14
  from scipy import signal
15
15
 
16
- from ..core.types import WaveformTrace
16
+ from oscura.core.types import WaveformTrace
17
17
 
18
18
  if TYPE_CHECKING:
19
19
  from collections.abc import Callable, Generator
@@ -69,57 +69,99 @@ def load_trace_chunks(
69
69
  References:
70
70
  API-003: Streaming/Generator API for Large Files
71
71
  """
72
- file_path = Path(file_path)
72
+ full_trace = _load_full_trace(file_path, loader)
73
+ chunk_samples, num_chunks = _compute_chunk_parameters(len(full_trace.data), chunk_size, overlap)
74
+ yield from _generate_chunks(full_trace, chunk_samples, overlap, num_chunks, progress_callback)
75
+
76
+
77
+ def _load_full_trace(
78
+ file_path: str | Path, loader: Callable[[str | Path], WaveformTrace] | None
79
+ ) -> WaveformTrace:
80
+ """Load full trace metadata.
81
+
82
+ Args:
83
+ file_path: Path to trace file.
84
+ loader: Optional custom loader.
85
+
86
+ Returns:
87
+ WaveformTrace object.
73
88
 
74
- # Import loader here to avoid circular dependency
75
- from ..loaders import load
89
+ Raises:
90
+ ValueError: If failed to load trace metadata.
91
+ TypeError: If loaded object is not WaveformTrace.
92
+ """
93
+ from oscura.loaders import load
76
94
 
77
- # Use provided loader or default
95
+ file_path = Path(file_path)
78
96
  load_func = loader if loader is not None else load
79
97
 
80
- # Load full trace metadata to get total size
81
- # For memory-mapped files, this doesn't load data
82
98
  try:
83
- full_trace = load_func(file_path)
99
+ loaded_trace = load_func(file_path)
84
100
  except Exception as e:
85
101
  raise ValueError(f"Failed to load trace metadata: {e}") from e
86
102
 
87
- total_samples = len(full_trace.data) # type: ignore[union-attr]
88
- chunk_samples = int(chunk_size) if chunk_size < 1e6 else int(chunk_size / 8)
103
+ if not isinstance(loaded_trace, WaveformTrace):
104
+ raise TypeError(f"Expected WaveformTrace, got {type(loaded_trace)}")
105
+
106
+ return loaded_trace
107
+
89
108
 
90
- # Calculate number of chunks
109
+ def _compute_chunk_parameters(
110
+ total_samples: int, chunk_size: int | float, overlap: int
111
+ ) -> tuple[int, int]:
112
+ """Compute chunk size and number of chunks.
113
+
114
+ Args:
115
+ total_samples: Total number of samples.
116
+ chunk_size: Size specification.
117
+ overlap: Overlap samples.
118
+
119
+ Returns:
120
+ Tuple of (chunk_samples, num_chunks).
121
+ """
122
+ chunk_samples = int(chunk_size) if chunk_size < 1e6 else int(chunk_size / 8)
91
123
  num_chunks = (total_samples - overlap) // (chunk_samples - overlap)
92
124
  if (total_samples - overlap) % (chunk_samples - overlap) != 0:
93
125
  num_chunks += 1
126
+ return chunk_samples, num_chunks
127
+
128
+
129
+ def _generate_chunks(
130
+ full_trace: WaveformTrace,
131
+ chunk_samples: int,
132
+ overlap: int,
133
+ num_chunks: int,
134
+ progress_callback: Callable[[int, int], None] | None,
135
+ ) -> Generator[WaveformTrace, None, None]:
136
+ """Generate trace chunks.
137
+
138
+ Args:
139
+ full_trace: Full trace object.
140
+ chunk_samples: Samples per chunk.
141
+ overlap: Overlap samples.
142
+ num_chunks: Total number of chunks.
143
+ progress_callback: Optional progress callback.
94
144
 
95
- # Yield chunks
145
+ Yields:
146
+ WaveformTrace chunks.
147
+ """
148
+ total_samples = len(full_trace.data)
96
149
  chunk_num = 0
97
150
  start_idx = 0
98
151
 
99
152
  while start_idx < total_samples:
100
153
  end_idx = min(start_idx + chunk_samples, total_samples)
154
+ chunk_data = full_trace.data[start_idx:end_idx]
155
+ chunk_trace = WaveformTrace(data=chunk_data, metadata=full_trace.metadata)
101
156
 
102
- # Extract chunk
103
- chunk_data = full_trace.data[start_idx:end_idx] # type: ignore[union-attr]
104
-
105
- # Create chunk trace with same metadata
106
- # Cast needed for mypy: slicing a floating array returns a floating array
107
- chunk_trace = WaveformTrace(
108
- data=cast("NDArray[np.floating[Any]]", chunk_data),
109
- metadata=full_trace.metadata,
110
- )
111
-
112
- # Call progress callback if provided
113
157
  if progress_callback is not None:
114
158
  progress_callback(chunk_num, num_chunks)
115
159
 
116
160
  yield chunk_trace
117
161
 
118
- # Move to next chunk, accounting for overlap
119
162
  start_idx = end_idx - overlap
120
163
  chunk_num += 1
121
164
 
122
- # Break if we've reached the end
123
165
  if end_idx >= total_samples:
124
166
  break
125
167
 
@@ -347,6 +389,121 @@ class StreamingAnalyzer:
347
389
  self._hist_edges = None
348
390
 
349
391
 
392
+ def _prepare_spectrogram_params(
393
+ n: int, nperseg: int, noverlap: int | None, overlap: int, chunk_size: int
394
+ ) -> tuple[int, int]:
395
+ """Prepare spectrogram parameters and validate inputs.
396
+
397
+ Args:
398
+ n: Signal length.
399
+ nperseg: Segment length for STFT.
400
+ noverlap: Overlap between STFT segments.
401
+ overlap: Overlap between chunks.
402
+ chunk_size: Chunk size.
403
+
404
+ Returns:
405
+ Tuple of (noverlap, overlap).
406
+ """
407
+ if noverlap is None:
408
+ noverlap = nperseg // 2
409
+
410
+ # Auto-adjust overlap if not specified to ensure continuity
411
+ if overlap == 0:
412
+ overlap = 2 * nperseg
413
+
414
+ return noverlap, overlap
415
+
416
+
417
+ def _compute_single_chunk_spectrogram(
418
+ data: NDArray[np.float64],
419
+ sample_rate: float,
420
+ window: str,
421
+ nperseg: int,
422
+ noverlap: int,
423
+ ) -> tuple[NDArray[np.float64], NDArray[np.float64], NDArray[np.float64]]:
424
+ """Compute spectrogram for single chunk that fits in memory.
425
+
426
+ Args:
427
+ data: Input signal.
428
+ sample_rate: Sample rate in Hz.
429
+ window: Window function name.
430
+ nperseg: Segment length for STFT.
431
+ noverlap: Overlap between STFT segments.
432
+
433
+ Returns:
434
+ (times, frequencies, Sxx_db) tuple.
435
+ """
436
+ freq, times, Sxx = signal.spectrogram(
437
+ data,
438
+ fs=sample_rate,
439
+ window=window,
440
+ nperseg=nperseg,
441
+ noverlap=noverlap,
442
+ scaling="spectrum",
443
+ )
444
+ Sxx = np.maximum(Sxx, 1e-20)
445
+ Sxx_db = 10 * np.log10(Sxx)
446
+ return times, freq, Sxx_db
447
+
448
+
449
+ def _process_spectrogram_chunk(
450
+ data: NDArray[np.float64],
451
+ chunk_start: int,
452
+ chunk_end: int,
453
+ overlap: int,
454
+ n: int,
455
+ sample_rate: float,
456
+ window: str,
457
+ nperseg: int,
458
+ noverlap: int,
459
+ ) -> tuple[NDArray[np.float64], NDArray[np.float64], NDArray[np.float64]]:
460
+ """Process single chunk of spectrogram.
461
+
462
+ Args:
463
+ data: Full signal array.
464
+ chunk_start: Chunk start index.
465
+ chunk_end: Chunk end index.
466
+ overlap: Overlap samples.
467
+ n: Total signal length.
468
+ sample_rate: Sample rate in Hz.
469
+ window: Window function.
470
+ nperseg: Segment length.
471
+ noverlap: Overlap between segments.
472
+
473
+ Returns:
474
+ (freq, times_adjusted, Sxx_chunk) for this chunk.
475
+ """
476
+ extended_start = max(0, chunk_start - overlap)
477
+ extended_end = min(n, chunk_end + overlap)
478
+
479
+ chunk_data = data[extended_start:extended_end]
480
+
481
+ freq, times_chunk, Sxx_chunk = signal.spectrogram(
482
+ chunk_data,
483
+ fs=sample_rate,
484
+ window=window,
485
+ nperseg=nperseg,
486
+ noverlap=noverlap,
487
+ scaling="spectrum",
488
+ )
489
+
490
+ # Adjust time axis and trim overlap
491
+ time_offset = extended_start / sample_rate
492
+ times_chunk_adjusted = times_chunk + time_offset
493
+
494
+ valid_time_start = chunk_start / sample_rate
495
+ valid_time_end = chunk_end / sample_rate
496
+
497
+ valid_mask = (times_chunk_adjusted >= valid_time_start) & (
498
+ times_chunk_adjusted < valid_time_end
499
+ )
500
+
501
+ Sxx_chunk = Sxx_chunk[:, valid_mask]
502
+ times_chunk_adjusted = times_chunk_adjusted[valid_mask]
503
+
504
+ return freq, times_chunk_adjusted, Sxx_chunk
505
+
506
+
350
507
  def chunked_spectrogram(
351
508
  data: NDArray[np.float64],
352
509
  sample_rate: float,
@@ -394,31 +551,14 @@ def chunked_spectrogram(
394
551
  """
395
552
  n = len(data)
396
553
 
397
- # Handle empty input
398
554
  if n == 0:
399
555
  return np.array([]), np.array([]), np.array([]).reshape(0, 0)
400
556
 
401
- if noverlap is None:
402
- noverlap = nperseg // 2
403
-
404
- # Auto-adjust overlap if not specified to ensure continuity
405
- if overlap == 0:
406
- overlap = 2 * nperseg
557
+ noverlap, overlap = _prepare_spectrogram_params(n, nperseg, noverlap, overlap, chunk_size)
407
558
 
408
- # If data fits in one chunk, use scipy directly
559
+ # Single chunk optimization
409
560
  if n <= chunk_size:
410
- freq, times, Sxx = signal.spectrogram(
411
- data,
412
- fs=sample_rate,
413
- window=window,
414
- nperseg=nperseg,
415
- noverlap=noverlap,
416
- scaling="spectrum",
417
- )
418
- # Convert to dB
419
- Sxx = np.maximum(Sxx, 1e-20)
420
- Sxx_db = 10 * np.log10(Sxx)
421
- return times, freq, Sxx_db
561
+ return _compute_single_chunk_spectrogram(data, sample_rate, window, nperseg, noverlap)
422
562
 
423
563
  # Process chunks
424
564
  chunks_stft = []
@@ -426,61 +566,133 @@ def chunked_spectrogram(
426
566
  chunk_start = 0
427
567
 
428
568
  while chunk_start < n:
429
- # Determine chunk boundaries with overlap
430
569
  chunk_end = min(chunk_start + chunk_size, n)
431
570
 
432
- # Extract chunk with overlap extension on both sides
433
- extended_start = max(0, chunk_start - overlap)
434
- extended_end = min(n, chunk_end + overlap)
435
-
436
- chunk_data = data[extended_start:extended_end]
437
-
438
- # Compute spectrogram for chunk
439
- freq, times_chunk, Sxx_chunk = signal.spectrogram(
440
- chunk_data,
441
- fs=sample_rate,
442
- window=window,
443
- nperseg=nperseg,
444
- noverlap=noverlap,
445
- scaling="spectrum",
446
- )
447
-
448
- # Adjust time axis for chunk position
449
- time_offset = extended_start / sample_rate
450
- times_chunk_adjusted = times_chunk + time_offset
451
-
452
- # Trim overlap regions to avoid duplication
453
- valid_time_start = chunk_start / sample_rate
454
- valid_time_end = chunk_end / sample_rate
455
-
456
- valid_mask = (times_chunk_adjusted >= valid_time_start) & (
457
- times_chunk_adjusted < valid_time_end
571
+ freq, times_adjusted, Sxx_chunk = _process_spectrogram_chunk(
572
+ data, chunk_start, chunk_end, overlap, n, sample_rate, window, nperseg, noverlap
458
573
  )
459
574
 
460
- if np.any(valid_mask):
461
- Sxx_chunk = Sxx_chunk[:, valid_mask]
462
- times_chunk_adjusted = times_chunk_adjusted[valid_mask]
463
-
575
+ if Sxx_chunk.shape[1] > 0:
464
576
  chunks_stft.append(Sxx_chunk)
465
- chunks_times.append(times_chunk_adjusted)
577
+ chunks_times.append(times_adjusted)
466
578
 
467
- # Move to next chunk
468
579
  chunk_start = chunk_end
469
580
 
470
- # Concatenate all chunks
471
581
  if len(chunks_stft) == 0:
472
582
  raise ValueError("No valid chunks produced")
473
583
 
474
584
  Sxx = np.concatenate(chunks_stft, axis=1)
475
585
  times = np.concatenate(chunks_times)
476
586
 
477
- # Convert to dB
478
587
  Sxx = np.maximum(Sxx, 1e-20)
479
- Sxx_db = 10 * np.log10(Sxx)
588
+ Sxx_db: NDArray[np.float64] = np.asarray(10 * np.log10(Sxx), dtype=np.float64)
480
589
 
481
590
  return times, freq, Sxx_db
482
591
 
483
592
 
593
+ def _compute_single_chunk_fft(
594
+ data: NDArray[np.float64],
595
+ sample_rate: float,
596
+ window: str,
597
+ nfft: int | None,
598
+ ) -> tuple[NDArray[np.float64], NDArray[np.float64]]:
599
+ """Compute FFT for signal that fits in single chunk.
600
+
601
+ Args:
602
+ data: Input signal.
603
+ sample_rate: Sample rate in Hz.
604
+ window: Window function name.
605
+ nfft: FFT length.
606
+
607
+ Returns:
608
+ Tuple of (frequencies, magnitude_db).
609
+ """
610
+ from oscura.utils.windowing import get_window
611
+
612
+ n = len(data)
613
+ nfft = nfft if nfft is not None else int(2 ** np.ceil(np.log2(n)))
614
+
615
+ w = get_window(window, n)
616
+ data_windowed = data * w
617
+ spectrum = np.fft.rfft(data_windowed, n=nfft)
618
+ freq = np.fft.rfftfreq(nfft, d=1.0 / sample_rate)
619
+
620
+ window_gain = np.sum(w) / n
621
+ magnitude = np.abs(spectrum) / (n * window_gain)
622
+ magnitude = np.maximum(magnitude, 1e-20)
623
+ magnitude_db = 20 * np.log10(magnitude)
624
+
625
+ return freq, magnitude_db
626
+
627
+
628
+ def _setup_chunked_fft_params(
629
+ chunk_size: int, overlap: float, nfft_in: int | None, window: str, sample_rate: float
630
+ ) -> tuple[int, int, NDArray[np.float64], float, NDArray[np.float64]]:
631
+ """Setup parameters for chunked FFT processing.
632
+
633
+ Args:
634
+ chunk_size: Chunk size.
635
+ overlap: Overlap percentage.
636
+ nfft: FFT length.
637
+ window: Window function name.
638
+ sample_rate: Sample rate in Hz.
639
+
640
+ Returns:
641
+ Tuple of (hop, nfft, w, window_gain, freq).
642
+ """
643
+ from oscura.utils.windowing import get_window
644
+
645
+ overlap_samples = int(chunk_size * overlap / 100.0)
646
+ hop = chunk_size - overlap_samples
647
+ nfft = nfft_in if nfft_in is not None else int(2 ** np.ceil(np.log2(chunk_size)))
648
+
649
+ w = get_window(window, chunk_size)
650
+ window_gain_raw = np.sum(w) / chunk_size
651
+ window_gain: float = float(window_gain_raw)
652
+ freq: NDArray[np.float64] = np.asarray(
653
+ np.fft.rfftfreq(nfft, d=1.0 / sample_rate), dtype=np.float64
654
+ )
655
+
656
+ return hop, nfft, w, window_gain, freq
657
+
658
+
659
+ def _process_fft_segment(
660
+ data: NDArray[np.float64],
661
+ start: int,
662
+ chunk_size: int,
663
+ n: int,
664
+ w: NDArray[np.float64],
665
+ window_gain: float,
666
+ nfft: int,
667
+ ) -> NDArray[np.float64]:
668
+ """Process single FFT segment.
669
+
670
+ Args:
671
+ data: Input signal.
672
+ start: Segment start index.
673
+ chunk_size: Chunk size.
674
+ n: Total signal length.
675
+ w: Window function.
676
+ window_gain: Window gain.
677
+ nfft: FFT length.
678
+
679
+ Returns:
680
+ Magnitude spectrum for segment.
681
+ """
682
+ end = min(start + chunk_size, n)
683
+
684
+ if end - start < chunk_size:
685
+ segment = np.zeros(chunk_size)
686
+ segment[: end - start] = data[start:end]
687
+ else:
688
+ segment = data[start:end]
689
+
690
+ segment = segment - np.mean(segment)
691
+ segment_windowed = segment * w
692
+ spectrum = np.fft.rfft(segment_windowed, n=nfft)
693
+ return np.abs(spectrum) / (chunk_size * window_gain)
694
+
695
+
484
696
  def chunked_fft(
485
697
  data: NDArray[np.float64],
486
698
  sample_rate: float,
@@ -519,84 +731,31 @@ def chunked_fft(
519
731
  MEM-006: Chunked FFT requirement
520
732
  Welch's method for spectral estimation
521
733
  """
522
- from ..utils.windowing import get_window
523
-
524
734
  n = len(data)
525
735
 
526
- # Handle empty input
736
+ # Setup: handle empty or single-chunk signals
527
737
  if n == 0:
528
738
  return np.array([]), np.array([])
529
739
 
530
- # If data fits in one chunk, compute single FFT
531
740
  if n <= chunk_size:
532
- if nfft is None:
533
- nfft = int(2 ** np.ceil(np.log2(n)))
534
-
535
- # Apply window
536
- w = get_window(window, n)
537
- data_windowed = data * w
538
-
539
- # Compute FFT
540
- spectrum = np.fft.rfft(data_windowed, n=nfft)
541
-
542
- # Frequency axis
543
- freq = np.fft.rfftfreq(nfft, d=1.0 / sample_rate)
544
-
545
- # Magnitude in dB (normalized by window gain)
546
- window_gain = np.sum(w) / n
547
- magnitude = np.abs(spectrum) / (n * window_gain)
548
- magnitude = np.maximum(magnitude, 1e-20)
549
- magnitude_db = 20 * np.log10(magnitude)
741
+ return _compute_single_chunk_fft(data, sample_rate, window, nfft)
550
742
 
551
- return freq, magnitude_db
743
+ # Processing: setup parameters and process segments
744
+ hop, nfft, w, window_gain, freq = _setup_chunked_fft_params(
745
+ chunk_size, overlap, nfft, window, sample_rate
746
+ )
552
747
 
553
- # Calculate overlap
554
748
  overlap_samples = int(chunk_size * overlap / 100.0)
555
- hop = chunk_size - overlap_samples
556
-
557
- # Determine number of segments
558
749
  num_segments = max(1, (n - overlap_samples) // hop)
559
-
560
- if nfft is None:
561
- nfft = int(2 ** np.ceil(np.log2(chunk_size)))
562
-
563
- # Prepare window
564
- w = get_window(window, chunk_size)
565
- window_gain = np.sum(w) / chunk_size
566
-
567
- # Accumulate magnitude spectra
568
- freq = np.fft.rfftfreq(nfft, d=1.0 / sample_rate)
569
750
  magnitude_sum = np.zeros(len(freq))
570
751
 
571
752
  for i in range(num_segments):
572
753
  start = i * hop
573
- end = min(start + chunk_size, n)
574
-
575
- # Extract segment
576
- if end - start < chunk_size:
577
- # Last segment: pad with zeros
578
- segment = np.zeros(chunk_size)
579
- segment[: end - start] = data[start:end]
580
- else:
581
- segment = data[start:end]
582
-
583
- # Detrend (remove mean)
584
- segment = segment - np.mean(segment)
585
-
586
- # Window
587
- segment_windowed = segment * w
588
-
589
- # FFT
590
- spectrum = np.fft.rfft(segment_windowed, n=nfft)
591
-
592
- # Accumulate magnitude
593
- magnitude = np.abs(spectrum) / (chunk_size * window_gain)
754
+ magnitude = _process_fft_segment(data, start, chunk_size, n, w, window_gain, nfft)
594
755
  magnitude_sum += magnitude
595
756
 
596
- # Average
757
+ # Result building: average and convert to dB
597
758
  magnitude_avg = magnitude_sum / num_segments
598
-
599
- # Convert to dB
600
759
  magnitude_avg = np.maximum(magnitude_avg, 1e-20)
601
760
  magnitude_db = 20 * np.log10(magnitude_avg)
602
761
 
@@ -14,7 +14,7 @@ from typing import TYPE_CHECKING, Any
14
14
 
15
15
  import numpy as np
16
16
 
17
- from ..quality.scoring import assess_data_quality
17
+ from oscura.validation.quality.scoring import assess_data_quality
18
18
 
19
19
  if TYPE_CHECKING:
20
20
  from numpy.typing import NDArray
@@ -16,7 +16,7 @@ from typing import TYPE_CHECKING, Any
16
16
 
17
17
  import numpy as np
18
18
 
19
- from ..core.types import TraceMetadata, WaveformTrace
19
+ from oscura.core.types import TraceMetadata, WaveformTrace
20
20
 
21
21
  if TYPE_CHECKING:
22
22
  from collections.abc import Generator
@@ -335,7 +335,8 @@ class SimulatedSource(RealtimeSource):
335
335
  ) % (2 * np.pi)
336
336
 
337
337
  # Cast to the expected return type
338
- return signal.astype(np.float64) # type: ignore[return-value,no-any-return]
338
+ result: NDArray[np.floating[Any]] = signal.astype(np.float64)
339
+ return result
339
340
 
340
341
  def start(self) -> None:
341
342
  """Start acquisition."""
@@ -5,35 +5,35 @@ triggering, pattern triggering, pulse width triggering, glitch detection,
5
5
  runt pulse detection, and window/zone triggering.
6
6
 
7
7
  Example:
8
- >>> from oscura.triggering import EdgeTrigger, find_triggers
8
+ >>> from oscura.utils.triggering import EdgeTrigger, find_triggers
9
9
  >>> trigger = EdgeTrigger(level=1.5, edge="rising")
10
10
  >>> events = trigger.find_events(trace)
11
11
  >>> # Or use convenience function
12
12
  >>> events = find_triggers(trace, "edge", level=1.5, edge="rising")
13
13
  """
14
14
 
15
- from oscura.triggering.base import (
15
+ from oscura.utils.triggering.base import (
16
16
  Trigger,
17
17
  TriggerEvent,
18
18
  find_triggers,
19
19
  )
20
- from oscura.triggering.edge import (
20
+ from oscura.utils.triggering.edge import (
21
21
  EdgeTrigger,
22
22
  find_all_edges,
23
23
  find_falling_edges,
24
24
  find_rising_edges,
25
25
  )
26
- from oscura.triggering.pattern import (
26
+ from oscura.utils.triggering.pattern import (
27
27
  PatternTrigger,
28
28
  find_pattern,
29
29
  )
30
- from oscura.triggering.pulse import (
30
+ from oscura.utils.triggering.pulse import (
31
31
  PulseWidthTrigger,
32
32
  find_glitches,
33
33
  find_pulses,
34
34
  find_runt_pulses,
35
35
  )
36
- from oscura.triggering.window import (
36
+ from oscura.utils.triggering.window import (
37
37
  WindowTrigger,
38
38
  ZoneTrigger,
39
39
  check_limits,
@@ -134,9 +134,9 @@ def find_triggers(
134
134
  >>> events = find_triggers(trace, "pulse_width", min_width=1e-6, max_width=2e-6)
135
135
  >>> events = find_triggers(trace, "glitch", max_width=50e-9)
136
136
  """
137
- from oscura.triggering.edge import EdgeTrigger
138
- from oscura.triggering.pulse import PulseWidthTrigger
139
- from oscura.triggering.window import WindowTrigger
137
+ from oscura.utils.triggering.edge import EdgeTrigger
138
+ from oscura.utils.triggering.pulse import PulseWidthTrigger
139
+ from oscura.utils.triggering.window import WindowTrigger
140
140
 
141
141
  if trigger_type == "edge":
142
142
  trigger = EdgeTrigger(
@@ -145,7 +145,7 @@ def find_triggers(
145
145
  hysteresis=kwargs.get("hysteresis", 0.0),
146
146
  )
147
147
  elif trigger_type == "pattern":
148
- from oscura.triggering.pattern import PatternTrigger
148
+ from oscura.utils.triggering.pattern import PatternTrigger
149
149
 
150
150
  trigger = PatternTrigger( # type: ignore[assignment]
151
151
  pattern=kwargs.get("pattern", []),
@@ -159,7 +159,7 @@ def find_triggers(
159
159
  max_width=kwargs.get("max_width"),
160
160
  )
161
161
  elif trigger_type == "glitch":
162
- from oscura.triggering.pulse import GlitchTrigger
162
+ from oscura.utils.triggering.pulse import GlitchTrigger
163
163
 
164
164
  trigger = GlitchTrigger( # type: ignore[assignment]
165
165
  level=kwargs.get("level", 0.0),
@@ -167,7 +167,7 @@ def find_triggers(
167
167
  polarity=kwargs.get("polarity", "either"),
168
168
  )
169
169
  elif trigger_type == "runt":
170
- from oscura.triggering.pulse import RuntTrigger
170
+ from oscura.utils.triggering.pulse import RuntTrigger
171
171
 
172
172
  trigger = RuntTrigger( # type: ignore[assignment]
173
173
  low_threshold=kwargs.get("low_threshold", 0.0),
@@ -4,7 +4,7 @@ Provides edge detection with configurable thresholds, hysteresis,
4
4
  and edge polarity (rising, falling, or both).
5
5
 
6
6
  Example:
7
- >>> from oscura.triggering.edge import EdgeTrigger, find_rising_edges
7
+ >>> from oscura.utils.triggering.edge import EdgeTrigger, find_rising_edges
8
8
  >>> # Object-oriented approach
9
9
  >>> trigger = EdgeTrigger(level=1.5, edge="rising", hysteresis=0.1)
10
10
  >>> events = trigger.find_events(trace)
@@ -19,7 +19,7 @@ from typing import TYPE_CHECKING, Any, Literal
19
19
  import numpy as np
20
20
 
21
21
  from oscura.core.types import DigitalTrace, WaveformTrace
22
- from oscura.triggering.base import (
22
+ from oscura.utils.triggering.base import (
23
23
  Trigger,
24
24
  TriggerEvent,
25
25
  TriggerType,