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.
Files changed (31) hide show
  1. {prtools-1.4.0/prtools.egg-info → prtools-1.4.2}/PKG-INFO +1 -1
  2. {prtools-1.4.0 → prtools-1.4.2}/prtools/__init__.py +1 -1
  3. {prtools-1.4.0 → prtools-1.4.2}/prtools/array.py +9 -5
  4. {prtools-1.4.0 → prtools-1.4.2}/prtools/backend/_jax.py +7 -0
  5. {prtools-1.4.0 → prtools-1.4.2}/prtools/convolve.py +74 -83
  6. {prtools-1.4.0 → prtools-1.4.2}/prtools/fourier.py +100 -47
  7. {prtools-1.4.0 → prtools-1.4.2}/prtools/jax.py +54 -16
  8. {prtools-1.4.0 → prtools-1.4.2/prtools.egg-info}/PKG-INFO +1 -1
  9. {prtools-1.4.0 → prtools-1.4.2}/prtools.egg-info/SOURCES.txt +3 -1
  10. {prtools-1.4.0 → prtools-1.4.2}/tests/test_array.py +14 -0
  11. prtools-1.4.2/tests/test_convolve.py +75 -0
  12. {prtools-1.4.0 → prtools-1.4.2}/tests/test_fourier.py +20 -30
  13. prtools-1.4.2/tests/test_jax.py +18 -0
  14. {prtools-1.4.0 → prtools-1.4.2}/LICENSE +0 -0
  15. {prtools-1.4.0 → prtools-1.4.2}/NOTICE +0 -0
  16. {prtools-1.4.0 → prtools-1.4.2}/README.rst +0 -0
  17. {prtools-1.4.0 → prtools-1.4.2}/prtools/backend/__init__.py +0 -0
  18. {prtools-1.4.0 → prtools-1.4.2}/prtools/backend/_base.py +0 -0
  19. {prtools-1.4.0 → prtools-1.4.2}/prtools/backend/_numpy.py +0 -0
  20. {prtools-1.4.0 → prtools-1.4.2}/prtools/cost.py +0 -0
  21. {prtools-1.4.0 → prtools-1.4.2}/prtools/misc.py +0 -0
  22. {prtools-1.4.0 → prtools-1.4.2}/prtools/segmented.py +0 -0
  23. {prtools-1.4.0 → prtools-1.4.2}/prtools/shape.py +0 -0
  24. {prtools-1.4.0 → prtools-1.4.2}/prtools/sparse.py +0 -0
  25. {prtools-1.4.0 → prtools-1.4.2}/prtools/stats.py +0 -0
  26. {prtools-1.4.0 → prtools-1.4.2}/prtools/zernike.py +0 -0
  27. {prtools-1.4.0 → prtools-1.4.2}/prtools.egg-info/dependency_links.txt +0 -0
  28. {prtools-1.4.0 → prtools-1.4.2}/prtools.egg-info/requires.txt +0 -0
  29. {prtools-1.4.0 → prtools-1.4.2}/prtools.egg-info/top_level.txt +0 -0
  30. {prtools-1.4.0 → prtools-1.4.2}/pyproject.toml +0 -0
  31. {prtools-1.4.0 → prtools-1.4.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: prtools
3
- Version: 1.4.0
3
+ Version: 1.4.2
4
4
  Summary: Utility functions for image-based phase retrieval
5
5
  Author: Andy Kee
6
6
  License: MIT License
@@ -1,4 +1,4 @@
1
- __version__ = '1.4.0'
1
+ __version__ = '1.4.2'
2
2
 
3
3
  from prtools.backend import __backend__, use
4
4
 
@@ -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
- rebinned_shape = (img.shape[0], img.shape[1]//factor, img.shape[2]//factor)
304
- img_rebinned = np.zeros(rebinned_shape, dtype=img.dtype)
305
- for i in range(img.shape[0]):
306
- img_rebinned[i] = img[i].reshape(rebinned_shape[1], factor,
307
- rebinned_shape[2], factor).sum(-1).sum(1)
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=True):
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 `kernel`.
17
+ Array to be convolved with ``kernel``.
18
18
  kernel : array_like
19
- Convolution kernel. Should have the same shape as `array`.
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 `kernel =
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. If False, the array is assumed to be provided in the frequency
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 (default), the kernel is assumed to be provided in the spatial
29
- domain. If False, the kernel is assumed to be provided in the frequency
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.fft2(array) if fft_kernel else kernel
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 of Gaussian. Providing two values allows for
60
- non-symmetric Gaussian interpreted as `(sigma_row, sigma_col)`
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 = prtools.circle((256,256), 100)
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
- return fftconv(img, kernel, normalize_kernel=True, fft_array=True,
86
- fft_kernel=False)
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 on a discretely
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 `img`. Default is 1.
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, oversample=1, indexing='ij', normalize=False):
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 cartesian ('xy') indexing of output.
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,)) / oversample
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"""Normalized 2D sinc function
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 cartesian ('xy') indexing of output.
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=True):
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 transfer function of a normalized 2D Gaussian
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 `(sigma_row, sigma_col)`
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 (default), the kernel is FFT-shifted before it is returned.
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
- >>> blur_kernel = prtools.gauss_kernel((256,256), 1)
260
- >>> plt.imshow(blur_kernel, cmap='gray')
249
+ >>> G = prtools.gauss_kernel((256,256), sigma=1, fftshift=True)
250
+ >>> plt.imshow(G, cmap='gray')
261
251
 
262
- Note this is functionally equivalent to
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
- >>> r = np.fft.fftfreq(256)
270
- >>> c = np.fft.fftfreq(256)
271
- >>> sigma = 1
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), oversample, indexing='ij',
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=True):
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 (default), the kernel is FFT-shifted before it is returned.
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]) / oversample
341
- x2 = np.fft.fftfreq(shape[1]) / oversample
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), unitary=True,
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. If not provided or None, a
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
- #__backend__ = prtools.__backend__
88
+ def _dftcore(f, alpha, shape, shift, offset, axes, unitary, forward, out):
85
89
 
86
- if out is not None:
87
- if __backend__ == 'numpy':
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
- alpha_row, alpha_col = np.broadcast_to(alpha, (2,))
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
- if shape is None:
99
- shape = (m, n)
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, shift_col,
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, offsetc, forward):
124
+ def _dft2_matrices(m, n, M, N, alphar, alphac, shiftr, shiftc, offsetr,
125
+ offsetc, forward):
121
126
  if forward:
122
- sign = -1
127
+ c = -1j
123
128
  else:
124
- sign = 1
129
+ c = 1j
125
130
  R, S, U, V = _dft2_coords(m, n, M, N)
126
- E1 = np.exp(sign*2.0 * 1j * np.pi * alphar * np.outer(R+offsetr, U-shiftr)).T
127
- E2 = np.exp(sign*2.0 * 1j * np.pi * alphac * np.outer(S+offsetc, V-shiftc))
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 idft2(F, alpha, shape=None, shift=(0,0), offset=(0,0), unitary=True, out=None):
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 array,
169
- ``alpha[1]`` represents row-wise sampling and ``alpha[2]`` represents
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 with
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 resolved
209
- by either flipping the sign of the y component of ``shift`` or by passing
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 propagation (2007)
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, unitary, out, forward=False)
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, fn_args=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, *fn_args, **fn_kwargs)
47
-
48
- where ``x`` is a 1-D array with shape (n,) and ``fn_args`` and
49
- ``fn_kwargs`` are optional positional and keyword arguments.
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 ``fn()``
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, *fn_args, state=state,
98
- **fn_kwargs)
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)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: prtools
3
- Version: 1.4.0
3
+ Version: 1.4.2
4
4
  Summary: Utility functions for image-based phase retrieval
5
5
  Author: Andy Kee
6
6
  License: MIT License
@@ -24,4 +24,6 @@ prtools/backend/_base.py
24
24
  prtools/backend/_jax.py
25
25
  prtools/backend/_numpy.py
26
26
  tests/test_array.py
27
- tests/test_fourier.py
27
+ tests/test_convolve.py
28
+ tests/test_fourier.py
29
+ tests/test_jax.py
@@ -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
- def test_dft2_even(backend):
9
+ @pytest.mark.parametrize('shape', ((10,10), (11,11), (10,11)))
10
+ def test_dft2(backend, shape):
10
11
  prtools.use(backend)
11
- n = 10
12
- f = np.random.rand(n, n) + 1j * np.random.rand(n, n)
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
- def test_idft2_even(backend):
22
+ @pytest.mark.parametrize('axes', ((0,1), (1,2), (0,2)))
23
+ def test_dft2_cube(backend, axes):
22
24
  prtools.use(backend)
23
- n = 10
24
- F = np.random.rand(n, n) + 1j * np.random.rand(n, n)
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
- f_dft = prtools.idft2(F, 1/n, unitary=False)
27
- f_fft = np.fft.fftshift(np.fft.ifft2(np.fft.ifftshift(F)))
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(f_dft, f_fft, atol=1e-5)
32
+ assert np.allclose(F_dft, F_fft, atol=5e-5)
30
33
 
31
34
 
32
35
  @pytest.mark.parametrize('backend', BACKENDS)
33
- def test_dft2_odd(backend):
36
+ @pytest.mark.parametrize('shape', ((10,10), (11,11), (10,11)))
37
+ def test_idft2(backend, shape):
34
38
  prtools.use(backend)
35
- n = 11
36
- f = np.random.rand(n, n) + 1j * np.random.rand(n, n)
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
- f_dft = prtools.idft2(F, 1/n, unitary=False)
53
- f_fft = np.fft.fftshift(np.fft.ifft2(np.fft.ifftshift(F)))
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(f_dft, f_fft, atol=1e-5)
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