OpenPIV 0.25.4__tar.gz → 0.26.0__tar.gz

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 (171) hide show
  1. {openpiv-0.25.4 → openpiv-0.26.0}/PKG-INFO +3 -1
  2. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/conf.py +1 -1
  3. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/index.rst +1 -0
  4. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/src/developers.rst +61 -0
  5. openpiv-0.26.0/openpiv/docs/src/fft_correlation_backends.rst +162 -0
  6. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/lib.py +26 -42
  7. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/pyprocess.py +129 -59
  8. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/settings.py +22 -4
  9. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/test/test_lib.py +28 -0
  10. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/test/test_performance.py +4 -4
  11. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/test/test_process.py +2 -2
  12. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/test/test_pyprocess.py +62 -14
  13. openpiv-0.26.0/openpiv/test/test_rust_backend.py +174 -0
  14. openpiv-0.26.0/openpiv/test/test_rust_subpixel.py +101 -0
  15. openpiv-0.26.0/openpiv/test/test_rust_windef.py +121 -0
  16. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/test/test_validation.py +26 -0
  17. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/tools.py +2 -2
  18. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/validation.py +34 -17
  19. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/windef.py +241 -204
  20. {openpiv-0.25.4 → openpiv-0.26.0}/pyproject.toml +7 -2
  21. {openpiv-0.25.4 → openpiv-0.26.0}/LICENSE.txt +0 -0
  22. {openpiv-0.25.4 → openpiv-0.26.0}/README.md +0 -0
  23. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/PIV_3D_plotting.py +0 -0
  24. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/__init__.py +0 -0
  25. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/data/test1/exp1_001_a.bmp +0 -0
  26. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/data/test1/exp1_001_b.bmp +0 -0
  27. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/data/test1/test_data.vec +0 -0
  28. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/data/test2/2image_00.tif +0 -0
  29. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/data/test2/2image_01.tif +0 -0
  30. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/data/test2/2image_10.tif +0 -0
  31. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/data/test2/2image_11.tif +0 -0
  32. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/data/test2/2image_20.tif +0 -0
  33. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/data/test2/2image_21.tif +0 -0
  34. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/data/test2/2image_30.tif +0 -0
  35. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/data/test2/2image_31.tif +0 -0
  36. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/data/test2/2image_40.tif +0 -0
  37. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/data/test2/2image_41.tif +0 -0
  38. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/data/test2/2image_50.tif +0 -0
  39. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/data/test2/2image_51.tif +0 -0
  40. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/data/test3/pair_4_frame_0.jpg +0 -0
  41. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/data/test3/pair_4_frame_1.jpg +0 -0
  42. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/data/test4/Camera1-0101.tif +0 -0
  43. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/data/test4/Camera1-0102.tif +0 -0
  44. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/data/test5/Pattern_10_A.tif +0 -0
  45. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/data/test5/Pattern_10_B.tif +0 -0
  46. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/data/test5/Pattern_1_A.tif +0 -0
  47. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/data/test5/Pattern_1_B.tif +0 -0
  48. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/data/test5/Pattern_2_A.tif +0 -0
  49. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/data/test5/Pattern_2_B.tif +0 -0
  50. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/data/test5/Pattern_3_A.tif +0 -0
  51. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/data/test5/Pattern_3_B.tif +0 -0
  52. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/data/test5/Pattern_4_A.tif +0 -0
  53. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/data/test5/Pattern_4_B.tif +0 -0
  54. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/data/test5/Pattern_5_A.tif +0 -0
  55. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/data/test5/Pattern_5_B.tif +0 -0
  56. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/data/test5/Pattern_6_A.tif +0 -0
  57. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/data/test5/Pattern_6_B.tif +0 -0
  58. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/data/test5/Pattern_7_A.tif +0 -0
  59. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/data/test5/Pattern_7_B.tif +0 -0
  60. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/data/test5/Pattern_8_A.tif +0 -0
  61. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/data/test5/Pattern_8_B.tif +0 -0
  62. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/data/test5/Pattern_9_A.tif +0 -0
  63. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/data/test5/Pattern_9_B.tif +0 -0
  64. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/data/test6/Pattern_0001_A.tif +0 -0
  65. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/data/test6/Pattern_0002_A.tif +0 -0
  66. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/data/test6/Pattern_0003_A.tif +0 -0
  67. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/data/test6/Pattern_0004_A.tif +0 -0
  68. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/data/test6/Pattern_0005_A.tif +0 -0
  69. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/data/test6/Pattern_1001_B.tif +0 -0
  70. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/data/test6/Pattern_1002_B.tif +0 -0
  71. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/data/test6/Pattern_1003_B.tif +0 -0
  72. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/data/test6/Pattern_1004_B.tif +0 -0
  73. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/data/test6/Pattern_1005_B.tif +0 -0
  74. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/Makefile +0 -0
  75. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/OpenPIV.pdf +0 -0
  76. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/OpenPIV_advanced_processing_parameters.pdf +0 -0
  77. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/images/B005_1.tif +0 -0
  78. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/images/B005_2.tif +0 -0
  79. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/images/image1.png +0 -0
  80. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/images/image2.png +0 -0
  81. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/images/image3.png +0 -0
  82. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/images/image4.png +0 -0
  83. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/make.bat +0 -0
  84. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/requirements.txt +0 -0
  85. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/src/api_reference.rst +0 -0
  86. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/src/faq.rst +0 -0
  87. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/src/generated/openpiv.filters._gaussian_kernel.rst +0 -0
  88. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/src/generated/openpiv.filters.gaussian.rst +0 -0
  89. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/src/generated/openpiv.filters.replace_outliers.rst +0 -0
  90. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/src/generated/openpiv.lib.replace_nans.rst +0 -0
  91. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/src/generated/openpiv.lib.sincinterp.rst +0 -0
  92. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/src/generated/openpiv.preprocess.dynamic_masking.rst +0 -0
  93. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/src/generated/openpiv.process.CorrelationFunction.rst +0 -0
  94. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/src/generated/openpiv.process.correlate_windows.rst +0 -0
  95. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/src/generated/openpiv.process.extended_search_area_piv.rst +0 -0
  96. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/src/generated/openpiv.process.get_coordinates.rst +0 -0
  97. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/src/generated/openpiv.process.get_field_shape.rst +0 -0
  98. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/src/generated/openpiv.process.normalize_intensity.rst +0 -0
  99. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/src/generated/openpiv.pyprocess.correlate_windows.rst +0 -0
  100. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/src/generated/openpiv.pyprocess.find_first_peak.rst +0 -0
  101. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/src/generated/openpiv.pyprocess.find_second_peak.rst +0 -0
  102. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/src/generated/openpiv.pyprocess.find_subpixel_peak_position.rst +0 -0
  103. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/src/generated/openpiv.pyprocess.get_coordinates.rst +0 -0
  104. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/src/generated/openpiv.pyprocess.get_field_shape.rst +0 -0
  105. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/src/generated/openpiv.pyprocess.moving_window_array.rst +0 -0
  106. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/src/generated/openpiv.pyprocess.normalize_intensity.rst +0 -0
  107. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/src/generated/openpiv.pyprocess.piv.rst +0 -0
  108. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/src/generated/openpiv.scaling.uniform.rst +0 -0
  109. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/src/generated/openpiv.tools.Multiprocesser.rst +0 -0
  110. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/src/generated/openpiv.tools.display.rst +0 -0
  111. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/src/generated/openpiv.tools.display_vector_field.rst +0 -0
  112. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/src/generated/openpiv.tools.imread.rst +0 -0
  113. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/src/generated/openpiv.tools.save.rst +0 -0
  114. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/src/generated/openpiv.validation.global_std.rst +0 -0
  115. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/src/generated/openpiv.validation.global_val.rst +0 -0
  116. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/src/generated/openpiv.validation.local_median_val.rst +0 -0
  117. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/src/generated/openpiv.validation.sig2noise_val.rst +0 -0
  118. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/src/gui_doc.rst +0 -0
  119. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/src/installation_instruction.rst +0 -0
  120. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/src/introduction.rst +0 -0
  121. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/src/masking.ipynb +0 -0
  122. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/src/modules.rst +0 -0
  123. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/src/openpiv.rst +0 -0
  124. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/src/openpiv_pivuq.ipynb +0 -0
  125. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/src/piv_basics.ipynb +0 -0
  126. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/src/tutorial1.ipynb +0 -0
  127. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/src/windef.ipynb +0 -0
  128. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/filters.py +0 -0
  129. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/phase_separation.py +0 -0
  130. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/piv.py +0 -0
  131. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/preprocess.py +0 -0
  132. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/preprocess.py,cover +0 -0
  133. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/pyprocess3D.py +0 -0
  134. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/scaling.py +0 -0
  135. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/smoothn.py +0 -0
  136. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/test/OpenPIV_results_16_test1/field_A0000.png +0 -0
  137. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/test/__init__.py +0 -0
  138. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/test/conftest.py +0 -0
  139. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/test/extended_search_area_vectorized.ipynb +0 -0
  140. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/test/moon.png +0 -0
  141. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/test/test_PIV_3D_plotting.py +0 -0
  142. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/test/test_filters.ipynb +0 -0
  143. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/test/test_filters.py +0 -0
  144. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/test/test_find_peaks.ipynb +0 -0
  145. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/test/test_package_metadata.py +0 -0
  146. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/test/test_piv.py +0 -0
  147. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/test/test_preprocess.py +0 -0
  148. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/test/test_process.ipynb +0 -0
  149. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/test/test_pyprocess3D.py +0 -0
  150. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/test/test_scaling.py +0 -0
  151. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/test/test_smoothn.py +0 -0
  152. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/test/test_tools.ipynb +0 -0
  153. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/test/test_tools.png +0 -0
  154. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/test/test_tools.py +0 -0
  155. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/test/test_tools_background.py +0 -0
  156. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/test/test_tools_basic_utils.py +0 -0
  157. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/test/test_tools_image_processing.py +0 -0
  158. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/test/test_tools_multiprocessing.py +0 -0
  159. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/test/test_tools_vector_field.py +0 -0
  160. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/test/test_validation.ipynb +0 -0
  161. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/test/test_vectorized_extended_search.py +0 -0
  162. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/test/test_windef.ipynb +0 -0
  163. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/test/test_windef.py +0 -0
  164. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/test/test_windef_coverage.py +0 -0
  165. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/test/test_windef_detailed.py +0 -0
  166. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/test/test_windef_final.py +0 -0
  167. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/tutorials/Example for overlap setting change.ipynb +0 -0
  168. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/tutorials/masking_tutorial.py +0 -0
  169. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/tutorials/tutorial1.py +0 -0
  170. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/tutorials/tutorial2.py +0 -0
  171. {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/tutorials/windef_tutorial.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: OpenPIV
3
- Version: 0.25.4
3
+ Version: 0.26.0
4
4
  Summary: OpenPIV consists in a Python and Cython modules for scripting and executing the analysis of a set of PIV image pairs. In addition, a Qt and Tk graphical user interfaces are in development, to ease the use for those users who don't have python skills.
5
5
  License: GPLv3
6
6
  License-File: LICENSE.txt
@@ -22,10 +22,12 @@ Classifier: Programming Language :: Python :: 3.12
22
22
  Classifier: Programming Language :: Python :: 3.13
23
23
  Classifier: Programming Language :: Python :: 3.14
24
24
  Classifier: Topic :: Scientific/Engineering
25
+ Provides-Extra: rust
25
26
  Requires-Dist: imageio (>=2.35.0)
26
27
  Requires-Dist: matplotlib (>=3.8.0)
27
28
  Requires-Dist: natsort (>=8.4.0)
28
29
  Requires-Dist: numpy (>=2.0.0)
30
+ Requires-Dist: openpiv-rust (>=0.1.0) ; extra == "rust"
29
31
  Requires-Dist: scikit-image (>=0.23.0)
30
32
  Requires-Dist: scipy (>=1.11.0)
31
33
  Requires-Dist: tqdm (>=4.66.0)
@@ -64,7 +64,7 @@ copyright = '2014, OpenPIV team'
64
64
  # built documents.
65
65
  #
66
66
  # The short X.Y version.
67
- version = '0.25.4'
67
+ version = '0.26.0'
68
68
  # The full version, including alpha/beta/rc tags.
69
69
  # release = '0.0.1a'
70
70
 
@@ -31,6 +31,7 @@ Contents:
31
31
  src/windef
32
32
  src/masking
33
33
  src/developers
34
+ src/fft_correlation_backends
34
35
  src/api_reference
35
36
  src/faq
36
37
 
@@ -63,3 +63,64 @@ If you need to install cv2::
63
63
  conda install -c conda-forge opencv
64
64
 
65
65
 
66
+ Developing with or without Rust
67
+ --------------------------------
68
+
69
+ OpenPIV supports dual computational backends: **pure Python / SciPy** (default) and **compiled Rust** (via ``openpiv_rust``).
70
+
71
+ Option A: Developing in Pure Python (without Rust)
72
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
73
+
74
+ You do not need a Rust compiler installed. Simply install OpenPIV in editable mode:
75
+
76
+ .. code-block:: bash
77
+
78
+ poetry install
79
+ # or: pip install -e .
80
+
81
+ All core functionality, single-pass, and multi-pass deformation will use the optimized
82
+ ``scipy.fft`` backend automatically.
83
+
84
+ To run the test suite:
85
+
86
+ .. code-block:: bash
87
+
88
+ pytest openpiv/test
89
+
90
+ Option B: Developing with the Rust Extension
91
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
92
+
93
+ If you have the Rust toolchain (``cargo``) installed:
94
+
95
+ 1. Install ``maturin``:
96
+
97
+ .. code-block:: bash
98
+
99
+ pip install maturin
100
+
101
+ 2. Compile and link ``openpiv_rust`` directly into your Python environment:
102
+
103
+ .. code-block:: bash
104
+
105
+ cd crates/openpiv_rust
106
+ maturin develop --release
107
+ cd ../..
108
+
109
+ 3. Verify that the Rust backend is active:
110
+
111
+ .. code-block:: bash
112
+
113
+ python -c "import openpiv_rust; print('Rust backend ready!')"
114
+
115
+ Running the Performance Benchmarks
116
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
117
+
118
+ OpenPIV includes a dedicated benchmark suite comparing cross-correlation, subpixel
119
+ peak finding, and end-to-end multi-pass Windef across backends:
120
+
121
+ .. code-block:: bash
122
+
123
+ python benchmarks/run_benchmarks.py
124
+
125
+
126
+
@@ -0,0 +1,162 @@
1
+ FFT Correlation Backends & Performance Study
2
+ ============================================
3
+
4
+ This document details the comparative study of 2D cross-correlation algorithms,
5
+ backends, and performance characteristics in OpenPIV, focusing on both **Circular**
6
+ and **Linear** correlation, `scipy.fft` (PocketFFT), and the `openpiv_rust` compiled backend.
7
+
8
+ .. contents:: Table of Contents
9
+ :depth: 2
10
+ :local:
11
+
12
+ Overview
13
+ --------
14
+
15
+ In Particle Image Velocimetry (PIV), cross-correlation between interrogation windows
16
+ is the core computational step. For an image pair split into :math:`N` interrogation
17
+ windows of size :math:`(H \times W)`, the correlation maps are computed using the
18
+ Wiener-Khinchin theorem via Fast Fourier Transforms:
19
+
20
+ .. math::
21
+
22
+ C = \mathcal{F}^{-1} \left( \mathcal{F}(I_B) \cdot \mathcal{F}^*(I_A) \right)
23
+
24
+ OpenPIV supports two primary correlation paradigms:
25
+
26
+ 1. **Circular Correlation (Standard OpenPIV)**:
27
+ Assumes periodic boundary conditions (toroidal wraparound).
28
+ Inputs of size :math:`(N \times N)` yield correlation maps of size :math:`(N \times N)`.
29
+ No zero-padding is required.
30
+
31
+ 2. **Linear Correlation (Full / Extended)**:
32
+ Evaluates true spatial shift without periodic wraparound.
33
+ For windows of size :math:`s_1` and :math:`s_2`, the full linear correlation has size
34
+ :math:`(s_1 + s_2 - 1)`. To prevent time-domain aliasing, inputs must be zero-padded
35
+ to at least this size before applying the FFT.
36
+
37
+
38
+ The 63x63 vs 64x64 Power-of-2 Finding
39
+ -------------------------------------
40
+
41
+ In previous versions of OpenPIV, linear correlation in ``openpiv.pyprocess.fft_correlate_images``
42
+ computed the padded transform size as:
43
+
44
+ .. code-block:: python
45
+
46
+ size = s1 + s2 - 1 # e.g., 32 + 32 - 1 = 63
47
+ fsize = 2 ** np.ceil(np.log2(size)).astype(int) - 1 # 64 - 1 = 63 (BUG!)
48
+
49
+ Why 63 Disabled PocketFFT Optimization
50
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
51
+
52
+ SciPy delegates FFT operations to **PocketFFT** (written in C++ by Martin Reinecke),
53
+ which includes hand-tuned AVX2/FMA vector kernels. However, these SIMD routines rely on
54
+ smooth radix factors (powers of 2: 2, 4, 8, 16, 32, 64).
55
+
56
+ 1. Subtracting 1 produced an **odd composite size** :math:`63 = 3 \times 3 \times 7`.
57
+ 2. This forced PocketFFT into slow Bluestein/composite transforms, disabling SIMD vectorization.
58
+ 3. Furthermore, padding to 63 left an off-by-one boundary alignment when slicing the central window.
59
+
60
+ The Power-of-2 Solution
61
+ ^^^^^^^^^^^^^^^^^^^^^^^
62
+
63
+ By keeping the clean power-of-2 transform size :math:`fsize = 2^{\lceil \log_2(size) \rceil}`
64
+ (e.g., :math:`64 \times 64` for :math:`32 \times 32` windows), PocketFFT achieves optimal
65
+ SIMD execution speed.
66
+
67
+ To recover the exact central correlation window matching ``scipy.signal.correlate``
68
+ to machine precision (:math:`\le 10^{-13}`), the centered slice is:
69
+
70
+ .. code-block:: python
71
+
72
+ # Exact power of 2 transform size
73
+ fsize = 2 ** np.ceil(np.log2(size)).astype(int)
74
+
75
+ # Centered slice extracting (s1) around the zero-displacement lag
76
+ fslice = (
77
+ slice(0, image_a.shape[0]),
78
+ slice(fsize[0] // 2 - s1[0] // 2, fsize[0] // 2 - s1[0] // 2 + s1[0]),
79
+ slice(fsize[1] // 2 - s1[1] // 2, fsize[1] // 2 - s1[1] // 2 + s1[1]),
80
+ )
81
+ f2a = conj(rfft2(image_a, fsize, axes=(-2, -1), workers=workers))
82
+ f2b = rfft2(image_b, fsize, axes=(-2, -1), workers=workers)
83
+ corr = fftshift(irfft2(f2a * f2b, axes=(-2, -1)).real, axes=(-2, -1))[fslice]
84
+
85
+
86
+ Batched Multi-Threading in scipy.fft
87
+ ------------------------------------
88
+
89
+ ``scipy.fft`` supports a ``workers`` parameter (e.g., ``workers=-1`` for all logical cores).
90
+ However, profiling reveals:
91
+
92
+ * On large 2D or 3D volumes, PocketFFT multi-threading scales well.
93
+ * On **batches of tiny 2D windows** (:math:`16 \times 16` or :math:`32 \times 32`),
94
+ thread synchronization overhead inside PocketFFT often negates multi-core gains
95
+ (scaling from :math:`1.0\times` to :math:`1.1\times`), because thread splitting occurs
96
+ per-transform rather than distributing independent windows across workers.
97
+
98
+
99
+ The Rust Acceleration Engine (openpiv_rust)
100
+ -------------------------------------------
101
+
102
+ To overcome Python GIL overhead and achieve linear CPU scaling across window batches,
103
+ OpenPIV provides an optional compiled Rust extension (``openpiv_rust``) built with
104
+ PyO3, Rayon, and RealFFT.
105
+
106
+ Key Architectural Optimizations in Rust
107
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
108
+
109
+ 1. **Zero-Allocation Rayon Scratch Pool**:
110
+ Window transforms use ``par_chunks_exact_mut().for_each_init(...)``, pre-allocating
111
+ frequency buffers once per worker thread. For 961 windows, this eliminates over
112
+ 8,600 heap allocations per pass.
113
+
114
+ 2. **Native Power-of-2 Real FFT**:
115
+ Uses ``realfft`` (Real-to-Complex forward and Complex-to-Real inverse), cutting
116
+ arithmetic and memory bandwidth by 50% compared to full complex transforms.
117
+
118
+ 3. **Batched Subpixel Peak Finding**:
119
+ In addition to cross-correlation, `openpiv_rust` provides
120
+ ``batch_correlation_to_displacement``, executing Gaussian/Centroid/Parabolic subpixel
121
+ peak fitting directly across all windows in parallel, yielding a **28x to 65x speedup**
122
+ over the nested Python loop.
123
+
124
+
125
+ Benchmark Results
126
+ -----------------
127
+
128
+ Linear Correlation (225 windows of 32x32)
129
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
130
+
131
+ ============================================ ==================== ==================
132
+ Implementation Runtime Relative Speed
133
+ ============================================ ==================== ==================
134
+ ``scipy.signal.correlate`` (Python loop) 70.63 ms 1.0x (baseline)
135
+ OpenPIV SciPy (legacy ``fsize=63``) 49.28 ms 1.4x
136
+ OpenPIV SciPy (power-of-2 ``fsize=64``) 39.10 ms 1.8x
137
+ **openpiv_rust** (Rayon + realfft) **8.26 ms** **8.5x**
138
+ ============================================ ==================== ==================
139
+
140
+ Displacement Calculation (Peak Finding)
141
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
142
+
143
+ ============================================ ==================== ==================
144
+ Grid / Windows Python Loop openpiv_rust
145
+ ============================================ ==================== ==================
146
+ 225 windows (32x32) 5.19 ms **0.18 ms (28x)**
147
+ 961 windows (16x16) 19.84 ms **0.30 ms (65x)**
148
+ ============================================ ==================== ==================
149
+
150
+ End-to-End Multi-Pass Windef (3 passes on 256x256 image)
151
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
152
+
153
+ * **SciPy backend**: 110.59 ms
154
+ * **Rust backend (with subpixel Rust engine)**: **81.66 ms** (:math:`1.35\times` speedup)
155
+
156
+ Conclusion & Usage Guidelines
157
+ -----------------------------
158
+
159
+ * For systems with compiled binary extensions available, set ``settings.backend = 'rust'``
160
+ to maximize throughput in both single-pass and multi-pass deformation workflows.
161
+ * On pure Python / NumPy / SciPy environments, using the exact power-of-2 padding rule
162
+ ensures optimal PocketFFT vectorization while maintaining exact numerical consistency.
@@ -1,4 +1,5 @@
1
1
  import numpy as np
2
+ from numpy.lib.stride_tricks import sliding_window_view
2
3
 
3
4
 
4
5
  def replace_nans(array, max_iter, tol, kernel_size=2, method="disk"):
@@ -92,56 +93,39 @@ def replace_nans(array, max_iter, tol, kernel_size=2, method="disk"):
92
93
  n_nans = len(nan_indices)
93
94
 
94
95
  # arrays which contain replaced values to check for convergence
95
- replaced_new = np.zeros(n_nans)
96
96
  replaced_old = np.zeros(n_nans)
97
97
 
98
+ # broadcastable view of the kernel weights, one axis per array dimension
99
+ # plus one matching axis per kernel dimension (for the windowed reduction)
100
+ kernel_b = kernel.reshape((1,) * n_dim + kernel.shape)
101
+ window_axes = tuple(range(n_dim, 2 * n_dim))
102
+
98
103
  # make several passes
99
104
  # until we reach convergence
100
105
  for _ in range(max_iter):
101
106
  # note: identifying new nan indices and looping other the new indices
102
107
  # would give slightly different result
103
108
 
104
- # for each NaN element
105
- for k in range(n_nans):
106
- ind = nan_indices[
107
- k
108
- ] # 2 or 3 indices indicating the position of a nan element
109
- # init to 0.0
110
- replaced_new[k] = 0.0
111
-
112
- # generating a list of indices of the convolution window in the
113
- # array
114
- slice_indices = np.array(np.meshgrid(*[range(i - kernel_size,
115
- i + kernel_size + 1) for i in ind]))
116
-
117
- # identifying all indices strictly inside the image edges:
118
- in_mask = np.array(
119
- [
120
- np.logical_and(
121
- slice_indices[i] < array.shape[i],
122
- slice_indices[i] >= 0
123
- )
124
- for i in range(n_dim)
125
- ]
126
- )
127
- # logical and over x,y (and z) indices
128
- in_mask = np.prod(in_mask, axis=0).astype(bool)
129
-
130
- # extract window from array
131
- win = filled[tuple(slice_indices[:, in_mask])]
132
-
133
- # selecting the same points from the kernel
134
- kernel_in = kernel[in_mask]
135
-
136
- # sum of elements of the kernel that are not nan in the window
137
- non_nan = np.sum(kernel_in[~np.isnan(win)])
138
-
139
- if non_nan > 0:
140
- # convolution with the kernel
141
- replaced_new[k] = np.nansum(win * kernel_in) / non_nan
142
- else:
143
- # don't do anything if there is only nans around
144
- replaced_new[k] = np.nan
109
+ # NaN-pad by kernel_size so that out-of-bounds neighbours behave like
110
+ # the boundary check in the original per-element loop (i.e. they are
111
+ # excluded from both the weighted sum and its normalization).
112
+ padded = np.pad(np.asarray(filled), kernel_size, mode="constant",
113
+ constant_values=np.nan)
114
+ # windows.shape == array.shape + kernel.shape: one (2*kernel_size+1)^n_dim
115
+ # neighbourhood per array element, vectorized instead of a per-NaN
116
+ # Python loop with a fresh np.meshgrid call each time.
117
+ windows = sliding_window_view(padded, kernel.shape)
118
+
119
+ valid = ~np.isnan(windows)
120
+ weights = np.where(valid, kernel_b, 0)
121
+ non_nan = weights.sum(axis=window_axes)
122
+ weighted_sum = (np.where(valid, windows, 0) * kernel_b).sum(axis=window_axes)
123
+
124
+ # convolution with the kernel; stays NaN where only NaNs are around
125
+ full_new = np.divide(weighted_sum, non_nan,
126
+ out=np.full(array.shape, np.nan), where=non_nan > 0)
127
+
128
+ replaced_new = full_new[tuple(nan_indices.T)]
145
129
 
146
130
  # bulk replace all new values in array
147
131
  filled[tuple(nan_indices.T)] = replaced_new