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.
- phaser/__init__.py +0 -0
- phaser/__main__.py +5 -0
- phaser/engines/common/__init__.py +0 -0
- phaser/engines/common/noise_models.py +113 -0
- phaser/engines/common/output.py +189 -0
- phaser/engines/common/position_correction.py +62 -0
- phaser/engines/common/regularizers.py +403 -0
- phaser/engines/common/simulation.py +270 -0
- phaser/engines/conventional/__init__.py +0 -0
- phaser/engines/conventional/run.py +142 -0
- phaser/engines/conventional/solvers.py +476 -0
- phaser/engines/gradient/run.py +451 -0
- phaser/engines/gradient/solvers.py +139 -0
- phaser/execute.py +371 -0
- phaser/hooks/__init__.py +158 -0
- phaser/hooks/hook.py +159 -0
- phaser/hooks/io/empad.py +88 -0
- phaser/hooks/object.py +25 -0
- phaser/hooks/preprocessing.py +133 -0
- phaser/hooks/probe.py +24 -0
- phaser/hooks/regularization.py +97 -0
- phaser/hooks/scan.py +27 -0
- phaser/hooks/schedule.py +75 -0
- phaser/hooks/solver.py +169 -0
- phaser/io/__init__.py +0 -0
- phaser/io/empad.py +212 -0
- phaser/main.py +92 -0
- phaser/plan.py +184 -0
- phaser/py.typed +0 -0
- phaser/state.py +249 -0
- phaser/types.py +305 -0
- phaser/utils/__init__.py +0 -0
- phaser/utils/_cuda_kernels.py +213 -0
- phaser/utils/_jax_kernels.py +98 -0
- phaser/utils/analysis.py +263 -0
- phaser/utils/image.py +201 -0
- phaser/utils/io.py +402 -0
- phaser/utils/misc.py +295 -0
- phaser/utils/num.py +800 -0
- phaser/utils/object.py +578 -0
- phaser/utils/optics.py +377 -0
- phaser/utils/physics.py +88 -0
- phaser/utils/plotting.py +699 -0
- phaser/utils/scan.py +60 -0
- phaser/web/__init__.py +0 -0
- phaser/web/dist/03510a839ccb97b0da9f.module.wasm +0 -0
- phaser/web/dist/9573273f862f4f5d9644.module.wasm +0 -0
- phaser/web/dist/bundle-dashboard.js +712 -0
- phaser/web/dist/bundle-manager.js +210 -0
- phaser/web/dist/bundle-vendors-node_modules_wasm-array_wasm_array_js.js +106 -0
- phaser/web/dist/style.css +152 -0
- phaser/web/notebook.py +269 -0
- phaser/web/routes.py +211 -0
- phaser/web/server.py +540 -0
- phaser/web/slurm.py +180 -0
- phaser/web/templates/base.html +13 -0
- phaser/web/templates/dashboard.html +14 -0
- phaser/web/templates/manager.html +19 -0
- phaser/web/types.py +255 -0
- phaser/web/util.py +116 -0
- phaser/web/worker.py +200 -0
- phaserem-0.1.dist-info/METADATA +121 -0
- phaserem-0.1.dist-info/RECORD +67 -0
- phaserem-0.1.dist-info/WHEEL +5 -0
- phaserem-0.1.dist-info/entry_points.txt +2 -0
- phaserem-0.1.dist-info/licenses/LICENSE.txt +373 -0
- 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))
|
phaser/utils/analysis.py
ADDED
|
@@ -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)]
|