Examples

Field Retrieval & Unwrapping

Field retrieval (qpretrieve) and phase unwrapping (xpunwrap) on GPU.

home/docs/checkouts/readthedocs.org/user_builds/xpunwrap/checkouts/latest/examples/gpu_field_retr_phase_unwrapping.png

gpu_field_retr_phase_unwrapping.py

 1import matplotlib.pyplot as plt
 2import numpy as np
 3import qpretrieve
 4import xpunwrap
 5
 6# Force GPU backend for both libraries.
 7qpretrieve.set_ndarray_backend("cupy")
 8xpunwrap.set_ndarray_backend("cupy")
 9xp = xpunwrap.get_ndarray_backend()
10
11fft_interface = qpretrieve.fourier.FFTFilterCupy
12edata = np.load("./data/hologram_cell.npz")
13holo = qpretrieve.OffAxisHologram(edata["data"], fft_interface)
14bg = qpretrieve.OffAxisHologram(edata["bg_data"], fft_interface)
15
16holo.run_pipeline(filter_name="disk", filter_size=1 / 2, scale_to_filter=True)
17bg.process_like(holo)
18phase_wrapped = xp.asarray(holo.phase - bg.phase).astype(xp.float32)
19
20# Unwrap the phase with all available algorithms
21outputs = {}
22for algo_name, algo in xpunwrap.algos_available().items():
23    outputs[algo_name] = algo(phase_wrapped)
24skimage_out = outputs.get("algo_skimage_unwrap", None)
25if skimage_out is not None:
26    outputs_no_skimage = {k: v for k, v in outputs.items() if k != "algo_skimage_unwrap"}
27else:
28    outputs_no_skimage = outputs
29
30# plot the wrapped and unwrapped phases
31plt.style.use("dark_background")
32fig, axes = plt.subplots(2, 3, figsize=(8, 6))
33fig.suptitle("Field Retrieval + Phase Unwrapping (GPU)", fontsize=18)
34axes = axes.flatten(order="F")
35
36axes[0].imshow(phase_wrapped.get()[0])
37axes[0].set_title("Wrapped Phase")
38
39# With column-major flattening, bottom-right is index 5.
40skimage_ax = axes[5]
41
42# Fill remaining non-skimage algorithms first (slots 1..4).
43plot_slots = [2, 3, 4]
44for slot, (algo_name, arr) in zip(plot_slots, outputs_no_skimage.items()):
45    ax = axes[slot]
46    ax.imshow(arr.get()[0])
47    ax.set_title(f"Unwrapped\n{algo_name}")
48
49if skimage_out is not None:
50    skimage_ax.imshow(skimage_out.get()[0])
51    skimage_ax.set_title("Unwrapped (CPU-only)\nalgo_skimage_unwrap")
52else:
53    skimage_ax.text(0.5, 0.5, "skimage missing", ha="center", va="center")
54    skimage_ax.set_title("Unwrapped\nalgo_skimage_unwrap")
55
56for ax in axes:
57    ax.set_axis_off()
58
59plt.tight_layout(w_pad=4.5)
60plt.savefig("gpu_field_retr_phase_unwrapping.png")
61# plt.show()

CPU vs GPU phase unwrapping speed comparison

Compares four pipelines on hologram_cell data:

  • CPU skimage + NumPy (qpretrieve NumPy FFT + skimage unwrap)

  • CPU skimage + pyFFTW (qpretrieve pyFFTW FFT + skimage unwrap)

  • CPU ls_poisson + pyFFTW (qpretrieve pyFFTW FFT + xpunwrap Poisson)

  • GPU ls_poisson + CuPy (full CuPy pipeline)

Run:

python examples/compare_cpu_gpu.py
home/docs/checkouts/readthedocs.org/user_builds/xpunwrap/checkouts/latest/examples/compare_cpu_gpu.png

compare_cpu_gpu.py

  1from __future__ import annotations
  2
  3import time
  4from pathlib import Path
  5
  6import matplotlib.pyplot as plt
  7import numpy as np
  8
  9import qpretrieve
 10import xpunwrap
 11import xpunwrap.fourier as xp_fourier
 12
 13DATA_PATH = Path(__file__).parent.parent / "tests" / "data" / "hologram_cell.npz"
 14OUTPUT_PNG = Path(__file__).parent / "compare_cpu_gpu.png"
 15STACK_DEPTH = 256
 16TILE_SIZE = 256
 17
 18
 19def _load_raw_numpy() -> dict[str, np.ndarray]:
 20    edata = np.load(DATA_PATH)
 21    data = np.asarray(edata["data"], dtype=np.float32)
 22    bg = np.asarray(edata["bg_data"], dtype=np.float32)
 23    tile_h = int(np.ceil(TILE_SIZE / data.shape[-2]))
 24    tile_w = int(np.ceil(TILE_SIZE / data.shape[-1]))
 25    data = np.tile(data, (tile_h, tile_w))[:TILE_SIZE, :TILE_SIZE]
 26    bg = np.tile(bg, (tile_h, tile_w))[:TILE_SIZE, :TILE_SIZE]
 27    return {"data": data, "bg": bg}
 28
 29
 30def _stack_phase(arr: np.ndarray) -> np.ndarray:
 31    return np.repeat(arr[None, ...], repeats=STACK_DEPTH, axis=0)
 32
 33
 34def _time_skimage_pipeline(
 35        raw: dict[str, np.ndarray],
 36        fft_interface,
 37        repeats: int = 3,
 38):
 39    """Shared timing loop for skimage-based pipelines."""
 40    qpretrieve.set_ndarray_backend("numpy")
 41    try:
 42        algo = xpunwrap.algos_available()["algo_skimage_unwrap"]
 43    except Exception:
 44        return None
 45
 46    def _run():
 47        holo = qpretrieve.OffAxisHologram(raw["data"], fft_interface)
 48        bg = qpretrieve.OffAxisHologram(raw["bg"], fft_interface)
 49        holo.run_pipeline(
 50            filter_name="disk", filter_size=1 / 2, scale_to_filter=True)
 51        bg.process_like(holo)
 52        algo(np.asarray(holo.phase - bg.phase, dtype=np.float32))
 53
 54    _run()  # warmup (important for pyFFTW plan compilation)
 55    times = []
 56    for _ in range(repeats):
 57        start = time.perf_counter()
 58        _run()
 59        times.append((time.perf_counter() - start) * 1000.0)
 60    return float(np.mean(times)), float(np.std(times))
 61
 62
 63def time_cpu_skimage_numpy(raw: dict[str, np.ndarray], repeats: int = 3):
 64    """CPU pipeline: qpretrieve (NumPy FFT) + skimage unwrap."""
 65    return _time_skimage_pipeline(
 66        raw, qpretrieve.fourier.FFTFilterNumpy, repeats)
 67
 68
 69def time_cpu_skimage_pyfftw(raw: dict[str, np.ndarray], repeats: int = 3):
 70    """CPU pipeline: qpretrieve (pyFFTW) + skimage unwrap."""
 71    return _time_skimage_pipeline(
 72        raw, qpretrieve.fourier.FFTFilterPyFFTW, repeats)
 73
 74
 75def time_cpu_ls_poisson_pyfftw(raw: dict[str, np.ndarray], repeats: int = 3):
 76    """CPU pipeline: qpretrieve (pyFFTW) + xpunwrap ls_poisson (pyFFTW)."""
 77    from xpunwrap.fourier import FFTEnginePyFFTW
 78    if FFTEnginePyFFTW is None:
 79        return None
 80
 81    qpretrieve.set_ndarray_backend("numpy")
 82    xpunwrap.set_ndarray_backend("numpy")
 83    fft_interface = qpretrieve.fourier.FFTFilterPyFFTW
 84    xp_fourier.PREFERRED_ENGINE = "FFTEnginePyFFTW"
 85    algo = xpunwrap.algos_available()["algo_ls_poisson"]
 86
 87    try:
 88        # Warmup
 89        holo = qpretrieve.OffAxisHologram(raw["data"], fft_interface)
 90        bg = qpretrieve.OffAxisHologram(raw["bg"], fft_interface)
 91        holo.run_pipeline(
 92            filter_name="disk", filter_size=1 / 2, scale_to_filter=True)
 93        bg.process_like(holo)
 94        algo(np.asarray(holo.phase - bg.phase, dtype=np.float32))
 95
 96        times = []
 97        for _ in range(repeats):
 98            start = time.perf_counter()
 99            holo = qpretrieve.OffAxisHologram(raw["data"], fft_interface)
100            bg = qpretrieve.OffAxisHologram(raw["bg"], fft_interface)
101            holo.run_pipeline(
102                filter_name="disk", filter_size=1 / 2, scale_to_filter=True)
103            bg.process_like(holo)
104            algo(np.asarray(holo.phase - bg.phase, dtype=np.float32))
105            times.append((time.perf_counter() - start) * 1000.0)
106    finally:
107        xp_fourier.PREFERRED_ENGINE = None
108
109    return float(np.mean(times)), float(np.std(times))
110
111
112def time_gpu_pipeline(raw: dict[str, np.ndarray], repeats: int = 3):
113    """GPU pipeline: qpretrieve (CuPy FFT) + xpunwrap ls_poisson (CuPy FFT)."""
114    try:
115        import cupy as cp  # noqa: F401
116    except Exception:
117        return None
118
119    qpretrieve.set_ndarray_backend("cupy")
120    fft_interface = qpretrieve.fourier.FFTFilterCupy
121    xpunwrap.set_ndarray_backend("cupy")
122    xp = xpunwrap.get_ndarray_backend()
123    algo = xpunwrap.algos_available()["algo_ls_poisson"]
124
125    def _sync():
126        if hasattr(xp, "cuda"):
127            xp.cuda.Stream.null.synchronize()
128
129    # Warmup
130    _sync()
131    holo_gpu = xp.asarray(raw["data"])
132    bg_gpu = xp.asarray(raw["bg"])
133    holo_obj = qpretrieve.OffAxisHologram(holo_gpu, fft_interface)
134    bg_obj = qpretrieve.OffAxisHologram(bg_gpu, fft_interface)
135    holo_obj.run_pipeline(
136        filter_name="disk", filter_size=1 / 2, scale_to_filter=True)
137    bg_obj.process_like(holo_obj)
138    algo(xp.asarray(holo_obj.phase - bg_obj.phase, dtype=xp.float32))
139    _sync()
140
141    times = []
142    for _ in range(repeats):
143        _sync()
144        start = time.perf_counter()
145        holo_gpu = xp.asarray(raw["data"])
146        bg_gpu = xp.asarray(raw["bg"])
147        holo_obj = qpretrieve.OffAxisHologram(holo_gpu, fft_interface)
148        bg_obj = qpretrieve.OffAxisHologram(bg_gpu, fft_interface)
149        holo_obj.run_pipeline(
150            filter_name="disk", filter_size=1 / 2, scale_to_filter=True)
151        bg_obj.process_like(holo_obj)
152        algo(xp.asarray(holo_obj.phase - bg_obj.phase, dtype=xp.float32))
153        _sync()
154        times.append((time.perf_counter() - start) * 1000.0)
155    return float(np.mean(times)), float(np.std(times))
156
157
158def main():
159    raw = _load_raw_numpy()
160
161    skimage_numpy = time_cpu_skimage_numpy(raw)
162    skimage_pyfftw = time_cpu_skimage_pyfftw(raw)
163    ls_pyfftw = time_cpu_ls_poisson_pyfftw(raw)
164    ls_cupy = time_gpu_pipeline(raw)
165
166    # Wong (2011) / Nature Methods colorblind-safe palette
167    clr_blue, clr_green = "#56B4E9", "#009E73"
168    clr_orange, clr_purple = "#D55E00", "#CC79A7"
169    entries = [
170        ("skimage\n+ NumPy\n(CPU)", skimage_numpy, clr_blue),
171        ("skimage\n+ pyFFTW\n(CPU)", skimage_pyfftw, clr_green),
172        ("ls_poisson\n+ pyFFTW\n(CPU)", ls_pyfftw, clr_orange),
173        ("ls_poisson\n+ CuPy\n(GPU)", ls_cupy, clr_purple),
174    ]
175    entries = [(lbl, res, col) for lbl, res, col in entries if res is not None]
176
177    labels = [e[0] for e in entries]
178    means = [e[1][0] for e in entries]
179    stds = [e[1][1] for e in entries]
180    colors = [e[2] for e in entries]
181
182    plt.style.use("dark_background")
183    fig, ax = plt.subplots(figsize=(6, 4), facecolor="#181C24")
184    x = np.arange(len(labels))
185    ax.bar(x, means, yerr=stds, color=colors, edgecolor="white",
186           error_kw={"ecolor": "white", "elinewidth": 1.5,
187                     "capsize": 5, "capthick": 1.5})
188    ax.set_xticks(x)
189    ax.set_xticklabels(labels, ha="center")
190    ax.set_ylabel("Time (ms)")
191    ax.set_title("Phase unwrapping speed comparison")
192    ax.grid(axis="y", linestyle="--", alpha=0.3)
193    ax.set_facecolor("#181C24")
194
195    for xi, mean, std in zip(x, means, stds):
196        ax.text(xi, mean + std, f"{mean:.1f} ms",
197                ha="center", va="bottom", fontsize=8)
198
199    fig.tight_layout()
200    fig.savefig(OUTPUT_PNG, dpi=200)
201    print(f"Saved {OUTPUT_PNG}")
202
203
204if __name__ == "__main__":
205    main()

Poisson LS Algorithm Walkthrough

Step-by-step walkthrough of the Least-squares Poisson Periodic Gradient algorithm.

The aim here is to visualise the steps of the algorithm with matplotlib with example datasets, showing edge cases where phase unwrapping fails. Where possible the scikit-image 2D phase unwrapping algorithm will be used for comparison.

The example data is the data used by M. Gdeisat and F. Lilley in their excellent “Two-Dimensional Phase Unwrapping Problem” guide.

home/docs/checkouts/readthedocs.org/user_builds/xpunwrap/checkouts/latest/examples/walkthrough-ls_poisson_pg.png

walkthrough-ls_poisson_pg.py

  1import numpy as np
  2import matplotlib.pyplot as plt
  3from typing import Tuple
  4
  5import xpunwrap
  6from xpunwrap.algorithms.ls_poisson_pg import (
  7    wrapped_gradients_stack,
  8    enforce_periodic_gradients_stack,
  9    divergence_stack,
 10    poisson_solve_fft_stack,
 11)
 12from xpunwrap.algorithms._plane_utils import restore_mean_plane
 13
 14# use numpy for this example for simplicity
 15xpunwrap.set_ndarray_backend('numpy')
 16xp = xpunwrap.get_ndarray_backend()
 17
 18
 19# load/create the example data
 20def _generate_phase(nrx: int = 512, nry: int = 512) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
 21    """Create the continuous phase image f(x, y) = 20*exp(-0.25*(x^2+y^2)) + 2x + y."""
 22    tx = np.linspace(-3.0, 3.0, nrx)
 23    ty = np.linspace(-3.0, 3.0, nry)
 24    x, y = np.meshgrid(tx, ty)
 25    image = 20.0 * np.exp(-0.25 * (x ** 2 + y ** 2)) + 2.0 * x + y
 26    return x, y, image
 27
 28
 29def _wrap_phase(image: np.ndarray) -> np.ndarray:
 30    """Wrap phase to [-pi, pi] using angle of complex exponential."""
 31    return np.angle(np.exp(1j * image))
 32
 33
 34x, y, image = _generate_phase()
 35arr = _wrap_phase(image)
 36phase_wrapped = np.repeat(arr[None, ...], repeats=10, axis=0)
 37
 38# the `algo_ls_poisson_pg` is broken up into the following steps:
 39#    initial dimensionality check
 40#    gx, gy = wrapped_gradients_stack(phase_wrapped)
 41#    gx, gy = enforce_periodic_gradients_stack(gx, gy)
 42#    rhs = divergence_stack(gx, gy)
 43#    phi = poisson_solve_fft_stack(rhs)
 44#    phase inversion
 45#    restore_mean_plane
 46
 47# Compute forward finite differences of the wrapped phase.
 48# Then wrap those gradients back into [-pi, pi) so they remain a valid
 49# representation of wrapped phase changes.
 50gx1, gy1 = wrapped_gradients_stack(phase_wrapped)
 51# Enforce periodic boundary conditions on the wrapped gradients so that the
 52# edges match up. This makes the subsequent FFT-based Poisson solve consistent
 53# with a periodic domain.
 54gx2, gy2 = enforce_periodic_gradients_stack(gx1, gy1)
 55# Take the divergence of those periodic wrapped gradients; this produces the
 56# right-hand side for the Poisson equation whose solution is the unwrapped phase.
 57rhs = divergence_stack(gx2, gy2)
 58# Solve the Poisson equation in the Fourier domain (batched over the stack);
 59# this yields an unwrapped phase up to a linear ramp.
 60phi = poisson_solve_fft_stack(rhs)
 61# Negate to match the sign convention used by the wrapped-gradient computation
 62# so that gradients of the solution align with the original wrapped gradients.
 63phi *= -1
 64phi_restored = restore_mean_plane(phi, phase_wrapped)
 65
 66# plotting
 67fontsize = 12
 68nrows, ncols = 4, 2
 69fig = plt.figure(figsize=(8, 12))
 70
 71ax1 = fig.add_subplot(nrows, ncols, 1)
 72ax1.set_title("Wrapped Phase Example", fontsize=fontsize)
 73im1 = ax1.imshow(phase_wrapped[1])
 74fig.colorbar(im1, ax=ax1, fraction=0.046, pad=0.04)
 75
 76ax2 = fig.add_subplot(nrows, ncols, 2, projection='3d')
 77ax2.set_title("Wrapped Phase Example 3D proj", fontsize=fontsize)
 78surf2 = ax2.plot_surface(x, y, phase_wrapped[1], cmap="viridis", linewidth=0, antialiased=True)
 79fig.colorbar(surf2, ax=ax2, fraction=0.046, pad=0.04, shrink=0.6)
 80
 81ax3 = fig.add_subplot(nrows, ncols, 3)
 82ax3.set_title("Wrapped Gradients + Per. Grad. (x)", fontsize=fontsize)
 83im3 = ax3.imshow(gx2[1])
 84fig.colorbar(im3, ax=ax3, fraction=0.046, pad=0.04)
 85
 86ax4 = fig.add_subplot(nrows, ncols, 4)
 87ax4.set_title("Wrapped Gradients + Per. Grad. (y)", fontsize=fontsize)
 88im4 = ax4.imshow(gy2[1])
 89fig.colorbar(im4, ax=ax4, fraction=0.046, pad=0.04)
 90
 91ax5 = fig.add_subplot(nrows, ncols, 5)
 92ax5.set_title("Poisson RHS", fontsize=fontsize)
 93im5 = ax5.imshow(rhs[1])
 94fig.colorbar(im5, ax=ax5, fraction=0.046, pad=0.04)
 95
 96ax6 = fig.add_subplot(nrows, ncols, 6)
 97ax6.set_title("Unwrapped phase (with inversion)", fontsize=fontsize)
 98im6 = ax6.imshow(phi[1])
 99fig.colorbar(im6, ax=ax6, fraction=0.046, pad=0.04)
100
101ax7 = fig.add_subplot(nrows, ncols, 7)
102ax7.set_title("Unwrapped phase (restore)", fontsize=fontsize)
103im7 = ax7.imshow(phi_restored[1])
104fig.colorbar(im7, ax=ax7, fraction=0.046, pad=0.04)
105
106ax8 = fig.add_subplot(nrows, ncols, 8, projection='3d')
107ax8.set_title("Unwrapped phase (restore) 3D proj", fontsize=fontsize)
108surf8 = ax8.plot_surface(x, y, phi_restored[1], cmap="viridis", linewidth=0, antialiased=True)
109fig.colorbar(surf8, ax=ax8, fraction=0.046, pad=0.04, shrink=0.6)
110
111for ax in [ax1, ax2, ax3, ax4, ax5, ax6, ax7, ax8]:
112    ax.set_xticks([])
113    ax.set_yticks([])
114
115plt.tight_layout()
116plt.savefig("walkthrough-ls_poisson_pg.png")
117plt.show()

Download base simulation script: unwrapping_simulation.py

LS-Poisson variants

Generate only the LS-Poisson variants figure from unwrapping_simulation. Outputs: unwrapping_comparison.png

home/docs/checkouts/readthedocs.org/user_builds/xpunwrap/checkouts/latest/examples/unwrapping_simulation_poisson.png

unwrapping_simulation_poisson.py

1from unwrapping_simulation import main
2
3
4if __name__ == "__main__":
5    main(plot_itoh=False, plot_poisson=True, plot_2d_comparison=False)

Itoh and Skimage unwrapping

Generate only the Itoh/skimage comparison figure from unwrapping_simulation. This graph is a reproduction of the graph in “Two-Dimensional Phase Unwrapping Problem By Dr. Munther Gdeisat and Dr. Francis Lilley”.

Outputs: unwrapping_itoh.png

home/docs/checkouts/readthedocs.org/user_builds/xpunwrap/checkouts/latest/examples/unwrapping_simulation_itoh.png

unwrapping_simulation_itoh.py

1from unwrapping_simulation import main
2
3
4if __name__ == "__main__":
5    main(plot_itoh=True, plot_poisson=False, plot_2d_comparison=False)

Phase Unwrapping Algorithm comparison

Generate only the 2D unwrapped comparison/difference maps vs skimage. Outputs: unwrapping_2d_comparison.png

home/docs/checkouts/readthedocs.org/user_builds/xpunwrap/checkouts/latest/examples/unwrapping_simulation_comparison.png

unwrapping_simulation_comparison.py

1from unwrapping_simulation import main
2
3
4if __name__ == "__main__":
5    main(plot_itoh=False, plot_poisson=False, plot_2d_comparison=True)

NDArray backend and FFT Engine Selection

Demonstrates how to choose between the NumPy (CPU) and CuPy (GPU) array backends and how the FFT engine is selected automatically to match.

The FFT engine is only relevant for the least-squares Poisson solvers (algo_ls_poisson and algo_ls_poisson_pg); the weighted solver (algo_ls_weighted) is Jacobi-based and uses no FFT.

Run:

python examples/using_backends_and_fft-engines.py

using_backends_and_fft-engines.py

  1from __future__ import annotations
  2
  3import numpy as np
  4
  5import xpunwrap
  6import xpunwrap.fourier as fourier
  7
  8# 1. Dumm wrapped phase data (N=1, H=64, W=64)
  9rng = np.random.default_rng(0)
 10_tx = np.linspace(-3.0, 3.0, 64)
 11_x, _y = np.meshgrid(_tx, _tx)
 12_phase = (20.0 * np.exp(-0.25 * (_x ** 2 + _y ** 2)) +
 13          rng.normal(0, 0.3, (64, 64)))
 14wrapped_np = np.angle(np.exp(1j * _phase))[None, ...].astype(np.float32)
 15
 16# 2. NumPy array backend → FFT engine auto-selects pyFFTW (if installed)
 17xpunwrap.set_ndarray_backend("numpy")
 18xp = xpunwrap.get_ndarray_backend()
 19
 20fft_cls = fourier.get_best_engine()
 21print(f"Array backend : {xp.__name__} (CPU)")
 22print(f"FFT engine    : {fft_cls.__name__}")
 23
 24wrapped = xp.asarray(wrapped_np)
 25result_auto = xpunwrap.algo_ls_poisson(wrapped, restore_plane=False)
 26print(f"Result shape  : {result_auto.shape}, dtype: {result_auto.dtype}")
 27
 28# 3. Force the NumPy FFT engine via PREFERRED_ENGINE
 29fourier.PREFERRED_ENGINE = "FFTEngineNumpy"
 30fft_cls_forced = fourier.get_best_engine()
 31print(f"\nArray backend : {xp.__name__} (CPU)")
 32print(f"Preferred FFT : {fourier.PREFERRED_ENGINE}")
 33print(f"FFT engine    : {fft_cls_forced.__name__}")
 34
 35result_numpy_fft = xpunwrap.algo_ls_poisson(wrapped, restore_plane=False)
 36print(f"Result shape  : {result_numpy_fft.shape}, "
 37      f"dtype: {result_numpy_fft.dtype}")
 38
 39# Both FFT engines produce the same answer (within float32 rounding).
 40max_diff = float(np.max(np.abs(
 41    np.asarray(result_auto) - np.asarray(result_numpy_fft)
 42)))
 43print(f"Max |auto - numpy| : {max_diff:.2e}  (should be near zero)")
 44
 45# Reset so the rest of this script uses auto-selection again.
 46fourier.PREFERRED_ENGINE = None
 47
 48# 4. CuPy array backend (skipped when CuPy is not installed)
 49try:
 50    import cupy  # noqa: F401
 51
 52    _cupy_available = True
 53except ImportError:
 54    _cupy_available = False
 55
 56if _cupy_available:
 57    xpunwrap.set_ndarray_backend("cupy")
 58    xp_gpu = xpunwrap.get_ndarray_backend()
 59    fft_cls_gpu = fourier.get_best_engine()
 60    print(f"\nArray backend : {xp_gpu.__name__} (GPU)")
 61    print(f"FFT engine    : {fft_cls_gpu.__name__}")
 62
 63    wrapped_gpu = xp_gpu.asarray(wrapped_np)
 64    result_gpu = xpunwrap.algo_ls_poisson(wrapped_gpu, restore_plane=False)
 65    print(f"Result shape  : {result_gpu.shape}, dtype: {result_gpu.dtype}")
 66
 67    # Compare GPU result against CPU result.
 68    max_diff_gpu = float(np.max(np.abs(
 69        np.asarray(result_auto) - result_gpu.get()
 70    )))
 71    print(f"Max |cpu - gpu| : {max_diff_gpu:.2e}")
 72else:
 73    print("\nCuPy not installed — GPU section skipped.")
 74
 75# 5. Use xp directly for array operations alongside the unwrapper
 76xpunwrap.set_ndarray_backend("numpy")
 77xp = xpunwrap.get_ndarray_backend()
 78
 79wrapped = xp.asarray(wrapped_np)
 80result = xpunwrap.algo_ls_poisson(wrapped, restore_plane=False)
 81
 82# All array ops go through xp, so this code is backend-agnostic.
 83residual = xp.angle(xp.exp(1j * result)) - xp.angle(xp.exp(1j * wrapped))
 84print(f"\nMean abs residual (wrapped): {float(
 85    xp.mean(xp.abs(residual))):.4f} rad")
 86
 87"""
 88Example output:
 89
 90Array backend : numpy (CPU)
 91FFT engine    : FFTEnginePyFFTW
 92Result shape  : (1, 64, 64), dtype: float32
 93
 94Array backend : numpy (CPU)
 95Preferred FFT : FFTEngineNumpy
 96FFT engine    : FFTEngineNumpy
 97Result shape  : (1, 64, 64), dtype: float32
 98Max |auto - numpy| : 4.77e-06  (should be near zero)
 99
100Array backend : cupy (GPU)
101FFT engine    : FFTEngineCupy
102Result shape  : (1, 64, 64), dtype: float32
103Max |cpu - gpu| : 4.29e-06
104
105Mean abs residual (wrapped): 0.1254 rad
106"""

Processing Pipeline with GPU and Zarr

Example processing pipeline for phase data on the GPU: - Load with Zarr in GPU - Field retrieval - Phase unwrapping - Write to Zarr

processing_pipeline_gpu.py

 1import zarr
 2import qpretrieve
 3import xpunwrap
 4
 5# set cupy backend
 6xpunwrap.set_ndarray_backend("cupy")
 7qpretrieve.set_ndarray_backend("cupy")
 8xp = xpunwrap.get_ndarray_backend()
 9
10# load data with zarr via GPU memory
11zarr.config.enable_gpu()
12
13# load to cpu (mightn't need this)
14edata = xp.load("./data/hologram_cell.npz")
15
16# load to gpu
17# caveat: Zarr loads the data into GPU, but encoding and decoding happens
18# on the CPU. It is a work in progress.
19# One can use `kvikio` to directly do GPU loading, but
20# `pip install kvikio-cu12` only works on linux, or use conda/mamba to install
21# on Windows, I assume. See https://docs.rapids.ai/api/kvikio/stable/zarr/
22store_holo = zarr.storage.MemoryStore()
23store_bg = zarr.storage.MemoryStore()
24data_holo = zarr.create_array(store=store_holo, data=edata["data"])
25data_bg = zarr.create_array(store=store_bg, data=edata["bg_data"])
26
27# field retrieval on the GPU
28fft_interface = qpretrieve.fourier.FFTFilterCupy
29holo = qpretrieve.OffAxisHologram(data_holo[:], fft_interface)
30bg = qpretrieve.OffAxisHologram(data_bg[:], fft_interface)
31holo.run_pipeline(filter_name="disk", filter_size=1 / 2, scale_to_filter=True)
32bg.process_like(holo)
33phase_wrapped = xp.asarray(holo.phase - bg.phase).astype(xp.float32)
34
35# unwrap the phase on the GPU
36phase_unwrapped = xpunwrap.algo_ls_poisson_pg(phase_wrapped)
37
38# write from GPU to Zarr file
39path = "temp_unwrapped_cell_phase.zip"
40with zarr.storage.ZipStore(path, mode='w') as disk:
41    data_phase = zarr.create_array(store=disk, data=phase_unwrapped)
42
43# reopen the file to ensure it is correct
44with zarr.storage.ZipStore(path, mode='r') as disk_reload:
45    data_reload = zarr.open_array(store=disk_reload, mode='r')
46    assert data_phase.shape == data_reload.shape
47    assert xp.allclose(phase_unwrapped, data_reload[:])