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.
- {prtools-2.0.2/prtools.egg-info → prtools-2.1.0}/PKG-INFO +2 -2
- {prtools-2.0.2 → prtools-2.1.0}/prtools/__init__.py +5 -2
- {prtools-2.0.2 → prtools-2.1.0}/prtools/array.py +3 -3
- {prtools-2.0.2 → prtools-2.1.0}/prtools/convolve.py +1 -1
- prtools-2.1.0/prtools/cost.py +120 -0
- {prtools-2.0.2 → prtools-2.1.0}/prtools/jax.py +1 -1
- prtools-2.1.0/prtools/mesh.py +40 -0
- {prtools-2.0.2 → prtools-2.1.0}/prtools/segmented.py +18 -13
- {prtools-2.0.2 → prtools-2.1.0}/prtools/shape.py +91 -97
- {prtools-2.0.2 → prtools-2.1.0}/prtools/zernike.py +259 -43
- {prtools-2.0.2 → prtools-2.1.0/prtools.egg-info}/PKG-INFO +2 -2
- {prtools-2.0.2 → prtools-2.1.0}/prtools.egg-info/SOURCES.txt +5 -1
- {prtools-2.0.2 → prtools-2.1.0}/prtools.egg-info/requires.txt +1 -1
- {prtools-2.0.2 → prtools-2.1.0}/pyproject.toml +1 -1
- {prtools-2.0.2 → prtools-2.1.0}/tests/test_array.py +54 -0
- prtools-2.1.0/tests/test_cost.py +103 -0
- prtools-2.1.0/tests/test_shape.py +109 -0
- prtools-2.1.0/tests/test_zernike.py +246 -0
- prtools-2.0.2/prtools/cost.py +0 -115
- {prtools-2.0.2 → prtools-2.1.0}/LICENSE +0 -0
- {prtools-2.0.2 → prtools-2.1.0}/NOTICE +0 -0
- {prtools-2.0.2 → prtools-2.1.0}/README.rst +0 -0
- {prtools-2.0.2 → prtools-2.1.0}/prtools/_array_api.py +0 -0
- {prtools-2.0.2 → prtools-2.1.0}/prtools/fourier.py +0 -0
- {prtools-2.0.2 → prtools-2.1.0}/prtools/misc.py +0 -0
- {prtools-2.0.2 → prtools-2.1.0}/prtools/morphology.py +0 -0
- {prtools-2.0.2 → prtools-2.1.0}/prtools/sparse.py +0 -0
- {prtools-2.0.2 → prtools-2.1.0}/prtools/stats.py +0 -0
- {prtools-2.0.2 → prtools-2.1.0}/prtools.egg-info/dependency_links.txt +0 -0
- {prtools-2.0.2 → prtools-2.1.0}/prtools.egg-info/top_level.txt +0 -0
- {prtools-2.0.2 → prtools-2.1.0}/setup.cfg +0 -0
- {prtools-2.0.2 → prtools-2.1.0}/tests/test_convolve.py +0 -0
- {prtools-2.0.2 → prtools-2.1.0}/tests/test_fourier.py +0 -0
- {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
|
|
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
|
|
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 = (
|
|
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 = (
|
|
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
|
-
|
|
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.
|
|
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=
|
|
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
|
-
|
|
22
|
-
|
|
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),
|
|
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),
|
|
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
|
|