Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ENH: Add Scipy and Cupy as fft interfaces #8

Draft
wants to merge 31 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
f09a64f
enh: add scipy fft interface
Aug 26, 2024
41b77e4
tests: correct path for ci use
Aug 26, 2024
97e7c5c
tests: fft interface checks and lint code
Aug 26, 2024
93618fb
docs: update docstrings and docs example output image
Aug 26, 2024
b19a817
enh, tests: add cupy3D class and basic tests for debugging
Sep 17, 2024
8525abd
enh: making the code 3d-proof
Sep 18, 2024
839214e
test: compare 2D and 3D mean value consistency
Nov 7, 2024
7d1ca55
enh: allow fourier base to subtract mean from 3D data arrays
Nov 7, 2024
4f01683
enh: create utility functions for padding and subt_mean
Nov 7, 2024
63b0c4d
tests: add tests for new util funcs and comparing 2D and 3D Fourier p…
Nov 7, 2024
5054745
tests: remove use of matplotlib from tests
Nov 7, 2024
5319043
docs: create speed comparison example for Cupy 3d
Nov 7, 2024
35975f3
test: reorg the cupy tests for clarity
Nov 8, 2024
23d367f
ci: ignore cupy tests for now during the cicd pipeline
Nov 8, 2024
b812c07
docs: update figure labels; ci: correct github actions syntax
Nov 8, 2024
2361769
fix: correct define FFTFilters upon import
Nov 8, 2024
32cf2ce
test: check ci tests to see if Pyfftw is the issue
Nov 8, 2024
59f67ec
docs: lint examples
Nov 8, 2024
2468569
docs: update README
Nov 8, 2024
7d9f1ae
ref: use negative indexing for np array shape
Nov 8, 2024
d2e2f61
enh: add use of 3D array for the FFTFilter init, not incl padding
Nov 8, 2024
e427cc5
enh: ensure ifft with padding works with 3D stack
Nov 8, 2024
d7d84f5
enh: ensure all data is converted to 3D image stack
Nov 20, 2024
2da1582
enh: add data format conversion functions
Nov 20, 2024
57947e6
ref: use the data format conversion convenience fnuctions to handle f…
Nov 20, 2024
1ac98ff
test: refactor relevant oah tests to expect new format and shape
Nov 20, 2024
fe33bf0
enh: add rgb warning for user
Nov 20, 2024
568d511
test: ensure the users provided data format is returned
Nov 20, 2024
7d88bb9
enh: add 3d array usage to qsli
Nov 20, 2024
d7457e1
ref: align the 2d qlsi code to 3d
Nov 21, 2024
528a038
fix: match qlsi 2d with new 3d implementation
Dec 5, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
python-version: ['3.10']
python-version: ['3.10', '3.11']
os: [macos-latest, ubuntu-latest, windows-latest]

steps:
Expand Down
7 changes: 3 additions & 4 deletions docs/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
sphinx==4.3.0
sphinxcontrib.bibtex>=2.0
sphinx_rtd_theme==1.0

sphinx
sphinxcontrib.bibtex
sphinx_rtd_theme
17 changes: 15 additions & 2 deletions docs/sec_code_reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -23,21 +23,34 @@ Fourier transform methods
=========================

.. _sec_code_fourier_numpy:

Numpy
-----
.. automodule:: qpretrieve.fourier.ff_numpy
:members:
:inherited-members:

.. _sec_code_fourier_pyfftw:

PyFFTW
------
.. automodule:: qpretrieve.fourier.ff_pyfftw
:members:
:inherited-members:

.. _sec_code_fourier_scipy:
Scipy
------
.. automodule:: qpretrieve.fourier.ff_scipy
:members:
:inherited-members:

.. _sec_code_fourier_cupy:
Cupy
----
.. automodule:: qpretrieve.fourier.ff_cupy
:members:
:inherited-members:


.. _sec_code_ifer:

Interference image analysis
Expand Down
2 changes: 2 additions & 0 deletions docs/sec_examples.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,5 @@ Examples
.. fancy_include:: filter_visualization.py

.. fancy_include:: fourier_scale.py

.. fancy_include:: fft_options.py
Binary file added examples/fft_options.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
73 changes: 73 additions & 0 deletions examples/fft_options.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
"""Fourier Transform interfaces available

This example visualizes the different backends and packages available to the
user for performing Fourier transforms.

- PyFFTW is initially slow, but over many FFTs is very quick.
- CuPy using CUDA can be very fast, but is currently limited because we are
transferring one image at a time to the GPU.

"""
import time
import matplotlib.pylab as plt
import numpy as np
import qpretrieve

# load the experimental data
edata = np.load("./data/hologram_cell.npz")

# get the available fft interfaces
interfaces_available = qpretrieve.fourier.get_available_interfaces()

n_transforms = 100

# one transform
results_1 = {}
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I need to clean this script up a bit to make it simpler

for fft_interface in interfaces_available:
t0 = time.time()
holo = qpretrieve.OffAxisHologram(data=edata["data"],
fft_interface=fft_interface)
holo.run_pipeline(filter_name="disk", filter_size=1 / 2)
bg = qpretrieve.OffAxisHologram(data=edata["bg_data"])
bg.process_like(holo)
t1 = time.time()
results_1[fft_interface.__name__] = t1 - t0
num_interfaces = len(results_1)

# multiple transforms (should see speed increase for PyFFTW)
results = {}
for fft_interface in interfaces_available:
t0 = time.time()
for _ in range(n_transforms):
holo = qpretrieve.OffAxisHologram(data=edata["data"],
fft_interface=fft_interface)
holo.run_pipeline(filter_name="disk", filter_size=1 / 2)
bg = qpretrieve.OffAxisHologram(data=edata["bg_data"])
bg.process_like(holo)
t1 = time.time()
results[fft_interface.__name__] = t1 - t0
num_interfaces = len(results)

fft_interfaces = list(results.keys())
speed_1 = list(results_1.values())
speed = list(results.values())

fig, axes = plt.subplots(1, 2, figsize=(8, 5))
ax1, ax2 = axes
labels = [fftstr[9:] for fftstr in fft_interfaces]

ax1.bar(range(num_interfaces), height=speed_1, color='lightseagreen')
ax1.set_xticks(range(num_interfaces), labels=labels,
rotation=45)
ax1.set_ylabel("Speed (s)")
ax1.set_title("1 Transform")

ax2.bar(range(num_interfaces), height=speed, color='lightseagreen')
ax2.set_xticks(range(num_interfaces), labels=labels,
rotation=45)
ax2.set_ylabel("Speed (s)")
ax2.set_title(f"{n_transforms} Transforms")

plt.suptitle("Speed of FFT Interfaces")
plt.tight_layout()
plt.show()
1 change: 1 addition & 0 deletions examples/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
matplotlib
8 changes: 5 additions & 3 deletions qpretrieve/filter.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def get_filter_array(filter_name, filter_size, freq_pos, fft_shape):
and must be between 0 and `max(fft_shape)/2`
freq_pos: tuple of floats
The position of the filter in frequency coordinates as
returned by :func:`nunpy.fft.fftfreq`.
returned by :func:`numpy.fft.fftfreq`.
fft_shape: tuple of int
The shape of the Fourier transformed image for which the
filter will be applied. The shape must be squared (two
Expand Down Expand Up @@ -104,8 +104,10 @@ def get_filter_array(filter_name, filter_size, freq_pos, fft_shape):
# TODO: avoid the np.roll, instead use the indices directly
alpha = 0.1
rsize = int(min(fx.size, fy.size) * filter_size) * 2
tukey_window_x = signal.tukey(rsize, alpha=alpha).reshape(-1, 1)
tukey_window_y = signal.tukey(rsize, alpha=alpha).reshape(1, -1)
tukey_window_x = signal.windows.tukey(
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

scipy's new versions imports tukey from signal.window, not signal

rsize, alpha=alpha).reshape(-1, 1)
tukey_window_y = signal.windows.tukey(
rsize, alpha=alpha).reshape(1, -1)
tukey = tukey_window_x * tukey_window_y
base = np.zeros(fft_shape)
s1 = (np.array(fft_shape) - rsize) // 2
Expand Down
21 changes: 21 additions & 0 deletions qpretrieve/fourier/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,36 @@
import warnings

from .ff_numpy import FFTFilterNumpy
from .ff_scipy import FFTFilterScipy

try:
from .ff_pyfftw import FFTFilterPyFFTW
except ImportError:
FFTFilterPyFFTW = None

try:
from .ff_cupy import FFTFilterCupy
except ImportError:
FFTFilterCupy = None

PREFERRED_INTERFACE = None


def get_available_interfaces():
"""Return a list of available FFT algorithms"""
interfaces = [
FFTFilterPyFFTW,
FFTFilterNumpy,
FFTFilterScipy,
FFTFilterCupy,
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't necessarily the "perferred order" we want, but I'd like to keep it as is due to old default pipelines.

]
interfaces_available = []
for interface in interfaces:
if interface is not None and interface.is_available:
interfaces_available.append(interface)
return interfaces_available


def get_best_interface():
"""Return the fastest refocusing interface available

Expand Down
7 changes: 5 additions & 2 deletions qpretrieve/fourier/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,11 @@ def __init__(self, data, subtract_mean=True, padding=2, copy=True):
else:
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here we check and allow complex data. But in the docstring above (originally), we specify real-valued inputs. In what situation would we take complex data?

# convert integer-arrays to floating point arrays
dtype = float
if not copy:
# numpy v2.x behaviour requires asarray with copy=False
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

numpy has made copying a bit awkward. When an array can't be copied (with either np.array or np.asarray), and the user has copy=False, then it throws an error!

copy = None
data_ed = np.array(data, dtype=dtype, copy=copy)
#: original data (with subtracted mean)
#: original data (with subtracted mean)
self.origin = data_ed
#: whether padding is enabled
self.padding = padding
Expand Down Expand Up @@ -175,7 +178,7 @@ def filter(self, filter_name: str, filter_size: float,
and must be between 0 and `max(fft_shape)/2`
freq_pos: tuple of floats
The position of the filter in frequency coordinates as
returned by :func:`nunpy.fft.fftfreq`.
returned by :func:`numpy.fft.fftfreq`.
scale_to_filter: bool or float
Crop the image in Fourier space after applying the filter,
effectively removing surplus (zero-padding) data and
Expand Down
40 changes: 40 additions & 0 deletions qpretrieve/fourier/ff_cupy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import scipy as sp
import cupy as cp
import cupyx.scipy.fft as cufft

from .base import FFTFilter


class FFTFilterCupy(FFTFilter):
"""Wraps the cupy Fourier transform and uses it via the scipy backend
"""
is_available = True
# sp.fft.set_backend(cufft)

def _init_fft(self, data):
"""Perform initial Fourier transform of the input data

Parameters
----------
data: 2d real-valued np.ndarray
Input field to be refocused

Returns
-------
fft_fdata: 2d complex-valued ndarray
Fourier transform `data`
"""
data_gpu = cp.asarray(data)
# likely an inefficiency here, could use `set_global_backend`
with sp.fft.set_backend(cufft):
fft_gpu = sp.fft.fft2(data_gpu)
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not ideal, this is something to work on.

fft_cpu = fft_gpu.get()
return fft_cpu

def _ifft(self, data):
"""Perform inverse Fourier transform"""
data_gpu = cp.asarray(data)
with sp.fft.set_backend(cufft):
ifft_gpu = sp.fft.ifft2(data_gpu)
ifft_cpu = ifft_gpu.get()
return ifft_cpu
30 changes: 30 additions & 0 deletions qpretrieve/fourier/ff_scipy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import scipy as sp


from .base import FFTFilter


class FFTFilterScipy(FFTFilter):
"""Wraps the scipy Fourier transform
"""
# always available, because scipy is a dependency
is_available = True

def _init_fft(self, data):
"""Perform initial Fourier transform of the input data

Parameters
----------
data: 2d real-valued np.ndarray
Input field to be refocused

Returns
-------
fft_fdata: 2d complex-valued ndarray
Fourier transform `data`
"""
return sp.fft.fft2(data)

def _ifft(self, data):
"""Perform inverse Fourier transform"""
return sp.fft.ifft2(data)
36 changes: 28 additions & 8 deletions qpretrieve/interfere/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

import numpy as np

from ..fourier import get_best_interface
from ..fourier import get_best_interface, get_available_interfaces
from ..fourier.base import FFTFilter


class BaseInterferogram(ABC):
Expand All @@ -15,11 +16,19 @@ class BaseInterferogram(ABC):
"invert_phase": False,
}

def __init__(self, data, subtract_mean=True, padding=2, copy=True,
def __init__(self, data, fft_interface: FFTFilter = None,
subtract_mean=True, padding=2, copy=True,
**pipeline_kws):
"""
Parameters
----------
fft_interface: FFTFilter
A Fourier transform interface.
See :func:`qpretrieve.fourier.get_available_interfaces`
to get a list of implemented interfaces.
Default is None, which will use
:func:`qpretrieve.fourier.get_best_interface`. This is in line
with old behaviour.
subtract_mean: bool
If True, remove the mean of the hologram before performing
the Fourier transform. This setting is recommended as it
Expand All @@ -38,15 +47,26 @@ def __init__(self, data, subtract_mean=True, padding=2, copy=True,
Any additional keyword arguments for :func:`run_pipeline`
as defined in :const:`default_pipeline_kws`.
"""
ff_iface = get_best_interface()
if fft_interface == 'auto' or fft_interface is None:
self.ff_iface = get_best_interface()
else:
if fft_interface in get_available_interfaces():
self.ff_iface = fft_interface
else:
raise ValueError(
f"User-chosen FFT Interface '{fft_interface}' is not "
f"available. The available interfaces are: "
f"{get_available_interfaces()}.\n"
f"You can use `fft_interface='auto'` to get the best "
f"available interface.")
if len(data.shape) == 3:
# take the first slice (we have alpha or RGB information)
data = data[:, :, 0]
#: qpretrieve Fourier transform interface class
self.fft = ff_iface(data=data,
subtract_mean=subtract_mean,
padding=padding,
copy=copy)
self.fft = self.ff_iface(data=data,
subtract_mean=subtract_mean,
padding=padding,
copy=copy)
#: originally computed Fourier transform
self.fft_origin = self.fft.fft_origin
#: filtered Fourier data from last run of `run_pipeline`
Expand Down Expand Up @@ -94,7 +114,7 @@ def compute_filter_size(self, filter_size, filter_size_interpretation,
raise ValueError("For sideband distance interpretation, "
"`filter_size` must be between 0 and 1; "
f"got '{filter_size}'!")
fsize = np.sqrt(np.sum(np.array(sideband_freq)**2)) * filter_size
fsize = np.sqrt(np.sum(np.array(sideband_freq) ** 2)) * filter_size
elif filter_size_interpretation == "frequency index":
# filter size given in Fourier index (number of Fourier pixels)
# The user probably does not know that we are padding in
Expand Down
14 changes: 9 additions & 5 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
from setuptools import setup, find_packages
import sys


author = u"Paul Müller"
authors = [author]
description = 'library for phase retrieval from holograms'
Expand All @@ -27,8 +26,13 @@
"numpy>=1.9.0",
"scikit-image>=0.11.0",
"scipy>=0.18.0",
],
extras_require={"FFTW": "pyfftw>=0.12.0"},
],
extras_require={
"FFTW": "pyfftw>=0.12.0",
# manually install 'cupy-cuda11x' if you have older CUDA.
# See https://cupy.dev/
"CUPY": "cupy-cuda12x",
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will have to check this version.

},
python_requires='>=3.10, <4',
keywords=["digital holographic microscopy",
"optics",
Expand All @@ -41,6 +45,6 @@
'Operating System :: OS Independent',
'Programming Language :: Python :: 3',
'Intended Audience :: Science/Research'
],
],
platforms=['ALL'],
)
)
Loading
Loading