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
@@ -25,12 +25,68 @@ from typing import TYPE_CHECKING, Any, Literal, cast
25
25
 
26
26
  import numpy as np
27
27
 
28
+ from oscura.core.numba_backend import njit, prange
28
29
  from oscura.core.types import WaveformTrace
29
30
 
30
31
  if TYPE_CHECKING:
31
32
  from numpy.typing import NDArray
32
33
 
33
34
 
35
+ @njit(parallel=True, cache=True) # type: ignore[untyped-decorator] # Numba JIT decorator
36
+ def _autocorr_direct_numba(
37
+ data: np.ndarray, # type: ignore[type-arg]
38
+ max_lag: int,
39
+ ) -> np.ndarray: # type: ignore[type-arg]
40
+ """Compute autocorrelation using direct method with Numba JIT compilation.
41
+
42
+ Alternative implementation using Numba for autocorrelation computation.
43
+ Uses parallel execution across lags for potential speedup on multi-core systems.
44
+
45
+ **Note**: In practice, numpy.correlate is faster for most cases due to highly
46
+ optimized BLAS/LAPACK routines. This function is provided for educational
47
+ purposes and specific use cases where custom computation logic is needed.
48
+
49
+ Args:
50
+ data: Mean-centered input signal data (1D array).
51
+ max_lag: Maximum lag to compute (inclusive).
52
+
53
+ Returns:
54
+ Autocorrelation values from lag 0 to max_lag (unnormalized).
55
+
56
+ Performance characteristics:
57
+ - First call: ~100-200ms compilation overhead (cached for subsequent calls)
58
+ - Typical performance: ~2ms for n=100, ~10ms for n=256 (compiled)
59
+ - NumPy's correlate: ~0.02ms for n=100 (100x faster due to BLAS)
60
+ - Parallel execution: Benefits from multi-core for large max_lag
61
+
62
+ Example:
63
+ >>> import numpy as np
64
+ >>> data = np.random.randn(128) - np.mean(np.random.randn(128))
65
+ >>> acf = _autocorr_direct_numba(data, max_lag=64)
66
+ >>> print(acf.shape) # (65,)
67
+
68
+ Notes:
69
+ - Input data should be mean-centered before calling this function
70
+ - Result is NOT normalized; caller must normalize if needed
71
+ - NumPy's correlate is recommended for production use (faster)
72
+ - Thread safety: Numba releases GIL, safe for parallel execution
73
+
74
+ References:
75
+ Box, G. E. P. & Jenkins, G. M. (1976). Time Series Analysis
76
+ """
77
+ n = len(data)
78
+ acf = np.zeros(max_lag + 1, dtype=np.float64)
79
+
80
+ # Compute autocorrelation for each lag in parallel
81
+ for lag in prange(max_lag + 1):
82
+ sum_val = 0.0
83
+ for i in range(n - lag):
84
+ sum_val += data[i] * data[i + lag]
85
+ acf[lag] = sum_val
86
+
87
+ return acf
88
+
89
+
34
90
  @dataclass
35
91
  class CrossCorrelationResult:
36
92
  """Result of cross-correlation analysis.
@@ -66,6 +122,10 @@ def autocorrelation(
66
122
  Measures self-similarity of a signal at different time lags.
67
123
  Useful for detecting periodicities and characteristic time scales.
68
124
 
125
+ This function automatically selects the optimal computation method:
126
+ - Small signals (n < 256): Direct method using numpy.correlate (optimized BLAS)
127
+ - Large signals (n >= 256): FFT-based method (O(n log n) complexity)
128
+
69
129
  Args:
70
130
  trace: Input trace or numpy array.
71
131
  max_lag: Maximum lag to compute (samples). If None, uses n // 2.
@@ -80,6 +140,11 @@ def autocorrelation(
80
140
  Raises:
81
141
  ValueError: If sample_rate is not provided when trace is array.
82
142
 
143
+ Performance:
144
+ - Small signals (n<256): ~0.02-0.05ms using numpy.correlate
145
+ - Large signals (n>=256): ~0.1-1ms using FFT method
146
+ - Both methods use highly optimized numerical libraries
147
+
83
148
  Example:
84
149
  >>> lag_times, acf = autocorrelation(trace, max_lag=1000)
85
150
  >>> # Find first zero crossing for decorrelation time
@@ -108,15 +173,19 @@ def autocorrelation(
108
173
  # Remove mean for proper correlation
109
174
  data_centered = data - np.mean(data)
110
175
 
111
- # Compute autocorrelation via FFT (faster for large n)
112
- if n > 256:
176
+ # Compute autocorrelation using optimal method based on signal size:
177
+ # - n >= 256: FFT-based method (O(n log n) complexity, fastest for large signals)
178
+ # - n < 256: Direct method using numpy.correlate (highly optimized BLAS)
179
+ # Note: Numba implementation available (_autocorr_direct_numba) but numpy.correlate
180
+ # is faster due to optimized BLAS/LAPACK routines
181
+ if n >= 256:
113
182
  # Zero-pad for full correlation
114
183
  nfft = int(2 ** np.ceil(np.log2(2 * n)))
115
184
  fft_data = np.fft.rfft(data_centered, n=nfft)
116
185
  acf_full = np.fft.irfft(fft_data * np.conj(fft_data), n=nfft)
117
186
  acf = acf_full[: max_lag + 1]
118
187
  else:
119
- # Direct computation for small n
188
+ # Direct computation for small signals (numpy.correlate uses optimized BLAS)
120
189
  acf = np.correlate(data_centered, data_centered, mode="full")
121
190
  acf = acf[n - 1 : n + max_lag]
122
191
 
@@ -131,6 +200,71 @@ def autocorrelation(
131
200
  return lag_times, acf.astype(np.float64)
132
201
 
133
202
 
203
+ def _extract_correlation_data(
204
+ trace1: WaveformTrace | NDArray[np.floating[Any]],
205
+ trace2: WaveformTrace | NDArray[np.floating[Any]],
206
+ sample_rate: float | None,
207
+ ) -> tuple[NDArray[np.floating[Any]], NDArray[np.floating[Any]], float]:
208
+ """Extract data arrays and sample rate from traces.
209
+
210
+ Args:
211
+ trace1: First input trace or array
212
+ trace2: Second input trace or array
213
+ sample_rate: Sample rate if traces are arrays
214
+
215
+ Returns:
216
+ Tuple of (data1, data2, sample_rate)
217
+
218
+ Raises:
219
+ ValueError: If sample_rate needed but not provided
220
+ """
221
+ if isinstance(trace1, WaveformTrace):
222
+ data1 = trace1.data
223
+ fs = trace1.metadata.sample_rate
224
+ else:
225
+ data1 = trace1
226
+ if sample_rate is None:
227
+ raise ValueError("sample_rate required when traces are arrays")
228
+ fs = sample_rate
229
+
230
+ if isinstance(trace2, WaveformTrace):
231
+ data2 = trace2.data
232
+ if not isinstance(trace1, WaveformTrace):
233
+ fs = trace2.metadata.sample_rate
234
+ else:
235
+ data2 = trace2
236
+
237
+ return data1, data2, fs
238
+
239
+
240
+ def _compute_normalized_xcorr(
241
+ data1_centered: NDArray[np.floating[Any]],
242
+ data2_centered: NDArray[np.floating[Any]],
243
+ xcorr: NDArray[np.floating[Any]],
244
+ ) -> NDArray[np.floating[Any]]:
245
+ """Normalize cross-correlation to [-1, 1].
246
+
247
+ Args:
248
+ data1_centered: First centered data array
249
+ data2_centered: Second centered data array
250
+ xcorr: Raw cross-correlation
251
+
252
+ Returns:
253
+ Normalized cross-correlation
254
+ """
255
+ norm1 = np.sqrt(np.sum(data1_centered**2))
256
+ norm2 = np.sqrt(np.sum(data2_centered**2))
257
+ if norm1 > 0 and norm2 > 0:
258
+ # Division returns proper NDArray type
259
+ if TYPE_CHECKING:
260
+ from typing import cast
261
+
262
+ return cast("NDArray[np.floating[Any]]", xcorr / (norm1 * norm2))
263
+ else:
264
+ return xcorr / (norm1 * norm2) # type: ignore[return-value]
265
+ return xcorr
266
+
267
+
134
268
  def cross_correlation(
135
269
  trace1: WaveformTrace | NDArray[np.floating[Any]],
136
270
  trace2: WaveformTrace | NDArray[np.floating[Any]],
@@ -165,71 +299,37 @@ def cross_correlation(
165
299
  References:
166
300
  Oppenheim, A. V. & Schafer, R. W. (2009). Discrete-Time Signal Processing
167
301
  """
168
- if isinstance(trace1, WaveformTrace):
169
- data1 = trace1.data
170
- fs = trace1.metadata.sample_rate
171
- else:
172
- data1 = trace1
173
- if sample_rate is None:
174
- raise ValueError("sample_rate required when traces are arrays")
175
- fs = sample_rate
302
+ data1, data2, fs = _extract_correlation_data(trace1, trace2, sample_rate)
303
+ n1 = len(data1)
304
+ max_lag = min(len(data1), len(data2)) // 2 if max_lag is None else max_lag
176
305
 
177
- if isinstance(trace2, WaveformTrace):
178
- data2 = trace2.data
179
- # Use trace2 sample rate if available and trace1 wasn't a WaveformTrace
180
- if not isinstance(trace1, WaveformTrace):
181
- fs = trace2.metadata.sample_rate
182
- else:
183
- data2 = trace2
184
-
185
- n1, n2 = len(data1), len(data2)
186
-
187
- if max_lag is None:
188
- max_lag = min(n1, n2) // 2
189
-
190
- # Center the data
306
+ # Center and compute correlation
191
307
  data1_centered = data1 - np.mean(data1)
192
308
  data2_centered = data2 - np.mean(data2)
193
-
194
- # Full cross-correlation
195
- # Note: np.correlate(a, b) computes sum(a[n+k] * conj(b[k]))
196
- # For cross-correlation where we want to detect b delayed relative to a,
197
- # we need correlate(b, a) so positive lag means b is delayed
198
309
  xcorr_full = np.correlate(data2_centered, data1_centered, mode="full")
199
310
 
200
- # Extract relevant portion around zero lag
201
- # Full correlation has length n1 + n2 - 1, with zero lag at index n1 - 1
202
- # (since we swapped the order above)
311
+ # Extract relevant portion
203
312
  zero_lag_idx = n1 - 1
204
313
  start_idx = max(0, zero_lag_idx - max_lag)
205
314
  end_idx = min(len(xcorr_full), zero_lag_idx + max_lag + 1)
206
315
  xcorr = xcorr_full[start_idx:end_idx]
207
-
208
- # Create lag array
209
316
  lags = np.arange(start_idx - zero_lag_idx, end_idx - zero_lag_idx)
210
317
 
211
- # Normalize
318
+ # Normalize if requested
212
319
  if normalized:
213
- norm1 = np.sqrt(np.sum(data1_centered**2))
214
- norm2 = np.sqrt(np.sum(data2_centered**2))
215
- if norm1 > 0 and norm2 > 0:
216
- xcorr = xcorr / (norm1 * norm2)
320
+ xcorr = _compute_normalized_xcorr(data1_centered, data2_centered, xcorr)
217
321
 
218
322
  # Find peak
219
323
  peak_local_idx = np.argmax(np.abs(xcorr))
220
324
  peak_lag = int(lags[peak_local_idx])
221
325
  peak_coefficient = float(xcorr[peak_local_idx])
222
326
 
223
- # Time values
224
- lag_times = lags / fs
225
- peak_lag_time = peak_lag / fs
226
-
227
327
  return CrossCorrelationResult(
228
328
  correlation=xcorr.astype(np.float64),
229
329
  lags=lags,
230
- lag_times=lag_times.astype(np.float64),
330
+ lag_times=(lags / fs).astype(np.float64),
231
331
  peak_lag=peak_lag,
232
- peak_lag_time=peak_lag_time,
332
+ peak_lag_time=peak_lag / fs,
233
333
  peak_coefficient=peak_coefficient,
234
334
  sample_rate=fs,
235
335
  )
@@ -267,6 +367,92 @@ def correlation_coefficient(
267
367
  return float(np.corrcoef(data1, data2)[0, 1])
268
368
 
269
369
 
370
+ def _extract_periodicity_data(
371
+ trace: WaveformTrace | NDArray[np.floating[Any]], sample_rate: float | None
372
+ ) -> tuple[NDArray[np.floating[Any]], float]:
373
+ """Extract data and sample rate from trace.
374
+
375
+ Args:
376
+ trace: Input trace or array.
377
+ sample_rate: Sample rate if array.
378
+
379
+ Returns:
380
+ Tuple of (data, sample_rate).
381
+
382
+ Raises:
383
+ ValueError: If sample_rate not provided for array.
384
+ """
385
+ if isinstance(trace, WaveformTrace):
386
+ return trace.data, trace.metadata.sample_rate
387
+ else:
388
+ if sample_rate is None:
389
+ raise ValueError("sample_rate required when trace is array")
390
+ return trace, sample_rate
391
+
392
+
393
+ def _find_primary_peak(acf: NDArray[np.float64], min_period_samples: int) -> tuple[int, float]:
394
+ """Find primary peak in autocorrelation function.
395
+
396
+ Args:
397
+ acf: Autocorrelation function.
398
+ min_period_samples: Minimum period to consider.
399
+
400
+ Returns:
401
+ Tuple of (period_samples, strength).
402
+ """
403
+ acf_search = acf[min_period_samples:]
404
+
405
+ if len(acf_search) < 3:
406
+ return -1, np.nan
407
+
408
+ local_max = (acf_search[1:-1] > acf_search[:-2]) & (acf_search[1:-1] > acf_search[2:])
409
+ max_indices = np.where(local_max)[0] + 1
410
+
411
+ if len(max_indices) == 0:
412
+ primary_idx = int(np.argmax(acf_search)) + min_period_samples
413
+ else:
414
+ peak_values = acf_search[max_indices]
415
+ best_peak_idx = int(np.argmax(peak_values))
416
+ primary_idx = int(max_indices[best_peak_idx]) + min_period_samples
417
+
418
+ return primary_idx, float(acf[primary_idx])
419
+
420
+
421
+ def _find_harmonics(acf: NDArray[np.float64], period_samples: int) -> list[dict[str, int | float]]:
422
+ """Find harmonic peaks at multiples of primary period.
423
+
424
+ Args:
425
+ acf: Autocorrelation function.
426
+ period_samples: Primary period in samples.
427
+
428
+ Returns:
429
+ List of harmonic dictionaries.
430
+ """
431
+ harmonics: list[dict[str, int | float]] = []
432
+
433
+ for h in range(2, 6):
434
+ harmonic_lag = h * period_samples
435
+ if harmonic_lag >= len(acf):
436
+ break
437
+
438
+ search_range = max(1, period_samples // 4)
439
+ start = int(max(0, harmonic_lag - search_range))
440
+ end = int(min(len(acf), harmonic_lag + search_range))
441
+ local_max_idx = int(start + int(np.argmax(acf[start:end])))
442
+ harmonic_strength = float(acf[local_max_idx])
443
+
444
+ if harmonic_strength > 0.3:
445
+ harmonics.append(
446
+ {
447
+ "harmonic": h,
448
+ "lag_samples": local_max_idx,
449
+ "strength": harmonic_strength,
450
+ }
451
+ )
452
+
453
+ return harmonics
454
+
455
+
270
456
  def find_periodicity(
271
457
  trace: WaveformTrace | NDArray[np.floating[Any]],
272
458
  *,
@@ -301,32 +487,19 @@ def find_periodicity(
301
487
  >>> print(f"Period: {result['period_time']*1e6:.2f} us")
302
488
  >>> print(f"Frequency: {result['frequency']/1e3:.1f} kHz")
303
489
  """
304
- if isinstance(trace, WaveformTrace):
305
- data = trace.data
306
- fs = trace.metadata.sample_rate
307
- else:
308
- data = trace
309
- if sample_rate is None:
310
- raise ValueError("sample_rate required when trace is array")
311
- fs = sample_rate
312
-
490
+ # Setup: extract data and compute autocorrelation
491
+ data, fs = _extract_periodicity_data(trace, sample_rate)
313
492
  n = len(data)
493
+ max_period_samples = max_period_samples if max_period_samples is not None else n // 2
314
494
 
315
- if max_period_samples is None:
316
- max_period_samples = n // 2
317
-
318
- # Compute autocorrelation
319
495
  _lag_times, acf = autocorrelation(
320
- trace,
321
- max_lag=max_period_samples,
322
- sample_rate=sample_rate if sample_rate else fs,
496
+ trace, max_lag=max_period_samples, sample_rate=sample_rate if sample_rate else fs
323
497
  )
324
498
 
325
- # Find peaks in autocorrelation (after lag 0)
326
- # Look for local maxima
327
- acf_search = acf[min_period_samples:]
499
+ # Processing: find primary peak and harmonics
500
+ period_samples, strength = _find_primary_peak(acf, min_period_samples)
328
501
 
329
- if len(acf_search) < 3:
502
+ if period_samples < 0:
330
503
  return {
331
504
  "period_samples": np.nan,
332
505
  "period_time": np.nan,
@@ -335,46 +508,11 @@ def find_periodicity(
335
508
  "harmonics": [],
336
509
  }
337
510
 
338
- # Find local maxima
339
- local_max = (acf_search[1:-1] > acf_search[:-2]) & (acf_search[1:-1] > acf_search[2:])
340
- max_indices = np.where(local_max)[0] + 1 # +1 for offset from [1:-1]
341
-
342
- if len(max_indices) == 0:
343
- # No local maxima found, use global max
344
- primary_idx = int(np.argmax(acf_search)) + min_period_samples
345
- strength = float(acf[primary_idx])
346
- else:
347
- # Find strongest peak
348
- peak_values = acf_search[max_indices]
349
- best_peak_idx = int(np.argmax(peak_values))
350
- primary_idx = int(max_indices[best_peak_idx]) + min_period_samples
351
- strength = float(acf[primary_idx])
352
-
353
- period_samples = int(primary_idx)
354
511
  period_time = period_samples / fs
355
512
  frequency = 1.0 / period_time if period_time > 0 else np.nan
513
+ harmonics = _find_harmonics(acf, period_samples)
356
514
 
357
- # Find harmonics (peaks at multiples of period)
358
- harmonics: list[dict[str, int | float]] = []
359
- for h in range(2, 6): # Check up to 5th harmonic
360
- harmonic_lag = h * period_samples
361
- if harmonic_lag < len(acf):
362
- # Look for peak near expected harmonic
363
- search_range = max(1, period_samples // 4)
364
- start = int(max(0, harmonic_lag - search_range))
365
- end = int(min(len(acf), harmonic_lag + search_range))
366
- local_max_idx = int(start + int(np.argmax(acf[start:end])))
367
- harmonic_strength = float(acf[local_max_idx])
368
-
369
- if harmonic_strength > 0.3: # Threshold for significant harmonic
370
- harmonics.append(
371
- {
372
- "harmonic": h,
373
- "lag_samples": local_max_idx,
374
- "strength": harmonic_strength,
375
- }
376
- )
377
-
515
+ # Result building: construct result dictionary
378
516
  return {
379
517
  "period_samples": period_samples,
380
518
  "period_time": float(period_time),
@@ -483,140 +621,198 @@ def correlate_chunked(
483
621
  MEM-008: Chunked Correlation
484
622
  Oppenheim & Schafer (2009): Discrete-Time Signal Processing, Ch 8
485
623
  """
624
+ _validate_chunked_inputs(signal1, signal2, mode)
625
+
626
+ n1, n2 = len(signal1), len(signal2)
627
+ chunk_size_final = _determine_chunk_size(chunk_size, n1, n2)
628
+
629
+ # Use direct correlation for small signals
630
+ if _should_use_direct_method(n1, n2, chunk_size_final):
631
+ return _direct_correlate(signal1, signal2, mode)
632
+
633
+ # Setup overlap-save parameters
634
+ params = _setup_overlap_save_params(chunk_size_final, n2)
635
+ if params is None:
636
+ return _direct_correlate(signal1, signal2, mode)
637
+
638
+ # Prepare kernel FFT and output buffer
639
+ signal2_flipped = signal2[::-1].copy()
640
+ kernel_fft = np.fft.fft(signal2_flipped, n=params.nfft)
641
+ output = np.zeros(_get_output_length(n1, n2, mode), dtype=np.float64)
642
+
643
+ # Process signal in chunks
644
+ _process_chunks_overlap_save(signal1, kernel_fft, output, params, mode)
645
+
646
+ return output
647
+
648
+
649
+ def _validate_chunked_inputs(
650
+ signal1: NDArray[np.floating[Any]], signal2: NDArray[np.floating[Any]], mode: str
651
+ ) -> None:
652
+ """Validate inputs for chunked correlation."""
486
653
  if len(signal1) == 0 or len(signal2) == 0:
487
654
  raise ValueError("Input signals cannot be empty")
488
-
489
655
  if mode not in ("same", "valid", "full"):
490
656
  raise ValueError(f"Invalid mode: {mode}. Must be 'same', 'valid', or 'full'")
491
657
 
492
- n1 = len(signal1)
493
- n2 = len(signal2)
494
-
495
- # Determine chunk size
496
- if chunk_size is None:
497
- # Auto-select: aim for ~100MB chunks
498
- bytes_per_sample = 8 # float64
499
- target_bytes = 100 * 1024 * 1024
500
- chunk_size = min(target_bytes // bytes_per_sample, n1)
501
- # Round to power of 2 for FFT efficiency
502
- chunk_size = 2 ** int(np.log2(chunk_size))
503
-
504
- # Ensure chunk_size is larger than filter length for overlap-save
505
- # Otherwise overlap-save doesn't make sense
658
+
659
+ def _determine_chunk_size(chunk_size: int | None, n1: int, n2: int) -> int:
660
+ """Determine optimal chunk size for processing."""
661
+ if chunk_size is not None:
662
+ return chunk_size
663
+
664
+ # Auto-select: aim for ~100MB chunks (float64 = 8 bytes)
665
+ target_bytes = 100 * 1024 * 1024
666
+ auto_chunk = min(target_bytes // 8, n1)
667
+ log2_val: np.floating[Any] = np.log2(auto_chunk)
668
+ chunk_power_of_2: int = int(2 ** int(log2_val)) # Round to power of 2
669
+ return chunk_power_of_2
670
+
671
+
672
+ def _should_use_direct_method(n1: int, n2: int, chunk_size: int) -> bool:
673
+ """Check if direct correlation should be used instead of chunked."""
506
674
  min_chunk_size = max(2 * n2, 64)
675
+ return n1 <= min_chunk_size or n2 >= n1 or chunk_size < min_chunk_size
507
676
 
508
- # For small signals or when chunk_size is too small, use direct method
509
- if n1 <= min_chunk_size or n2 >= n1 or chunk_size < min_chunk_size:
510
- mode_literal = cast("Literal['same', 'valid', 'full']", mode)
511
- result = np.correlate(signal1, signal2, mode=mode_literal)
512
- return result.astype(np.float64)
513
677
 
514
- # For correlation, we need to flip signal2
515
- signal2_flipped = signal2[::-1].copy()
678
+ def _direct_correlate(
679
+ signal1: NDArray[np.floating[Any]], signal2: NDArray[np.floating[Any]], mode: str
680
+ ) -> NDArray[np.float64]:
681
+ """Perform direct correlation using numpy."""
682
+ mode_literal = cast("Literal['same', 'valid', 'full']", mode)
683
+ result = np.correlate(signal1, signal2, mode=mode_literal)
684
+ return result.astype(np.float64)
685
+
686
+
687
+ @dataclass
688
+ class _OverlapSaveParams:
689
+ """Parameters for overlap-save algorithm."""
516
690
 
517
- # Overlap-save parameters
518
- # L = chunk size, M = filter length
691
+ chunk_size: int
692
+ filter_len: int
693
+ overlap: int
694
+ step_size: int
695
+ nfft: int
696
+
697
+
698
+ def _setup_overlap_save_params(chunk_size: int, n2: int) -> _OverlapSaveParams | None:
699
+ """Setup overlap-save algorithm parameters."""
700
+ min_chunk_size = max(2 * n2, 64)
519
701
  L = max(chunk_size, min_chunk_size)
520
702
  M = n2
521
703
  overlap = M - 1
522
-
523
- # Ensure step size is positive (L must be > overlap)
524
704
  step_size = L - overlap
705
+
525
706
  if step_size <= 0:
526
- # Fall back to direct method if chunk is too small
527
- mode_literal = cast("Literal['same', 'valid', 'full']", mode)
528
- result = np.correlate(signal1, signal2, mode=mode_literal)
529
- return result.astype(np.float64)
707
+ return None
530
708
 
531
- # FFT size (power of 2, >= L + M - 1)
532
709
  nfft = int(2 ** np.ceil(np.log2(L + M - 1)))
710
+ return _OverlapSaveParams(L, M, overlap, step_size, nfft)
533
711
 
534
- # Pre-compute FFT of flipped signal2 (kernel)
535
- kernel_fft = np.fft.fft(signal2_flipped, n=nfft)
536
712
 
537
- # Output length based on mode
713
+ def _get_output_length(n1: int, n2: int, mode: str) -> int:
714
+ """Calculate output length based on correlation mode."""
538
715
  if mode == "full":
539
- output_len = n1 + n2 - 1
716
+ return n1 + n2 - 1
540
717
  elif mode == "same":
541
- output_len = n1
718
+ return n1
542
719
  else: # valid
543
- output_len = max(0, n1 - n2 + 1)
720
+ return max(0, n1 - n2 + 1)
544
721
 
545
- # Initialize output
546
- output = np.zeros(output_len, dtype=np.float64)
547
722
 
548
- # Process chunks with overlap-save
549
- pos = 0 # Position in signal1
550
- max_iterations = (n1 // step_size) + 2 # Safety limit
551
- iteration = 0
723
+ def _process_chunks_overlap_save(
724
+ signal1: NDArray[np.floating[Any]],
725
+ kernel_fft: NDArray[np.complexfloating[Any, Any]],
726
+ output: NDArray[np.float64],
727
+ params: _OverlapSaveParams,
728
+ mode: str,
729
+ ) -> None:
730
+ """Process signal in chunks using overlap-save method."""
731
+ n1 = len(signal1)
732
+ pos = 0
733
+ max_iterations = (n1 // params.step_size) + 2
734
+
735
+ for _iteration in range(max_iterations):
736
+ if pos >= n1:
737
+ break
738
+
739
+ # Extract and process chunk
740
+ chunk = _extract_chunk(signal1, pos, params, n1)
741
+ conv_result = _convolve_chunk_fft(chunk, kernel_fft, params.nfft)
742
+ valid_output = _extract_valid_portion(conv_result, pos, params)
743
+
744
+ # Write to output buffer
745
+ _write_chunk_output(output, valid_output, pos, params, mode)
746
+
747
+ pos += params.step_size
748
+
749
+
750
+ def _extract_chunk(
751
+ signal1: NDArray[np.floating[Any]], pos: int, params: _OverlapSaveParams, n1: int
752
+ ) -> NDArray[np.floating[Any]]:
753
+ """Extract chunk with appropriate overlap."""
754
+ if pos == 0:
755
+ return signal1[0 : min(params.chunk_size, n1)]
756
+
757
+ chunk_start = max(0, pos - params.overlap)
758
+ chunk_end = min(chunk_start + params.chunk_size, n1)
759
+ return signal1[chunk_start:chunk_end]
760
+
761
+
762
+ def _convolve_chunk_fft(
763
+ chunk: NDArray[np.floating[Any]],
764
+ kernel_fft: NDArray[np.complexfloating[Any, Any]],
765
+ nfft: int,
766
+ ) -> NDArray[np.floating[Any]]:
767
+ """Perform FFT-based convolution on chunk."""
768
+ chunk_padded = np.zeros(nfft, dtype=np.float64)
769
+ chunk_padded[: len(chunk)] = chunk
770
+ chunk_fft = np.fft.fft(chunk_padded)
771
+ conv_fft = chunk_fft * kernel_fft
772
+ return np.fft.ifft(conv_fft).real
773
+
774
+
775
+ def _extract_valid_portion(
776
+ conv_result: NDArray[np.floating[Any]], pos: int, params: _OverlapSaveParams
777
+ ) -> NDArray[np.floating[Any]]:
778
+ """Extract valid portion from convolution result."""
779
+ if pos == 0:
780
+ valid_start = 0
781
+ valid_end = min(params.chunk_size, len(conv_result))
782
+ else:
783
+ valid_start = params.overlap
784
+ valid_end = min(params.chunk_size, len(conv_result))
552
785
 
553
- while pos < n1 and iteration < max_iterations:
554
- iteration += 1
786
+ return conv_result[valid_start:valid_end]
555
787
 
556
- # Extract chunk with overlap from previous chunk
557
- if pos == 0:
558
- # First chunk: no overlap needed
559
- chunk_start = 0
560
- chunk = signal1[0 : min(L, n1)]
561
- else:
562
- # Subsequent chunks: include overlap
563
- chunk_start = max(0, pos - overlap)
564
- chunk_end = min(chunk_start + L, n1)
565
- chunk = signal1[chunk_start:chunk_end]
566
-
567
- # Zero-pad chunk to FFT size
568
- chunk_padded = np.zeros(nfft, dtype=np.float64)
569
- chunk_padded[: len(chunk)] = chunk
570
-
571
- # Perform FFT-based convolution
572
- chunk_fft = np.fft.fft(chunk_padded)
573
- conv_fft = chunk_fft * kernel_fft
574
- conv_result = np.fft.ifft(conv_fft).real
575
-
576
- # Extract valid portion (discard transient at start)
577
- if pos == 0:
578
- # First chunk
579
- valid_start = 0
580
- valid_end = min(L, len(conv_result))
581
- else:
582
- # Subsequent chunks: discard overlap region
583
- valid_start = overlap
584
- valid_end = min(len(chunk), len(conv_result))
585
-
586
- valid_output = conv_result[valid_start:valid_end]
587
-
588
- # Determine output range based on mode
589
- if mode == "full":
590
- # Full convolution includes all overlap
591
- out_start = pos
592
- out_end = min(out_start + len(valid_output), output_len)
593
- elif mode == "same":
594
- # Same mode: center-aligned
595
- offset = (M - 1) // 2
596
- out_start = max(0, pos - offset)
597
- out_end = min(out_start + len(valid_output), output_len)
598
- # Adjust valid_output if we're at boundaries
599
- if pos == 0 and offset > 0:
600
- valid_output = valid_output[offset:]
601
- else: # valid
602
- # Valid mode: only where signals fully overlap
603
- offset = M - 1
604
- if pos < offset:
605
- # Skip this chunk, not in valid region yet
606
- pos += step_size
607
- continue
608
- out_start = pos - offset
609
- out_end = min(out_start + len(valid_output), output_len)
610
-
611
- # Copy to output
612
- copy_len = min(len(valid_output), out_end - out_start)
613
- if copy_len > 0:
614
- output[out_start : out_start + copy_len] = valid_output[:copy_len]
615
-
616
- # Move to next chunk with guaranteed progress
617
- pos += step_size
618
788
 
619
- return output
789
+ def _write_chunk_output(
790
+ output: NDArray[np.float64],
791
+ valid_output: NDArray[np.floating[Any]],
792
+ pos: int,
793
+ params: _OverlapSaveParams,
794
+ mode: str,
795
+ ) -> None:
796
+ """Write chunk output to final buffer based on mode."""
797
+ output_len = len(output)
798
+ offset = (params.filter_len - 1) // 2 if mode == "same" else params.filter_len - 1
799
+
800
+ if mode == "full":
801
+ out_start = pos
802
+ elif mode == "same":
803
+ out_start = max(0, pos - offset)
804
+ if pos == 0 and offset > 0:
805
+ valid_output = valid_output[offset:]
806
+ else: # valid
807
+ if pos < offset:
808
+ return
809
+ out_start = pos - offset
810
+
811
+ out_end = min(out_start + len(valid_output), output_len)
812
+ copy_len = min(len(valid_output), out_end - out_start)
813
+
814
+ if copy_len > 0:
815
+ output[out_start : out_start + copy_len] = valid_output[:copy_len]
620
816
 
621
817
 
622
818
  __all__ = [