prtools 1.4.0__tar.gz → 1.4.2__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.
- {prtools-1.4.0/prtools.egg-info → prtools-1.4.2}/PKG-INFO +1 -1
- {prtools-1.4.0 → prtools-1.4.2}/prtools/__init__.py +1 -1
- {prtools-1.4.0 → prtools-1.4.2}/prtools/array.py +9 -5
- {prtools-1.4.0 → prtools-1.4.2}/prtools/backend/_jax.py +7 -0
- {prtools-1.4.0 → prtools-1.4.2}/prtools/convolve.py +74 -83
- {prtools-1.4.0 → prtools-1.4.2}/prtools/fourier.py +100 -47
- {prtools-1.4.0 → prtools-1.4.2}/prtools/jax.py +54 -16
- {prtools-1.4.0 → prtools-1.4.2/prtools.egg-info}/PKG-INFO +1 -1
- {prtools-1.4.0 → prtools-1.4.2}/prtools.egg-info/SOURCES.txt +3 -1
- {prtools-1.4.0 → prtools-1.4.2}/tests/test_array.py +14 -0
- prtools-1.4.2/tests/test_convolve.py +75 -0
- {prtools-1.4.0 → prtools-1.4.2}/tests/test_fourier.py +20 -30
- prtools-1.4.2/tests/test_jax.py +18 -0
- {prtools-1.4.0 → prtools-1.4.2}/LICENSE +0 -0
- {prtools-1.4.0 → prtools-1.4.2}/NOTICE +0 -0
- {prtools-1.4.0 → prtools-1.4.2}/README.rst +0 -0
- {prtools-1.4.0 → prtools-1.4.2}/prtools/backend/__init__.py +0 -0
- {prtools-1.4.0 → prtools-1.4.2}/prtools/backend/_base.py +0 -0
- {prtools-1.4.0 → prtools-1.4.2}/prtools/backend/_numpy.py +0 -0
- {prtools-1.4.0 → prtools-1.4.2}/prtools/cost.py +0 -0
- {prtools-1.4.0 → prtools-1.4.2}/prtools/misc.py +0 -0
- {prtools-1.4.0 → prtools-1.4.2}/prtools/segmented.py +0 -0
- {prtools-1.4.0 → prtools-1.4.2}/prtools/shape.py +0 -0
- {prtools-1.4.0 → prtools-1.4.2}/prtools/sparse.py +0 -0
- {prtools-1.4.0 → prtools-1.4.2}/prtools/stats.py +0 -0
- {prtools-1.4.0 → prtools-1.4.2}/prtools/zernike.py +0 -0
- {prtools-1.4.0 → prtools-1.4.2}/prtools.egg-info/dependency_links.txt +0 -0
- {prtools-1.4.0 → prtools-1.4.2}/prtools.egg-info/requires.txt +0 -0
- {prtools-1.4.0 → prtools-1.4.2}/prtools.egg-info/top_level.txt +0 -0
- {prtools-1.4.0 → prtools-1.4.2}/pyproject.toml +0 -0
- {prtools-1.4.0 → prtools-1.4.2}/setup.cfg +0 -0
|
@@ -3,6 +3,7 @@ import warnings
|
|
|
3
3
|
from prtools import __backend__
|
|
4
4
|
from prtools.backend import numpy as np
|
|
5
5
|
from prtools.backend import scipy
|
|
6
|
+
import prtools.jax
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
def centroid(a, where=None, kind='absolute', indexing='ij'):
|
|
@@ -300,11 +301,14 @@ def rebin(img, factor):
|
|
|
300
301
|
raise ValueError('rebin is not defined for complex data')
|
|
301
302
|
|
|
302
303
|
if img.ndim == 3:
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
304
|
+
if __backend__ == 'jax':
|
|
305
|
+
return prtools.jax._ndrebin(img, factor)
|
|
306
|
+
else:
|
|
307
|
+
rebinned_shape = (img.shape[0], img.shape[1]//factor, img.shape[2]//factor)
|
|
308
|
+
img_rebinned = np.zeros(rebinned_shape, dtype=img.dtype)
|
|
309
|
+
for i in range(img.shape[0]):
|
|
310
|
+
img_rebinned[i] = img[i].reshape(rebinned_shape[1], factor,
|
|
311
|
+
rebinned_shape[2], factor).sum(-1).sum(1)
|
|
308
312
|
else:
|
|
309
313
|
img_rebinned = img.reshape(img.shape[0]//factor, factor, img.shape[1]//factor,
|
|
310
314
|
factor).sum(-1).sum(1)
|
|
@@ -6,6 +6,7 @@ from ._base import BackendLibrary
|
|
|
6
6
|
class Numpy(BackendLibrary):
|
|
7
7
|
def __init__(self):
|
|
8
8
|
super().__init__(importlib.import_module('jax.numpy'))
|
|
9
|
+
self.jax = importlib.import_module('jax')
|
|
9
10
|
|
|
10
11
|
def broadcast_to(self, array, shape):
|
|
11
12
|
# jax numpy.broadcast_to expects an array input
|
|
@@ -43,6 +44,12 @@ class Numpy(BackendLibrary):
|
|
|
43
44
|
a = self.module.asarray(a)
|
|
44
45
|
return self.module.sum(a, *args, **kwargs)
|
|
45
46
|
|
|
47
|
+
#def take(self, a, indices, *args, **kwargs):
|
|
48
|
+
# # jax numpy.take expects an array input for a and indices
|
|
49
|
+
# a = self.module.asarray(a)
|
|
50
|
+
# indices = self.module.asarray(indices)
|
|
51
|
+
# return self.module.take(a, indices, *args, **kwargs)
|
|
52
|
+
|
|
46
53
|
|
|
47
54
|
class Scipy(BackendLibrary):
|
|
48
55
|
def __init__(self):
|
|
@@ -2,7 +2,7 @@ from prtools.backend import numpy as np
|
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
def fftconv(array, kernel, normalize_kernel=True, fft_array=True,
|
|
5
|
-
fft_kernel=
|
|
5
|
+
fft_kernel=False, fftshift_kernel=False):
|
|
6
6
|
r"""Convolve an array with a kernel using the FFT.
|
|
7
7
|
|
|
8
8
|
The colvolution is computed as
|
|
@@ -14,20 +14,24 @@ def fftconv(array, kernel, normalize_kernel=True, fft_array=True,
|
|
|
14
14
|
Parameters
|
|
15
15
|
----------
|
|
16
16
|
array : array_like
|
|
17
|
-
Array to be convolved with
|
|
17
|
+
Array to be convolved with ``kernel``.
|
|
18
18
|
kernel : array_like
|
|
19
|
-
Convolution kernel. Should have the same shape as
|
|
19
|
+
Convolution kernel. Should have the same shape as ``array``.
|
|
20
20
|
normalize_kernel : bool, optional
|
|
21
|
-
If True (default), kernel is normalized so that
|
|
22
|
-
kernel / np.sum(kernel)`.
|
|
21
|
+
If True (default), kernel is normalized so that ``sum(kernel) == 1``.
|
|
23
22
|
fft_array : bool, optional
|
|
24
23
|
If True (default), the array is assumed to be provided in the spatial
|
|
25
|
-
domain
|
|
26
|
-
domain.
|
|
24
|
+
domain and its FFT will be computed by this function. If False, the
|
|
25
|
+
array is assumed to be provided in the frequency domain.
|
|
27
26
|
fft_kernel : bool, optional
|
|
28
|
-
If True
|
|
29
|
-
domain
|
|
27
|
+
If True, the kernel is assumed to be provided in the spatial
|
|
28
|
+
domain and its FFT will be computed by this function. If False
|
|
29
|
+
(default), the kernel is assumed to be provided in the frequency
|
|
30
30
|
domain.
|
|
31
|
+
fftshift_kernel : bool, optional
|
|
32
|
+
If True, the supplied kernel will be shifted so that its
|
|
33
|
+
zero-frequency (DC) term is placed at the upper left ``(0,0)`` corner
|
|
34
|
+
of the array. Default is False.
|
|
31
35
|
|
|
32
36
|
Returns
|
|
33
37
|
-------
|
|
@@ -40,7 +44,8 @@ def fftconv(array, kernel, normalize_kernel=True, fft_array=True,
|
|
|
40
44
|
"""
|
|
41
45
|
|
|
42
46
|
a = np.fft.fft2(array) if fft_array else array
|
|
43
|
-
k = np.fft.
|
|
47
|
+
k = np.fft.fftshift(kernel) if fftshift_kernel else kernel
|
|
48
|
+
k = np.fft.fft2(k) if fft_kernel else k
|
|
44
49
|
|
|
45
50
|
if normalize_kernel:
|
|
46
51
|
k /= np.sum(k)
|
|
@@ -56,8 +61,9 @@ def gauss_blur(img, sigma, oversample=1):
|
|
|
56
61
|
img : array_like
|
|
57
62
|
Array to be blurred
|
|
58
63
|
sigma : float or (2,) array_like
|
|
59
|
-
Standard deviation
|
|
60
|
-
non-symmetric Gaussian interpreted as `(sigma_row,
|
|
64
|
+
Standard deviation for the Gaussian kernel. Providing two values
|
|
65
|
+
allows for non-symmetric Gaussian interpreted as `(sigma_row,
|
|
66
|
+
sigma_col)`
|
|
61
67
|
oversample : float, optional
|
|
62
68
|
Oversampling factor of `img`. Default is 1.
|
|
63
69
|
|
|
@@ -72,7 +78,10 @@ def gauss_blur(img, sigma, oversample=1):
|
|
|
72
78
|
:context: reset
|
|
73
79
|
:scale: 50
|
|
74
80
|
|
|
75
|
-
>>> img =
|
|
81
|
+
>>> img = np.zeros((100,100))
|
|
82
|
+
>>> img[0:50,0:50] = 1
|
|
83
|
+
>>> img[50:100,50:100] = 1
|
|
84
|
+
>>> img = np.tile(img, (3,3))
|
|
76
85
|
>>> img_blurred = prtools.gauss_blur(img, sigma=3)
|
|
77
86
|
>>> fig, ax = plt.subplots(nrows=1, ncols=2, figsize=(5,2))
|
|
78
87
|
>>> ax[0].imshow(img, cmap='gray')
|
|
@@ -82,20 +91,20 @@ def gauss_blur(img, sigma, oversample=1):
|
|
|
82
91
|
"""
|
|
83
92
|
img = np.asarray(img)
|
|
84
93
|
kernel = gauss_kernel(img.shape, sigma, oversample, fftshift=False)
|
|
85
|
-
|
|
86
|
-
|
|
94
|
+
# gauss_kernel always returns a normalized kernel
|
|
95
|
+
return fftconv(img, kernel, normalize_kernel=False, fft_array=True,
|
|
96
|
+
fft_kernel=False, fftshift_kernel=False)
|
|
87
97
|
|
|
88
98
|
|
|
89
99
|
def pixelate(img, oversample=1):
|
|
90
|
-
"""Apply the aperture effects of an idealized square pixel
|
|
91
|
-
sampled image using the FFT.
|
|
100
|
+
"""Apply the aperture effects of an idealized square pixel using the FFT.
|
|
92
101
|
|
|
93
102
|
Parameters
|
|
94
103
|
----------
|
|
95
104
|
img : array_like
|
|
96
105
|
Input image
|
|
97
106
|
oversample : float, optional
|
|
98
|
-
Oversampling factor of
|
|
107
|
+
Oversampling factor of ``img``. Default is 1.
|
|
99
108
|
|
|
100
109
|
Returns
|
|
101
110
|
-------
|
|
@@ -106,36 +115,14 @@ def pixelate(img, oversample=1):
|
|
|
106
115
|
-----
|
|
107
116
|
To avoid the introduction of numerical artifacts, this function should be
|
|
108
117
|
performed on data that is at least 2x oversampled.
|
|
109
|
-
|
|
110
|
-
Examples
|
|
111
|
-
--------
|
|
112
|
-
.. plot::
|
|
113
|
-
:include-source:
|
|
114
|
-
:context: reset
|
|
115
|
-
:scale: 50
|
|
116
|
-
|
|
117
|
-
>>> img = prtools.circle((17,17), 5)
|
|
118
|
-
>>> img_pixelate = prtools.pixelate(img)
|
|
119
|
-
>>> fig, ax = plt.subplots(nrows=1, ncols=2, figsize=(5,2))
|
|
120
|
-
>>> ax[0].imshow(img, cmap='gray')
|
|
121
|
-
>>> ax[0].set_title('Input image')
|
|
122
|
-
>>> ax[1].imshow(img_pixelate, cmap='gray')
|
|
123
|
-
>>> ax[1].set_title('Pixelated image')
|
|
124
|
-
|
|
125
|
-
Note that the pixelation process preserves image power:
|
|
126
|
-
|
|
127
|
-
.. code:: pycon
|
|
128
|
-
|
|
129
|
-
>>> print(np.sum(img), np.sum(img_pixelate))
|
|
130
|
-
64.0, 64.0
|
|
131
118
|
"""
|
|
132
119
|
img = np.asarray(img)
|
|
133
120
|
kernel = pixel_kernel(img.shape, oversample=oversample, fftshift=False)
|
|
134
121
|
return fftconv(img, kernel, normalize_kernel=False, fft_array=True,
|
|
135
|
-
fft_kernel=False)
|
|
122
|
+
fft_kernel=False, fftshift_kernel=False)
|
|
136
123
|
|
|
137
124
|
|
|
138
|
-
def gauss(x1, x2, sigma,
|
|
125
|
+
def gauss(x1, x2, sigma, indexing='ij', normalize=False):
|
|
139
126
|
"""2D Gaussian function
|
|
140
127
|
|
|
141
128
|
Parameters
|
|
@@ -145,10 +132,8 @@ def gauss(x1, x2, sigma, oversample=1, indexing='ij', normalize=False):
|
|
|
145
132
|
sigma : float or (2,) array_like
|
|
146
133
|
Standard deviation of Gaussian. Providing two values allows for
|
|
147
134
|
non-symmetric Gaussian interpreted as `(sigma_row, sigma_col)`
|
|
148
|
-
oversample : float, optional
|
|
149
|
-
Oversampling factor. Defailt is 1.
|
|
150
135
|
indexing : {'ij', 'xy'}, optional
|
|
151
|
-
Matrix ('ij', default) or
|
|
136
|
+
Matrix ('ij', default) or Cartesian ('xy') indexing of output.
|
|
152
137
|
normalize : bool, optional
|
|
153
138
|
If True, the output is normalized such that its sum is equal to 1.
|
|
154
139
|
If False (default), the output has max equal to one.
|
|
@@ -170,7 +155,7 @@ def gauss(x1, x2, sigma, oversample=1, indexing='ij', normalize=False):
|
|
|
170
155
|
>>> plt.imshow(g, cmap='gray')
|
|
171
156
|
"""
|
|
172
157
|
xx1, xx2 = np.meshgrid(x1, x2, indexing=indexing)
|
|
173
|
-
sigma = np.broadcast_to(sigma, (2,))
|
|
158
|
+
sigma = np.broadcast_to(sigma, (2,))
|
|
174
159
|
g = np.exp(-((xx1**2/(2*sigma[0]**2)) + (xx2**2/(2*sigma[1]**2))))
|
|
175
160
|
if normalize:
|
|
176
161
|
g = g / (2*np.pi * np.prod(sigma))
|
|
@@ -178,14 +163,14 @@ def gauss(x1, x2, sigma, oversample=1, indexing='ij', normalize=False):
|
|
|
178
163
|
|
|
179
164
|
|
|
180
165
|
def sinc(x1, x2, indexing='ij'):
|
|
181
|
-
r"""
|
|
166
|
+
r"""2D sinc function
|
|
182
167
|
|
|
183
168
|
Parameters
|
|
184
169
|
----------
|
|
185
170
|
x1, x2 : array_like
|
|
186
171
|
1-D arrays representing the grid coordinates
|
|
187
172
|
indexing : {'ij', 'xy'}, optional
|
|
188
|
-
Matrix ('ij', default) or
|
|
173
|
+
Matrix ('ij', default) or Cartesian ('xy') indexing of output.
|
|
189
174
|
normalize : bool, optional
|
|
190
175
|
If True, the output is normalized such that its sum is equal to 1.
|
|
191
176
|
If False (default), the output has max equal to one.
|
|
@@ -221,10 +206,10 @@ def sinc(x1, x2, indexing='ij'):
|
|
|
221
206
|
return np.sinc(xx1) * np.sinc(xx2)
|
|
222
207
|
|
|
223
208
|
|
|
224
|
-
def gauss_kernel(shape, sigma, oversample=1, fftshift=
|
|
209
|
+
def gauss_kernel(shape, sigma, oversample=1, pixelscale=1.0, fftshift=False):
|
|
225
210
|
"""2D Gaussian filter kernel
|
|
226
211
|
|
|
227
|
-
This function returns the
|
|
212
|
+
This function returns the Fourier transform of a normalized 2D Gaussian
|
|
228
213
|
function.
|
|
229
214
|
|
|
230
215
|
Parameters
|
|
@@ -232,12 +217,16 @@ def gauss_kernel(shape, sigma, oversample=1, fftshift=True):
|
|
|
232
217
|
shape : int or tuple of int
|
|
233
218
|
Shape of the kernel
|
|
234
219
|
sigma : float
|
|
235
|
-
Standard deviation of Gaussian. Providing two values allows for
|
|
236
|
-
non-symmetric Gaussian interpreted as
|
|
220
|
+
Standard deviation of Gaussian. Providing two values allows for a
|
|
221
|
+
non-symmetric Gaussian interpreted as ``(sigma_row, sigma_col)``.
|
|
237
222
|
oversample : float, optional
|
|
238
223
|
Oversampling factor to represent in the kernel. Default is 1.
|
|
224
|
+
pixelscale : scalar or tuple of scalars, optional
|
|
225
|
+
Sample spacing. Providing two values defines non-symmetric sampling
|
|
226
|
+
interpreted as ``(pixelscale_row, pixelscale_col)``. Default is 1.
|
|
239
227
|
fftshift : bool, optional
|
|
240
|
-
If True
|
|
228
|
+
If True, the kernel is FFT-shifted so that the zero-frequency (DC)
|
|
229
|
+
term is placed at the center of the returned array. Default is False.
|
|
241
230
|
|
|
242
231
|
Returns
|
|
243
232
|
-------
|
|
@@ -251,35 +240,44 @@ def gauss_kernel(shape, sigma, oversample=1, fftshift=True):
|
|
|
251
240
|
|
|
252
241
|
Examples
|
|
253
242
|
--------
|
|
243
|
+
Create a Gaussian kernel with
|
|
244
|
+
|
|
254
245
|
.. plot::
|
|
255
246
|
:include-source:
|
|
256
|
-
:context: reset
|
|
257
247
|
:scale: 50
|
|
258
248
|
|
|
259
|
-
>>>
|
|
260
|
-
>>> plt.imshow(
|
|
249
|
+
>>> G = prtools.gauss_kernel((256,256), sigma=1, fftshift=True)
|
|
250
|
+
>>> plt.imshow(G, cmap='gray')
|
|
261
251
|
|
|
262
|
-
|
|
252
|
+
Oversampling is in effect the same as zero-padding:
|
|
253
|
+
|
|
254
|
+
.. plot::
|
|
255
|
+
:include-source:
|
|
256
|
+
:scale: 50
|
|
257
|
+
|
|
258
|
+
>>> G = prtools.gauss_kernel((512,512), sigma=1, oversample=2,
|
|
259
|
+
... fftshift=True)
|
|
260
|
+
>>> plt.imshow(G, cmap='gray')
|
|
261
|
+
|
|
262
|
+
For a kernel with custom sample spacing:
|
|
263
263
|
|
|
264
264
|
.. plot::
|
|
265
265
|
:include-source:
|
|
266
|
-
:context: reset
|
|
267
266
|
:scale: 50
|
|
268
267
|
|
|
269
|
-
>>>
|
|
270
|
-
|
|
271
|
-
>>>
|
|
272
|
-
>>> blur_kernel = np.fft.fftshift(prtools.gauss(r, c, 1/(2*np.pi*sigma)))
|
|
273
|
-
>>> plt.imshow(blur_kernel, cmap='gray')
|
|
268
|
+
>>> G = prtools.gauss_kernel((256,256), sigma=1, pixelscale=0.25,
|
|
269
|
+
... fftshift=True)
|
|
270
|
+
>>> plt.imshow(G, cmap='gray')
|
|
274
271
|
"""
|
|
275
272
|
|
|
276
273
|
shape = np.broadcast_to(shape, (2,))
|
|
274
|
+
pixelscale = np.broadcast_to(pixelscale, (2,))
|
|
277
275
|
sigma = np.broadcast_to(sigma, (2,))
|
|
278
276
|
|
|
279
|
-
x1 = np.fft.fftfreq(shape[0])
|
|
280
|
-
x2 = np.fft.fftfreq(shape[1])
|
|
277
|
+
x1 = np.fft.fftfreq(n=shape[0], d=pixelscale[0])
|
|
278
|
+
x2 = np.fft.fftfreq(n=shape[1], d=pixelscale[1])
|
|
281
279
|
|
|
282
|
-
k = gauss(x1, x2, 1/(2*np.pi*sigma),
|
|
280
|
+
k = gauss(x1, x2, sigma=1/(2*np.pi*sigma*oversample), indexing='ij',
|
|
283
281
|
normalize=False)
|
|
284
282
|
|
|
285
283
|
if fftshift:
|
|
@@ -288,7 +286,7 @@ def gauss_kernel(shape, sigma, oversample=1, fftshift=True):
|
|
|
288
286
|
return k
|
|
289
287
|
|
|
290
288
|
|
|
291
|
-
def pixel_kernel(shape, oversample=1, fftshift=
|
|
289
|
+
def pixel_kernel(shape, oversample=1, pixelscale=1.0, fftshift=False):
|
|
292
290
|
r"""2D pixel MTF filter kernel
|
|
293
291
|
|
|
294
292
|
This function returns a normalized 2D sinc function sized to represent
|
|
@@ -300,8 +298,12 @@ def pixel_kernel(shape, oversample=1, fftshift=True):
|
|
|
300
298
|
Shape of the kernel
|
|
301
299
|
oversample : float, optional
|
|
302
300
|
Oversampling factor to represent in the kernel. Default is 1.
|
|
301
|
+
pixelscale : scalar or tuple of scalars, optional
|
|
302
|
+
Sample spacing. Providing two values defines non-symmetric sampling
|
|
303
|
+
interpreted as ``(pixelscale_row, pixelscale_col)``. Default is 1.
|
|
303
304
|
fftshift : bool, optional
|
|
304
|
-
If True
|
|
305
|
+
If True, the kernel is FFT-shifted so that the zero-frequency (DC)
|
|
306
|
+
term is placed at the center of the returned array. Default is False.
|
|
305
307
|
|
|
306
308
|
Returns
|
|
307
309
|
-------
|
|
@@ -320,25 +322,14 @@ def pixel_kernel(shape, oversample=1, fftshift=True):
|
|
|
320
322
|
:context: reset
|
|
321
323
|
:scale: 50
|
|
322
324
|
|
|
323
|
-
>>> pixel_mtf = prtools.pixel_kernel((256,256), 1)
|
|
324
|
-
>>> plt.imshow(pixel_mtf, cmap='gray', vmin=0)
|
|
325
|
-
|
|
326
|
-
Note this is functionally equivalent to
|
|
327
|
-
|
|
328
|
-
.. plot::
|
|
329
|
-
:include-source:
|
|
330
|
-
:context: reset
|
|
331
|
-
:scale: 50
|
|
332
|
-
|
|
333
|
-
>>> r = np.fft.fftfreq(256)
|
|
334
|
-
>>> c = np.fft.fftfreq(256)
|
|
335
|
-
>>> pixel_mtf = np.fft.fftshift(np.abs(prtools.sinc(r, c)))
|
|
325
|
+
>>> pixel_mtf = prtools.pixel_kernel((256,256), 1, fftshift=True)
|
|
336
326
|
>>> plt.imshow(pixel_mtf, cmap='gray', vmin=0)
|
|
337
327
|
"""
|
|
338
328
|
shape = np.broadcast_to(shape, (2,))
|
|
329
|
+
pixelscale = np.broadcast_to(pixelscale, (2,))
|
|
339
330
|
|
|
340
|
-
x1 = np.fft.fftfreq(shape[0])
|
|
341
|
-
x2 = np.fft.fftfreq(shape[1])
|
|
331
|
+
x1 = np.fft.fftfreq(n=shape[0], d=pixelscale[0]) * oversample
|
|
332
|
+
x2 = np.fft.fftfreq(n=shape[1], d=pixelscale[1]) * oversample
|
|
342
333
|
|
|
343
334
|
k = np.abs(sinc(x1, x2, indexing='ij'))
|
|
344
335
|
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
from prtools import __backend__
|
|
2
2
|
from prtools.backend import numpy as np
|
|
3
|
+
import prtools.jax
|
|
3
4
|
|
|
4
5
|
|
|
5
|
-
def dft2(f, alpha, shape=None, shift=(0, 0), offset=(0, 0),
|
|
6
|
-
out=None):
|
|
6
|
+
def dft2(f, alpha, shape=None, shift=(0, 0), offset=(0, 0), axes=(-2, -1),
|
|
7
|
+
unitary=True, out=None):
|
|
7
8
|
r"""Compute the 2-dimensional discrete Fourier Transform.
|
|
8
9
|
|
|
9
10
|
The DFT is defined in one dimension as
|
|
@@ -31,10 +32,13 @@ def dft2(f, alpha, shape=None, shift=(0, 0), offset=(0, 0), unitary=True,
|
|
|
31
32
|
``F.shape = (shape[0], shape[1])``. Default is ``f.shape``.
|
|
32
33
|
shift : array_like, optional
|
|
33
34
|
Number of pixels in (r,c) to shift the DC pixel in the output plane
|
|
34
|
-
with the origin centrally located in the plane. Default is ``(0,0)``.
|
|
35
|
+
with the origin centrally located in the plane. Default is ``(0, 0)``.
|
|
35
36
|
offset : array_like, optional
|
|
36
37
|
Number of pixels in (r,c) that the input plane is shifted relative to
|
|
37
|
-
the origin. Default is ``(0,0)``.
|
|
38
|
+
the origin. Default is ``(0, 0)``.
|
|
39
|
+
axes : (2,) array_like of ints, optional
|
|
40
|
+
Axes over which to compute the DFT. If not given, the last two axes are
|
|
41
|
+
used.
|
|
38
42
|
unitary : bool, optional
|
|
39
43
|
Normalization flag. If ``True``, a normalization is performed on the
|
|
40
44
|
output such that the DFT operation is unitary and energy is conserved
|
|
@@ -42,8 +46,8 @@ def dft2(f, alpha, shape=None, shift=(0, 0), offset=(0, 0), unitary=True,
|
|
|
42
46
|
way, the energy in in a limited-area DFT is a fraction of the total
|
|
43
47
|
energy corresponding to the limited area. Default is ``True``.
|
|
44
48
|
out : ndarray or None
|
|
45
|
-
A location into which the result is stored. If provided, out.shape ==
|
|
46
|
-
shape and out.dtype == np.complex
|
|
49
|
+
A location into which the result is stored. If provided, ``out.shape ==
|
|
50
|
+
shape`` and ``out.dtype == np.complex``. If not provided or None, a
|
|
47
51
|
freshly-allocated array is returned.
|
|
48
52
|
|
|
49
53
|
Returns
|
|
@@ -76,36 +80,36 @@ def dft2(f, alpha, shape=None, shift=(0, 0), offset=(0, 0), unitary=True,
|
|
|
76
80
|
[1] Soummer, et. al. Fast computation of Lyot-style coronagraph
|
|
77
81
|
propagation (2007)
|
|
78
82
|
"""
|
|
79
|
-
return _dftcore(f, alpha, shape, shift, offset, unitary, out, forward=True)
|
|
80
83
|
|
|
84
|
+
return _dftcore(f, alpha, shape, shift, offset, axes, unitary,
|
|
85
|
+
forward=True, out=out)
|
|
81
86
|
|
|
82
|
-
def _dftcore(f, alpha, shape, shift, offset, unitary, out, forward):
|
|
83
87
|
|
|
84
|
-
|
|
88
|
+
def _dftcore(f, alpha, shape, shift, offset, axes, unitary, forward, out):
|
|
85
89
|
|
|
86
|
-
if
|
|
87
|
-
|
|
90
|
+
if __backend__ == 'numpy':
|
|
91
|
+
_mtp = prtools.fourier._multi_dot_three
|
|
92
|
+
if out is not None:
|
|
88
93
|
if not np.can_cast(complex, out.dtype):
|
|
89
94
|
raise TypeError(f"Cannot cast complex output to dtype('{out.dtype}')")
|
|
90
|
-
elif __backend__ == 'jax':
|
|
91
|
-
raise ValueError('JAX backend does not support the out parameter')
|
|
92
95
|
|
|
93
|
-
|
|
96
|
+
elif __backend__ == 'jax':
|
|
97
|
+
_mtp = prtools.jax._multi_dot_three
|
|
98
|
+
if out is not None:
|
|
99
|
+
raise ValueError('JAX backend does not support the out parameter')
|
|
94
100
|
|
|
95
101
|
f = np.asarray(f)
|
|
96
|
-
m, n = f.shape
|
|
97
102
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
M, N = shape
|
|
103
|
+
(M, N), axes = _cook_nd_args(f, shape, axes)
|
|
104
|
+
m, n = (f.shape[axes[0]], f.shape[axes[1]])
|
|
101
105
|
|
|
106
|
+
alpha_row, alpha_col = np.broadcast_to(alpha, (2,))
|
|
102
107
|
shift_row, shift_col = np.broadcast_to(shift, (2,))
|
|
103
108
|
offset_row, offset_col = np.broadcast_to(offset, (2,))
|
|
104
109
|
|
|
105
|
-
E1, E2 = _dft2_matrices(m, n, M, N, alpha_row, alpha_col, shift_row,
|
|
106
|
-
offset_row, offset_col, forward)
|
|
107
|
-
|
|
108
|
-
F = np.dot(E1.dot(f), E2, out=out)
|
|
110
|
+
E1, E2 = _dft2_matrices(m, n, M, N, alpha_row, alpha_col, shift_row,
|
|
111
|
+
shift_col, offset_row, offset_col, forward)
|
|
112
|
+
F = _mtp(E1, f, E2, axes, out)
|
|
109
113
|
|
|
110
114
|
if unitary:
|
|
111
115
|
F = np.multiply(F, np.sqrt(np.abs(alpha_row * alpha_col)), out=F)
|
|
@@ -117,28 +121,21 @@ def _dftcore(f, alpha, shape, shift, offset, unitary, out, forward):
|
|
|
117
121
|
return F
|
|
118
122
|
|
|
119
123
|
|
|
120
|
-
def _dft2_matrices(m, n, M, N, alphar, alphac, shiftr, shiftc, offsetr,
|
|
124
|
+
def _dft2_matrices(m, n, M, N, alphar, alphac, shiftr, shiftc, offsetr,
|
|
125
|
+
offsetc, forward):
|
|
121
126
|
if forward:
|
|
122
|
-
|
|
127
|
+
c = -1j
|
|
123
128
|
else:
|
|
124
|
-
|
|
129
|
+
c = 1j
|
|
125
130
|
R, S, U, V = _dft2_coords(m, n, M, N)
|
|
126
|
-
E1 = np.exp(
|
|
127
|
-
E2 = np.exp(
|
|
128
|
-
return E1, E2
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
def _idft2_matrices(m, n, M, N, alphar, alphac, shiftr, shiftc, offsetr, offsetc):
|
|
132
|
-
R, S, U, V = _dft2_coords(m, n, M, N)
|
|
133
|
-
E1 = np.exp(2.0 * 1j * np.pi * alphar * np.outer(R+offsetr, U-shiftr)).T
|
|
134
|
-
E2 = np.exp(2.0 * 1j * np.pi * alphac * np.outer(S+offsetc, V-shiftc))
|
|
131
|
+
E1 = np.exp(2.0 * c * np.pi * alphar * np.outer(R+offsetr, U-shiftr)).T
|
|
132
|
+
E2 = np.exp(2.0 * c * np.pi * alphac * np.outer(S+offsetc, V-shiftc))
|
|
135
133
|
return E1, E2
|
|
136
134
|
|
|
137
135
|
|
|
138
136
|
def _dft2_coords(m, n, M, N):
|
|
139
137
|
# R and S are (r,c) coordinates in the (m x n) input plane f
|
|
140
138
|
# V and U are (r,c) coordinates in the (M x N) output plane F
|
|
141
|
-
|
|
142
139
|
R = np.arange(m) - np.floor(m/2.0)
|
|
143
140
|
S = np.arange(n) - np.floor(n/2.0)
|
|
144
141
|
U = np.arange(M) - np.floor(M/2.0)
|
|
@@ -147,7 +144,51 @@ def _dft2_coords(m, n, M, N):
|
|
|
147
144
|
return R, S, U, V
|
|
148
145
|
|
|
149
146
|
|
|
150
|
-
def
|
|
147
|
+
def _cook_nd_args(a, s=None, axes=None):
|
|
148
|
+
# slightly modified version of numpy's function of the same name
|
|
149
|
+
if s is None:
|
|
150
|
+
if axes is None:
|
|
151
|
+
if a.ndim == 2:
|
|
152
|
+
s = list(a.shape)
|
|
153
|
+
elif a.ndim == 3:
|
|
154
|
+
s = list(a.shape[1:3])
|
|
155
|
+
else:
|
|
156
|
+
raise ValueError("Array must have ndim == 2 or 3")
|
|
157
|
+
else:
|
|
158
|
+
s = np.take(np.asarray(a.shape), np.asarray(axes))
|
|
159
|
+
s = list(s)
|
|
160
|
+
if axes is None:
|
|
161
|
+
axes = list(range(-len(s), 0))
|
|
162
|
+
if len(s) != len(axes):
|
|
163
|
+
raise ValueError("Shape and axes have different lengths.")
|
|
164
|
+
return s, axes
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _multi_dot_three(a, b, c, axes, out):
|
|
168
|
+
# compute the matrix triple product
|
|
169
|
+
#
|
|
170
|
+
# a few notes:
|
|
171
|
+
# * this method is similar to np.linalg.multi_dot although it is less
|
|
172
|
+
# general - here we only consider the matrix triple product used as
|
|
173
|
+
# a part of prtools.dft2
|
|
174
|
+
# * because we use np.matmul instead of np.linalg.multi_dot, we can
|
|
175
|
+
# take advantage of broadcasting a and c when b.ndim = 3. This
|
|
176
|
+
# eliminates a for loop in the code
|
|
177
|
+
# * the implementation used here supports b with ndim in (2, 3)
|
|
178
|
+
# iterating over any of the 3 axes when b.ndim == 3
|
|
179
|
+
# * the implementation used here is actually slightly faster than
|
|
180
|
+
# an equivalent call to np.linalg.multi_dot when b.ndim == 2
|
|
181
|
+
# * np.linalg.multi_dot chooses the fastest multiplication order from
|
|
182
|
+
# [(ab)c, a(bc)] depending on the shapes of a, b, and c. There is no
|
|
183
|
+
# difference when computing the dft because both a and c are square
|
|
184
|
+
# matrices
|
|
185
|
+
ab = np.matmul(a, b, axes=[(0, 1), axes, axes])
|
|
186
|
+
out = np.matmul(ab, c, axes=[axes, (0, 1), axes], out=out)
|
|
187
|
+
return out
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def idft2(F, alpha, shape=None, shift=(0, 0), offset=(0, 0), axes=(-2, -1),
|
|
191
|
+
unitary=True, out=None):
|
|
151
192
|
r"""Compute the 2-dimensional inverse discrete Fourier Transform.
|
|
152
193
|
|
|
153
194
|
The IDFT is defined in one dimension as
|
|
@@ -165,9 +206,9 @@ def idft2(F, alpha, shape=None, shift=(0,0), offset=(0,0), unitary=True, out=Non
|
|
|
165
206
|
F : array_like
|
|
166
207
|
2D array to Fourier Transform
|
|
167
208
|
alpha : float or array_like
|
|
168
|
-
Input plane sampling interval (frequency). If :attr:`alpha` is an
|
|
169
|
-
``alpha[1]`` represents row-wise sampling and ``alpha[2]``
|
|
170
|
-
column-wise sampling. If :attr:`alpha` is a scalar,
|
|
209
|
+
Input plane sampling interval (frequency). If :attr:`alpha` is an
|
|
210
|
+
array, ``alpha[1]`` represents row-wise sampling and ``alpha[2]``
|
|
211
|
+
represents column-wise sampling. If :attr:`alpha` is a scalar,
|
|
171
212
|
``alpha[1] = alpha[2] = alpha`` represents uniform sampling across the
|
|
172
213
|
rows and columns of the input plane.
|
|
173
214
|
shape : int or array_like, optional
|
|
@@ -175,14 +216,24 @@ def idft2(F, alpha, shape=None, shift=(0,0), offset=(0,0), unitary=True, out=Non
|
|
|
175
216
|
``F.shape = (shape[0], shape[1])``. If :attr:`shape` is a scalar,
|
|
176
217
|
``F.shape = (shape, shape)``. Default is ``F.shape``
|
|
177
218
|
shift : array_like, optional
|
|
178
|
-
Number of pixels in (x,y) to shift the DC pixel in the output plane
|
|
179
|
-
the origin centrally located in the plane. Default is `[0,0]`.
|
|
219
|
+
Number of pixels in (x,y) to shift the DC pixel in the output plane
|
|
220
|
+
with the origin centrally located in the plane. Default is `[0,0]`.
|
|
221
|
+
offset : array_like, optional
|
|
222
|
+
Number of pixels in (r,c) that the input plane is shifted relative to
|
|
223
|
+
the origin. Default is ``(0, 0)``.
|
|
224
|
+
axes : (2,) array_like of ints, optional
|
|
225
|
+
Axes over which to compute the DFT. If not given, the last two axes are
|
|
226
|
+
used.
|
|
180
227
|
unitary : bool, optional
|
|
181
228
|
Normalization flag. If ``True``, a normalization is performed on the
|
|
182
229
|
output such that the DFT operation is unitary and energy is conserved
|
|
183
230
|
through the Fourier transform operation (Parseval's theorem). In this
|
|
184
231
|
way, the energy in in a limited-area DFT is a fraction of the total
|
|
185
232
|
energy corresponding to the limited area. Default is ``True``.
|
|
233
|
+
out : ndarray or None
|
|
234
|
+
A location into which the result is stored. If provided, ``out.shape ==
|
|
235
|
+
shape`` and ``out.dtype == np.complex``. If not provided or None, a
|
|
236
|
+
freshly-allocated array is returned.
|
|
186
237
|
|
|
187
238
|
Returns
|
|
188
239
|
-------
|
|
@@ -205,13 +256,15 @@ def idft2(F, alpha, shape=None, shift=(0,0), offset=(0,0), unitary=True, out=Non
|
|
|
205
256
|
|
|
206
257
|
* If the y-axis shift behavior is not what you are expecting, you most
|
|
207
258
|
likely have your plotting axes flipped (matplotlib's default behavior is
|
|
208
|
-
to place [0,0] in the upper left corner of the axes). This may be
|
|
209
|
-
by either flipping the sign of the y component of ``shift`` or
|
|
210
|
-
``origin = 'lower'`` to ``imshow()``.
|
|
259
|
+
to place [0,0] in the upper left corner of the axes). This may be
|
|
260
|
+
resolved by either flipping the sign of the y component of ``shift`` or
|
|
261
|
+
by passing ``origin = 'lower'`` to ``imshow()``.
|
|
211
262
|
|
|
212
263
|
References
|
|
213
264
|
----------
|
|
214
|
-
[1] Soummer, et. al. Fast computation of Lyot-style coronagraph
|
|
215
|
-
|
|
265
|
+
[1] Soummer, et. al. Fast computation of Lyot-style coronagraph
|
|
266
|
+
propagation (2007)
|
|
267
|
+
|
|
216
268
|
"""
|
|
217
|
-
return _dftcore(F, alpha, shape, shift, offset,
|
|
269
|
+
return _dftcore(F, alpha, shape, shift, offset, axes, unitary,
|
|
270
|
+
forward=False, out=out)
|
|
@@ -31,8 +31,7 @@ class JaxOptimizeResult:
|
|
|
31
31
|
state: Any #: Optimizer state
|
|
32
32
|
|
|
33
33
|
|
|
34
|
-
def lbfgs(fn, x0, gtol=None, maxiter=None, callback=None,
|
|
35
|
-
fn_kwargs=None):
|
|
34
|
+
def lbfgs(fn, x0, gtol=None, maxiter=None, callback=None, fn_kwargs=None):
|
|
36
35
|
"""Minimize a scalar function of one or more variables using the L-BFGS
|
|
37
36
|
algorithm
|
|
38
37
|
|
|
@@ -43,10 +42,10 @@ def lbfgs(fn, x0, gtol=None, maxiter=None, callback=None, fn_args=None,
|
|
|
43
42
|
|
|
44
43
|
.. code:: python
|
|
45
44
|
|
|
46
|
-
fn(x,
|
|
47
|
-
|
|
48
|
-
where ``x`` is a 1-D array with shape (n,) and ``
|
|
49
|
-
|
|
45
|
+
fn(x, **fn_kwargs)
|
|
46
|
+
|
|
47
|
+
where ``x`` is a 1-D array with shape (n,) and ``fn_kwargs`` are
|
|
48
|
+
optional keyword arguments.
|
|
50
49
|
x0 : jax.Array
|
|
51
50
|
Initial guess
|
|
52
51
|
gtol : float
|
|
@@ -61,10 +60,9 @@ def lbfgs(fn, x0, gtol=None, maxiter=None, callback=None, fn_args=None,
|
|
|
61
60
|
callback(intermediate_result: JaxOptimizeResult)
|
|
62
61
|
|
|
63
62
|
where ``intermediate_result`` is a :class:`JaxOptimizeResult`.
|
|
64
|
-
fn_args : iterable or None
|
|
65
|
-
Extra positional arguments passed to ``fn()``
|
|
66
63
|
fn_kwargs : dict or None
|
|
67
|
-
Extra keyword arguments passed to the
|
|
64
|
+
Extra keyword arguments passed to the objective function as
|
|
65
|
+
``fn(x, **fn_kwargs)``
|
|
68
66
|
|
|
69
67
|
Returns
|
|
70
68
|
-------
|
|
@@ -83,21 +81,21 @@ def lbfgs(fn, x0, gtol=None, maxiter=None, callback=None, fn_args=None,
|
|
|
83
81
|
if not any((gtol, maxiter)):
|
|
84
82
|
raise ValueError('At least one termination tolerance must be specified.')
|
|
85
83
|
|
|
86
|
-
fn_args = [] if fn_args is None else fn_args
|
|
87
84
|
fn_kwargs = {} if fn_kwargs is None else fn_kwargs
|
|
88
85
|
|
|
89
86
|
opt = optax.lbfgs()
|
|
90
87
|
value_and_grad_fn = optax.value_and_grad_from_state(fn)
|
|
91
|
-
|
|
88
|
+
|
|
92
89
|
def step(carry):
|
|
93
90
|
params, state = carry
|
|
94
91
|
# NOTE: passing *args and **kwargs to value_and_grad_fun is very
|
|
95
92
|
# poorly documented in optax (as of v0.2.6 - 10/2025) but this
|
|
96
|
-
# seems to work for now
|
|
97
|
-
value, grad = value_and_grad_fn(params,
|
|
98
|
-
|
|
93
|
+
# seems to work for now.
|
|
94
|
+
value, grad = value_and_grad_fn(params, state=state, **fn_kwargs)
|
|
95
|
+
# NOTE: opt.update() doesn't have any mechanism for accepting *args so
|
|
96
|
+
# we're limited to using **kwargs here
|
|
99
97
|
updates, state = opt.update(
|
|
100
|
-
grad, state, params, value=value, grad=grad, value_fn=fn)
|
|
98
|
+
grad, state, params, value=value, grad=grad, value_fn=fn, **fn_kwargs)
|
|
101
99
|
if callback:
|
|
102
100
|
res = JaxOptimizeResult(
|
|
103
101
|
n=otu.tree_get(state, 'count')-1,
|
|
@@ -126,8 +124,48 @@ def lbfgs(fn, x0, gtol=None, maxiter=None, callback=None, fn_args=None,
|
|
|
126
124
|
grad=otu.tree_get(final_state, 'grad'),
|
|
127
125
|
value=otu.tree_get(final_state, 'value'),
|
|
128
126
|
state=final_state)
|
|
129
|
-
|
|
127
|
+
|
|
130
128
|
if callback:
|
|
131
129
|
jax.debug.callback(callback, res)
|
|
132
130
|
|
|
133
131
|
return res
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _multi_dot_three(a, b, c, axes, out):
|
|
135
|
+
# compute the matrix triple product
|
|
136
|
+
#
|
|
137
|
+
# a few notes:
|
|
138
|
+
# * while numpy-based implementation of this method is based on
|
|
139
|
+
# np.matmul, jax.numpy.matmul doesn't implement the axes argument so
|
|
140
|
+
# we have to use jax.numpy.linalg.multi_dot instead
|
|
141
|
+
# * the implementation used here supports b with ndim in (2, 3)
|
|
142
|
+
# iterating over any of the 3 axes when b.ndim == 3
|
|
143
|
+
# * jax.vmap handles the case when b.ndim == 3 compared with the numpy
|
|
144
|
+
# equivalent of this function which does everything within the
|
|
145
|
+
# confines of matmul using the axes argument
|
|
146
|
+
if b.ndim == 2:
|
|
147
|
+
return jax.numpy.linalg.multi_dot((a, b, c))
|
|
148
|
+
else:
|
|
149
|
+
iter_axis = _iter_axis(axes)
|
|
150
|
+
return jax.vmap(_multi_dot, in_axes=[None, iter_axis, None], out_axes=iter_axis)(a, b, c)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _multi_dot(a, b, c):
|
|
154
|
+
# wrapper function to support vmap call signature
|
|
155
|
+
return jax.numpy.linalg.multi_dot((a, b, c))
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _iter_axis(axes):
|
|
159
|
+
# pure Python to avoid dealing with JAX array mutability issues
|
|
160
|
+
mask = [0, 1, 2]
|
|
161
|
+
for ax in axes:
|
|
162
|
+
mask[ax] = None
|
|
163
|
+
return [ax for ax in mask if ax is not None][0]
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _ndrebin(a, f):
|
|
167
|
+
return jax.vmap(_rebin, in_axes=[0, None])(a, f)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _rebin(a, f):
|
|
171
|
+
return jax.numpy.reshape(a, (a.shape[0]//f, f, a.shape[1]//f, f)).sum(-1).sum(1)
|
|
@@ -65,3 +65,17 @@ def test_boundary(backend):
|
|
|
65
65
|
x = np.zeros((10, 10))
|
|
66
66
|
x[3:7, 2:8] = 1
|
|
67
67
|
assert np.array_equal(prtools.boundary(x), (3, 6, 2, 7))
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@pytest.mark.parametrize('backend', BACKENDS)
|
|
71
|
+
def test_rebin(backend):
|
|
72
|
+
prtools.use(backend)
|
|
73
|
+
x = np.ones((10, 10))
|
|
74
|
+
assert np.array_equal(prtools.rebin(x, 2), 4*np.ones((5, 5)))
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@pytest.mark.parametrize('backend', BACKENDS)
|
|
78
|
+
def test_ndrebin(backend):
|
|
79
|
+
prtools.use(backend)
|
|
80
|
+
x = np.ones((3, 10, 10))
|
|
81
|
+
assert np.array_equal(prtools.rebin(x, 2), 4*np.ones((3, 5, 5)))
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import pytest
|
|
3
|
+
|
|
4
|
+
import prtools
|
|
5
|
+
from .import BACKENDS
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@pytest.mark.parametrize('backend', BACKENDS)
|
|
9
|
+
@pytest.mark.parametrize('shape', ((255,255), (256,256)))
|
|
10
|
+
def test_gauss_kernel(backend, shape):
|
|
11
|
+
prtools.use(backend)
|
|
12
|
+
|
|
13
|
+
# note we need to use a sufficiently large sigma here to avoid wrapping
|
|
14
|
+
# in the FFT-computed version of the kernel
|
|
15
|
+
sigma = 5
|
|
16
|
+
#shape = (256,256)
|
|
17
|
+
G = prtools.gauss_kernel(shape, sigma, fftshift=True)
|
|
18
|
+
|
|
19
|
+
r = np.arange(-shape[0]//2,shape[0]//2)
|
|
20
|
+
c = np.arange(-shape[1]//2,shape[1]//2)
|
|
21
|
+
g = prtools.gauss(r, c, sigma)
|
|
22
|
+
G_fft = np.abs(np.fft.fftshift(np.fft.fft2(np.fft.ifftshift(g))))
|
|
23
|
+
G_fft = G_fft / np.max(G_fft)
|
|
24
|
+
|
|
25
|
+
assert np.allclose(G, G_fft, atol=1e-6)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@pytest.mark.parametrize('backend', BACKENDS)
|
|
29
|
+
@pytest.mark.parametrize('shape', ((255,255), (256,256)))
|
|
30
|
+
def test_gauss_kernel_pixelscale(backend, shape):
|
|
31
|
+
prtools.use(backend)
|
|
32
|
+
|
|
33
|
+
r = np.linspace(-10, 10, shape[0])
|
|
34
|
+
c = np.linspace(-10, 10, shape[1])
|
|
35
|
+
nr, nc = len(r), len(c)
|
|
36
|
+
dr, dc = r[1] - r[0], c[1] - c[0]
|
|
37
|
+
|
|
38
|
+
G = prtools.gauss_kernel((nr, nc), sigma=1, pixelscale=(dr, dc),
|
|
39
|
+
fftshift=True)
|
|
40
|
+
|
|
41
|
+
g = prtools.gauss(r, c, sigma=1)
|
|
42
|
+
G_fft = np.abs(np.fft.fftshift(np.fft.fft2(np.fft.ifftshift(g))))
|
|
43
|
+
G_fft = G_fft / np.max(G_fft)
|
|
44
|
+
|
|
45
|
+
assert np.allclose(G, G_fft, atol=1e-6)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@pytest.mark.parametrize('backend', BACKENDS)
|
|
49
|
+
@pytest.mark.parametrize('shape', ((255,255), (256,256)))
|
|
50
|
+
def test_gauss_kernel_oversample(backend, shape):
|
|
51
|
+
prtools.use(backend)
|
|
52
|
+
|
|
53
|
+
sigma = 5
|
|
54
|
+
oversample = 3
|
|
55
|
+
|
|
56
|
+
G = prtools.gauss_kernel(shape, sigma=sigma, fftshift=False)
|
|
57
|
+
Go = prtools.gauss_kernel((shape[0]*oversample, shape[1]*oversample),
|
|
58
|
+
sigma=sigma, oversample=oversample,
|
|
59
|
+
fftshift=False)
|
|
60
|
+
|
|
61
|
+
assert np.allclose(G[0:100,0:100], Go[0:100,0:100])
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@pytest.mark.parametrize('backend', BACKENDS)
|
|
65
|
+
@pytest.mark.parametrize('shape', ((255,255), (256,256)))
|
|
66
|
+
def test_pixel_kernel_oversample(backend, shape):
|
|
67
|
+
prtools.use(backend)
|
|
68
|
+
|
|
69
|
+
oversample = 3
|
|
70
|
+
|
|
71
|
+
P = prtools.pixel_kernel(shape, fftshift=False)
|
|
72
|
+
Po = prtools.pixel_kernel((shape[0]*oversample, shape[1]*oversample),
|
|
73
|
+
oversample=oversample, fftshift=False)
|
|
74
|
+
|
|
75
|
+
assert np.allclose(P[0:100,0:100], Po[0:100,0:100])
|
|
@@ -6,53 +6,43 @@ from .import BACKENDS
|
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
@pytest.mark.parametrize('backend', BACKENDS)
|
|
9
|
-
|
|
9
|
+
@pytest.mark.parametrize('shape', ((10,10), (11,11), (10,11)))
|
|
10
|
+
def test_dft2(backend, shape):
|
|
10
11
|
prtools.use(backend)
|
|
11
|
-
n =
|
|
12
|
-
f = np.random.rand(
|
|
12
|
+
m, n = shape
|
|
13
|
+
f = np.random.rand(m, n) + 1j * np.random.rand(m, n)
|
|
13
14
|
|
|
14
|
-
F_dft = prtools.dft2(f, 1/n, unitary=False)
|
|
15
|
+
F_dft = prtools.dft2(f, [1/m, 1/n], unitary=False)
|
|
15
16
|
F_fft = np.fft.fftshift(np.fft.fft2(np.fft.ifftshift(f)))
|
|
16
17
|
|
|
17
18
|
assert np.allclose(F_dft, F_fft, atol=1e-5)
|
|
18
19
|
|
|
19
20
|
|
|
20
21
|
@pytest.mark.parametrize('backend', BACKENDS)
|
|
21
|
-
|
|
22
|
+
@pytest.mark.parametrize('axes', ((0,1), (1,2), (0,2)))
|
|
23
|
+
def test_dft2_cube(backend, axes):
|
|
22
24
|
prtools.use(backend)
|
|
23
|
-
|
|
24
|
-
|
|
25
|
+
shape = (10, 11, 12)
|
|
26
|
+
m, n = np.take(shape, axes)
|
|
27
|
+
f = np.random.uniform(size=shape) + 1j * np.random.uniform(size=shape)
|
|
25
28
|
|
|
26
|
-
|
|
27
|
-
|
|
29
|
+
F_dft = prtools.dft2(f, [1/m, 1/n], axes=axes, unitary=False)
|
|
30
|
+
F_fft = np.fft.fftshift(np.fft.fft2(np.fft.ifftshift(f), axes=axes))
|
|
28
31
|
|
|
29
|
-
assert np.allclose(
|
|
32
|
+
assert np.allclose(F_dft, F_fft, atol=5e-5)
|
|
30
33
|
|
|
31
34
|
|
|
32
35
|
@pytest.mark.parametrize('backend', BACKENDS)
|
|
33
|
-
|
|
36
|
+
@pytest.mark.parametrize('shape', ((10,10), (11,11), (10,11)))
|
|
37
|
+
def test_idft2(backend, shape):
|
|
34
38
|
prtools.use(backend)
|
|
35
|
-
n =
|
|
36
|
-
f = np.random.rand(
|
|
37
|
-
|
|
38
|
-
F_dft = prtools.dft2(f, 1/n, unitary=False)
|
|
39
|
-
F_fft = np.fft.fftshift(np.fft.fft2(np.fft.ifftshift(f)))
|
|
40
|
-
|
|
41
|
-
print(F_dft.dtype)
|
|
42
|
-
print(F_fft.dtype)
|
|
43
|
-
assert np.allclose(F_dft, F_fft, atol=1e-5)
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
@pytest.mark.parametrize('backend', BACKENDS)
|
|
47
|
-
def test_idft2_odd(backend):
|
|
48
|
-
prtools.use(backend)
|
|
49
|
-
n = 11
|
|
50
|
-
F = np.random.rand(n, n) + 1j * np.random.rand(n, n)
|
|
39
|
+
m, n = shape
|
|
40
|
+
f = np.random.rand(m, n) + 1j * np.random.rand(m, n)
|
|
51
41
|
|
|
52
|
-
|
|
53
|
-
|
|
42
|
+
F_dft = prtools.idft2(f, [1/m, 1/n], unitary=False)
|
|
43
|
+
F_fft = np.fft.fftshift(np.fft.ifft2(np.fft.ifftshift(f)))
|
|
54
44
|
|
|
55
|
-
assert np.allclose(
|
|
45
|
+
assert np.allclose(F_dft, F_fft, atol=1e-5)
|
|
56
46
|
|
|
57
47
|
|
|
58
48
|
@pytest.mark.parametrize('backend', BACKENDS)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import jax.numpy as jnp
|
|
3
|
+
|
|
4
|
+
import prtools
|
|
5
|
+
|
|
6
|
+
def test_lbfgs_fn_kwargs():
|
|
7
|
+
prtools.use('jax')
|
|
8
|
+
|
|
9
|
+
def fn(x, a, b):
|
|
10
|
+
return (x[0] + a)**2 + (x[1] + b)**2
|
|
11
|
+
|
|
12
|
+
a = 2
|
|
13
|
+
b = -5
|
|
14
|
+
x0 = jnp.zeros(2)
|
|
15
|
+
|
|
16
|
+
res = prtools.jax.lbfgs(fn, x0, gtol=1e-5, maxiter=25,
|
|
17
|
+
fn_kwargs={'a': a, 'b': b})
|
|
18
|
+
assert(np.allclose(res.x, (-a, -b)))
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|