prtools 2.0.0__tar.gz → 2.0.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 (29) hide show
  1. {prtools-2.0.0/prtools.egg-info → prtools-2.0.2}/PKG-INFO +1 -1
  2. {prtools-2.0.0 → prtools-2.0.2}/prtools/__init__.py +3 -1
  3. {prtools-2.0.0 → prtools-2.0.2}/prtools/array.py +76 -0
  4. {prtools-2.0.0 → prtools-2.0.2}/prtools/convolve.py +26 -10
  5. {prtools-2.0.0 → prtools-2.0.2}/prtools/shape.py +62 -4
  6. {prtools-2.0.0 → prtools-2.0.2}/prtools/zernike.py +14 -12
  7. {prtools-2.0.0 → prtools-2.0.2/prtools.egg-info}/PKG-INFO +1 -1
  8. {prtools-2.0.0 → prtools-2.0.2}/tests/test_array.py +42 -1
  9. {prtools-2.0.0 → prtools-2.0.2}/LICENSE +0 -0
  10. {prtools-2.0.0 → prtools-2.0.2}/NOTICE +0 -0
  11. {prtools-2.0.0 → prtools-2.0.2}/README.rst +0 -0
  12. {prtools-2.0.0 → prtools-2.0.2}/prtools/_array_api.py +0 -0
  13. {prtools-2.0.0 → prtools-2.0.2}/prtools/cost.py +0 -0
  14. {prtools-2.0.0 → prtools-2.0.2}/prtools/fourier.py +0 -0
  15. {prtools-2.0.0 → prtools-2.0.2}/prtools/jax.py +0 -0
  16. {prtools-2.0.0 → prtools-2.0.2}/prtools/misc.py +0 -0
  17. {prtools-2.0.0 → prtools-2.0.2}/prtools/morphology.py +0 -0
  18. {prtools-2.0.0 → prtools-2.0.2}/prtools/segmented.py +0 -0
  19. {prtools-2.0.0 → prtools-2.0.2}/prtools/sparse.py +0 -0
  20. {prtools-2.0.0 → prtools-2.0.2}/prtools/stats.py +0 -0
  21. {prtools-2.0.0 → prtools-2.0.2}/prtools.egg-info/SOURCES.txt +0 -0
  22. {prtools-2.0.0 → prtools-2.0.2}/prtools.egg-info/dependency_links.txt +0 -0
  23. {prtools-2.0.0 → prtools-2.0.2}/prtools.egg-info/requires.txt +0 -0
  24. {prtools-2.0.0 → prtools-2.0.2}/prtools.egg-info/top_level.txt +0 -0
  25. {prtools-2.0.0 → prtools-2.0.2}/pyproject.toml +0 -0
  26. {prtools-2.0.0 → prtools-2.0.2}/setup.cfg +0 -0
  27. {prtools-2.0.0 → prtools-2.0.2}/tests/test_convolve.py +0 -0
  28. {prtools-2.0.0 → prtools-2.0.2}/tests/test_fourier.py +0 -0
  29. {prtools-2.0.0 → prtools-2.0.2}/tests/test_jax.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: prtools
3
- Version: 2.0.0
3
+ Version: 2.0.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__ = '2.0.0'
1
+ __version__ = '2.0.2'
2
2
 
3
3
 
4
4
  from prtools._array_api import use
@@ -8,6 +8,7 @@ from prtools.array import (
8
8
  pad,
9
9
  crop,
10
10
  boundary,
11
+ sample,
11
12
  rebin,
12
13
  rescale,
13
14
  normpow,
@@ -55,6 +56,7 @@ from prtools.shape import (
55
56
  circle,
56
57
  hexagon,
57
58
  rectangle,
59
+ ellipse,
58
60
  spider,
59
61
  sin,
60
62
  waffle
@@ -326,6 +326,82 @@ def boundary(x, threshold=0):
326
326
  return rmin, rmax, cmin, cmax
327
327
 
328
328
 
329
+ def sample(x, oversample, offset=0, axes=(-2, -1), indexing='ij'):
330
+ """Extract samples from an oversampled array.
331
+
332
+ The sampled axes are tiled into contiguous blocks of ``oversample``
333
+ samples, with one sample being taken from each block. No averaging or
334
+ filtering is performed.
335
+
336
+ Parameters
337
+ ----------
338
+ x : array_like
339
+ Array to sample
340
+ oversample : int or tuple of ints
341
+ Decimation factor. Providing two values defines asymmetric
342
+ oversampling with ordering interpreted according to ``indexing``.
343
+ offset : int or tuple of ints, optional
344
+ Position of the sample with each block, measured from
345
+ ``oversample//2``. The sub-sampled position reachable by ``offset`` is
346
+ quantized to ``1/oversample``. Default is 0. If two values are
347
+ provided, their ordering is interpreted according to ``indexing``.
348
+ axes : (2,) sequence of ints, optional
349
+ Axes over which to sample. If not provided, the last two axes are
350
+ used.
351
+ indexing : {'ij', 'xy'}, optional
352
+ Matrix (‘ij’, default) or cartesian (‘xy’) indexing of ``x`` grid.
353
+
354
+ Returns
355
+ -------
356
+ x : ndarray
357
+ Each sampled axis has length ``x.shape[axis]//oversample``. If axis
358
+ length is not a multiple of ``oversample``, samples taken beyond the
359
+ last whole block are ignored.
360
+
361
+ Examples
362
+ --------
363
+ .. code-block:: python
364
+
365
+ >>> psf = prtools.calcpsf(amp, opd, wave, sampling, (32, 32),
366
+ ... oversample=8)
367
+ >>> k = prtools.pixel_kernel(psf.shape, oversample=8)
368
+ >>> native = prtools.sample(prtools.fftconv(psf, k), 8)
369
+ >>> native.shape
370
+ (32, 32)
371
+
372
+ """
373
+ xp = array_namespace(x)
374
+ x = xp.asarray(x)
375
+
376
+ if indexing not in ('ij', 'xy'):
377
+ raise ValueError("Valid values for indexing are 'xy' and 'ij'.")
378
+
379
+ oversample = tuple(int(v) for v in np.broadcast_to(oversample, (2,)))
380
+ offset = tuple(int(v) for v in np.broadcast_to(offset, (2,)))
381
+ axes = tuple(int(a) % x.ndim for a in axes)
382
+
383
+ if indexing == 'xy':
384
+ offset = (-offset[1], offset[0])
385
+ oversample = (oversample[1], oversample[0])
386
+
387
+ idx = [slice(None)] * x.ndim
388
+
389
+ for ax, m, o in zip(axes, oversample, offset):
390
+
391
+ lo, hi = -(m // 2), m - 1 - m // 2
392
+ if not lo <= o <= hi:
393
+ raise ValueError(f'offset {o} lies outside one pixel for oversample '
394
+ f'{m} on axis {ax}; require {lo} <= offset <= {hi}')
395
+
396
+ n = x.shape[ax] // m
397
+ start = m // 2 + o
398
+ stop = start + m * n
399
+
400
+ idx[ax] = slice(start, stop, m)
401
+
402
+ return x[tuple(idx)]
403
+
404
+
329
405
  def rebin(x, factor):
330
406
  """Rebin an array by an integer factor.
331
407
 
@@ -7,7 +7,7 @@ def fftconv(array, kernel, axes=(-2, -1), normalize_kernel=True,
7
7
  fft_array=True, fft_kernel=False, fftshift_kernel=False):
8
8
  r"""Convolve an array with a kernel using the FFT.
9
9
 
10
- The colvolution is computed as
10
+ The convolution is computed as
11
11
 
12
12
  .. math::
13
13
 
@@ -18,12 +18,13 @@ def fftconv(array, kernel, axes=(-2, -1), normalize_kernel=True,
18
18
  array : array_like
19
19
  Array to be convolved with ``kernel``.
20
20
  kernel : array_like
21
- Convolution kernel. Should have the same shape as ``array``.
21
+ Convolution kernel. Should have the same shape as ``array``. May be
22
+ given in either the frequency or spatial domain, as indicated by
23
+ ``fft_kernel``. The kernel is applied exactly as supplied. See Notes
24
+ for normalization details.
22
25
  axes : sequence of ints, optional
23
26
  Axes over which to apply the convolution. If not given, the last two
24
27
  axes are used.
25
- normalize_kernel : bool, optional
26
- If True (default), kernel is normalized so that ``sum(kernel) == 1``.
27
28
  fft_array : bool, optional
28
29
  If True (default), the array is assumed to be provided in the spatial
29
30
  domain and its FFT will be computed by this function. If False, the
@@ -46,6 +47,24 @@ def fftconv(array, kernel, axes=(-2, -1), normalize_kernel=True,
46
47
  --------
47
48
  :func:`~prtools.gauss_blur`
48
49
  :func:`~prtools.pixelate`
50
+
51
+ Notes
52
+ -----
53
+ **Kernel normalization** This function does not normalize or scale the
54
+ supplied kernel in any way.
55
+
56
+ The sum of ``array`` is conserved when the kernel's transfer function is
57
+ unity at zero frequency. Depending on which domain the kernel is supplied
58
+ in, the kernel normaliztion approach is different:
59
+
60
+ * A frequency-domain kernel (``fft_kernel=False``) should be scaled such
61
+ that ``kernel[0,0] == 1``. Note that ``kernel[0,0]`` is the
62
+ zero-frequency term only for a kernel in standard (unshifted) FFT layout.
63
+ If the kernel origin lies at the center of the array
64
+ (``fftshift_kernel=True``), the DC term is at ``kernel[nr//2, nc//2]``.
65
+
66
+ * A spatial-domain kernal(``fft_kernel=True``) should have
67
+ ``sum(kernel) == 1``.
49
68
  """
50
69
  xp = array_namespace(array)
51
70
 
@@ -53,9 +72,6 @@ def fftconv(array, kernel, axes=(-2, -1), normalize_kernel=True,
53
72
  k = xp.fft.fftshift(kernel) if fftshift_kernel else kernel
54
73
  k = xp.fft.fft2(k) if fft_kernel else k
55
74
 
56
- if normalize_kernel:
57
- k = k / xp.sum(k)
58
-
59
75
  return xp.fft.ifft2(a*k, axes=axes).real
60
76
 
61
77
 
@@ -312,10 +328,10 @@ def gauss_kernel(shape, sigma, oversample=1, pixelscale=1.0, fftshift=False,
312
328
  return k
313
329
 
314
330
 
315
- def pixel_kernel(shape, oversample=1, pixelscale=1.0, fftshift=False,
331
+ def pixel_kernel(shape, oversample=1, pixelscale=1.0, fftshift=False,
316
332
  xp=None):
317
333
  r"""2D pixel MTF filter kernel
318
-
334
+
319
335
  This function returns a normalized 2D sinc function sized to represent
320
336
  the transfer function of an idealized square pixel.
321
337
 
@@ -361,7 +377,7 @@ def pixel_kernel(shape, oversample=1, pixelscale=1.0, fftshift=False,
361
377
  x1 = xp.fft.fftfreq(shape[0], d=pixelscale[0]) * oversample
362
378
  x2 = xp.fft.fftfreq(shape[1], d=pixelscale[1]) * oversample
363
379
 
364
- k = xp.abs(sinc(x1, x2, indexing='ij', xp=xp))
380
+ k = sinc(x1, x2, indexing='ij', xp=xp)
365
381
 
366
382
  if fftshift:
367
383
  return xp.fft.fftshift(k)
@@ -219,6 +219,68 @@ def rectangle(shape, width, height, shift=(0, 0), angle=0, antialias=True,
219
219
  return rect
220
220
 
221
221
 
222
+ def ellipse(shape, xradius, yradius, shift=(0,0), angle=0, antialias=True,
223
+ indexing='ij'):
224
+ """Draw an ellipse
225
+
226
+ Parameters
227
+ ----------
228
+ shape : array_like
229
+ Size of output in pixels (nrows, ncols)
230
+ xradius : float
231
+ Semi-axis of the ellipse in pixels in the x (horizontal) direction.
232
+ yradius : float
233
+ Semi-axis of the ellipse in pixels in the y (vertical) direction.
234
+ shift : (2,) array_like, optional
235
+ How far to shift center in float (rows, cols). Default is (0, 0).
236
+ angle : float, optional
237
+ Rotation of the ellipse in degrees counterclockwise from horizontal.
238
+ Default is 0.
239
+ antialias : bool, optional
240
+ If True (default), the shape edges are antialiased.
241
+ indexing : {'ij', 'xy'}, optional
242
+ Matrix ('ij', default) or cartesian ('xy') indexing of output.
243
+
244
+ Returns
245
+ -------
246
+ ndarray
247
+
248
+ Examples
249
+ --------
250
+ .. plot::
251
+ :include-source:
252
+ :context: reset
253
+ :scale: 50
254
+
255
+ >>> ell = prtools.ellipse(shape=(256, 256), xradius=100, yradius=50)
256
+ >>> plt.imshow(ell, cmap='gray')
257
+
258
+ .. plot::
259
+ :include-source:
260
+ :context: reset
261
+ :scale: 50
262
+
263
+ >>> ell = prtools.ellipse(shape=(256, 256), xradius=100, yradius=50, angle=30)
264
+ >>> plt.imshow(ell, cmap='gray')
265
+
266
+ """
267
+ a, b = yradius, xradius
268
+ rr, cc = mesh(shape, shift, angle, indexing=indexing)
269
+
270
+ rho = np.sqrt((rr / a)**2 + (cc / b)**2)
271
+ grad = np.sqrt((rr / a**2)**2 + (cc / b**2)**2)
272
+
273
+ with np.errstate(divide='ignore', invalid='ignore'):
274
+ dist = (1 - rho) * rho / grad
275
+ dist = np.where(grad == 0, 1.0, dist) # center is always interior
276
+ ell = np.clip(dist + 0.5, 0.0, 1.0)
277
+
278
+ if not antialias:
279
+ ell[ell > 0] = 1
280
+
281
+ return ell
282
+
283
+
222
284
  def spider(shape, width, angle=0, shift=(0, 0), antialias=True, indexing='ij'):
223
285
  """Draw a spider
224
286
 
@@ -261,10 +323,6 @@ def spider(shape, width, angle=0, shift=(0, 0), antialias=True, indexing='ij'):
261
323
  return 1 - rectangle(shape, len, width, shift, angle, antialias, indexing)
262
324
 
263
325
 
264
- def ellipse():
265
- pass
266
-
267
-
268
326
  def gauss(shape, sigma, shift=(0, 0), indexing='ij'):
269
327
  """Generate a 2D Gaussian function.
270
328
 
@@ -14,7 +14,7 @@ def zernike(mask, index, normalize=True, order='noll', rho=None, theta=None):
14
14
  Binary mask defining the extent to compute the Zernike polynomial
15
15
  over.
16
16
  index : int
17
- Noll Zernike index as defined in [1]
17
+ Zernike mode index
18
18
  normalize : bool, optional
19
19
  If True (default), the output is normalized according to [1]. If False,
20
20
  the output value ranges [-1, 1] over the mask.
@@ -112,8 +112,8 @@ def zernike_compose(mask, coeffs, normalize=True, order='noll', rho=None, theta=
112
112
  mask : array_like
113
113
  Binary mask defining the extent to compute the Zernike polynomial over.
114
114
  coeffs : array_like
115
- List of coefficients corresponding to Zernike indices (Noll ordering)
116
- used to create the OPD.
115
+ List of coefficients corresponding to Zernike indices used to create
116
+ the OPD.
117
117
  normalize : bool, optional
118
118
  If True (default), the output is normalized according to [1]. If False,
119
119
  the output value ranges [-1, 1] over the mask.
@@ -177,7 +177,7 @@ def zernike_basis(mask, modes, vectorize=False, normalize=True, order='noll',
177
177
  mask : array_like
178
178
  Binary mask defining the extent to compute the Zernike polynomial over.
179
179
  modes : array_like
180
- List of modes (Noll ordering) to return.
180
+ List of modes to return.
181
181
  vectorize : bool, optional
182
182
  If True, the output is returned as a
183
183
  ``(length(modes), modes.shape[0]*modes.shape[1])`` array If False
@@ -228,11 +228,11 @@ def zernike_fit(opd, mask, modes, normalize=True, order='noll', rho=None,
228
228
  Parameters
229
229
  ----------
230
230
  opd : array_like
231
- OPD to fit.
231
+ OPD to fit. NaN values in ``opd`` are excluded from the fit.
232
232
  mask : array_like
233
233
  Binary mask defining the extent to compute the Zernike basis over.
234
234
  modes : array_like
235
- List of modes (Noll ordering) to fit.
235
+ List of modes to fit.
236
236
  normalize : bool, optional
237
237
  If True (default), the output is normalized according to [1]. If False,
238
238
  the output value ranges [-1, 1] over the mask.
@@ -271,14 +271,16 @@ def zernike_fit(opd, mask, modes, normalize=True, order='noll', rho=None,
271
271
  [1] Noll, RJ. Zernike polynomials and atmospheric turbulence. J Opt Soc Am 66, 207-211 (1976).
272
272
 
273
273
  """
274
- opd = np.asarray(opd)
274
+ opd = np.asarray(opd).ravel()
275
275
  mask = np.asarray(mask)
276
-
277
276
  basis = zernike_basis(mask, modes, True, normalize, order, rho, theta)
277
+
278
+ # valid data includes all non-NaN values inside the mask
279
+ valid = np.isfinite(opd) & mask.ravel().astype(bool)
280
+
281
+ coeff, *_ = np.linalg.lstsq(basis[:, valid].T, opd[valid], rcond=None)
278
282
 
279
- basis = np.linalg.pinv(basis)
280
-
281
- return np.einsum('ij,i->j', basis, opd.ravel())
283
+ return coeff
282
284
 
283
285
 
284
286
  def zernike_remove(opd, mask, modes, order='noll', rho=None, theta=None):
@@ -291,7 +293,7 @@ def zernike_remove(opd, mask, modes, order='noll', rho=None, theta=None):
291
293
  mask : array_like
292
294
  Binary mask defining the extent to compute the Zernike basis over.
293
295
  modes : array_like
294
- List of modes (Noll ordering) to remove.
296
+ List of modes to remove.
295
297
  order : {'noll', 'fringe', 'ansi'}, optional
296
298
  Zernike ordering scheme. Default is 'noll'.
297
299
  rho : array_like, optional
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: prtools
3
- Version: 2.0.0
3
+ Version: 2.0.2
4
4
  Summary: Utility functions for image-based phase retrieval
5
5
  Author: Andy Kee
6
6
  License: MIT License
@@ -109,4 +109,45 @@ def test_register(xp):
109
109
 
110
110
  s = prtools.register(x2, x1, oversample=2)
111
111
 
112
- assert(xp.allclose(xp.asarray(s), xp.asarray((-1,-1))))
112
+ assert xp.allclose(xp.asarray(s), xp.asarray((-1, -1)))
113
+
114
+
115
+ @pytest.mark.parametrize('xp', BACKENDS)
116
+ @pytest.mark.parametrize('shape, oversample, offset, result', [
117
+ ((2, 2), 1, 0, [[0, 1], [2, 3]]),
118
+ ((2, 2), 2, 0, [3]),
119
+ ((2, 2), 2, (-1, 0), [1]),
120
+ ((2, 2), 2, (0, -1), [2]),
121
+ ((2, 2), 2, (-1, -1), [0]),
122
+ ((3, 3), 1, 0, [[0, 1, 2], [3, 4, 5], [6, 7, 8]]),
123
+ ((3, 3), 3, 0, [4]),
124
+ ((3, 3), 3, (-1, 0), [1]),
125
+ ((3, 3), 3, (1, 0), [7]),
126
+ ((3, 3), 3, (0, -1), [3]),
127
+ ((3, 3), 3, (0, 1), [5]),
128
+ ((3, 3), 3, (-1, -1), [0]),
129
+ ((3, 3), 3, (-1, 1), [2]),
130
+ ((3, 3), 3, (1, -1), [6]),
131
+ ((3, 3), 3, (1, 1), [8]),
132
+ ((4, 4), 1, 0, [[0, 1, 2, 3], [4, 5, 6, 7],
133
+ [8, 9, 10, 11], [12, 13, 14, 15]]),
134
+ ((4, 4), 2, 0, [[5, 7], [13, 15]]),
135
+ ])
136
+ def test_sample(xp, shape, oversample, offset, result):
137
+ x = np.arange(np.prod(shape)).reshape(shape)
138
+ assert np.all(prtools.sample(x, oversample, offset) == result)
139
+
140
+ @pytest.mark.parametrize('xp', BACKENDS)
141
+ @pytest.mark.parametrize('shape, oversample, offset', [
142
+ ((2, 2), 1, 1),
143
+ ((2, 2), 1, -2),
144
+ ((2, 2), 2, 1),
145
+ ((3, 3), 3, -2),
146
+ ((3, 3), 3, 2),
147
+ ((4, 4), 2, 1),
148
+ ((4, 4), 2, -2)
149
+ ])
150
+ def test_sample_value_error(xp, shape, oversample, offset):
151
+ x = np.arange(np.prod(shape)).reshape(shape)
152
+ with pytest.raises(ValueError):
153
+ prtools.sample(x, oversample, offset)
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