phaserEM 0.1__py3-none-any.whl

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 (67) hide show
  1. phaser/__init__.py +0 -0
  2. phaser/__main__.py +5 -0
  3. phaser/engines/common/__init__.py +0 -0
  4. phaser/engines/common/noise_models.py +113 -0
  5. phaser/engines/common/output.py +189 -0
  6. phaser/engines/common/position_correction.py +62 -0
  7. phaser/engines/common/regularizers.py +403 -0
  8. phaser/engines/common/simulation.py +270 -0
  9. phaser/engines/conventional/__init__.py +0 -0
  10. phaser/engines/conventional/run.py +142 -0
  11. phaser/engines/conventional/solvers.py +476 -0
  12. phaser/engines/gradient/run.py +451 -0
  13. phaser/engines/gradient/solvers.py +139 -0
  14. phaser/execute.py +371 -0
  15. phaser/hooks/__init__.py +158 -0
  16. phaser/hooks/hook.py +159 -0
  17. phaser/hooks/io/empad.py +88 -0
  18. phaser/hooks/object.py +25 -0
  19. phaser/hooks/preprocessing.py +133 -0
  20. phaser/hooks/probe.py +24 -0
  21. phaser/hooks/regularization.py +97 -0
  22. phaser/hooks/scan.py +27 -0
  23. phaser/hooks/schedule.py +75 -0
  24. phaser/hooks/solver.py +169 -0
  25. phaser/io/__init__.py +0 -0
  26. phaser/io/empad.py +212 -0
  27. phaser/main.py +92 -0
  28. phaser/plan.py +184 -0
  29. phaser/py.typed +0 -0
  30. phaser/state.py +249 -0
  31. phaser/types.py +305 -0
  32. phaser/utils/__init__.py +0 -0
  33. phaser/utils/_cuda_kernels.py +213 -0
  34. phaser/utils/_jax_kernels.py +98 -0
  35. phaser/utils/analysis.py +263 -0
  36. phaser/utils/image.py +201 -0
  37. phaser/utils/io.py +402 -0
  38. phaser/utils/misc.py +295 -0
  39. phaser/utils/num.py +800 -0
  40. phaser/utils/object.py +578 -0
  41. phaser/utils/optics.py +377 -0
  42. phaser/utils/physics.py +88 -0
  43. phaser/utils/plotting.py +699 -0
  44. phaser/utils/scan.py +60 -0
  45. phaser/web/__init__.py +0 -0
  46. phaser/web/dist/03510a839ccb97b0da9f.module.wasm +0 -0
  47. phaser/web/dist/9573273f862f4f5d9644.module.wasm +0 -0
  48. phaser/web/dist/bundle-dashboard.js +712 -0
  49. phaser/web/dist/bundle-manager.js +210 -0
  50. phaser/web/dist/bundle-vendors-node_modules_wasm-array_wasm_array_js.js +106 -0
  51. phaser/web/dist/style.css +152 -0
  52. phaser/web/notebook.py +269 -0
  53. phaser/web/routes.py +211 -0
  54. phaser/web/server.py +540 -0
  55. phaser/web/slurm.py +180 -0
  56. phaser/web/templates/base.html +13 -0
  57. phaser/web/templates/dashboard.html +14 -0
  58. phaser/web/templates/manager.html +19 -0
  59. phaser/web/types.py +255 -0
  60. phaser/web/util.py +116 -0
  61. phaser/web/worker.py +200 -0
  62. phaserem-0.1.dist-info/METADATA +121 -0
  63. phaserem-0.1.dist-info/RECORD +67 -0
  64. phaserem-0.1.dist-info/WHEEL +5 -0
  65. phaserem-0.1.dist-info/entry_points.txt +2 -0
  66. phaserem-0.1.dist-info/licenses/LICENSE.txt +373 -0
  67. phaserem-0.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,213 @@
1
+
2
+ import functools
3
+ import typing as t
4
+
5
+ import cupy # pyright: ignore[reportMissingImports]
6
+ import numpy
7
+
8
+ # grid
9
+ # block
10
+ # thread
11
+ # blocks contain threads, a grid contains blocks
12
+ # max 1024 threads/block, maximum per axis (1024, 1024, 64)
13
+ # max grid size per axis ~2^32
14
+ # x is the fast axis (warp axis)
15
+
16
+ # TODO:
17
+ # optimize block size, optimize warps?
18
+
19
+ def set_cutouts(obj: cupy.ndarray, cutouts: cupy.ndarray, start_idxs: cupy.ndarray):
20
+ assert obj.ndim >= 2
21
+
22
+ kernel = _get_cutout_kernel(obj.dtype, 'set')
23
+ if kernel is NotImplemented:
24
+ raise NotImplementedError()
25
+
26
+ # output array must be the correct dtype
27
+ assert obj.dtype == cutouts.dtype
28
+ # output array must be contiguous
29
+ assert obj.flags.c_contiguous
30
+
31
+ # cutouts must be the correct shape
32
+ assert cutouts.shape[:-2] == (*start_idxs.shape[:-1], *obj.shape[:-2])
33
+ assert obj.shape[:-2] == cutouts.shape[-obj.ndim:-2]
34
+ assert start_idxs.shape[-1] == 2
35
+
36
+ n_cutouts = numpy.prod((1, *start_idxs.shape[:-1]))
37
+ n_objects = numpy.prod((1, *obj.shape[:-2]))
38
+
39
+ block = (128, 8, 1)
40
+ shape = (cutouts.shape[-1], cutouts.shape[-2], n_cutouts * n_objects)
41
+ grid = tuple((s + b - 1) // b for (s, b) in zip(shape, block))
42
+
43
+ start_idxs = cupy.ascontiguousarray(start_idxs.astype(cupy.uint64))
44
+
45
+ default = numpy.bool_(0).astype(obj.dtype)
46
+ args = (obj, cupy.ascontiguousarray(cutouts), start_idxs, n_cutouts, n_objects, *obj.shape[-2:], *cutouts.shape[-2:], default)
47
+ kernel(grid, block, args)
48
+ return cutouts
49
+
50
+
51
+ def get_cutouts(obj: cupy.ndarray, start_idxs: cupy.ndarray, cutout_shape: t.Tuple[int, int], default: cupy.generic) -> cupy.ndarray:
52
+ assert obj.ndim >= 2
53
+
54
+ kernel = _get_cutout_kernel(obj.dtype, 'get')
55
+ if kernel is NotImplemented:
56
+ raise NotImplementedError()
57
+
58
+ # check start_idxs shape
59
+ assert start_idxs.shape[-1] == 2
60
+ cutouts = cupy.empty((*start_idxs.shape[:-1], *obj.shape[:-2], *cutout_shape), dtype=obj.dtype)
61
+ # output array must be contiguous
62
+ assert cutouts.flags.c_contiguous
63
+
64
+ n_cutouts = numpy.prod((1, *start_idxs.shape[:-1]))
65
+ n_objects = numpy.prod((1, *obj.shape[:-2]))
66
+
67
+ block = (128, 8, 1)
68
+ shape = (cutouts.shape[-1], cutouts.shape[-2], n_cutouts * n_objects)
69
+ grid = tuple((s + b - 1) // b for (s, b) in zip(shape, block))
70
+
71
+ start_idxs = cupy.ascontiguousarray(start_idxs.astype(cupy.uint64))
72
+
73
+ args = (cupy.ascontiguousarray(obj), cutouts, start_idxs, n_cutouts, n_objects, *obj.shape[-2:], *cutouts.shape[-2:], default)
74
+ kernel(grid, block, args)
75
+ return cutouts
76
+
77
+
78
+ def add_cutouts(obj: cupy.ndarray, cutouts: cupy.ndarray, start_idxs: cupy.ndarray):
79
+ assert obj.ndim >= 2
80
+
81
+ kernel = _get_cutout_kernel(obj.dtype, 'add')
82
+ if kernel is NotImplemented:
83
+ raise NotImplementedError()
84
+
85
+ # output array must be the correct dtype
86
+ assert obj.dtype == cutouts.dtype
87
+ # output array must be contiguous
88
+ assert obj.flags.c_contiguous
89
+
90
+ # cutouts must be the correct shape
91
+ assert cutouts.shape[:-2] == (*start_idxs.shape[:-1], *obj.shape[:-2])
92
+ assert obj.shape[:-2] == cutouts.shape[-obj.ndim:-2]
93
+ assert start_idxs.shape[-1] == 2
94
+
95
+ n_cutouts = numpy.prod((1, *start_idxs.shape[:-1]))
96
+ n_objects = numpy.prod((1, *obj.shape[:-2]))
97
+
98
+ block = (128, 8, 1)
99
+ shape = (cutouts.shape[-1], cutouts.shape[-2], n_cutouts * n_objects)
100
+ grid = tuple((s + b - 1) // b for (s, b) in zip(shape, block))
101
+
102
+ start_idxs = cupy.ascontiguousarray(start_idxs.astype(cupy.uint64))
103
+
104
+ default = numpy.bool_(0).astype(obj.dtype)
105
+ args = (obj, cupy.ascontiguousarray(cutouts), start_idxs, n_cutouts, n_objects, *obj.shape[-2:], *cutouts.shape[-2:], default)
106
+ kernel(grid, block, args)
107
+ return cutouts
108
+
109
+
110
+ _CUTOUT_OPERATIONS = {
111
+ 'get': "cutouts[cutout_idx] = obj[obj_idx];",
112
+ 'add': "atomicAdd(&obj[obj_idx], cutouts[cutout_idx]);",
113
+ # TODO does this need to be atomic?
114
+ 'set': "obj[obj_idx] = cutouts[cutout_idx];",
115
+ }
116
+
117
+ _CUTOUT_CONST = {
118
+ 'get': (True, False),
119
+ 'set': (False, True),
120
+ 'add': (False, True),
121
+ }
122
+
123
+ # cupy/_core/_scalar.pyx
124
+ _DTYPE_TO_KERNEL_TYPE: t.Dict[t.Type[numpy.generic], str] = {
125
+ numpy.bool_: "bool",
126
+ numpy.int8: "signed char",
127
+ numpy.uint8: "unsigned char",
128
+ numpy.int16: "signed short",
129
+ numpy.uint16: "unsigned short",
130
+ numpy.int32: "signed int",
131
+ numpy.uint32: "unsigned int",
132
+ numpy.int64: "signed long long",
133
+ numpy.uint64: "unsigned long long",
134
+ numpy.float16: "float16",
135
+ numpy.float32: "float",
136
+ numpy.float64: "double",
137
+ numpy.complex64: "complex<float>",
138
+ numpy.complex128: "complex<double>",
139
+ }
140
+
141
+ @functools.cache
142
+ def _get_cutout_kernel(dtype: numpy.dtype, operation: str) -> cupy.RawKernel:
143
+ try:
144
+ ty = _DTYPE_TO_KERNEL_TYPE[dtype.type]
145
+ op = _CUTOUT_OPERATIONS[operation]
146
+ const_s = tuple("const " if c else "" for c in _CUTOUT_CONST[operation])
147
+ except KeyError:
148
+ return NotImplemented
149
+
150
+ if operation == 'add':
151
+ if ty not in {
152
+ "float16", "float", "double", "complex<float>", "complex<double>",
153
+ "signed int", "unsigned int", "unsigned long long",
154
+ }:
155
+ return NotImplemented
156
+ # double atomicAdd requires compute capability >=6.x
157
+ device = cupy.cuda.Device()
158
+ if ty in ('complex<double>', 'double') and int(device.compute_capability[0]) < 6:
159
+ return NotImplemented
160
+
161
+ kernel_name = f"{operation}_cutouts_{dtype.type.__name__}"
162
+
163
+ kernel = cupy.RawKernel(rf"""
164
+ #include <cupy/complex.cuh>
165
+
166
+ #define T {ty}
167
+
168
+ // support atomicAdd for complex datatypes
169
+ // we can just add real and complex parts separately
170
+ template<typename U>
171
+ __device__ inline void atomicAdd(complex<U> *address, complex<U> val) {{
172
+ atomicAdd(&reinterpret_cast<U*>(address)[0], val.real());
173
+ atomicAdd(&reinterpret_cast<U*>(address)[1], val.imag());
174
+ }}
175
+
176
+ extern "C" __global__
177
+ void {kernel_name}({const_s[0]}T *obj, {const_s[1]}T *cutouts, const long long *start_idxs, long long n_cutouts, long long n_objects,
178
+ long long obj_shape_y, long long obj_shape_x, long long cutout_shape_y, long long cutout_shape_x, T default_value) {{
179
+ size_t j = blockIdx.x * blockDim.x + threadIdx.x;
180
+ size_t i = blockIdx.y * blockDim.y + threadIdx.y;
181
+ size_t cutout_num = blockIdx.z * blockDim.z + threadIdx.z;
182
+
183
+ size_t object_num = cutout_num % n_objects;
184
+ size_t pos_cutout_num = cutout_num / n_objects;
185
+
186
+
187
+
188
+ // check if our thread is outside the requested cutout
189
+ if (pos_cutout_num >= n_cutouts || object_num >= n_objects || i >= cutout_shape_y || j >= cutout_shape_x) {{
190
+ return;
191
+ }}
192
+
193
+ // compute cutout index
194
+ size_t cutout_idx = cutout_shape_y * cutout_shape_x * cutout_num + cutout_shape_x * i + j;
195
+
196
+ // offset to get object location
197
+ long long obj_i = i + start_idxs[2*pos_cutout_num];
198
+ long long obj_j = j + start_idxs[2*pos_cutout_num + 1];
199
+
200
+ // check that it's in bounds, get default value if not
201
+ if (obj_i < 0 || obj_i >= obj_shape_y || obj_j < 0 || obj_j >= obj_shape_x) {{
202
+ {'cutouts[cutout_idx] = default_value;' if op == 'get' else ''}
203
+ return;
204
+ }}
205
+
206
+ // compute object index
207
+ size_t obj_idx = obj_shape_y * obj_shape_x * object_num + obj_shape_x * obj_i + obj_j;
208
+ // and perform our operation
209
+ {op}
210
+ }}
211
+ """, kernel_name)
212
+ kernel.compile()
213
+ return kernel
@@ -0,0 +1,98 @@
1
+
2
+ from functools import partial
3
+ import typing as t
4
+
5
+ from numpy.typing import ArrayLike
6
+ import jax # pyright: ignore[reportMissingImports]
7
+ import jax.numpy as jnp # pyright: ignore[reportMissingImports]
8
+
9
+
10
+ def to_nd(arr: jax.Array, n: int) -> jax.Array:
11
+ if arr.ndim > n:
12
+ arr = arr.reshape(-1, *arr.shape[arr.ndim - n + 1:])
13
+ elif arr.ndim < n:
14
+ arr = jax.lax.expand_dims(arr, [0] * (n - arr.ndim))
15
+
16
+ return arr
17
+
18
+ to_2d = partial(to_nd, n=2)
19
+ to_3d = partial(to_nd, n=3)
20
+
21
+
22
+ def _flatten_ndim(arr: jax.Array, n: int) -> jax.Array:
23
+ return arr.reshape(-1, *arr.shape[n+1:])
24
+
25
+
26
+ @jax.jit
27
+ def set_cutouts(obj: jax.Array, cutouts: jax.Array, start_idxs: jax.Array) -> jax.Array:
28
+ def inner(obj: jax.Array, v: t.Tuple[jax.Array, jax.Array]) -> t.Tuple[jax.Array, None]:
29
+ (cutouts, start_idxs) = v
30
+ obj = jax.vmap(lambda slice, cutouts: jax.lax.dynamic_update_slice(slice, cutouts, start_idxs))(obj, to_3d(cutouts))
31
+ return (obj, None)
32
+
33
+ start_idxs = jnp.atleast_2d(start_idxs)
34
+ (cutouts, start_idxs) = map(lambda a: _flatten_ndim(a, start_idxs.ndim - 2), (cutouts, start_idxs))
35
+ return jax.lax.scan(inner, to_3d(obj), (cutouts, start_idxs))[0].reshape(obj.shape)
36
+
37
+
38
+ @jax.jit
39
+ def add_cutouts(obj: jax.Array, cutouts: jax.Array, start_idxs: jax.Array) -> jax.Array:
40
+ zero_obj = jax.numpy.zeros_like(obj, shape=obj.shape[-2:])
41
+
42
+ def inner(obj: jax.Array, v: t.Tuple[jax.Array, jax.Array]) -> t.Tuple[jax.Array, None]:
43
+ (cutouts, start_idxs) = v
44
+ obj += jax.vmap(lambda cutouts: jax.lax.dynamic_update_slice(zero_obj, cutouts, start_idxs))(to_3d(cutouts))
45
+ return (obj, None)
46
+
47
+ start_idxs = jnp.atleast_2d(start_idxs)
48
+ (cutouts, start_idxs) = map(lambda a: _flatten_ndim(a, start_idxs.ndim - 2), (cutouts, start_idxs))
49
+ return jax.lax.scan(inner, to_3d(obj), (cutouts, start_idxs))[0].reshape(obj.shape)
50
+
51
+
52
+ @partial(jax.jit, static_argnums=2)
53
+ def get_cutouts(obj: jax.Array, start_idxs: jax.Array, cutout_shape: t.Tuple[int, int]) -> jax.Array:
54
+ return jax.vmap(jax.vmap(lambda start_idx, obj: jax.lax.dynamic_slice(obj, start_idx, cutout_shape), (None, 0)), (0, None))(
55
+ to_2d(start_idxs), to_3d(obj)
56
+ ).reshape((*start_idxs.shape[:-1], *obj.shape[:-2], *cutout_shape))
57
+
58
+
59
+ @partial(jax.jit, static_argnums=0)
60
+ def outer(ufunc: t.Any, x: jax.Array, y: jax.Array) -> jax.Array:
61
+ return jax.vmap(jax.vmap(ufunc, (None, 0)), (0, None))(x, y)
62
+
63
+
64
+ @partial(jax.jit, static_argnames=('output_shape', 'order', 'mode', 'cval'))
65
+ def affine_transform(
66
+ input: jax.Array,
67
+ matrix: ArrayLike,
68
+ offset: t.Optional[ArrayLike] = None,
69
+ output_shape: t.Optional[t.Tuple[int, ...]] = None,
70
+ order: int = 1,
71
+ mode: str = 'constant',
72
+ cval: t.Any = 0.0,
73
+ ) -> jax.Array:
74
+ import jax.scipy.ndimage
75
+ jax_mode = {'grid-constant': 'constant', 'grid-wrap': 'wrap'}.get(mode, mode)
76
+
77
+ if output_shape is None:
78
+ output_shape = input.shape
79
+ n_axes = len(output_shape) # num axes to transform over
80
+
81
+ indices = jnp.indices(output_shape, dtype=float)
82
+
83
+ matrix = jnp.array(matrix)
84
+ if matrix.shape == (n_axes + 1, n_axes + 1):
85
+ # homogenous transform matrix
86
+ coords = jnp.tensordot(
87
+ matrix, jnp.stack((*indices, jnp.ones_like(indices[0])), axis=0), axes=1
88
+ )[:-1]
89
+ elif matrix.shape == (n_axes,):
90
+ coords = (indices.T * matrix + jnp.array(offset)).T
91
+ else:
92
+ raise ValueError(f"Expected matrix of shape ({n_axes + 1}, {n_axes + 1}) or ({n_axes},), instead got shape {matrix.shape}")
93
+
94
+ coords += jnp.finfo(coords.dtype).eps
95
+
96
+ return jax.vmap(
97
+ lambda a: jax.scipy.ndimage.map_coordinates(a, tuple(coords), order=order, mode=jax_mode, cval=cval),
98
+ )(to_nd(input, n_axes + 1)).reshape((*input.shape[:-n_axes], *output_shape))
@@ -0,0 +1,263 @@
1
+ import typing as t
2
+
3
+ import numpy
4
+ from numpy.typing import ArrayLike, NDArray
5
+
6
+ from phaser.utils.num import fft2, ifft2, abs2, Sampling, get_array_module, at, to_numpy, NumT
7
+ from phaser.utils.object import ObjectSampling
8
+ from phaser.utils.image import remove_linear_ramp, translation_matrix, rotation_matrix
9
+ from phaser.state import ObjectState
10
+ from phaser.types import cast_length
11
+
12
+
13
+ def split_image(img: NDArray[NumT]) -> t.Tuple[NDArray[NumT], NDArray[NumT], NDArray[NumT], NDArray[NumT]]:
14
+ """
15
+ Split image into 4 subimages (upper left, upper right, lower left, lower right).
16
+ """
17
+ shape = img.shape[-2:]
18
+ # crop image divisible by two
19
+ shape = tuple(s - s%2 for s in shape)
20
+ img = img[..., *(slice(0, s) for s in shape)]
21
+
22
+ return cast_length((
23
+ img[..., slice(row_start, shape[0], 2), slice(col_start, shape[1], 2)]
24
+ for row_start in (0, 1)
25
+ for col_start in (0, 1)
26
+ ), 4)
27
+
28
+
29
+ def fourier_correlate(img1: NDArray[NumT], img2: NDArray[NumT]) -> NDArray[numpy.complex128]:
30
+ assert img1.shape == img2.shape
31
+ from skimage.filters import window
32
+
33
+ xp = get_array_module(img1, img2)
34
+
35
+ win = xp.array(window('hann', img1.shape))
36
+ img1_fft = numpy.fft.fftshift(fft2(img1.astype(numpy.float64) * win), axes=(-2, -1))
37
+ img2_fft = xp.conj(numpy.fft.fftshift(fft2(img2.astype(numpy.float64) * win), axes=(-2, -1)))
38
+ fft1_mag = abs2(img1_fft)
39
+ fft2_mag = abs2(img2_fft)
40
+
41
+ with xp.errstate(invalid='ignore'):
42
+ corr = img1_fft * img2_fft / xp.sqrt(fft1_mag * fft2_mag)
43
+ return xp.nan_to_num(corr, nan=1.)
44
+
45
+
46
+ def _calc_r_binning(
47
+ shape: t.Tuple[int, ...], r_spacing: float = 1.0,
48
+ xp: t.Any = None
49
+ ) -> NDArray[numpy.int64]:
50
+ if xp is None or t.TYPE_CHECKING:
51
+ xp = numpy
52
+
53
+ c_y, c_x = tuple(int(s//2) for s in shape[-2:])
54
+ y, x = xp.indices(shape[-2:])
55
+ r = xp.sqrt((y - c_y)**2 + (x - c_x)**2)
56
+ return xp.floor(r / r_spacing).astype(numpy.int64).ravel()
57
+
58
+
59
+ def contrast_transfer(
60
+ img1: NDArray[NumT], img2: NDArray[NumT], *,
61
+ inscribed: bool = True, r_spacing: float = 1.0
62
+ ) -> t.Tuple[NDArray[numpy.float64], NDArray[numpy.float64], NDArray[numpy.float64]]:
63
+ assert img1.shape == img2.shape
64
+ from skimage.filters import window
65
+
66
+ xp = get_array_module(img1, img2)
67
+
68
+ win = xp.array(window(('tukey', 0.3), img1.shape), dtype=numpy.float64)
69
+ img1_fft = xp.fft.fftshift(fft2(img1.astype(numpy.float64) * win), axes=(-2, -1))
70
+ img2_fft = xp.conj(xp.fft.fftshift(fft2(img2.astype(numpy.float64) * win), axes=(-2, -1)))
71
+ fft1_mag = abs2(img1_fft)
72
+ fft2_mag = abs2(img2_fft)
73
+
74
+ r_i = _calc_r_binning(img1.shape, r_spacing, xp=xp)
75
+ mag1_count = xp.bincount(r_i, fft1_mag.ravel())
76
+ mag2_count = xp.bincount(r_i, fft2_mag.ravel())
77
+
78
+ with numpy.errstate(invalid='ignore', divide='ignore'):
79
+ vals = to_numpy(xp.sqrt(mag1_count / mag2_count))
80
+ vals = numpy.nan_to_num(vals, nan=1.)
81
+
82
+ rs = numpy.linspace(0., len(vals) * r_spacing, len(vals), endpoint=False, dtype=numpy.float64)
83
+ freq = rs / numpy.sqrt(numpy.prod(img1.shape[-2:]))
84
+
85
+ if inscribed:
86
+ # crop to inner radii
87
+ n_r = int(numpy.floor(min(img1.shape[-2:]) / (2.*r_spacing)))
88
+ return (vals[:n_r], rs[:n_r], freq[:n_r])
89
+ return (vals, rs, freq)
90
+
91
+
92
+ def fourier_ring_correlate(
93
+ img1: NDArray[NumT], img2: NDArray[NumT], *,
94
+ inscribed: bool = True, r_spacing: float = 1.0
95
+ ) -> t.Tuple[NDArray[numpy.float64], NDArray[numpy.float64], NDArray[numpy.float64]]:
96
+ assert img1.shape == img2.shape
97
+ from skimage.filters import window
98
+
99
+ xp = get_array_module(img1, img2)
100
+
101
+ win = xp.array(window(('tukey', 0.3), img1.shape), dtype=numpy.float64)
102
+ img1_fft = xp.fft.fftshift(fft2(img1.astype(numpy.float64) * win), axes=(-2, -1))
103
+ img2_fft = xp.conj(xp.fft.fftshift(fft2(img2.astype(numpy.float64) * win), axes=(-2, -1)))
104
+ fft1_mag = abs2(img1_fft)
105
+ fft2_mag = abs2(img2_fft)
106
+
107
+ r_i = _calc_r_binning(img1.shape, r_spacing, xp=xp)
108
+ # sum real and imaginary separately
109
+ real_count = xp.bincount(r_i, (img1_fft * xp.conj(img2_fft)).real.ravel())
110
+ mag1_count = xp.bincount(r_i, fft1_mag.ravel())
111
+ mag2_count = xp.bincount(r_i, fft2_mag.ravel())
112
+
113
+ with numpy.errstate(invalid='ignore', divide='ignore'):
114
+ vals = to_numpy(real_count / xp.sqrt(mag1_count * mag2_count))
115
+ vals = numpy.nan_to_num(vals, posinf=0., nan=1.)
116
+
117
+ #vals = numpy.bincount(r_i.ravel(), corr.ravel()) / numpy.bincount(r_i.ravel())
118
+ rs = numpy.linspace(0., len(vals) * r_spacing, len(vals), endpoint=False, dtype=numpy.float64)
119
+ freq = rs / numpy.sqrt(numpy.prod(img1.shape[-2:]))
120
+
121
+ if inscribed:
122
+ # crop to inner radii
123
+ n_r = int(numpy.floor(min(img1.shape[-2:]) / (2.*r_spacing)))
124
+ return (vals[:n_r], rs[:n_r], freq[:n_r])
125
+ return (vals, rs, freq)
126
+
127
+
128
+ def frc_intersect_threshold(frc: NDArray[numpy.floating], freq: NDArray[numpy.floating], threshold: float) -> t.Tuple[float, float]:
129
+ diff = frc - threshold
130
+ lastdiff = numpy.roll(diff, shift=1)
131
+ # find negative zero crossing
132
+ try:
133
+ i = numpy.nonzero((diff[1:] < 0) & (lastdiff[1:] > 0))[0][0]
134
+ except IndexError:
135
+ raise ValueError("No crossing found when evaluating FRC resolution")
136
+
137
+ m = (diff[i+1] - diff[i]) / (freq[i+1] - freq[i])
138
+ m_f = (frc[i+1] - frc[i]) / (freq[i+1] - freq[i])
139
+ x_d = diff[i]/m
140
+ return (float(freq[i] - x_d), float(frc[i] - x_d * m_f))
141
+
142
+
143
+ def _cross_correlate(x: NDArray[numpy.floating], y: NDArray[numpy.floating], max_shift: float) -> t.Tuple[float, float]:
144
+ xp = get_array_module(x, y)
145
+
146
+ samp = Sampling(tuple(x.shape), sampling=(1.0, 1.0)) # type: ignore
147
+ yy, xx = samp.real_grid(xp=xp)
148
+
149
+ cross_corr = ifft2(fft2(x) * xp.conj(fft2(y))).real
150
+ # limit maximum shift
151
+ cross_corr = at(cross_corr, yy**2 + xx**2 > max_shift**2).set(numpy.nan) # type: ignore
152
+
153
+ max_i = xp.nanargmax(cross_corr)
154
+ y = yy.ravel()[max_i]
155
+ x = xx.ravel()[max_i]
156
+ return (float(y), float(x))
157
+
158
+
159
+ def align_object_to_ground_truth(
160
+ object: ObjectState,
161
+ ground_truth: NDArray[numpy.floating],
162
+ ground_truth_sampling: ArrayLike,
163
+ rotation_angle: float = 0.0,
164
+ refinement_niter: int = 0,
165
+ order: int = 1,
166
+ ) -> t.Tuple[NDArray[numpy.floating], NDArray[numpy.floating]]:
167
+ """
168
+ ground_truth: Ground truth phase (in radians, or radians/angstrom for multislice data)
169
+
170
+ Returns a tuple (object, ground_truth)
171
+ """
172
+ xp = get_array_module(object.data, ground_truth)
173
+ ground_truth = xp.asarray(ground_truth)
174
+ object_phase = xp.angle(xp.asarray(object.data))
175
+
176
+ # normalize multislice objects to radians/angstrom
177
+ if len(object.thicknesses):
178
+ object_phase /= object.thicknesses[:, None, None]
179
+
180
+ # remove linear ramp
181
+ object_roi = object.sampling.get_region_mask(xp=xp)
182
+ object_phase = remove_linear_ramp(object_phase, object_roi)
183
+ object_phase -= xp.nanquantile(object_phase[..., object_roi], 0.01, axis=-1)[:, None, None]
184
+ # and get average
185
+ object_mean = xp.mean(object_phase, axis=0)
186
+
187
+ # initially center ground truth on object roi
188
+ obj_center = object.sampling.get_region_center()
189
+ ground_truth_corner = obj_center - numpy.array(ground_truth.shape) * ground_truth_sampling / 2.
190
+ ground_truth_samp = ObjectSampling(
191
+ t.cast(t.Tuple[int, int], ground_truth.shape),
192
+ sampling=ground_truth_sampling, corner=ground_truth_corner,
193
+ region_min=object.sampling.region_min, region_max=object.sampling.region_max,
194
+ )
195
+ crop = ground_truth_samp.get_region_crop(pad=-0.05 * object.sampling.get_region_extent())
196
+
197
+ # perform initial resampling
198
+ upsamp_obj = object.sampling.resample(object_mean, ground_truth_samp, cval=0., rotation=rotation_angle, order=order)
199
+
200
+ # cross correlate
201
+ max_shift = float(numpy.min(
202
+ (ground_truth_samp.extent - object.sampling.get_region_extent()) / (3.0 * ground_truth_samp.sampling)
203
+ ))
204
+ if max_shift < 0:
205
+ raise ValueError("Error: Ground truth extent smaller than object extent")
206
+ #print(f"max_shift: {max_shift:.3f} px ({max_shift * ground_truth_samp.sampling[0]:.3f} angstrom)")
207
+
208
+ shift = numpy.array(_cross_correlate(upsamp_obj[tuple(crop)], ground_truth[tuple(crop)], max_shift))
209
+
210
+ # shift ground truth to match experiment
211
+ ground_truth_samp = ObjectSampling(
212
+ tuple(ground_truth_samp.shape),
213
+ sampling=ground_truth_sampling, corner=ground_truth_corner + shift * ground_truth_sampling,
214
+ region_min=object.sampling.region_min, region_max=object.sampling.region_max,
215
+ )
216
+ crop = ground_truth_samp.get_region_crop()
217
+
218
+ if refinement_niter == 0:
219
+ # perform final upsampling
220
+ upsamp_obj = object.sampling.resample(object_phase, ground_truth_samp, cval=0., rotation=rotation_angle, order=order)
221
+ return upsamp_obj[(slice(None), *crop)], ground_truth[tuple(crop)]
222
+
223
+ import scipy.optimize
224
+
225
+ def _make_affine(mat: NDArray[numpy.floating]) -> NDArray[numpy.floating]:
226
+ (a, b, c, d, e, f) = mat
227
+ affine = translation_matrix(obj_center) @ numpy.array([
228
+ [a, b, e],
229
+ [c, d, f],
230
+ [0., 0., 1],
231
+ ]) @ rotation_matrix(rotation_angle) @ translation_matrix(-obj_center)
232
+ return affine
233
+
234
+ def align_and_correlate(mat: NDArray[numpy.floating]) -> NDArray[numpy.floating]:
235
+ affine = _make_affine(mat)
236
+ upsamp_obj = object.sampling.resample(object_mean, ground_truth_samp, cval=0., affine=affine, order=order)
237
+ return to_numpy(upsamp_obj[tuple(crop)] - ground_truth[tuple(crop)]).ravel()
238
+
239
+ max_shift_refine = max_shift * ground_truth_samp.sampling[0] / 5.
240
+
241
+ # (a, b, c, d, e, f)
242
+ init_mat = numpy.array([1., 0., 0., 1., 0., 0.])
243
+ min_bound = numpy.array([0.9, -0.1, -0.1, 0.9, -max_shift_refine, -max_shift_refine])
244
+ max_bound = numpy.array([1.1, 0.1, 0.1, 1.1, max_shift_refine, max_shift_refine])
245
+
246
+ result = scipy.optimize.least_squares(align_and_correlate, init_mat, bounds=(min_bound, max_bound),
247
+ method='dogbox', max_nfev=refinement_niter, xtol=1e-4)
248
+ print(f"""\
249
+ Refinement result: {result.message}
250
+ nfev: {result.nfev}
251
+ matrix: {result.x}""")
252
+
253
+ affine = _make_affine(result.x)
254
+ (a, b, c, d, e, f) = result.x
255
+ affine = translation_matrix(obj_center) @ numpy.array([
256
+ [a, b, e],
257
+ [c, d, f],
258
+ [0., 0., 1],
259
+ ]) @ rotation_matrix(rotation_angle) @ translation_matrix(-obj_center)
260
+
261
+ # perform final upsampling
262
+ upsamp_obj = object.sampling.resample(object_phase, ground_truth_samp, cval=0., affine=affine, order=order)
263
+ return upsamp_obj[(slice(None), *crop)], ground_truth[tuple(crop)]