Skill Tracer
Skills › Scientific Agent Skills › openpiv › advanced_algorithms.md

advanced_algorithms.md

288 lines · skills/openpiv/references/advanced_algorithms.md at 5f2bc03

Open on GitHub
1# Advanced OpenPIV Algorithms and Settings
2
3Reviewed against the released **openpiv 0.26.1** source and SciPy runtime on 2026-10-01.
4Hosted documentation can lag the wheel; for example its API listing omits the new `backend` argument.
5

Advanced OpenPIV Algorithms and Settings

Reviewed against the released openpiv 0.26.1 source and SciPy runtime on 2026-10-01. Hosted documentation can lag the wheel; for example its API listing omits the new backend argument.

Raw lines 1 to 5
6## Correlation methods
7
8`pyprocess.extended_search_area_piv(..., correlation_method=...)`:
9
10| Value | Behaviour |
11|-------|-----------|
12| `"circular"` (default) | FFT correlation with no zero-padding. Fastest and lowest memory. Wrap-around means a displacement past half the window aliases back as a small one in the opposite direction. |
13| `"linear"` | FFT padded to a power of two covering the full correlation, then cropped back to search-window size. Reduces wrap-around; displacement range is still limited by that crop and particle overlap. |
15Use these two names. `"direct"` appears in some legacy docstrings but is not implemented by the
16SciPy `extended_search_area_piv` correlation path. Do not offer it as an option.
18Both methods support extended search; larger extreme velocities alone do not establish aliasing.
19The bundled CLI chooses `"linear"` with `normalized_correlation=True` for extended windows,
20following the API's normalization requirement. Validate either choice using known displacements.
22`normalized_correlation=True` normalizes intensities per window before correlating, making peak
23heights comparable across windows of differing brightness — useful under uneven illumination. It also
24shifts the `s2n` scale, so re-tune the threshold after switching it on.
26`use_vectorized=True` batches peak extraction and signal-to-noise calculations. Both paths already
27hold the correlation stack; edge/degenerate peak behavior can differ. It remains False in
28`extended_search_area_piv`, but `PIVSettings.use_vectorized` defaults to True since 0.25.5.
30`backend="scipy"` selects the tested reference path. `"auto"` can use installed Rust components;
31in 0.26.1 the FFT dispatcher still selects SciPy unless `"rust"` is explicit. `"rust"` requires
32`openpiv_rust` and raises ImportError when absent. Record backend and package versions; do not
33assume every wheel contains the extension or infer speed/numerical parity from release notes.

Correlation methods

pyprocess.extended_search_area_piv(..., correlation_method=...):

ValueBehaviour
"circular" (default)FFT correlation with no zero-padding. Fastest and lowest memory. Wrap-around means a displacement past half the window aliases back as a small one in the opposite direction.
"linear"FFT padded to a power of two covering the full correlation, then cropped back to search-window size. Reduces wrap-around; displacement range is still limited by that crop and particle overlap.

Use these two names. "direct" appears in some legacy docstrings but is not implemented by the SciPy extended_search_area_piv correlation path. Do not offer it as an option.

Both methods support extended search; larger extreme velocities alone do not establish aliasing. The bundled CLI chooses "linear" with normalized_correlation=True for extended windows, following the API's normalization requirement. Validate either choice using known displacements.

normalized_correlation=True normalizes intensities per window before correlating, making peak heights comparable across windows of differing brightness — useful under uneven illumination. It also shifts the s2n scale, so re-tune the threshold after switching it on.

use_vectorized=True batches peak extraction and signal-to-noise calculations. Both paths already hold the correlation stack; edge/degenerate peak behavior can differ. It remains False in extended_search_area_piv, but PIVSettings.use_vectorized defaults to True since 0.25.5.

backend="scipy" selects the tested reference path. "auto" can use installed Rust components; in 0.26.1 the FFT dispatcher still selects SciPy unless "rust" is explicit. "rust" requires openpiv_rust and raises ImportError when absent. Record backend and package versions; do not assume every wheel contains the extension or infer speed/numerical parity from release notes.

Raw lines 6 to 34
35## Subpixel peak fitting
37`subpixel_method` selects how the integer correlation peak is refined:
39| Value | Notes |
40|-------|-------|
41| `"gaussian"` (default) | Three-point Gaussian fit per axis. Standard choice; biased toward integer values ("peak locking") when particle images are under 2 px. |
42| `"parabolic"` | Three-point parabolic fit. Cheaper, slightly less accurate for Gaussian particle images. |
43| `"centroid"` | Intensity-weighted centroid. More robust for wide or saturated peaks. |
45Peak locking is a particle-imaging problem, not a fitting problem: aim for 2–3 px particle image
46diameter rather than switching estimators.

Subpixel peak fitting

subpixel_method selects how the integer correlation peak is refined:

ValueNotes
"gaussian" (default)Three-point Gaussian fit per axis. Standard choice; biased toward integer values ("peak locking") when particle images are under 2 px.
"parabolic"Three-point parabolic fit. Cheaper, slightly less accurate for Gaussian particle images.
"centroid"Intensity-weighted centroid. More robust for wide or saturated peaks.

Peak locking is a particle-imaging problem, not a fitting problem: aim for 2–3 px particle image diameter rather than switching estimators.

Raw lines 35 to 47
48## Signal-to-noise measures
50`pyprocess.sig2noise_ratio(correlation, sig2noise_method="peak2peak", width=2)`:
52- `"peak2mean"` — first peak divided by the mean of the correlation map. This is the default of both
53 `extended_search_area_piv` and `PIVSettings`. Values run higher and depend on map size.
54- `"peak2peak"` — first peak divided by the second-highest peak, excluding a `width`-pixel
55 neighbourhood around the first. The classic PIV detectability ratio; usable thresholds are
56 ~1.05–1.3.
58The two scales are not interchangeable. A threshold copied from one to the other silently rejects
59everything or nothing.

Signal-to-noise measures

pyprocess.sig2noise_ratio(correlation, sig2noise_method="peak2peak", width=2):

  • "peak2mean" — first peak divided by the mean of the correlation map. This is the default of both extended_search_area_piv and PIVSettings. Values run higher and depend on map size.
  • "peak2peak" — first peak divided by the second-highest peak, excluding a width-pixel neighbourhood around the first. The classic PIV detectability ratio; usable thresholds are ~1.05–1.3.

The two scales are not interchangeable. A threshold copied from one to the other silently rejects everything or nothing.

Raw lines 48 to 60
61## Multi-pass window deformation (`openpiv.windef`)
63`windef.simple_multipass(frame_a, frame_b, settings)` runs the full loop:
651. `windef.first_pass` — coarse correlation on `windowsizes[0]`.
662. `validation.typical_validation` — applies every enabled check in `settings` at once.
673. `filters.replace_outliers` — fills flagged vectors.
684. `windef.multipass_img_deform` for iterations `1 .. num_iterations-1` — deforms the interrogation
69 windows using the previous pass as a predictor, then re-correlates on the next smaller window.
705. Remaining NaNs filled with zeros; `transform_coordinates` applied.
72It returns `(x, y, u, v, flags)` with coordinates in pixels and velocities in **pixels per frame**.
73`simple_multipass` (and alias `multigrid_windef`) ignore `settings.dt` and
74`settings.scaling_factor`; convert afterwards:
76```python
77x, y, u, v = scaling.uniform(x, y, u, v, scaling_factor=96.52)
78u, v = u / dt, v / dt # scaling.uniform does not divide by dt
79```
81Batch `windef.piv(settings)` does apply both before saving. The array convenience wrapper always
82validates/replaces the first pass, does not perform the batch image preprocessing or between-pass
83smoothing, and discards the final masked-array mask when returning `.data`. For masks, full switch
84control, and preserved quality information use the batch pipeline or manage lower-level passes.
85Always supply settings: the no-settings wrapper resets window lists to two entries but leaves
86`num_iterations=3`. Construction checks list lengths and overlap; changing attributes bypasses those
87checks, and the number of iterations must still match the requested passes.
89The upside of working in px/frame is that the validation defaults (`min_max_u_disp=(-30, 30)`,
90`median_threshold=3`) are stated in px/frame and therefore mean what the PIV literature says they
91mean. The same numbers applied to `extended_search_area_piv` output, which *has* been divided by
92`dt`, would reject the whole field.
94`deformation_method="symmetric"` (the default) deforms both frames toward the midpoint, which halves
95the interpolation bias of deforming only the second frame. `interpolation_order` (default 3) is the
96spline order used for that deformation.
98Window deformation is what makes multi-pass worth the cost: it handles velocity gradients within a
99window, which a fixed-window single pass cannot.

Multi-pass window deformation (openpiv.windef)

windef.simple_multipass(frame_a, frame_b, settings) runs the full loop:

  1. windef.first_pass — coarse correlation on windowsizes[0].
  2. validation.typical_validation — applies every enabled check in settings at once.
  3. filters.replace_outliers — fills flagged vectors.
  4. windef.multipass_img_deform for iterations 1 .. num_iterations-1 — deforms the interrogation windows using the previous pass as a predictor, then re-correlates on the next smaller window.
  5. Remaining NaNs filled with zeros; transform_coordinates applied.

It returns (x, y, u, v, flags) with coordinates in pixels and velocities in pixels per frame. simple_multipass (and alias multigrid_windef) ignore settings.dt and settings.scaling_factor; convert afterwards:

x, y, u, v = scaling.uniform(x, y, u, v, scaling_factor=96.52)
u, v = u / dt, v / dt      # scaling.uniform does not divide by dt

Batch windef.piv(settings) does apply both before saving. The array convenience wrapper always validates/replaces the first pass, does not perform the batch image preprocessing or between-pass smoothing, and discards the final masked-array mask when returning .data. For masks, full switch control, and preserved quality information use the batch pipeline or manage lower-level passes. Always supply settings: the no-settings wrapper resets window lists to two entries but leaves num_iterations=3. Construction checks list lengths and overlap; changing attributes bypasses those checks, and the number of iterations must still match the requested passes.

The upside of working in px/frame is that the validation defaults (min_max_u_disp=(-30, 30), median_threshold=3) are stated in px/frame and therefore mean what the PIV literature says they mean. The same numbers applied to extended_search_area_piv output, which has been divided by dt, would reject the whole field.

deformation_method="symmetric" (the default) deforms both frames toward the midpoint, which halves the interpolation bias of deforming only the second frame. interpolation_order (default 3) is the spline order used for that deformation.

Window deformation is what makes multi-pass worth the cost: it handles velocity gradients within a window, which a fixed-window single pass cannot.

Raw lines 61 to 100
101## `PIVSettings` reference
103`windef.PIVSettings()` is a dataclass. Prefer constructor keywords so its consistency checks run.
104This table describes batch settings; do not assume the simple array wrapper uses every field.
106**Input and region**
108| Field | Default | Meaning |
109|-------|---------|---------|
110| `filepath_images`, `save_path`, `frame_pattern_a`, `frame_pattern_b` | OpenPIV's bundled `data/test1` | Batch-mode paths; irrelevant when calling `simple_multipass` with arrays |
111| `roi` | `"full"` | `"full"` or `(y1, y2, x1, x2)` crop |
112| `invert` | `False` | Invert intensities, for dark particles on a bright background |
114**Masking**
116| Field | Default | Meaning |
117|-------|---------|---------|
118| `dynamic_masking_method` | `None` | `None`, `"edges"`, or `"intensity"` |
119| `dynamic_masking_threshold` | `0.005` | Edge-strength threshold for `"edges"` |
120| `dynamic_masking_filter_size` | `7` | Gaussian/median filter size in px |
121| `static_mask` | `None` | Boolean array marking permanently excluded pixels |
123The 0.26.1 batch dispatcher checks `"edge"` (singular), while `dynamic_masking` expects `"edges"`;
124the latter also has the uint8-indexing bug described in SKILL.md. Neither spelling is a safe edge
125masking recipe for this release. Use intensity/static masks and verify excluded regions on outputs.
127**Correlation**
129| Field | Default |
130|-------|---------|
131| `correlation_method` | `"circular"` (or `"linear"`) |
132| `backend` | `"auto"` (or `"scipy"`, `"rust"`) |
133| `normalized_correlation` | `False` |
134| `windowsizes` | `(64, 32, 16)` |
135| `overlap` | `(32, 16, 8)` |
136| `num_iterations` | `3` |
137| `subpixel_method` | `"gaussian"` |
138| `use_vectorized` | `True` |
139| `deformation_method` | `"symmetric"` |
140| `interpolation_order` | `3` |
142`windowsizes` and `overlap` must be at least `num_iterations` long — each pass reads its own entry.
144**Scaling**
146| Field | Default | Meaning |
147|-------|---------|---------|
148| `dt` | `1.0` | Seconds between frames; applied by batch `piv`, ignored by `simple_multipass` |
149| `scaling_factor` | `1.0` | Pixels per physical unit; same batch/array distinction |
151**Validation** — thresholds consumed by `validation.typical_validation`; the batch runner controls
152whether the first pass is validated
154| Field | Default | Meaning |
155|-------|---------|---------|
156| `sig2noise_method` | `"peak2mean"` | See above |
157| `sig2noise_mask` | `2` | `width` around the first peak for `"peak2peak"` |
158| `sig2noise_threshold` | `1.0` | Reject below this |
159| `sig2noise_validate` | `True` | Enable the s2n check |
160| `validation_first_pass` | `True` | Also validate the coarse pass |
161| `min_max_u_disp`, `min_max_v_disp` | `(-30, 30)` | Global range in px/frame |
162| `std_threshold` | `10` | Reject beyond N standard deviations |
163| `median_threshold` | `3` | Local-median residual threshold |
164| `median_size` | `1` | Neighbourhood half-width for the median test |
165| `median_normalized` | `False` | Normalize the median residual by local fluctuation |
167**Replacement and smoothing**
169| Field | Default | Meaning |
170|-------|---------|---------|
171| `replace_vectors` | `True` | Run `replace_outliers` after validation |
172| `filter_method` | `"localmean"` | `"localmean"`, `"disk"`, or `"distance"` |
173| `max_filter_iteration` | `4` | Inpainting iterations |
174| `filter_kernel_size` | `2` | Inpainting kernel size |
175| `smoothn` | `False` | Apply `smoothn` between passes |
176| `smoothn_p` | `0.05` | Smoothing strength when enabled |
178Smoothing between passes stabilizes the predictor for the next pass. It also propagates smoothing
179into the final result, so report it as part of the processing chain.
181**Output**
183| Field | Default |
184|-------|---------|
185| `save_plot`, `show_plot`, `show_all_plots` | `False` |
186| `scale_plot` | `100` |
187| `fmt` | `"%.4e"` |
188| `n_cpus` | `1` (batch image-pair multiprocessing) |

PIVSettings reference

windef.PIVSettings() is a dataclass. Prefer constructor keywords so its consistency checks run. This table describes batch settings; do not assume the simple array wrapper uses every field.

Input and region

FieldDefaultMeaning
filepath_images, save_path, frame_pattern_a, frame_pattern_bOpenPIV's bundled data/test1Batch-mode paths; irrelevant when calling simple_multipass with arrays
roi"full""full" or (y1, y2, x1, x2) crop
invertFalseInvert intensities, for dark particles on a bright background

Masking

FieldDefaultMeaning
dynamic_masking_methodNoneNone, "edges", or "intensity"
dynamic_masking_threshold0.005Edge-strength threshold for "edges"
dynamic_masking_filter_size7Gaussian/median filter size in px
static_maskNoneBoolean array marking permanently excluded pixels

The 0.26.1 batch dispatcher checks "edge" (singular), while dynamic_masking expects "edges"; the latter also has the uint8-indexing bug described in SKILL.md. Neither spelling is a safe edge masking recipe for this release. Use intensity/static masks and verify excluded regions on outputs.

Correlation

FieldDefault
correlation_method"circular" (or "linear")
backend"auto" (or "scipy", "rust")
normalized_correlationFalse
windowsizes(64, 32, 16)
overlap(32, 16, 8)
num_iterations3
subpixel_method"gaussian"
use_vectorizedTrue
deformation_method"symmetric"
interpolation_order3

windowsizes and overlap must be at least num_iterations long — each pass reads its own entry.

Scaling

FieldDefaultMeaning
dt1.0Seconds between frames; applied by batch piv, ignored by simple_multipass
scaling_factor1.0Pixels per physical unit; same batch/array distinction

Validation — thresholds consumed by validation.typical_validation; the batch runner controls whether the first pass is validated

FieldDefaultMeaning
sig2noise_method"peak2mean"See above
sig2noise_mask2width around the first peak for "peak2peak"
sig2noise_threshold1.0Reject below this
sig2noise_validateTrueEnable the s2n check
validation_first_passTrueAlso validate the coarse pass
min_max_u_disp, min_max_v_disp(-30, 30)Global range in px/frame
std_threshold10Reject beyond N standard deviations
median_threshold3Local-median residual threshold
median_size1Neighbourhood half-width for the median test
median_normalizedFalseNormalize the median residual by local fluctuation

Replacement and smoothing

FieldDefaultMeaning
replace_vectorsTrueRun replace_outliers after validation
filter_method"localmean""localmean", "disk", or "distance"
max_filter_iteration4Inpainting iterations
filter_kernel_size2Inpainting kernel size
smoothnFalseApply smoothn between passes
smoothn_p0.05Smoothing strength when enabled

Smoothing between passes stabilizes the predictor for the next pass. It also propagates smoothing into the final result, so report it as part of the processing chain.

Output

FieldDefault
save_plot, show_plot, show_all_plotsFalse
scale_plot100
fmt"%.4e"
n_cpus1 (batch image-pair multiprocessing)
Raw lines 101 to 189
190## Volumetric PIV (`openpiv.pyprocess3D`)
192```python
193from openpiv import pyprocess3D
195u, v, w, s2n = pyprocess3D.extended_search_area_piv3D(
196 vol_a, vol_b,
197 window_size=(32, 32, 32),
198 overlap=(16, 16, 16),
199 dt=(1.0, 1.0, 1.0),
200 search_area_size=(38, 38, 38),
201 correlation_method="fft", # note: "fft" here, not the 2D "circular"/"linear"
202 subpixel_method="gaussian",
203 sig2noise_method="peak2peak",
206# Note the extra window_size argument -- this signature differs from pyprocess.get_coordinates.
207x, y, z = pyprocess3D.get_coordinates(
208 vol_a.shape, search_area_size=(38, 38, 38), window_size=(32, 32, 32), overlap=(16, 16, 16)
210# 0.26.1's FFT direction differs from 2D. These components align with
211# increasing column, row, depth coordinates (verify on a known translation).
212column_velocity, row_velocity, depth_velocity = -u, v, w
213```
215Inputs are 3D intensity volumes — this module correlates reconstructed volumes; it does not perform
216the tomographic reconstruction itself. `dt` is a per-axis tuple. Memory scales with the cube of
217window size, so 32³ windows on a large volume are already demanding.
219The 0.26.1 3D FFT reverses frame B, unlike the 2D path. Positive input translations along
220`(row, column, depth)` give raw `(u, v, w)=(-dcolumn, +drow, +ddepth)/dt`, confirmed for both
221shift directions. Do not reuse the 2D sign conversion. The mapping above aligns velocities with
222the increasing column/row/depth coordinates returned by `get_coordinates`; apply a separately
223calibrated laboratory-axis transform to both positions and velocities. The grid stride uses
224`window_size-overlap`, unlike the 2D extended
225search stride. `sig2noise_method=None` returns three arrays here, rather than the 2D API's three
226arrays with a NaN s2n. The 32³ recipe ran on synthetic volumes; smaller positive and negative
227translations established the component signs. Real volumetric experiments remain unvalidated.

Volumetric PIV (openpiv.pyprocess3D)

from openpiv import pyprocess3D

u, v, w, s2n = pyprocess3D.extended_search_area_piv3D(
    vol_a, vol_b,
    window_size=(32, 32, 32),
    overlap=(16, 16, 16),
    dt=(1.0, 1.0, 1.0),
    search_area_size=(38, 38, 38),
    correlation_method="fft",       # note: "fft" here, not the 2D "circular"/"linear"
    subpixel_method="gaussian",
    sig2noise_method="peak2peak",
)

# Note the extra window_size argument -- this signature differs from pyprocess.get_coordinates.
x, y, z = pyprocess3D.get_coordinates(
    vol_a.shape, search_area_size=(38, 38, 38), window_size=(32, 32, 32), overlap=(16, 16, 16)
)
# 0.26.1's FFT direction differs from 2D. These components align with
# increasing column, row, depth coordinates (verify on a known translation).
column_velocity, row_velocity, depth_velocity = -u, v, w

Inputs are 3D intensity volumes — this module correlates reconstructed volumes; it does not perform the tomographic reconstruction itself. dt is a per-axis tuple. Memory scales with the cube of window size, so 32³ windows on a large volume are already demanding.

The 0.26.1 3D FFT reverses frame B, unlike the 2D path. Positive input translations along (row, column, depth) give raw (u, v, w)=(-dcolumn, +drow, +ddepth)/dt, confirmed for both shift directions. Do not reuse the 2D sign conversion. The mapping above aligns velocities with the increasing column/row/depth coordinates returned by get_coordinates; apply a separately calibrated laboratory-axis transform to both positions and velocities. The grid stride uses window_size-overlap, unlike the 2D extended search stride. sig2noise_method=None returns three arrays here, rather than the 2D API's three arrays with a NaN s2n. The 32³ recipe ran on synthetic volumes; smaller positive and negative translations established the component signs. Real volumetric experiments remain unvalidated.

Raw lines 190 to 228
229## Phase separation (`openpiv.phase_separation`)
231For two-phase flows where large particles (droplets, bubbles) must be separated from tracers before
232correlation:
234```python
235from openpiv import phase_separation
237big, small = phase_separation.khalitov_longmire(
238 image,
239 big_particles_criteria={"min_size": 20, "min_brightness": 30},
240 small_particles_criteria={"max_size": 20, "min_brightness": 5},
241 blur_kernel_size=1,
242 I_sat=230,
244```
246Criteria dicts accept `min_size`, `max_size`, `min_brightness`, and `max_brightness`. `min_size` is
247mandatory for the big-particle dict and `max_size` for the small-particle dict; unrecognized keys are
248ignored silently, so check spelling.
249Size cutoffs are segmented region areas in pixels, not particle diameters; strict inequalities
250exclude equality. Brightness and `I_sat` must match the input intensity scale (the example is 8-bit).
252Also available: `median_filter_method(image, kernel_size)` (Kiger & Pan) and
253`opening_method(image, kernel_size, iterations=1, thresh_factor=1.1)` for simpler size-based
254separation. Run PIV separately on each returned phase — tracer statistics computed on an unseparated
255image are contaminated by the dispersed phase.

Phase separation (openpiv.phase_separation)

For two-phase flows where large particles (droplets, bubbles) must be separated from tracers before correlation:

from openpiv import phase_separation

big, small = phase_separation.khalitov_longmire(
    image,
    big_particles_criteria={"min_size": 20, "min_brightness": 30},
    small_particles_criteria={"max_size": 20, "min_brightness": 5},
    blur_kernel_size=1,
    I_sat=230,
)

Criteria dicts accept min_size, max_size, min_brightness, and max_brightness. min_size is mandatory for the big-particle dict and max_size for the small-particle dict; unrecognized keys are ignored silently, so check spelling. Size cutoffs are segmented region areas in pixels, not particle diameters; strict inequalities exclude equality. Brightness and I_sat must match the input intensity scale (the example is 8-bit).

Also available: median_filter_method(image, kernel_size) (Kiger & Pan) and opening_method(image, kernel_size, iterations=1, thresh_factor=1.1) for simpler size-based separation. Run PIV separately on each returned phase — tracer statistics computed on an unseparated image are contaminated by the dispersed phase.

Raw lines 229 to 256
257## Choosing an approach
259| Situation | Approach |
260|-----------|----------|
261| Small displacements, uniform flow | `extended_search_area_piv`, `correlation_method="circular"` |
262| Displacements above ~1/4 window | `search_area_size > window_size` with `correlation_method="linear"` |
263| Strong velocity gradients, shear layers | `windef.simple_multipass` with decreasing `windowsizes` |
264| Uneven illumination | `normalized_correlation=True`, plus background subtraction |
265| Solid bodies, reflections, free surfaces | `preprocess.dynamic_masking` or a `static_mask` |
266| Two-phase flow | `phase_separation` first, then PIV per phase |
267| Volumetric data | `pyprocess3D.extended_search_area_piv3D` |

Choosing an approach

SituationApproach
Small displacements, uniform flowextended_search_area_piv, correlation_method="circular"
Displacements above ~1/4 windowsearch_area_size > window_size with correlation_method="linear"
Strong velocity gradients, shear layerswindef.simple_multipass with decreasing windowsizes
Uneven illuminationnormalized_correlation=True, plus background subtraction
Solid bodies, reflections, free surfacespreprocess.dynamic_masking or a static_mask
Two-phase flowphase_separation first, then PIV per phase
Volumetric datapyprocess3D.extended_search_area_piv3D
Raw lines 257 to 268
269## Review sources and verification
271- [0.26.1 release and backend notes](https://github.com/OpenPIV/openpiv-python/releases/tag/v0.26.1)
272- [Official API reference](https://openpiv.readthedocs.io/en/latest/src/api_reference.html)
273- [Released correlation source](https://github.com/OpenPIV/openpiv-python/blob/v0.26.1/openpiv/pyprocess.py)
274- [Released settings](https://github.com/OpenPIV/openpiv-python/blob/v0.26.1/openpiv/settings.py)
275- [Batch and array multipass source](https://github.com/OpenPIV/openpiv-python/blob/v0.26.1/openpiv/windef.py)
276- [Masking source](https://github.com/OpenPIV/openpiv-python/blob/v0.26.1/openpiv/preprocess.py)
277- [Validation](https://github.com/OpenPIV/openpiv-python/blob/v0.26.1/openpiv/validation.py),
278 [replacement](https://github.com/OpenPIV/openpiv-python/blob/v0.26.1/openpiv/lib.py), and
279 [smoothing](https://github.com/OpenPIV/openpiv-python/blob/v0.26.1/openpiv/smoothn.py)
280- [3D correlation](https://github.com/OpenPIV/openpiv-python/blob/v0.26.1/openpiv/pyprocess3D.py) and
281 [phase separation](https://github.com/OpenPIV/openpiv-python/blob/v0.26.1/openpiv/phase_separation.py)
283Tests use generated particle translations, analytic velocity fields, mask persistence, fractional
284intensities, multipass units, NaN smoothing, and small volumetric/phase-separation examples. These
285are API/numerical checks, not validation of a real experiment's calibration, uncertainty, tracer
286fidelity, time/ensemble convergence, or particle-phase classification. Rust was unavailable in
287the tested environment; its execution and claimed speedup were not verified. No remote service,
288authentication, endpoints, or pagination are involved in local OpenPIV processing.

Review sources and verification

Tests use generated particle translations, analytic velocity fields, mask persistence, fractional intensities, multipass units, NaN smoothing, and small volumetric/phase-separation examples. These are API/numerical checks, not validation of a real experiment's calibration, uncertainty, tracer fidelity, time/ensemble convergence, or particle-phase classification. Rust was unavailable in the tested environment; its execution and claimed speedup were not verified. No remote service, authentication, endpoints, or pagination are involved in local OpenPIV processing.

Raw lines 269 to 288