oscura 0.0.1__py3-none-any.whl → 0.1.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 (465) hide show
  1. oscura/__init__.py +813 -8
  2. oscura/__main__.py +392 -0
  3. oscura/analyzers/__init__.py +37 -0
  4. oscura/analyzers/digital/__init__.py +177 -0
  5. oscura/analyzers/digital/bus.py +691 -0
  6. oscura/analyzers/digital/clock.py +805 -0
  7. oscura/analyzers/digital/correlation.py +720 -0
  8. oscura/analyzers/digital/edges.py +632 -0
  9. oscura/analyzers/digital/extraction.py +413 -0
  10. oscura/analyzers/digital/quality.py +878 -0
  11. oscura/analyzers/digital/signal_quality.py +877 -0
  12. oscura/analyzers/digital/thresholds.py +708 -0
  13. oscura/analyzers/digital/timing.py +1104 -0
  14. oscura/analyzers/eye/__init__.py +46 -0
  15. oscura/analyzers/eye/diagram.py +434 -0
  16. oscura/analyzers/eye/metrics.py +555 -0
  17. oscura/analyzers/jitter/__init__.py +83 -0
  18. oscura/analyzers/jitter/ber.py +333 -0
  19. oscura/analyzers/jitter/decomposition.py +759 -0
  20. oscura/analyzers/jitter/measurements.py +413 -0
  21. oscura/analyzers/jitter/spectrum.py +220 -0
  22. oscura/analyzers/measurements.py +40 -0
  23. oscura/analyzers/packet/__init__.py +171 -0
  24. oscura/analyzers/packet/daq.py +1077 -0
  25. oscura/analyzers/packet/metrics.py +437 -0
  26. oscura/analyzers/packet/parser.py +327 -0
  27. oscura/analyzers/packet/payload.py +2156 -0
  28. oscura/analyzers/packet/payload_analysis.py +1312 -0
  29. oscura/analyzers/packet/payload_extraction.py +236 -0
  30. oscura/analyzers/packet/payload_patterns.py +670 -0
  31. oscura/analyzers/packet/stream.py +359 -0
  32. oscura/analyzers/patterns/__init__.py +266 -0
  33. oscura/analyzers/patterns/clustering.py +1036 -0
  34. oscura/analyzers/patterns/discovery.py +539 -0
  35. oscura/analyzers/patterns/learning.py +797 -0
  36. oscura/analyzers/patterns/matching.py +1091 -0
  37. oscura/analyzers/patterns/periodic.py +650 -0
  38. oscura/analyzers/patterns/sequences.py +767 -0
  39. oscura/analyzers/power/__init__.py +116 -0
  40. oscura/analyzers/power/ac_power.py +391 -0
  41. oscura/analyzers/power/basic.py +383 -0
  42. oscura/analyzers/power/conduction.py +314 -0
  43. oscura/analyzers/power/efficiency.py +297 -0
  44. oscura/analyzers/power/ripple.py +356 -0
  45. oscura/analyzers/power/soa.py +372 -0
  46. oscura/analyzers/power/switching.py +479 -0
  47. oscura/analyzers/protocol/__init__.py +150 -0
  48. oscura/analyzers/protocols/__init__.py +150 -0
  49. oscura/analyzers/protocols/base.py +500 -0
  50. oscura/analyzers/protocols/can.py +620 -0
  51. oscura/analyzers/protocols/can_fd.py +448 -0
  52. oscura/analyzers/protocols/flexray.py +405 -0
  53. oscura/analyzers/protocols/hdlc.py +399 -0
  54. oscura/analyzers/protocols/i2c.py +368 -0
  55. oscura/analyzers/protocols/i2s.py +296 -0
  56. oscura/analyzers/protocols/jtag.py +393 -0
  57. oscura/analyzers/protocols/lin.py +445 -0
  58. oscura/analyzers/protocols/manchester.py +333 -0
  59. oscura/analyzers/protocols/onewire.py +501 -0
  60. oscura/analyzers/protocols/spi.py +334 -0
  61. oscura/analyzers/protocols/swd.py +325 -0
  62. oscura/analyzers/protocols/uart.py +393 -0
  63. oscura/analyzers/protocols/usb.py +495 -0
  64. oscura/analyzers/signal_integrity/__init__.py +63 -0
  65. oscura/analyzers/signal_integrity/embedding.py +294 -0
  66. oscura/analyzers/signal_integrity/equalization.py +370 -0
  67. oscura/analyzers/signal_integrity/sparams.py +484 -0
  68. oscura/analyzers/spectral/__init__.py +53 -0
  69. oscura/analyzers/spectral/chunked.py +273 -0
  70. oscura/analyzers/spectral/chunked_fft.py +571 -0
  71. oscura/analyzers/spectral/chunked_wavelet.py +391 -0
  72. oscura/analyzers/spectral/fft.py +92 -0
  73. oscura/analyzers/statistical/__init__.py +250 -0
  74. oscura/analyzers/statistical/checksum.py +923 -0
  75. oscura/analyzers/statistical/chunked_corr.py +228 -0
  76. oscura/analyzers/statistical/classification.py +778 -0
  77. oscura/analyzers/statistical/entropy.py +1113 -0
  78. oscura/analyzers/statistical/ngrams.py +614 -0
  79. oscura/analyzers/statistics/__init__.py +119 -0
  80. oscura/analyzers/statistics/advanced.py +885 -0
  81. oscura/analyzers/statistics/basic.py +263 -0
  82. oscura/analyzers/statistics/correlation.py +630 -0
  83. oscura/analyzers/statistics/distribution.py +298 -0
  84. oscura/analyzers/statistics/outliers.py +463 -0
  85. oscura/analyzers/statistics/streaming.py +93 -0
  86. oscura/analyzers/statistics/trend.py +520 -0
  87. oscura/analyzers/validation.py +598 -0
  88. oscura/analyzers/waveform/__init__.py +36 -0
  89. oscura/analyzers/waveform/measurements.py +943 -0
  90. oscura/analyzers/waveform/measurements_with_uncertainty.py +371 -0
  91. oscura/analyzers/waveform/spectral.py +1689 -0
  92. oscura/analyzers/waveform/wavelets.py +298 -0
  93. oscura/api/__init__.py +62 -0
  94. oscura/api/dsl.py +538 -0
  95. oscura/api/fluent.py +571 -0
  96. oscura/api/operators.py +498 -0
  97. oscura/api/optimization.py +392 -0
  98. oscura/api/profiling.py +396 -0
  99. oscura/automotive/__init__.py +73 -0
  100. oscura/automotive/can/__init__.py +52 -0
  101. oscura/automotive/can/analysis.py +356 -0
  102. oscura/automotive/can/checksum.py +250 -0
  103. oscura/automotive/can/correlation.py +212 -0
  104. oscura/automotive/can/discovery.py +355 -0
  105. oscura/automotive/can/message_wrapper.py +375 -0
  106. oscura/automotive/can/models.py +385 -0
  107. oscura/automotive/can/patterns.py +381 -0
  108. oscura/automotive/can/session.py +452 -0
  109. oscura/automotive/can/state_machine.py +300 -0
  110. oscura/automotive/can/stimulus_response.py +461 -0
  111. oscura/automotive/dbc/__init__.py +15 -0
  112. oscura/automotive/dbc/generator.py +156 -0
  113. oscura/automotive/dbc/parser.py +146 -0
  114. oscura/automotive/dtc/__init__.py +30 -0
  115. oscura/automotive/dtc/database.py +3036 -0
  116. oscura/automotive/j1939/__init__.py +14 -0
  117. oscura/automotive/j1939/decoder.py +745 -0
  118. oscura/automotive/loaders/__init__.py +35 -0
  119. oscura/automotive/loaders/asc.py +98 -0
  120. oscura/automotive/loaders/blf.py +77 -0
  121. oscura/automotive/loaders/csv_can.py +136 -0
  122. oscura/automotive/loaders/dispatcher.py +136 -0
  123. oscura/automotive/loaders/mdf.py +331 -0
  124. oscura/automotive/loaders/pcap.py +132 -0
  125. oscura/automotive/obd/__init__.py +14 -0
  126. oscura/automotive/obd/decoder.py +707 -0
  127. oscura/automotive/uds/__init__.py +48 -0
  128. oscura/automotive/uds/decoder.py +265 -0
  129. oscura/automotive/uds/models.py +64 -0
  130. oscura/automotive/visualization.py +369 -0
  131. oscura/batch/__init__.py +55 -0
  132. oscura/batch/advanced.py +627 -0
  133. oscura/batch/aggregate.py +300 -0
  134. oscura/batch/analyze.py +139 -0
  135. oscura/batch/logging.py +487 -0
  136. oscura/batch/metrics.py +556 -0
  137. oscura/builders/__init__.py +41 -0
  138. oscura/builders/signal_builder.py +1131 -0
  139. oscura/cli/__init__.py +14 -0
  140. oscura/cli/batch.py +339 -0
  141. oscura/cli/characterize.py +273 -0
  142. oscura/cli/compare.py +775 -0
  143. oscura/cli/decode.py +551 -0
  144. oscura/cli/main.py +247 -0
  145. oscura/cli/shell.py +350 -0
  146. oscura/comparison/__init__.py +66 -0
  147. oscura/comparison/compare.py +397 -0
  148. oscura/comparison/golden.py +487 -0
  149. oscura/comparison/limits.py +391 -0
  150. oscura/comparison/mask.py +434 -0
  151. oscura/comparison/trace_diff.py +30 -0
  152. oscura/comparison/visualization.py +481 -0
  153. oscura/compliance/__init__.py +70 -0
  154. oscura/compliance/advanced.py +756 -0
  155. oscura/compliance/masks.py +363 -0
  156. oscura/compliance/reporting.py +483 -0
  157. oscura/compliance/testing.py +298 -0
  158. oscura/component/__init__.py +38 -0
  159. oscura/component/impedance.py +365 -0
  160. oscura/component/reactive.py +598 -0
  161. oscura/component/transmission_line.py +312 -0
  162. oscura/config/__init__.py +191 -0
  163. oscura/config/defaults.py +254 -0
  164. oscura/config/loader.py +348 -0
  165. oscura/config/memory.py +271 -0
  166. oscura/config/migration.py +458 -0
  167. oscura/config/pipeline.py +1077 -0
  168. oscura/config/preferences.py +530 -0
  169. oscura/config/protocol.py +875 -0
  170. oscura/config/schema.py +713 -0
  171. oscura/config/settings.py +420 -0
  172. oscura/config/thresholds.py +599 -0
  173. oscura/convenience.py +457 -0
  174. oscura/core/__init__.py +299 -0
  175. oscura/core/audit.py +457 -0
  176. oscura/core/backend_selector.py +405 -0
  177. oscura/core/cache.py +590 -0
  178. oscura/core/cancellation.py +439 -0
  179. oscura/core/confidence.py +225 -0
  180. oscura/core/config.py +506 -0
  181. oscura/core/correlation.py +216 -0
  182. oscura/core/cross_domain.py +422 -0
  183. oscura/core/debug.py +301 -0
  184. oscura/core/edge_cases.py +541 -0
  185. oscura/core/exceptions.py +535 -0
  186. oscura/core/gpu_backend.py +523 -0
  187. oscura/core/lazy.py +832 -0
  188. oscura/core/log_query.py +540 -0
  189. oscura/core/logging.py +931 -0
  190. oscura/core/logging_advanced.py +952 -0
  191. oscura/core/memoize.py +171 -0
  192. oscura/core/memory_check.py +274 -0
  193. oscura/core/memory_guard.py +290 -0
  194. oscura/core/memory_limits.py +336 -0
  195. oscura/core/memory_monitor.py +453 -0
  196. oscura/core/memory_progress.py +465 -0
  197. oscura/core/memory_warnings.py +315 -0
  198. oscura/core/numba_backend.py +362 -0
  199. oscura/core/performance.py +352 -0
  200. oscura/core/progress.py +524 -0
  201. oscura/core/provenance.py +358 -0
  202. oscura/core/results.py +331 -0
  203. oscura/core/types.py +504 -0
  204. oscura/core/uncertainty.py +383 -0
  205. oscura/discovery/__init__.py +52 -0
  206. oscura/discovery/anomaly_detector.py +672 -0
  207. oscura/discovery/auto_decoder.py +415 -0
  208. oscura/discovery/comparison.py +497 -0
  209. oscura/discovery/quality_validator.py +528 -0
  210. oscura/discovery/signal_detector.py +769 -0
  211. oscura/dsl/__init__.py +73 -0
  212. oscura/dsl/commands.py +246 -0
  213. oscura/dsl/interpreter.py +455 -0
  214. oscura/dsl/parser.py +689 -0
  215. oscura/dsl/repl.py +172 -0
  216. oscura/exceptions.py +59 -0
  217. oscura/exploratory/__init__.py +111 -0
  218. oscura/exploratory/error_recovery.py +642 -0
  219. oscura/exploratory/fuzzy.py +513 -0
  220. oscura/exploratory/fuzzy_advanced.py +786 -0
  221. oscura/exploratory/legacy.py +831 -0
  222. oscura/exploratory/parse.py +358 -0
  223. oscura/exploratory/recovery.py +275 -0
  224. oscura/exploratory/sync.py +382 -0
  225. oscura/exploratory/unknown.py +707 -0
  226. oscura/export/__init__.py +25 -0
  227. oscura/export/wireshark/README.md +265 -0
  228. oscura/export/wireshark/__init__.py +47 -0
  229. oscura/export/wireshark/generator.py +312 -0
  230. oscura/export/wireshark/lua_builder.py +159 -0
  231. oscura/export/wireshark/templates/dissector.lua.j2 +92 -0
  232. oscura/export/wireshark/type_mapping.py +165 -0
  233. oscura/export/wireshark/validator.py +105 -0
  234. oscura/exporters/__init__.py +94 -0
  235. oscura/exporters/csv.py +303 -0
  236. oscura/exporters/exporters.py +44 -0
  237. oscura/exporters/hdf5.py +219 -0
  238. oscura/exporters/html_export.py +701 -0
  239. oscura/exporters/json_export.py +291 -0
  240. oscura/exporters/markdown_export.py +367 -0
  241. oscura/exporters/matlab_export.py +354 -0
  242. oscura/exporters/npz_export.py +219 -0
  243. oscura/exporters/spice_export.py +210 -0
  244. oscura/extensibility/__init__.py +131 -0
  245. oscura/extensibility/docs.py +752 -0
  246. oscura/extensibility/extensions.py +1125 -0
  247. oscura/extensibility/logging.py +259 -0
  248. oscura/extensibility/measurements.py +485 -0
  249. oscura/extensibility/plugins.py +414 -0
  250. oscura/extensibility/registry.py +346 -0
  251. oscura/extensibility/templates.py +913 -0
  252. oscura/extensibility/validation.py +651 -0
  253. oscura/filtering/__init__.py +89 -0
  254. oscura/filtering/base.py +563 -0
  255. oscura/filtering/convenience.py +564 -0
  256. oscura/filtering/design.py +725 -0
  257. oscura/filtering/filters.py +32 -0
  258. oscura/filtering/introspection.py +605 -0
  259. oscura/guidance/__init__.py +24 -0
  260. oscura/guidance/recommender.py +429 -0
  261. oscura/guidance/wizard.py +518 -0
  262. oscura/inference/__init__.py +251 -0
  263. oscura/inference/active_learning/README.md +153 -0
  264. oscura/inference/active_learning/__init__.py +38 -0
  265. oscura/inference/active_learning/lstar.py +257 -0
  266. oscura/inference/active_learning/observation_table.py +230 -0
  267. oscura/inference/active_learning/oracle.py +78 -0
  268. oscura/inference/active_learning/teachers/__init__.py +15 -0
  269. oscura/inference/active_learning/teachers/simulator.py +192 -0
  270. oscura/inference/adaptive_tuning.py +453 -0
  271. oscura/inference/alignment.py +653 -0
  272. oscura/inference/bayesian.py +943 -0
  273. oscura/inference/binary.py +1016 -0
  274. oscura/inference/crc_reverse.py +711 -0
  275. oscura/inference/logic.py +288 -0
  276. oscura/inference/message_format.py +1305 -0
  277. oscura/inference/protocol.py +417 -0
  278. oscura/inference/protocol_dsl.py +1084 -0
  279. oscura/inference/protocol_library.py +1230 -0
  280. oscura/inference/sequences.py +809 -0
  281. oscura/inference/signal_intelligence.py +1509 -0
  282. oscura/inference/spectral.py +215 -0
  283. oscura/inference/state_machine.py +634 -0
  284. oscura/inference/stream.py +918 -0
  285. oscura/integrations/__init__.py +59 -0
  286. oscura/integrations/llm.py +1827 -0
  287. oscura/jupyter/__init__.py +32 -0
  288. oscura/jupyter/display.py +268 -0
  289. oscura/jupyter/magic.py +334 -0
  290. oscura/loaders/__init__.py +526 -0
  291. oscura/loaders/binary.py +69 -0
  292. oscura/loaders/configurable.py +1255 -0
  293. oscura/loaders/csv.py +26 -0
  294. oscura/loaders/csv_loader.py +473 -0
  295. oscura/loaders/hdf5.py +9 -0
  296. oscura/loaders/hdf5_loader.py +510 -0
  297. oscura/loaders/lazy.py +370 -0
  298. oscura/loaders/mmap_loader.py +583 -0
  299. oscura/loaders/numpy_loader.py +436 -0
  300. oscura/loaders/pcap.py +432 -0
  301. oscura/loaders/preprocessing.py +368 -0
  302. oscura/loaders/rigol.py +287 -0
  303. oscura/loaders/sigrok.py +321 -0
  304. oscura/loaders/tdms.py +367 -0
  305. oscura/loaders/tektronix.py +711 -0
  306. oscura/loaders/validation.py +584 -0
  307. oscura/loaders/vcd.py +464 -0
  308. oscura/loaders/wav.py +233 -0
  309. oscura/math/__init__.py +45 -0
  310. oscura/math/arithmetic.py +824 -0
  311. oscura/math/interpolation.py +413 -0
  312. oscura/onboarding/__init__.py +39 -0
  313. oscura/onboarding/help.py +498 -0
  314. oscura/onboarding/tutorials.py +405 -0
  315. oscura/onboarding/wizard.py +466 -0
  316. oscura/optimization/__init__.py +19 -0
  317. oscura/optimization/parallel.py +440 -0
  318. oscura/optimization/search.py +532 -0
  319. oscura/pipeline/__init__.py +43 -0
  320. oscura/pipeline/base.py +338 -0
  321. oscura/pipeline/composition.py +242 -0
  322. oscura/pipeline/parallel.py +448 -0
  323. oscura/pipeline/pipeline.py +375 -0
  324. oscura/pipeline/reverse_engineering.py +1119 -0
  325. oscura/plugins/__init__.py +122 -0
  326. oscura/plugins/base.py +272 -0
  327. oscura/plugins/cli.py +497 -0
  328. oscura/plugins/discovery.py +411 -0
  329. oscura/plugins/isolation.py +418 -0
  330. oscura/plugins/lifecycle.py +959 -0
  331. oscura/plugins/manager.py +493 -0
  332. oscura/plugins/registry.py +421 -0
  333. oscura/plugins/versioning.py +372 -0
  334. oscura/py.typed +0 -0
  335. oscura/quality/__init__.py +65 -0
  336. oscura/quality/ensemble.py +740 -0
  337. oscura/quality/explainer.py +338 -0
  338. oscura/quality/scoring.py +616 -0
  339. oscura/quality/warnings.py +456 -0
  340. oscura/reporting/__init__.py +248 -0
  341. oscura/reporting/advanced.py +1234 -0
  342. oscura/reporting/analyze.py +448 -0
  343. oscura/reporting/argument_preparer.py +596 -0
  344. oscura/reporting/auto_report.py +507 -0
  345. oscura/reporting/batch.py +615 -0
  346. oscura/reporting/chart_selection.py +223 -0
  347. oscura/reporting/comparison.py +330 -0
  348. oscura/reporting/config.py +615 -0
  349. oscura/reporting/content/__init__.py +39 -0
  350. oscura/reporting/content/executive.py +127 -0
  351. oscura/reporting/content/filtering.py +191 -0
  352. oscura/reporting/content/minimal.py +257 -0
  353. oscura/reporting/content/verbosity.py +162 -0
  354. oscura/reporting/core.py +508 -0
  355. oscura/reporting/core_formats/__init__.py +17 -0
  356. oscura/reporting/core_formats/multi_format.py +210 -0
  357. oscura/reporting/engine.py +836 -0
  358. oscura/reporting/export.py +366 -0
  359. oscura/reporting/formatting/__init__.py +129 -0
  360. oscura/reporting/formatting/emphasis.py +81 -0
  361. oscura/reporting/formatting/numbers.py +403 -0
  362. oscura/reporting/formatting/standards.py +55 -0
  363. oscura/reporting/formatting.py +466 -0
  364. oscura/reporting/html.py +578 -0
  365. oscura/reporting/index.py +590 -0
  366. oscura/reporting/multichannel.py +296 -0
  367. oscura/reporting/output.py +379 -0
  368. oscura/reporting/pdf.py +373 -0
  369. oscura/reporting/plots.py +731 -0
  370. oscura/reporting/pptx_export.py +360 -0
  371. oscura/reporting/renderers/__init__.py +11 -0
  372. oscura/reporting/renderers/pdf.py +94 -0
  373. oscura/reporting/sections.py +471 -0
  374. oscura/reporting/standards.py +680 -0
  375. oscura/reporting/summary_generator.py +368 -0
  376. oscura/reporting/tables.py +397 -0
  377. oscura/reporting/template_system.py +724 -0
  378. oscura/reporting/templates/__init__.py +15 -0
  379. oscura/reporting/templates/definition.py +205 -0
  380. oscura/reporting/templates/index.html +649 -0
  381. oscura/reporting/templates/index.md +173 -0
  382. oscura/schemas/__init__.py +158 -0
  383. oscura/schemas/bus_configuration.json +322 -0
  384. oscura/schemas/device_mapping.json +182 -0
  385. oscura/schemas/packet_format.json +418 -0
  386. oscura/schemas/protocol_definition.json +363 -0
  387. oscura/search/__init__.py +16 -0
  388. oscura/search/anomaly.py +292 -0
  389. oscura/search/context.py +149 -0
  390. oscura/search/pattern.py +160 -0
  391. oscura/session/__init__.py +34 -0
  392. oscura/session/annotations.py +289 -0
  393. oscura/session/history.py +313 -0
  394. oscura/session/session.py +445 -0
  395. oscura/streaming/__init__.py +43 -0
  396. oscura/streaming/chunked.py +611 -0
  397. oscura/streaming/progressive.py +393 -0
  398. oscura/streaming/realtime.py +622 -0
  399. oscura/testing/__init__.py +54 -0
  400. oscura/testing/synthetic.py +808 -0
  401. oscura/triggering/__init__.py +68 -0
  402. oscura/triggering/base.py +229 -0
  403. oscura/triggering/edge.py +353 -0
  404. oscura/triggering/pattern.py +344 -0
  405. oscura/triggering/pulse.py +581 -0
  406. oscura/triggering/window.py +453 -0
  407. oscura/ui/__init__.py +48 -0
  408. oscura/ui/formatters.py +526 -0
  409. oscura/ui/progressive_display.py +340 -0
  410. oscura/utils/__init__.py +99 -0
  411. oscura/utils/autodetect.py +338 -0
  412. oscura/utils/buffer.py +389 -0
  413. oscura/utils/lazy.py +407 -0
  414. oscura/utils/lazy_imports.py +147 -0
  415. oscura/utils/memory.py +836 -0
  416. oscura/utils/memory_advanced.py +1326 -0
  417. oscura/utils/memory_extensions.py +465 -0
  418. oscura/utils/progressive.py +352 -0
  419. oscura/utils/windowing.py +362 -0
  420. oscura/visualization/__init__.py +321 -0
  421. oscura/visualization/accessibility.py +526 -0
  422. oscura/visualization/annotations.py +374 -0
  423. oscura/visualization/axis_scaling.py +305 -0
  424. oscura/visualization/colors.py +453 -0
  425. oscura/visualization/digital.py +337 -0
  426. oscura/visualization/eye.py +420 -0
  427. oscura/visualization/histogram.py +281 -0
  428. oscura/visualization/interactive.py +858 -0
  429. oscura/visualization/jitter.py +702 -0
  430. oscura/visualization/keyboard.py +394 -0
  431. oscura/visualization/layout.py +365 -0
  432. oscura/visualization/optimization.py +1028 -0
  433. oscura/visualization/palettes.py +446 -0
  434. oscura/visualization/plot.py +92 -0
  435. oscura/visualization/power.py +290 -0
  436. oscura/visualization/power_extended.py +626 -0
  437. oscura/visualization/presets.py +467 -0
  438. oscura/visualization/protocols.py +932 -0
  439. oscura/visualization/render.py +207 -0
  440. oscura/visualization/rendering.py +444 -0
  441. oscura/visualization/reverse_engineering.py +791 -0
  442. oscura/visualization/signal_integrity.py +808 -0
  443. oscura/visualization/specialized.py +553 -0
  444. oscura/visualization/spectral.py +811 -0
  445. oscura/visualization/styles.py +381 -0
  446. oscura/visualization/thumbnails.py +311 -0
  447. oscura/visualization/time_axis.py +351 -0
  448. oscura/visualization/waveform.py +367 -0
  449. oscura/workflow/__init__.py +13 -0
  450. oscura/workflow/dag.py +377 -0
  451. oscura/workflows/__init__.py +58 -0
  452. oscura/workflows/compliance.py +280 -0
  453. oscura/workflows/digital.py +272 -0
  454. oscura/workflows/multi_trace.py +502 -0
  455. oscura/workflows/power.py +178 -0
  456. oscura/workflows/protocol.py +492 -0
  457. oscura/workflows/reverse_engineering.py +639 -0
  458. oscura/workflows/signal_integrity.py +227 -0
  459. oscura-0.1.0.dist-info/METADATA +300 -0
  460. oscura-0.1.0.dist-info/RECORD +463 -0
  461. oscura-0.1.0.dist-info/entry_points.txt +2 -0
  462. {oscura-0.0.1.dist-info → oscura-0.1.0.dist-info}/licenses/LICENSE +1 -1
  463. oscura-0.0.1.dist-info/METADATA +0 -63
  464. oscura-0.0.1.dist-info/RECORD +0 -5
  465. {oscura-0.0.1.dist-info → oscura-0.1.0.dist-info}/WHEEL +0 -0
@@ -0,0 +1,630 @@
1
+ """Correlation analysis for signal data.
2
+
3
+ This module provides autocorrelation, cross-correlation, and related
4
+ analysis functions for identifying signal relationships and periodicities.
5
+
6
+
7
+ Example:
8
+ >>> from oscura.analyzers.statistics.correlation import (
9
+ ... autocorrelation, cross_correlation, correlate_chunked
10
+ ... )
11
+ >>> acf = autocorrelation(trace, max_lag=1000)
12
+ >>> xcorr, lag, coef = cross_correlation(trace1, trace2)
13
+ >>> # Memory-efficient correlation for large signals
14
+ >>> result = correlate_chunked(large_signal1, large_signal2)
15
+
16
+ References:
17
+ Oppenheim, A. V. & Schafer, R. W. (2009). Discrete-Time Signal Processing
18
+ IEEE 1241-2010: Standard for Terminology and Test Methods for ADCs
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ from dataclasses import dataclass
24
+ from typing import TYPE_CHECKING, Any, Literal, cast
25
+
26
+ import numpy as np
27
+
28
+ from oscura.core.types import WaveformTrace
29
+
30
+ if TYPE_CHECKING:
31
+ from numpy.typing import NDArray
32
+
33
+
34
+ @dataclass
35
+ class CrossCorrelationResult:
36
+ """Result of cross-correlation analysis.
37
+
38
+ Attributes:
39
+ correlation: Full correlation array.
40
+ lags: Lag values in samples.
41
+ lag_times: Lag values in seconds.
42
+ peak_lag: Lag at maximum correlation (samples).
43
+ peak_lag_time: Lag at maximum correlation (seconds).
44
+ peak_coefficient: Maximum correlation coefficient.
45
+ sample_rate: Sample rate used for time conversion.
46
+ """
47
+
48
+ correlation: NDArray[np.float64]
49
+ lags: NDArray[np.intp]
50
+ lag_times: NDArray[np.float64]
51
+ peak_lag: int
52
+ peak_lag_time: float
53
+ peak_coefficient: float
54
+ sample_rate: float
55
+
56
+
57
+ def autocorrelation(
58
+ trace: WaveformTrace | NDArray[np.floating[Any]],
59
+ *,
60
+ max_lag: int | None = None,
61
+ normalized: bool = True,
62
+ sample_rate: float | None = None,
63
+ ) -> tuple[NDArray[np.float64], NDArray[np.float64]]:
64
+ """Compute autocorrelation of a signal.
65
+
66
+ Measures self-similarity of a signal at different time lags.
67
+ Useful for detecting periodicities and characteristic time scales.
68
+
69
+ Args:
70
+ trace: Input trace or numpy array.
71
+ max_lag: Maximum lag to compute (samples). If None, uses n // 2.
72
+ normalized: If True, normalize to correlation coefficients [-1, 1].
73
+ sample_rate: Sample rate in Hz (for time axis). Required if trace is array.
74
+
75
+ Returns:
76
+ Tuple of (lags_time, autocorrelation):
77
+ - lags_time: Time values for each lag in seconds
78
+ - autocorrelation: Normalized autocorrelation values
79
+
80
+ Raises:
81
+ ValueError: If sample_rate is not provided when trace is array.
82
+
83
+ Example:
84
+ >>> lag_times, acf = autocorrelation(trace, max_lag=1000)
85
+ >>> # Find first zero crossing for decorrelation time
86
+ >>> zero_idx = np.where(acf[1:] < 0)[0][0]
87
+ >>> decorr_time = lag_times[zero_idx]
88
+
89
+ References:
90
+ Box, G. E. P. & Jenkins, G. M. (1976). Time Series Analysis
91
+ """
92
+ if isinstance(trace, WaveformTrace):
93
+ data = trace.data
94
+ fs = trace.metadata.sample_rate
95
+ else:
96
+ data = trace
97
+ if sample_rate is None:
98
+ raise ValueError("sample_rate required when trace is array")
99
+ fs = sample_rate
100
+
101
+ n = len(data)
102
+
103
+ if max_lag is None:
104
+ max_lag = n // 2
105
+
106
+ max_lag = min(max_lag, n - 1)
107
+
108
+ # Remove mean for proper correlation
109
+ data_centered = data - np.mean(data)
110
+
111
+ # Compute autocorrelation via FFT (faster for large n)
112
+ if n > 256:
113
+ # Zero-pad for full correlation
114
+ nfft = int(2 ** np.ceil(np.log2(2 * n)))
115
+ fft_data = np.fft.rfft(data_centered, n=nfft)
116
+ acf_full = np.fft.irfft(fft_data * np.conj(fft_data), n=nfft)
117
+ acf = acf_full[: max_lag + 1]
118
+ else:
119
+ # Direct computation for small n
120
+ acf = np.correlate(data_centered, data_centered, mode="full")
121
+ acf = acf[n - 1 : n + max_lag]
122
+
123
+ # Normalize
124
+ if normalized and acf[0] > 0:
125
+ acf = acf / acf[0]
126
+
127
+ # Time axis
128
+ lags = np.arange(max_lag + 1)
129
+ lag_times = lags / fs
130
+
131
+ return lag_times, acf.astype(np.float64)
132
+
133
+
134
+ def cross_correlation(
135
+ trace1: WaveformTrace | NDArray[np.floating[Any]],
136
+ trace2: WaveformTrace | NDArray[np.floating[Any]],
137
+ *,
138
+ max_lag: int | None = None,
139
+ normalized: bool = True,
140
+ sample_rate: float | None = None,
141
+ ) -> CrossCorrelationResult:
142
+ """Compute cross-correlation between two signals.
143
+
144
+ Measures similarity between signals at different time lags.
145
+ Useful for finding time delays, alignments, and relationships.
146
+
147
+ Args:
148
+ trace1: First input trace or numpy array (reference).
149
+ trace2: Second input trace or numpy array.
150
+ max_lag: Maximum lag to compute (samples). If None, uses min(n1, n2) // 2.
151
+ normalized: If True, normalize to correlation coefficients [-1, 1].
152
+ sample_rate: Sample rate in Hz. Required if traces are arrays.
153
+
154
+ Returns:
155
+ CrossCorrelationResult with correlation data and optimal lag.
156
+
157
+ Raises:
158
+ ValueError: If sample_rate is not provided when traces are arrays.
159
+
160
+ Example:
161
+ >>> result = cross_correlation(trace1, trace2)
162
+ >>> print(f"Optimal lag: {result.peak_lag_time * 1e6:.1f} us")
163
+ >>> print(f"Correlation: {result.peak_coefficient:.3f}")
164
+
165
+ References:
166
+ Oppenheim, A. V. & Schafer, R. W. (2009). Discrete-Time Signal Processing
167
+ """
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
176
+
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
191
+ data1_centered = data1 - np.mean(data1)
192
+ 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
+ xcorr_full = np.correlate(data2_centered, data1_centered, mode="full")
199
+
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)
203
+ zero_lag_idx = n1 - 1
204
+ start_idx = max(0, zero_lag_idx - max_lag)
205
+ end_idx = min(len(xcorr_full), zero_lag_idx + max_lag + 1)
206
+ xcorr = xcorr_full[start_idx:end_idx]
207
+
208
+ # Create lag array
209
+ lags = np.arange(start_idx - zero_lag_idx, end_idx - zero_lag_idx)
210
+
211
+ # Normalize
212
+ 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)
217
+
218
+ # Find peak
219
+ peak_local_idx = np.argmax(np.abs(xcorr))
220
+ peak_lag = int(lags[peak_local_idx])
221
+ peak_coefficient = float(xcorr[peak_local_idx])
222
+
223
+ # Time values
224
+ lag_times = lags / fs
225
+ peak_lag_time = peak_lag / fs
226
+
227
+ return CrossCorrelationResult(
228
+ correlation=xcorr.astype(np.float64),
229
+ lags=lags,
230
+ lag_times=lag_times.astype(np.float64),
231
+ peak_lag=peak_lag,
232
+ peak_lag_time=peak_lag_time,
233
+ peak_coefficient=peak_coefficient,
234
+ sample_rate=fs,
235
+ )
236
+
237
+
238
+ def correlation_coefficient(
239
+ trace1: WaveformTrace | NDArray[np.floating[Any]],
240
+ trace2: WaveformTrace | NDArray[np.floating[Any]],
241
+ ) -> float:
242
+ """Compute Pearson correlation coefficient between two signals.
243
+
244
+ Simple measure of linear relationship between signals at zero lag.
245
+
246
+ Args:
247
+ trace1: First input trace or numpy array.
248
+ trace2: Second input trace or numpy array.
249
+
250
+ Returns:
251
+ Correlation coefficient in range [-1, 1].
252
+
253
+ Example:
254
+ >>> r = correlation_coefficient(trace1, trace2)
255
+ >>> print(f"Correlation: {r:.3f}")
256
+ """
257
+ data1 = trace1.data if isinstance(trace1, WaveformTrace) else trace1
258
+
259
+ data2 = trace2.data if isinstance(trace2, WaveformTrace) else trace2
260
+
261
+ # Ensure same length
262
+ n = min(len(data1), len(data2))
263
+ data1 = data1[:n]
264
+ data2 = data2[:n]
265
+
266
+ # Compute correlation
267
+ return float(np.corrcoef(data1, data2)[0, 1])
268
+
269
+
270
+ def find_periodicity(
271
+ trace: WaveformTrace | NDArray[np.floating[Any]],
272
+ *,
273
+ min_period_samples: int = 2,
274
+ max_period_samples: int | None = None,
275
+ sample_rate: float | None = None,
276
+ ) -> dict[str, float | int | list[dict[str, int | float]]]:
277
+ """Find dominant periodicity in signal using autocorrelation.
278
+
279
+ Detects the primary periodic component by finding the first
280
+ significant peak in the autocorrelation function.
281
+
282
+ Args:
283
+ trace: Input trace or numpy array.
284
+ min_period_samples: Minimum period to consider (samples).
285
+ max_period_samples: Maximum period to consider (samples).
286
+ sample_rate: Sample rate in Hz (required for array input).
287
+
288
+ Returns:
289
+ Dictionary with periodicity analysis:
290
+ - period_samples: Period in samples
291
+ - period_time: Period in seconds
292
+ - frequency: Frequency in Hz
293
+ - strength: Autocorrelation at period (0-1)
294
+ - harmonics: List of detected harmonics
295
+
296
+ Raises:
297
+ ValueError: If sample_rate is not provided when trace is array.
298
+
299
+ Example:
300
+ >>> result = find_periodicity(trace)
301
+ >>> print(f"Period: {result['period_time']*1e6:.2f} us")
302
+ >>> print(f"Frequency: {result['frequency']/1e3:.1f} kHz")
303
+ """
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
+
313
+ n = len(data)
314
+
315
+ if max_period_samples is None:
316
+ max_period_samples = n // 2
317
+
318
+ # Compute autocorrelation
319
+ _lag_times, acf = autocorrelation(
320
+ trace,
321
+ max_lag=max_period_samples,
322
+ sample_rate=sample_rate if sample_rate else fs,
323
+ )
324
+
325
+ # Find peaks in autocorrelation (after lag 0)
326
+ # Look for local maxima
327
+ acf_search = acf[min_period_samples:]
328
+
329
+ if len(acf_search) < 3:
330
+ return {
331
+ "period_samples": np.nan,
332
+ "period_time": np.nan,
333
+ "frequency": np.nan,
334
+ "strength": np.nan,
335
+ "harmonics": [],
336
+ }
337
+
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
+ period_time = period_samples / fs
355
+ frequency = 1.0 / period_time if period_time > 0 else np.nan
356
+
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
+
378
+ return {
379
+ "period_samples": period_samples,
380
+ "period_time": float(period_time),
381
+ "frequency": float(frequency),
382
+ "strength": strength,
383
+ "harmonics": harmonics,
384
+ }
385
+
386
+
387
+ def coherence(
388
+ trace1: WaveformTrace | NDArray[np.floating[Any]],
389
+ trace2: WaveformTrace | NDArray[np.floating[Any]],
390
+ *,
391
+ nperseg: int | None = None,
392
+ sample_rate: float | None = None,
393
+ ) -> tuple[NDArray[np.float64], NDArray[np.float64]]:
394
+ """Compute magnitude-squared coherence between two signals.
395
+
396
+ Measures frequency-domain correlation between signals.
397
+ Coherence of 1 indicates perfect linear relationship at that frequency.
398
+
399
+ Args:
400
+ trace1: First input trace or numpy array.
401
+ trace2: Second input trace or numpy array.
402
+ nperseg: Segment length for estimation. If None, auto-selected.
403
+ sample_rate: Sample rate in Hz (required for array input).
404
+
405
+ Returns:
406
+ Tuple of (frequencies, coherence):
407
+ - frequencies: Frequency values in Hz
408
+ - coherence: Magnitude-squared coherence [0, 1]
409
+
410
+ Raises:
411
+ ValueError: If sample_rate is not provided when traces are arrays.
412
+
413
+ Example:
414
+ >>> freq, coh = coherence(trace1, trace2)
415
+ >>> # Find frequencies with high coherence
416
+ >>> high_coh_freqs = freq[coh > 0.8]
417
+ """
418
+ from scipy import signal as sp_signal
419
+
420
+ if isinstance(trace1, WaveformTrace):
421
+ data1 = trace1.data
422
+ fs = trace1.metadata.sample_rate
423
+ else:
424
+ data1 = trace1
425
+ if sample_rate is None:
426
+ raise ValueError("sample_rate required when traces are arrays")
427
+ fs = sample_rate
428
+
429
+ data2 = trace2.data if isinstance(trace2, WaveformTrace) else trace2
430
+
431
+ # Ensure same length
432
+ n = min(len(data1), len(data2))
433
+ data1 = data1[:n]
434
+ data2 = data2[:n]
435
+
436
+ if nperseg is None:
437
+ nperseg = min(256, n // 4)
438
+ nperseg = max(nperseg, 16)
439
+
440
+ freq, coh = sp_signal.coherence(data1, data2, fs=fs, nperseg=nperseg, noverlap=nperseg // 2)
441
+
442
+ return freq, coh.astype(np.float64)
443
+
444
+
445
+ def correlate_chunked(
446
+ signal1: NDArray[np.floating[Any]],
447
+ signal2: NDArray[np.floating[Any]],
448
+ *,
449
+ mode: str = "same",
450
+ chunk_size: int | None = None,
451
+ ) -> NDArray[np.float64]:
452
+ """Memory-efficient cross-correlation using overlap-save FFT method.
453
+
454
+ Computes cross-correlation for large signals that don't fit in memory
455
+ by processing in chunks using the overlap-save method with FFT.
456
+
457
+ Args:
458
+ signal1: First input signal array.
459
+ signal2: Second input signal array (kernel/template).
460
+ mode: Correlation mode - 'same', 'valid', or 'full' (default 'same').
461
+ chunk_size: Size of chunks for processing. If None, auto-selected.
462
+
463
+ Returns:
464
+ Cross-correlation result with same semantics as numpy.correlate.
465
+
466
+ Raises:
467
+ ValueError: If signals are empty or mode is invalid.
468
+
469
+ Example:
470
+ >>> import numpy as np
471
+ >>> # Large signals
472
+ >>> signal1 = np.random.randn(100_000_000)
473
+ >>> signal2 = np.random.randn(10_000)
474
+ >>> # Memory-efficient correlation
475
+ >>> result = correlate_chunked(signal1, signal2, mode='same')
476
+ >>> print(f"Result shape: {result.shape}")
477
+
478
+ Notes:
479
+ Uses overlap-save FFT-based convolution which is memory-efficient
480
+ and faster than direct correlation for large signals.
481
+
482
+ References:
483
+ MEM-008: Chunked Correlation
484
+ Oppenheim & Schafer (2009): Discrete-Time Signal Processing, Ch 8
485
+ """
486
+ if len(signal1) == 0 or len(signal2) == 0:
487
+ raise ValueError("Input signals cannot be empty")
488
+
489
+ if mode not in ("same", "valid", "full"):
490
+ raise ValueError(f"Invalid mode: {mode}. Must be 'same', 'valid', or 'full'")
491
+
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
506
+ min_chunk_size = max(2 * n2, 64)
507
+
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
+
514
+ # For correlation, we need to flip signal2
515
+ signal2_flipped = signal2[::-1].copy()
516
+
517
+ # Overlap-save parameters
518
+ # L = chunk size, M = filter length
519
+ L = max(chunk_size, min_chunk_size)
520
+ M = n2
521
+ overlap = M - 1
522
+
523
+ # Ensure step size is positive (L must be > overlap)
524
+ step_size = L - overlap
525
+ 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)
530
+
531
+ # FFT size (power of 2, >= L + M - 1)
532
+ nfft = int(2 ** np.ceil(np.log2(L + M - 1)))
533
+
534
+ # Pre-compute FFT of flipped signal2 (kernel)
535
+ kernel_fft = np.fft.fft(signal2_flipped, n=nfft)
536
+
537
+ # Output length based on mode
538
+ if mode == "full":
539
+ output_len = n1 + n2 - 1
540
+ elif mode == "same":
541
+ output_len = n1
542
+ else: # valid
543
+ output_len = max(0, n1 - n2 + 1)
544
+
545
+ # Initialize output
546
+ output = np.zeros(output_len, dtype=np.float64)
547
+
548
+ # Process chunks with overlap-save
549
+ pos = 0 # Position in signal1
550
+ max_iterations = (n1 // step_size) + 2 # Safety limit
551
+ iteration = 0
552
+
553
+ while pos < n1 and iteration < max_iterations:
554
+ iteration += 1
555
+
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
+
619
+ return output
620
+
621
+
622
+ __all__ = [
623
+ "CrossCorrelationResult",
624
+ "autocorrelation",
625
+ "coherence",
626
+ "correlate_chunked",
627
+ "correlation_coefficient",
628
+ "cross_correlation",
629
+ "find_periodicity",
630
+ ]