Skip to content

Commit 30a0b98

Browse files
committed
Additional padding options for paganin_filter
1 parent 4124ddb commit 30a0b98

4 files changed

Lines changed: 153 additions & 25 deletions

File tree

httomolibgpu/cupywrapper.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
try:
33
import cupy as cp
44
import nvtx
5+
from cupyx.scipy.fft import next_fast_len
56

67
try:
78
cp.cuda.Device(0).compute_capability
@@ -15,5 +16,6 @@
1516
)
1617
from unittest.mock import Mock
1718
import numpy as cp
19+
from scipy.fft import next_fast_len
1820

1921
nvtx = Mock()

httomolibgpu/prep/phase.py

Lines changed: 62 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626

2727
cp = cupywrapper.cp
2828
cupy_run = cupywrapper.cupy_run
29+
next_fast_len = cupywrapper.next_fast_len
2930

3031
from unittest.mock import Mock
3132

@@ -38,7 +39,7 @@
3839
fftshift = Mock()
3940

4041
from numpy import float32
41-
from typing import Optional, Tuple
42+
from typing import Literal, Optional, Tuple
4243
import math
4344

4445
__all__ = [
@@ -56,6 +57,10 @@ def paganin_filter(
5657
distance: float = 1.0,
5758
energy: float = 53.0,
5859
ratio_delta_beta: float = 250,
60+
calculate_padding_value_method: Literal[
61+
"next_power_of_2", "next_fast_length", "use_pad_x_y"
62+
] = "next_power_of_2",
63+
pad_x_y: Optional[Tuple[int, int]] = None,
5964
calc_peak_gpu_mem: bool = False,
6065
) -> cp.ndarray:
6166
"""
@@ -74,6 +79,10 @@ def paganin_filter(
7479
Beam energy in keV.
7580
ratio_delta_beta : float
7681
The ratio of delta/beta, where delta is the phase shift and real part of the complex material refractive index and beta is the absorption.
82+
calculate_padding_value_method: str
83+
Method to calculate the padded size of the input data. Accepted values are 'next_power_of_2', 'next_fast_length' and 'use_pad_x_y`.
84+
pad_x_y (int, int) | None:
85+
Padding values in pixels horizontally and vertically. Must be None, unless `calculate_padding_value_method` is 'use_pad_x_y'.
7786
calc_peak_gpu_mem: bool
7887
Parameter to support memory estimation in HTTomo. Irrelevant to the method itself and can be ignored by user.
7988
@@ -93,9 +102,9 @@ def paganin_filter(
93102
mem_stack.malloc(np.prod(tomo) * np.float32().itemsize)
94103
dz_orig, dy_orig, dx_orig = tomo.shape if not mem_stack else tomo
95104

96-
# Perform padding to the power of 2 as FFT is O(n*log(n)) complexity
97-
# TODO: adding other options of padding?
98-
padded_tomo, pad_tup = _pad_projections_to_second_power(tomo, mem_stack)
105+
padded_tomo, pad_tup = _pad_projections(
106+
tomo, calculate_padding_value_method, pad_x_y, mem_stack
107+
)
99108

100109
dz, dy, dx = padded_tomo.shape if not mem_stack else padded_tomo
101110

@@ -219,21 +228,51 @@ def _shift_bit_length(x: int) -> int:
219228
return 1 << (x - 1).bit_length()
220229

221230

222-
def _calculate_pad_size(datashape: tuple) -> list:
231+
def _calculate_pad_size(
232+
datashape: tuple,
233+
calculate_padding_value_method: Literal[
234+
"next_power_of_2", "next_fast_length", "use_pad_x_y"
235+
],
236+
pad_x_y: Optional[Tuple[int, int]],
237+
) -> list:
223238
"""Calculating the padding size
224239
225240
Args:
226-
datashape (tuple): the shape of the 3D data
241+
datashape (tuple):
242+
the shape of the 3D data
243+
calculate_padding_value_method: str
244+
Method to calculate the padded size of the input data. Accepted values are 'next_power_of_2', 'next_fast_length' and 'use_pad_x_y`.
245+
pad_x_y (int, int) | None:
246+
Padding values in pixels horizontally and vertically. Must be None, unless `calculate_padding_value_method` is 'use_pad_x_y'.
227247
228248
Returns:
229249
list: the padded dimensions
230250
"""
251+
if pad_x_y is not None and calculate_padding_value_method != "use_pad_x_y":
252+
raise ValueError(
253+
'calculate_padding_value_method must be "use_pad_x_y" when pad_x_y is specified'
254+
)
255+
elif calculate_padding_value_method == "use_pad_x_y" and pad_x_y is None:
256+
raise ValueError(
257+
'pad_x_y must be provided when calculate_padding_value_method is "use_pad_x_y"'
258+
)
259+
260+
if calculate_padding_value_method == "next_power_of_2":
261+
calculate_padded_dim = lambda _, size: _shift_bit_length(size + 1)
262+
elif calculate_padding_value_method == "next_fast_length":
263+
calculate_padded_dim = lambda _, size: next_fast_len(size)
264+
elif calculate_padding_value_method == "use_pad_x_y":
265+
calculate_padded_dim = lambda dim, size: size + 2 * pad_x_y[2 - dim]
266+
else:
267+
raise ValueError(
268+
f'Unexpected calculate_padding_value_method: "{calculate_padding_value_method}"'
269+
)
231270
pad_list = []
232271
for index, element in enumerate(datashape):
233272
if index == 0:
234273
pad_width = (0, 0) # do not pad the slicing dim
235274
else:
236-
diff = _shift_bit_length(element + 1) - element
275+
diff = calculate_padded_dim(index, element) - element
237276
if element % 2 == 0:
238277
pad_width_scalar = diff // 2
239278
pad_width = (pad_width_scalar, pad_width_scalar)
@@ -248,17 +287,27 @@ def _calculate_pad_size(datashape: tuple) -> list:
248287
return pad_list
249288

250289

251-
def _pad_projections_to_second_power(
252-
tomo: cp.ndarray, mem_stack: Optional[_DeviceMemStack]
290+
def _pad_projections(
291+
tomo: cp.ndarray,
292+
calculate_padding_value_method: Literal[
293+
"next_power_of_2", "next_fast_length", "use_pad_x_y"
294+
],
295+
pad_x_y: Optional[Tuple[int, int]],
296+
mem_stack: Optional[_DeviceMemStack],
253297
) -> Tuple[cp.ndarray, Tuple[int, int]]:
254298
"""
255-
Performs padding of each projection to the next power of 2.
299+
Performs padding of each projection to a size optimal for FFT.
256300
If the shape is not even we also care of that before padding.
257301
258302
Parameters
259303
----------
260304
tomo : cp.ndarray
261305
3d projection data
306+
calculate_padding_value_method: str
307+
Method to calculate the padded size of the input data. Accepted values are 'next_power_of_2', 'next_fast_length' and 'use_pad_x_y`.
308+
pad_x_y (int, int) | None:
309+
Padding values in pixels horizontally and vertically. Must be None, unless `calculate_padding_value_method` is 'use_pad_x_y'.
310+
262311
263312
Returns
264313
-------
@@ -268,7 +317,9 @@ def _pad_projections_to_second_power(
268317
"""
269318
full_shape_tomo = cp.shape(tomo) if not mem_stack else tomo
270319

271-
pad_list = _calculate_pad_size(full_shape_tomo)
320+
pad_list = _calculate_pad_size(
321+
full_shape_tomo, calculate_padding_value_method, pad_x_y
322+
)
272323

273324
if mem_stack:
274325
padded_tomo = [

tests/test_prep/test_phase.py

Lines changed: 74 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,34 @@ def test_paganin_filter_dist3(data):
5151
assert_allclose(np.sum(filtered_data), -24870786.0, rtol=1e-6)
5252

5353

54+
@pytest.mark.parametrize(
55+
"test_case",
56+
[
57+
("next_power_of_2", None, -6.725061, -6.367116),
58+
("next_fast_length", None, -6.677313, -6.096187),
59+
("use_pad_x_y", (0, 0), -6.677313, -6.096187),
60+
("use_pad_x_y", (80, 80), -6.73193, -6.405338),
61+
("use_pad_x_y", (45, 75), -6.726483, -6.37466),
62+
],
63+
)
64+
def test_paganin_filter_padding_options(data, test_case):
65+
# --- testing the Paganin filter on tomo_standard ---#
66+
padding_method, pad_x_y, test_mean, test_max = test_case
67+
filtered_data = paganin_filter(
68+
data,
69+
calculate_padding_value_method=padding_method,
70+
pad_x_y=pad_x_y,
71+
).get()
72+
73+
assert filtered_data.ndim == 3
74+
assert_allclose(np.mean(filtered_data), test_mean, rtol=eps)
75+
assert_allclose(np.max(filtered_data), test_max, rtol=eps)
76+
77+
#: make sure the output is float32
78+
assert filtered_data.dtype == np.float32
79+
assert filtered_data.flags.c_contiguous
80+
81+
5482
@pytest.mark.perf
5583
def test_paganin_filter_performance(ensure_clean_memory):
5684
# Note: low/high and size values taken from sample2_medium.yaml real run
@@ -87,41 +115,77 @@ def test_paganin_filter_performance(ensure_clean_memory):
87115

88116
@pytest.mark.parametrize("slices", [3, 7, 32, 61, 109, 120, 150])
89117
@pytest.mark.parametrize("dim_x", [128, 140])
90-
def test_paganin_filter_calc_mem(slices, dim_x, ensure_clean_memory):
118+
@pytest.mark.parametrize(
119+
"padding",
120+
[("next_power_of_2", None), ("next_fast_length", None), ("use_pad_x_y", (45, 45))],
121+
)
122+
def test_paganin_filter_calc_mem(slices, dim_x, padding, ensure_clean_memory):
91123
dim_y = 159
124+
padding_method, pad_x_y = padding
125+
92126
data = cp.random.random_sample((slices, dim_x, dim_y), dtype=np.float32)
93127
hook = MaxMemoryHook()
94128
with hook:
95-
paganin_filter(cp.copy(data))
129+
paganin_filter(
130+
cp.copy(data),
131+
calculate_padding_value_method=padding_method,
132+
pad_x_y=pad_x_y,
133+
)
96134
actual_mem_peak = hook.max_mem
97135

98136
try:
99-
estimated_mem_peak = paganin_filter(data.shape, calc_peak_gpu_mem=True)
137+
estimated_mem_peak = paganin_filter(
138+
data.shape,
139+
calculate_padding_value_method=padding_method,
140+
pad_x_y=pad_x_y,
141+
calc_peak_gpu_mem=True,
142+
)
100143
except cp.cuda.memory.OutOfMemoryError:
101144
pytest.skip("Not enough GPU memory to estimate memory peak")
102145

103-
assert actual_mem_peak * 0.99 <= estimated_mem_peak
104-
assert estimated_mem_peak <= actual_mem_peak * 1.01
146+
assert actual_mem_peak == estimated_mem_peak
105147

106148

107149
@pytest.mark.parametrize("slices", [38, 177, 268, 320, 490, 607, 803, 859, 902, 951])
108150
@pytest.mark.parametrize("dims", [(900, 1280), (1801, 1540), (1801, 2560)])
109-
def test_paganin_filter_calc_mem_big(slices, dims, ensure_clean_memory):
151+
@pytest.mark.parametrize(
152+
"padding",
153+
[
154+
("next_power_of_2", None),
155+
("next_fast_length", None),
156+
("use_pad_x_y", (145, 122)),
157+
],
158+
)
159+
def test_paganin_filter_calc_mem_big(slices, dims, padding, ensure_clean_memory):
110160
dim_y, dim_x = dims
111161
data_shape = (slices, dim_x, dim_y)
162+
padding_method, pad_x_y = padding
112163
try:
113-
estimated_mem_peak = paganin_filter(data_shape, calc_peak_gpu_mem=True)
164+
estimated_mem_peak = paganin_filter(
165+
data_shape,
166+
calculate_padding_value_method=padding_method,
167+
pad_x_y=pad_x_y,
168+
calc_peak_gpu_mem=True,
169+
)
114170
except cp.cuda.memory.OutOfMemoryError:
115171
pytest.skip("Not enough GPU memory to estimate memory peak")
172+
except cp.cuda.cufft.CuFFTError as cufft_error:
173+
if cufft_error.result == 8: # CUFFT_INVALID_SIZE
174+
pytest.skip("Not usable FFT size")
175+
else:
176+
raise
116177
av_mem = cp.cuda.Device().mem_info[0]
117178
if av_mem < estimated_mem_peak:
118179
pytest.skip("Not enough GPU memory to run this test")
119180

120181
hook = MaxMemoryHook()
121182
with hook:
122183
data = cp.random.random_sample(data_shape, dtype=np.float32)
123-
paganin_filter(data)
184+
paganin_filter(
185+
data,
186+
calculate_padding_value_method=padding_method,
187+
pad_x_y=pad_x_y,
188+
)
124189
actual_mem_peak = hook.max_mem
125190

126-
assert actual_mem_peak * 0.99 <= estimated_mem_peak
127-
assert estimated_mem_peak <= actual_mem_peak * 1.01
191+
assert actual_mem_peak == estimated_mem_peak

zenodo-tests/test_prep/test_phase.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,16 @@
88

99
# ----------------------------------------------------------#
1010
# appplying paganin filter to i12_dataset3
11-
def test_paganin_filter_i12_dataset3(i12_dataset3, ensure_clean_memory):
11+
@pytest.mark.parametrize(
12+
"test_case",
13+
[
14+
("next_power_of_2", None, -6.2188172, -10.92260456),
15+
("next_fast_length", None, -6.867182, -10.92272),
16+
("use_pad_x_y", (80, 80), -6.32930, -10.92270),
17+
],
18+
)
19+
def test_paganin_filter_i12_dataset3(i12_dataset3, test_case, ensure_clean_memory):
20+
padding_method, pad_x_y, test_max, test_min = test_case
1221
inputdata = cp.empty((3, 2050, 2560))
1322
inputdata[0, :, :] = i12_dataset3[0]
1423
inputdata[1, :, :] = i12_dataset3[1]
@@ -17,7 +26,9 @@ def test_paganin_filter_i12_dataset3(i12_dataset3, ensure_clean_memory):
1726

1827
ensure_clean_memory
1928

20-
filtered_paganin = paganin_filter(inputdata)
29+
filtered_paganin = paganin_filter(
30+
inputdata, calculate_padding_value_method=padding_method, pad_x_y=pad_x_y
31+
)
2132

22-
assert pytest.approx(np.max(cp.asnumpy(filtered_paganin)), rel=1e-3) == -6.2188172
23-
assert pytest.approx(np.min(cp.asnumpy(filtered_paganin)), rel=1e-3) == -10.92260456
33+
assert pytest.approx(np.max(cp.asnumpy(filtered_paganin)), rel=1e-3) == test_max
34+
assert pytest.approx(np.min(cp.asnumpy(filtered_paganin)), rel=1e-3) == test_min

0 commit comments

Comments
 (0)