prtools 2.0.2__tar.gz → 2.1.0__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 (34) hide show
  1. {prtools-2.0.2/prtools.egg-info → prtools-2.1.0}/PKG-INFO +2 -2
  2. {prtools-2.0.2 → prtools-2.1.0}/prtools/__init__.py +5 -2
  3. {prtools-2.0.2 → prtools-2.1.0}/prtools/array.py +3 -3
  4. {prtools-2.0.2 → prtools-2.1.0}/prtools/convolve.py +1 -1
  5. prtools-2.1.0/prtools/cost.py +120 -0
  6. {prtools-2.0.2 → prtools-2.1.0}/prtools/jax.py +1 -1
  7. prtools-2.1.0/prtools/mesh.py +40 -0
  8. {prtools-2.0.2 → prtools-2.1.0}/prtools/segmented.py +18 -13
  9. {prtools-2.0.2 → prtools-2.1.0}/prtools/shape.py +91 -97
  10. {prtools-2.0.2 → prtools-2.1.0}/prtools/zernike.py +259 -43
  11. {prtools-2.0.2 → prtools-2.1.0/prtools.egg-info}/PKG-INFO +2 -2
  12. {prtools-2.0.2 → prtools-2.1.0}/prtools.egg-info/SOURCES.txt +5 -1
  13. {prtools-2.0.2 → prtools-2.1.0}/prtools.egg-info/requires.txt +1 -1
  14. {prtools-2.0.2 → prtools-2.1.0}/pyproject.toml +1 -1
  15. {prtools-2.0.2 → prtools-2.1.0}/tests/test_array.py +54 -0
  16. prtools-2.1.0/tests/test_cost.py +103 -0
  17. prtools-2.1.0/tests/test_shape.py +109 -0
  18. prtools-2.1.0/tests/test_zernike.py +246 -0
  19. prtools-2.0.2/prtools/cost.py +0 -115
  20. {prtools-2.0.2 → prtools-2.1.0}/LICENSE +0 -0
  21. {prtools-2.0.2 → prtools-2.1.0}/NOTICE +0 -0
  22. {prtools-2.0.2 → prtools-2.1.0}/README.rst +0 -0
  23. {prtools-2.0.2 → prtools-2.1.0}/prtools/_array_api.py +0 -0
  24. {prtools-2.0.2 → prtools-2.1.0}/prtools/fourier.py +0 -0
  25. {prtools-2.0.2 → prtools-2.1.0}/prtools/misc.py +0 -0
  26. {prtools-2.0.2 → prtools-2.1.0}/prtools/morphology.py +0 -0
  27. {prtools-2.0.2 → prtools-2.1.0}/prtools/sparse.py +0 -0
  28. {prtools-2.0.2 → prtools-2.1.0}/prtools/stats.py +0 -0
  29. {prtools-2.0.2 → prtools-2.1.0}/prtools.egg-info/dependency_links.txt +0 -0
  30. {prtools-2.0.2 → prtools-2.1.0}/prtools.egg-info/top_level.txt +0 -0
  31. {prtools-2.0.2 → prtools-2.1.0}/setup.cfg +0 -0
  32. {prtools-2.0.2 → prtools-2.1.0}/tests/test_convolve.py +0 -0
  33. {prtools-2.0.2 → prtools-2.1.0}/tests/test_fourier.py +0 -0
  34. {prtools-2.0.2 → prtools-2.1.0}/tests/test_jax.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: prtools
3
- Version: 2.0.2
3
+ Version: 2.1.0
4
4
  Summary: Utility functions for image-based phase retrieval
5
5
  Author: Andy Kee
6
6
  License: MIT License
@@ -45,7 +45,7 @@ Requires-Dist: scipy
45
45
  Requires-Dist: array_api_compat
46
46
  Provides-Extra: jax
47
47
  Requires-Dist: jax; extra == "jax"
48
- Requires-Dist: optax; extra == "jax"
48
+ Requires-Dist: optax>=0.2.5; extra == "jax"
49
49
  Dynamic: license-file
50
50
 
51
51
  prtools
@@ -1,4 +1,4 @@
1
- __version__ = '2.0.2'
1
+ __version__ = '2.1.0'
2
2
 
3
3
 
4
4
  from prtools._array_api import use
@@ -33,6 +33,8 @@ from prtools.fourier import dft2, idft2
33
33
 
34
34
  import prtools.jax
35
35
 
36
+ from prtools.mesh import mesh
37
+
36
38
  from prtools.misc import (
37
39
  calcpsf,
38
40
  translation_defocus,
@@ -86,5 +88,6 @@ from prtools.zernike import (
86
88
  zernike_basis,
87
89
  zernike_fit,
88
90
  zernike_remove,
89
- zernike_coordinates
91
+ zernike_coordinates,
92
+ zernike_coeff_legacy
90
93
  )
@@ -75,7 +75,7 @@ def centroid(x, where=None, mode='absolute', indexing='ij'):
75
75
  c = xp.dot(cc[where].ravel(), anorm.ravel())
76
76
 
77
77
  if mode == 'center':
78
- rc, cc = np.array(x.shape)/2
78
+ rc, cc = np.floor(np.array(x.shape)/2)
79
79
  r = r - rc
80
80
  c = c - cc
81
81
 
@@ -261,7 +261,7 @@ def crop(x, shape, shift=None, mode='absolute', indexing='ij'):
261
261
  if shift is not None:
262
262
  shift = np.asarray(shift, dtype=int)
263
263
  if indexing == 'xy':
264
- shift = (-shift[1], shift[0])
264
+ shift = (shift[1], shift[0])
265
265
  if mode == 'absolute':
266
266
  shift = np.array(shift - np.array(x.shape)//2)
267
267
  else:
@@ -381,7 +381,7 @@ def sample(x, oversample, offset=0, axes=(-2, -1), indexing='ij'):
381
381
  axes = tuple(int(a) % x.ndim for a in axes)
382
382
 
383
383
  if indexing == 'xy':
384
- offset = (-offset[1], offset[0])
384
+ offset = (offset[1], offset[0])
385
385
  oversample = (oversample[1], oversample[0])
386
386
 
387
387
  idx = [slice(None)] * x.ndim
@@ -64,7 +64,7 @@ def fftconv(array, kernel, axes=(-2, -1), normalize_kernel=True,
64
64
  (``fftshift_kernel=True``), the DC term is at ``kernel[nr//2, nc//2]``.
65
65
 
66
66
  * A spatial-domain kernal(``fft_kernel=True``) should have
67
- ``sum(kernel) == 1``.
67
+ ``sum(kernel) == 1``.
68
68
  """
69
69
  xp = array_namespace(array)
70
70
 
@@ -0,0 +1,120 @@
1
+ from prtools._array_api import array_namespace
2
+
3
+
4
+ def sserror(data, est, mask=None, gain_bias_invariant=False, ghat=None):
5
+ r"""Compute the normalized sum squared error between two arrays.
6
+
7
+ The error between measured data :math:`\tilde{s}` and estimated data
8
+ :math:`\hat{s}` is computed as the gain invariant error metric
9
+
10
+ .. math::
11
+
12
+ \Phi = \frac{1}{K}\sum_k
13
+ \frac{\sum_{x,y} w(x,y,k)\left[\alpha_k\hat{s}(x,y,k) -
14
+ \tilde{s}(x,y,k)\right]^2}{\sum_{x,y} w(x,y,k)\tilde{s}^2(x,y,k)}
15
+
16
+ for :math:`k = 1, 2, ... K` independent measurements, where
17
+ :math:`w` is a weighting function given by `mask` and each
18
+ :math:`\alpha_k` is chosen to minimize the error. The result is
19
+ independent of any relative scaling (gain) difference between the inputs.
20
+
21
+ If `gain_bias_invariant` is ``True``, the gain and bias invariant error
22
+ metric is computed instead
23
+
24
+ .. math::
25
+
26
+ \Phi = \frac{1}{K}\sum_k
27
+ \frac{\sum_{x,y} w(x,y,k)\left[\alpha_k\hat{g}(x,y,k) -
28
+ \tilde{g}(x,y,k)\right]^2}{\sum_{x,y} w(x,y,k)\tilde{s}^2(x,y,k)}
29
+
30
+ where :math:`\hat{g}` and :math:`\tilde{g}` are :math:`\hat{s}` and
31
+ :math:`\tilde{s}` with their weighted means subtracted and each
32
+ :math:`\alpha_k` is chosen to minimize the error. The result is
33
+ independent of any relative scaling (gain) and offset (bias) differences
34
+ between the inputs.
35
+
36
+ Parameters
37
+ ----------
38
+ data : array_like
39
+ Ideal or measured data with shape (M, N) or (K, M, N)
40
+ est : array_like
41
+ Estimated or modeled data with the same shape as `data`
42
+ mask : array_like, optional
43
+ Weighting function :math:`w` applied to the inputs. Entries equal to
44
+ 1 (or True) are fully included in the error computation and entries
45
+ equal to 0 (or False) are ignored. Intermediate values partially
46
+ weight the corresponding elements. Mask must either have the same
47
+ shape as the inputs or be broadcastable to the same shape. If None
48
+ (default), all elements are equally weighted.
49
+ gain_bias_invariant : bool, optional
50
+ If True, the error is computed to be independent of any gain or bias
51
+ differences between the inputs. If False (default), the error is
52
+ computed to be independent of any gain differences between the inputs.
53
+ ghat : array_like, optional
54
+ Precomputed bias-subtracted estimate :math:`\hat{g}`. Only used if
55
+ `gain_bias_invariant` is True. If None (default), :math:`\hat{g}`
56
+ is computed from `est`.
57
+
58
+ Returns
59
+ -------
60
+ error : float
61
+ Normalized sum squared error
62
+
63
+ References
64
+ ----------
65
+ [1] S. Thurman and J. Fienup, "Phase retrieval with signal bias", J. Opt. Soc. Am. A/Vol. 26, No. 4 (2009)
66
+
67
+ [2] A. Jurling and J. Fienup, "Applications of algorithmic differentiation to phase retrieval algorithms", J. Opt. Soc. Am. A/Vol. 31, No. 7 (2014)
68
+
69
+ """
70
+ xp = array_namespace(data, est)
71
+
72
+ if gain_bias_invariant:
73
+ if ghat is None:
74
+ ghat = g(est, mask, xp) # Eq. 12
75
+ mtil = g(data, mask, xp) # Eq. 13
76
+ mhat = ghat
77
+ else:
78
+ mtil = data
79
+ mhat = est
80
+
81
+ alpha = _a(mtil, mhat, mask, xp) # Eq. 4 or Eq. 14
82
+ alpha = alpha[..., xp.newaxis, xp.newaxis]
83
+
84
+ num = _wsum(xp.square(alpha * mhat - mtil), mask, xp)
85
+ den = _wsum(xp.square(data), mask, xp)
86
+
87
+ # Eq. 3 or Eq. 11
88
+ return xp.mean(num/den)
89
+
90
+
91
+ def _wsum(x, mask, xp):
92
+ # Weighted sum over the last two axes
93
+ if mask is not None:
94
+ x = mask * x
95
+ return xp.sum(x, axis=(-2, -1))
96
+
97
+
98
+ def g(data, mask, xp):
99
+ # This function implements Eqs. 12 and 13 in [1]
100
+ if mask is None:
101
+ numer = xp.sum(data, axis=(-2, -1))
102
+ denom = xp.prod(xp.asarray(data.shape[-2:]))
103
+ else:
104
+ numer = xp.sum(mask * data, axis=(-2, -1))
105
+ denom = xp.sum(mask, axis=(-2, -1))
106
+ x = numer/denom
107
+ if data.ndim == 3:
108
+ x = x[:, xp.newaxis, xp.newaxis]
109
+ return data - x
110
+
111
+
112
+ def _a(gtil, ghat, mask, xp):
113
+ # Eq. 4 or Eq. 14 in [1]
114
+ try:
115
+ return _wsum(gtil * ghat, mask, xp)/_wsum(xp.square(ghat), mask, xp)
116
+ except FloatingPointError as e:
117
+ if xp.all(ghat == 0):
118
+ raise ZeroDivisionError('ghat must be nonzero')
119
+ else:
120
+ raise e
@@ -87,7 +87,7 @@ def lbfgs(fn, x0, gtol=None, maxiter=None, callback=None, fn_kwargs=None):
87
87
  _, state = carry
88
88
  iter_num = otu.tree_get(state, 'count')
89
89
  grad = otu.tree_get(state, 'grad')
90
- grad_norm = otu.tree_l2_norm(grad)
90
+ grad_norm = otu.tree_norm(grad)
91
91
  return (iter_num == 0) | ((iter_num < maxiter) & (grad_norm >= gtol))
92
92
 
93
93
  init_carry = (x0, opt.init(x0))
@@ -0,0 +1,40 @@
1
+ import numpy as np
2
+
3
+
4
+ def mesh(shape, shift=(0, 0), angle=0, indexing='ij'):
5
+ """Generate a standard mesh.
6
+
7
+ Parameters
8
+ ----------
9
+ shape : array_like
10
+ Size of output in pixels (nrows, ncols)
11
+ shift : (2,) array_like, optional
12
+ Coordinate translation from center of containing array.
13
+ Default is (0, 0). Shift is applied according to the specified
14
+ ``indexing`` scheme.
15
+ angle : float, optional
16
+ Mesh rotation in degrees. When ``indexing = 'xy'`` the rotation is
17
+ measured from the +x axis toward the +y axis. When ``indexing = 'ij'``
18
+ the rotation is measured from the +i axis toward the +j axis. Note
19
+ that rotations occur about the shifted center if ``shift`` is nonzero.
20
+ Default is 0.
21
+ indexing : {'ij', 'xy'}, optional
22
+ Matrix ('ij', default) or Cartesian ('xy') indexing of mesh.
23
+
24
+ Returns
25
+ -------
26
+ list of ndarrays
27
+
28
+ """
29
+ shape = np.broadcast_to(np.asarray(shape), (2,))
30
+ if indexing == 'xy':
31
+ shape = np.flip(shape)
32
+ angle = np.radians(angle)
33
+
34
+ x1, x2 = np.meshgrid(np.arange(shape[0]) - np.floor(shape[0]/2) - shift[0],
35
+ np.arange(shape[1]) - np.floor(shape[1]/2) - shift[1],
36
+ indexing=indexing)
37
+
38
+ X1 = x1 * np.cos(angle) + x2 * np.sin(angle)
39
+ X2 = x1 * -np.sin(angle) + x2 * np.cos(angle)
40
+ return X1, X2
@@ -5,7 +5,7 @@ import numpy as np
5
5
  import prtools
6
6
 
7
7
 
8
- def hex_segments(rings, seg_radius, seg_gap, rotate=False, antialias=True,
8
+ def hex_segments(rings, seg_radius, seg_gap, rotate=True, antialias=True,
9
9
  flatten=False, pad=2, drop=(0,)):
10
10
  """Draw a segmented aperture made up of hexagonal segments
11
11
 
@@ -18,9 +18,8 @@ def hex_segments(rings, seg_radius, seg_gap, rotate=False, antialias=True,
18
18
  seg_gap : float
19
19
  Spacing between adjacent segments in pixels
20
20
  rotate : bool, optional
21
- Rotate segments so that flat sides are aligned with the Y direction
22
- instead of the default orientation which is aligned with the X
23
- direction.
21
+ If True (default), the segment mask is rotated so that flat sides are
22
+ aligned with the X direction instead of the Y direction.
24
23
  antialias : bool, optional
25
24
  If True (default), the segment edges are antialiased.
26
25
  flatten : bool, optional
@@ -29,8 +28,8 @@ def hex_segments(rings, seg_radius, seg_gap, rotate=False, antialias=True,
29
28
  pad : int, optional
30
29
  Number of additional pixels to zero-pad output by. Default is 2.
31
30
  drop : list_like, optional
32
- Segments to exclude from output. 0 represents the central segment.
33
-
31
+ Segments to exclude from output. 0 represents the central segment.
32
+
34
33
  Returns
35
34
  -------
36
35
  ndarray
@@ -63,20 +62,24 @@ def hex_segments(rings, seg_radius, seg_gap, rotate=False, antialias=True,
63
62
  mask = []
64
63
 
65
64
  if 0 not in drop:
66
- mask.append(prtools.hexagon(shape, seg_radius, shift=(0, 0), antialias=antialias, rotate=rotate))
65
+ mask.append(prtools.hexagon(shape, seg_radius, shift=(0, 0),
66
+ antialias=antialias, rotate=rotate,
67
+ indexing='ij'))
67
68
 
68
69
  seg = 1
69
70
  for ring in range(1, rings+1):
70
71
  for h in hex_ring(ring):
71
72
  r, c = hex_to_rc(h, seg_radius + seg_gap/2, rotate)
72
73
  if seg not in drop:
73
- mask.append(prtools.hexagon(shape, seg_radius, shift=(r, c), antialias=antialias, rotate=rotate))
74
+ mask.append(prtools.hexagon(shape, seg_radius, shift=(r, c),
75
+ antialias=antialias, rotate=rotate,
76
+ indexing='ij'))
74
77
  seg += 1
75
-
78
+
76
79
  mask = np.asarray(mask)
77
80
  if flatten:
78
81
  mask = np.sum(mask, axis=0)
79
-
82
+
80
83
  return mask
81
84
 
82
85
 
@@ -84,6 +87,8 @@ def hex_segments(rings, seg_radius, seg_gap, rotate=False, antialias=True,
84
87
  # exactly from http://www.redblobgames.com/grids/hexagons/
85
88
 
86
89
  _Hex = collections.namedtuple("Hex", ["q", "r", "s"])
90
+
91
+
87
92
  def Hex(q, r, s):
88
93
  assert not (round(q + r + s) != 0), "q + r + s must be 0"
89
94
  return _Hex(q, r, s)
@@ -93,7 +98,7 @@ def hex_add(a, b):
93
98
  return Hex(a.q + b.q, a.r + b.r, a.s + b.s)
94
99
 
95
100
 
96
- hex_directions = [Hex(1, 0, -1), Hex(1, -1, 0), Hex(0, -1, 1),
101
+ hex_directions = [Hex(1, 0, -1), Hex(1, -1, 0), Hex(0, -1, 1),
97
102
  Hex(-1, 0, 1), Hex(-1, 1, 0), Hex(0, 1, -1)]
98
103
 
99
104
 
@@ -117,13 +122,13 @@ def hex_ring(radius):
117
122
 
118
123
 
119
124
  def hex_to_xy(hex, radius, rotate=False):
120
- if rotate:
125
+ if not rotate:
121
126
  x = radius * (np.sqrt(3) * hex.q + np.sqrt(3)/2 * hex.r)
122
127
  y = radius * (3/2 * hex.r)
123
128
  else:
124
129
  x = radius * (3/2 * hex.q)
125
130
  y = radius * (np.sqrt(3)/2 * hex.q + np.sqrt(3) * hex.r)
126
-
131
+
127
132
  return x, y
128
133
 
129
134