pytme 0.1.8__cp311-cp311-macosx_14_0_arm64.whl → 0.2.0__cp311-cp311-macosx_14_0_arm64.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.
- pytme-0.2.0.data/scripts/match_template.py +1019 -0
- pytme-0.2.0.data/scripts/postprocess.py +570 -0
- {pytme-0.1.8.data → pytme-0.2.0.data}/scripts/preprocessor_gui.py +244 -60
- {pytme-0.1.8.dist-info → pytme-0.2.0.dist-info}/METADATA +3 -1
- pytme-0.2.0.dist-info/RECORD +72 -0
- {pytme-0.1.8.dist-info → pytme-0.2.0.dist-info}/WHEEL +1 -1
- scripts/extract_candidates.py +218 -0
- scripts/match_template.py +459 -218
- pytme-0.1.8.data/scripts/match_template.py → scripts/match_template_filters.py +459 -218
- scripts/postprocess.py +380 -435
- scripts/preprocessor_gui.py +244 -60
- scripts/refine_matches.py +218 -0
- tme/__init__.py +2 -1
- tme/__version__.py +1 -1
- tme/analyzer.py +533 -78
- tme/backends/cupy_backend.py +80 -15
- tme/backends/npfftw_backend.py +35 -6
- tme/backends/pytorch_backend.py +15 -7
- tme/density.py +173 -78
- tme/extensions.cpython-311-darwin.so +0 -0
- tme/matching_constrained.py +195 -0
- tme/matching_data.py +78 -32
- tme/matching_exhaustive.py +369 -221
- tme/matching_memory.py +1 -0
- tme/matching_optimization.py +753 -649
- tme/matching_utils.py +152 -8
- tme/orientations.py +561 -0
- tme/preprocessing/__init__.py +2 -0
- tme/preprocessing/_utils.py +176 -0
- tme/preprocessing/composable_filter.py +30 -0
- tme/preprocessing/compose.py +52 -0
- tme/preprocessing/frequency_filters.py +322 -0
- tme/preprocessing/tilt_series.py +967 -0
- tme/preprocessor.py +35 -25
- tme/structure.py +2 -37
- pytme-0.1.8.data/scripts/postprocess.py +0 -625
- pytme-0.1.8.dist-info/RECORD +0 -61
- {pytme-0.1.8.data → pytme-0.2.0.data}/scripts/estimate_ram_usage.py +0 -0
- {pytme-0.1.8.data → pytme-0.2.0.data}/scripts/preprocess.py +0 -0
- {pytme-0.1.8.dist-info → pytme-0.2.0.dist-info}/LICENSE +0 -0
- {pytme-0.1.8.dist-info → pytme-0.2.0.dist-info}/entry_points.txt +0 -0
- {pytme-0.1.8.dist-info → pytme-0.2.0.dist-info}/top_level.txt +0 -0
tme/matching_optimization.py
CHANGED
@@ -15,96 +15,306 @@ from scipy.optimize import (
|
|
15
15
|
differential_evolution,
|
16
16
|
LinearConstraint,
|
17
17
|
basinhopping,
|
18
|
+
minimize,
|
18
19
|
)
|
19
|
-
from scipy.ndimage import laplace
|
20
|
+
from scipy.ndimage import laplace, map_coordinates
|
20
21
|
from scipy.spatial import KDTree
|
21
22
|
|
23
|
+
from .types import ArrayLike
|
24
|
+
from .backends import backend
|
25
|
+
from .matching_data import MatchingData
|
22
26
|
from .matching_utils import rigid_transform, euler_to_rotationmatrix
|
27
|
+
from .matching_exhaustive import normalize_under_mask
|
23
28
|
|
24
29
|
|
25
|
-
|
30
|
+
def _format_rigid_transform(x: Tuple[float]) -> Tuple[ArrayLike, ArrayLike]:
|
26
31
|
"""
|
27
|
-
|
32
|
+
Returns a formated rigid transform definition.
|
28
33
|
|
29
34
|
Parameters
|
30
35
|
----------
|
31
|
-
|
32
|
-
|
36
|
+
x : tuple of float
|
37
|
+
Even-length tuple where the first half represents translations and the
|
38
|
+
second half Euler angles in zyx convention for each dimension.
|
39
|
+
|
40
|
+
Returns
|
41
|
+
-------
|
42
|
+
Tuple[ArrayLike, ArrayLike]
|
43
|
+
Translation of length [d, ] and rotation matrix with dimension [d x d].
|
44
|
+
"""
|
45
|
+
split = len(x) // 2
|
46
|
+
translation, angles = x[:split], x[split:]
|
47
|
+
|
48
|
+
translation = backend.to_backend_array(translation)
|
49
|
+
rotation_matrix = euler_to_rotationmatrix(backend.to_numpy_array(angles))
|
50
|
+
rotation_matrix = backend.to_backend_array(rotation_matrix)
|
51
|
+
|
52
|
+
return translation, rotation_matrix
|
53
|
+
|
54
|
+
|
55
|
+
class _MatchDensityToDensity(ABC):
|
56
|
+
"""
|
57
|
+
Parameters
|
58
|
+
----------
|
59
|
+
target : array_like
|
60
|
+
The target density array.
|
61
|
+
template : array_like
|
62
|
+
The template density array.
|
63
|
+
template_mask : array_like, optional
|
64
|
+
Mask array for the template density.
|
65
|
+
target_mask : array_like, optional
|
66
|
+
Mask array for the target density.
|
67
|
+
pad_target_edges : bool, optional
|
68
|
+
Whether to pad the edges of the target density array. Default is False.
|
69
|
+
pad_fourier : bool, optional
|
70
|
+
Whether to pad the Fourier transform of the target and template densities.
|
71
|
+
rotate_mask : bool, optional
|
72
|
+
Whether to rotate the mask arrays along with the densities. Default is True.
|
73
|
+
interpolation_order : int, optional
|
74
|
+
The interpolation order for rigid transforms. Default is 1.
|
75
|
+
negate_score : bool, optional
|
76
|
+
Whether the final score should be multiplied by negative one. Default is True.
|
77
|
+
**kwargs : Dict, optional
|
78
|
+
Keyword arguments propagated to downstream functions.
|
79
|
+
"""
|
80
|
+
|
81
|
+
def __init__(
|
82
|
+
self,
|
83
|
+
target: ArrayLike,
|
84
|
+
template: ArrayLike,
|
85
|
+
template_mask: ArrayLike = None,
|
86
|
+
target_mask: ArrayLike = None,
|
87
|
+
pad_target_edges: bool = False,
|
88
|
+
pad_fourier: bool = False,
|
89
|
+
rotate_mask: bool = True,
|
90
|
+
interpolation_order: int = 1,
|
91
|
+
negate_score: bool = True,
|
92
|
+
**kwargs: Dict,
|
93
|
+
):
|
94
|
+
self.rotate_mask = rotate_mask
|
95
|
+
self.interpolation_order = interpolation_order
|
96
|
+
|
97
|
+
matching_data = MatchingData(target=target, template=template)
|
98
|
+
if template_mask is not None:
|
99
|
+
matching_data.template_mask = template_mask
|
100
|
+
if target_mask is not None:
|
101
|
+
matching_data.target_mask = target_mask
|
102
|
+
|
103
|
+
target_pad = matching_data.target_padding(pad_target=pad_target_edges)
|
104
|
+
matching_data = matching_data.subset_by_slice(target_pad=target_pad)
|
105
|
+
|
106
|
+
fast_shape, fast_ft_shape, fourier_shift = matching_data.fourier_padding(
|
107
|
+
pad_fourier=pad_fourier
|
108
|
+
)
|
109
|
+
|
110
|
+
self.target = backend.topleft_pad(matching_data.target, fast_shape)
|
111
|
+
self.target_mask = matching_data.target_mask
|
112
|
+
|
113
|
+
self.template = matching_data.template
|
114
|
+
self.template_rot = backend.preallocate_array(
|
115
|
+
fast_shape, backend._default_dtype
|
116
|
+
)
|
117
|
+
|
118
|
+
self.template_mask, self.template_mask_rot = 1, 1
|
119
|
+
rotate_mask = False if matching_data.template_mask is None else rotate_mask
|
120
|
+
if matching_data.template_mask is not None:
|
121
|
+
self.template_mask = matching_data.template_mask
|
122
|
+
self.template_mask_rot = backend.topleft_pad(
|
123
|
+
matching_data.template_mask, fast_shape
|
124
|
+
)
|
125
|
+
|
126
|
+
self.score_sign = -1 if negate_score else 1
|
127
|
+
|
128
|
+
if hasattr(self, "_post_init"):
|
129
|
+
self._post_init(**kwargs)
|
130
|
+
|
131
|
+
@staticmethod
|
132
|
+
def rigid_transform(
|
133
|
+
arr,
|
134
|
+
rotation_matrix,
|
135
|
+
translation,
|
136
|
+
arr_mask=None,
|
137
|
+
out=None,
|
138
|
+
out_mask=None,
|
139
|
+
order: int = 1,
|
140
|
+
use_geometric_center: bool = False,
|
141
|
+
):
|
142
|
+
rotate_mask = arr_mask is not None
|
143
|
+
return_type = (out is None) + 2 * rotate_mask * (out_mask is None)
|
144
|
+
translation = np.zeros(arr.ndim) if translation is None else translation
|
145
|
+
|
146
|
+
center = np.floor(np.array(arr.shape) / 2)[:, None]
|
147
|
+
grid = np.indices(arr.shape, dtype=np.float32).reshape(arr.ndim, -1)
|
148
|
+
np.subtract(grid, center, out=grid)
|
149
|
+
np.matmul(rotation_matrix.T, grid, out=grid)
|
150
|
+
np.add(grid, center, out=grid)
|
151
|
+
|
152
|
+
if out is None:
|
153
|
+
out = np.zeros_like(arr)
|
154
|
+
|
155
|
+
map_coordinates(arr, grid, order=order, output=out.ravel())
|
156
|
+
|
157
|
+
if out_mask is None and arr_mask is not None:
|
158
|
+
out_mask = np.zeros_like(arr_mask)
|
159
|
+
|
160
|
+
if arr_mask is not None:
|
161
|
+
map_coordinates(arr_mask, grid, order=order, output=out_mask.ravel())
|
162
|
+
|
163
|
+
match return_type:
|
164
|
+
case 0:
|
165
|
+
return None
|
166
|
+
case 1:
|
167
|
+
return out
|
168
|
+
case 2:
|
169
|
+
return out_mask
|
170
|
+
case 3:
|
171
|
+
return out, out_mask
|
172
|
+
|
173
|
+
def score_translation(self, x: Tuple[float]) -> float:
|
174
|
+
"""
|
175
|
+
Computes the score after a given translation.
|
176
|
+
|
177
|
+
Parameters
|
178
|
+
----------
|
179
|
+
x : tuple of float
|
180
|
+
Tuple representing the translation transformation in each dimension.
|
181
|
+
|
182
|
+
Returns
|
183
|
+
-------
|
184
|
+
float
|
185
|
+
The score obtained for the translation transformation.
|
186
|
+
"""
|
187
|
+
return self.score((*x, *[0 for _ in range(len(x))]))
|
188
|
+
|
189
|
+
def score_angles(self, x: Tuple[float]) -> float:
|
190
|
+
"""
|
191
|
+
Computes the score after a given rotation.
|
192
|
+
|
193
|
+
Parameters
|
194
|
+
----------
|
195
|
+
x : tuple of float
|
196
|
+
Tuple of Euler angles in zyx convention for each dimension.
|
197
|
+
|
198
|
+
Returns
|
199
|
+
-------
|
200
|
+
float
|
201
|
+
The score obtained for the rotation transformation.
|
202
|
+
"""
|
203
|
+
return self.score((*[0 for _ in range(len(x))], *x))
|
204
|
+
|
205
|
+
def score(self, x: Tuple[float]) -> float:
|
206
|
+
"""
|
207
|
+
Compute the matching score for the given transformation parameters.
|
208
|
+
|
209
|
+
Parameters
|
210
|
+
----------
|
211
|
+
x : tuple of float
|
212
|
+
Even-length tuple where the first half represents translations and the
|
213
|
+
second half Euler angles in zyx convention for each dimension.
|
214
|
+
|
215
|
+
Returns
|
216
|
+
-------
|
217
|
+
float
|
218
|
+
The matching score obtained for the transformation.
|
219
|
+
"""
|
220
|
+
translation, rotation_matrix = _format_rigid_transform(x)
|
221
|
+
kw_dict = {
|
222
|
+
"arr": self.template,
|
223
|
+
"rotation_matrix": rotation_matrix,
|
224
|
+
"translation": translation,
|
225
|
+
"out": self.template_rot,
|
226
|
+
"use_geometric_center": False,
|
227
|
+
"order": self.interpolation_order,
|
228
|
+
}
|
229
|
+
if self.rotate_mask:
|
230
|
+
kw_dict["arr_mask"] = self.template_mask
|
231
|
+
kw_dict["out_mask"] = self.template_mask_rot
|
232
|
+
|
233
|
+
self.rigid_transform(**kw_dict)
|
234
|
+
|
235
|
+
return self()
|
236
|
+
|
237
|
+
@abstractmethod
|
238
|
+
def __call__(self) -> float:
|
239
|
+
"""Returns the score of the current configuration."""
|
240
|
+
|
241
|
+
|
242
|
+
class _MatchCoordinatesToDensity(_MatchDensityToDensity):
|
243
|
+
"""
|
244
|
+
Parameters
|
245
|
+
----------
|
246
|
+
target : NDArray
|
247
|
+
A d-dimensional target to match the template coordinate set to.
|
33
248
|
template_coordinates : NDArray
|
34
|
-
|
35
|
-
target_weights : NDArray
|
36
|
-
The weights of the target.
|
249
|
+
Template coordinate array with shape [d x N].
|
37
250
|
template_weights : NDArray
|
38
|
-
|
39
|
-
sampling_rate : NDArray
|
40
|
-
The size of the voxel.
|
251
|
+
Template weight array with shape [N].
|
41
252
|
template_mask_coordinates : NDArray, optional
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
253
|
+
Template mask coordinates with shape [d x N].
|
254
|
+
target_mask : NDArray, optional
|
255
|
+
A d-dimensional mask to be applied to the target.
|
256
|
+
negate_score : bool, optional
|
257
|
+
Whether the final score should be multiplied by negative one. Default is True.
|
258
|
+
**kwargs : Dict, optional
|
259
|
+
Keyword arguments propagated to downstream functions.
|
47
260
|
"""
|
48
261
|
|
49
262
|
def __init__(
|
50
263
|
self,
|
51
|
-
|
264
|
+
target: NDArray,
|
52
265
|
template_coordinates: NDArray,
|
53
|
-
target_weights: NDArray,
|
54
266
|
template_weights: NDArray,
|
55
|
-
sampling_rate: NDArray,
|
56
267
|
template_mask_coordinates: NDArray = None,
|
57
|
-
|
58
|
-
|
268
|
+
target_mask: NDArray = None,
|
269
|
+
negate_score: bool = True,
|
270
|
+
**kwargs: Dict,
|
59
271
|
):
|
60
|
-
target, _, origin = FitRefinement.array_from_coordinates(
|
61
|
-
target_coordinates, target_weights, sampling_rate
|
62
|
-
)
|
63
272
|
self.target_density = target
|
64
|
-
self.
|
65
|
-
self.sampling_rate = sampling_rate
|
273
|
+
self.target_mask_density = target_mask
|
66
274
|
|
67
275
|
self.template_weights = template_weights
|
68
276
|
self.template_coordinates = template_coordinates
|
69
|
-
self.template_coordinates_rotated = np.
|
70
|
-
|
277
|
+
self.template_coordinates_rotated = np.copy(self.template_coordinates).astype(
|
278
|
+
np.float32
|
71
279
|
)
|
72
280
|
|
73
|
-
self.
|
74
|
-
|
75
|
-
target_mask, *_ = FitRefinement.array_from_coordinates(
|
76
|
-
coordinates=target_mask_coordinates.astype(np.float32),
|
77
|
-
weights=np.ones(target_mask_coordinates.shape[1]),
|
78
|
-
shape=self.target_density.shape,
|
79
|
-
origin=self.target_origin,
|
80
|
-
sampling_rate=self.sampling_rate,
|
81
|
-
)
|
82
|
-
self.target_mask_density = target_mask
|
83
|
-
|
84
|
-
self.template_mask_coordinates = None
|
85
|
-
self.template_mask_coordinates_rotated = None
|
281
|
+
self.template_mask_coordinates = template_mask_coordinates
|
282
|
+
self.template_mask_coordinates_rotated = template_mask_coordinates
|
86
283
|
if template_mask_coordinates is not None:
|
87
|
-
self.
|
88
|
-
|
89
|
-
|
90
|
-
|
284
|
+
self.template_mask_coordinates_rotated = np.copy(
|
285
|
+
self.template_mask_coordinates
|
286
|
+
).astype(np.float32)
|
287
|
+
|
288
|
+
self.denominator = 1
|
289
|
+
self.score_sign = -1 if negate_score else 1
|
290
|
+
|
291
|
+
self.in_volume, self.in_volume_mask = self.map_coordinates_to_array(
|
292
|
+
coordinates=self.template_coordinates_rotated,
|
293
|
+
coordinates_mask=self.template_mask_coordinates_rotated,
|
294
|
+
array_origin=backend.zeros(target.ndim),
|
295
|
+
array_shape=self.target_density.shape,
|
296
|
+
sampling_rate=backend.full(target.ndim, fill_value=1),
|
297
|
+
)
|
91
298
|
|
92
|
-
|
299
|
+
if hasattr(self, "_post_init"):
|
300
|
+
self._post_init(**kwargs)
|
301
|
+
|
302
|
+
def score(self, x: Tuple[float]):
|
93
303
|
"""
|
94
|
-
|
304
|
+
Compute the matching score for the given transformation parameters.
|
95
305
|
|
96
306
|
Parameters
|
97
307
|
----------
|
98
|
-
x :
|
99
|
-
|
308
|
+
x : tuple of float
|
309
|
+
Even-length tuple where the first half represents translations and the
|
310
|
+
second half Euler angles in zyx convention for each dimension.
|
100
311
|
|
101
312
|
Returns
|
102
313
|
-------
|
103
314
|
float
|
104
|
-
The
|
315
|
+
The matching score obtained for the transformation.
|
105
316
|
"""
|
106
|
-
translation,
|
107
|
-
rotation_matrix = euler_to_rotationmatrix(rotation)
|
317
|
+
translation, rotation_matrix = _format_rigid_transform(x)
|
108
318
|
|
109
319
|
rigid_transform(
|
110
320
|
coordinates=self.template_coordinates,
|
@@ -116,54 +326,132 @@ class MatchCoordinatesToDensity(ABC):
|
|
116
326
|
use_geometric_center=False,
|
117
327
|
)
|
118
328
|
|
119
|
-
|
329
|
+
self.in_volume, self.in_volume_mask = self.map_coordinates_to_array(
|
120
330
|
coordinates=self.template_coordinates_rotated,
|
121
331
|
coordinates_mask=self.template_mask_coordinates_rotated,
|
122
|
-
array_origin=
|
332
|
+
array_origin=backend.zeros(rotation_matrix.shape[0]),
|
123
333
|
array_shape=self.target_density.shape,
|
124
|
-
sampling_rate=
|
334
|
+
sampling_rate=backend.full(rotation_matrix.shape[0], fill_value=1),
|
125
335
|
)
|
126
336
|
|
127
|
-
return
|
128
|
-
transformed_coordinates=mapping[0],
|
129
|
-
transformed_coordinates_mask=mapping[1],
|
130
|
-
in_volume=mapping[2],
|
131
|
-
in_volume_mask=mapping[3],
|
132
|
-
)
|
337
|
+
return self()
|
133
338
|
|
134
|
-
@
|
135
|
-
def
|
339
|
+
@staticmethod
|
340
|
+
def array_from_coordinates(
|
341
|
+
coordinates: NDArray,
|
342
|
+
weights: NDArray,
|
343
|
+
sampling_rate: NDArray,
|
344
|
+
origin: NDArray = None,
|
345
|
+
shape: NDArray = None,
|
346
|
+
) -> Tuple[NDArray, NDArray, NDArray]:
|
136
347
|
"""
|
137
|
-
|
348
|
+
Create a volume from coordinates, using given weights and voxel size.
|
349
|
+
|
350
|
+
Parameters
|
351
|
+
----------
|
352
|
+
coordinates : NDArray
|
353
|
+
An array representing the coordinates [d x N].
|
354
|
+
weights : NDArray
|
355
|
+
An array representing the weights for each coordinate [N].
|
356
|
+
sampling_rate : NDArray
|
357
|
+
The size of a voxel in the volume.
|
358
|
+
origin : NDArray, optional
|
359
|
+
The origin of the volume.
|
360
|
+
shape : NDArray, optional
|
361
|
+
The shape of the volume.
|
138
362
|
|
139
|
-
|
140
|
-
|
141
|
-
|
363
|
+
Returns
|
364
|
+
-------
|
365
|
+
tuple
|
366
|
+
Returns the generated volume, positions of coordinates, and origin.
|
142
367
|
"""
|
368
|
+
if origin is None:
|
369
|
+
origin = coordinates.min(axis=1)
|
143
370
|
|
371
|
+
positions = np.divide(coordinates - origin[:, None], sampling_rate[:, None])
|
372
|
+
positions = positions.astype(int)
|
144
373
|
|
145
|
-
|
146
|
-
|
147
|
-
A class to template match coordinate sets.
|
374
|
+
if shape is None:
|
375
|
+
shape = positions.max(axis=1) + 1
|
148
376
|
|
377
|
+
arr = np.zeros(shape, dtype=np.float32)
|
378
|
+
np.add.at(arr, tuple(positions), weights)
|
379
|
+
return arr, positions, origin
|
380
|
+
|
381
|
+
@staticmethod
|
382
|
+
def map_coordinates_to_array(
|
383
|
+
coordinates: NDArray,
|
384
|
+
array_shape: NDArray,
|
385
|
+
array_origin: NDArray,
|
386
|
+
sampling_rate: NDArray,
|
387
|
+
coordinates_mask: NDArray = None,
|
388
|
+
) -> Tuple[NDArray, NDArray]:
|
389
|
+
"""
|
390
|
+
Map coordinates to a volume based on given voxel size and origin.
|
391
|
+
|
392
|
+
Parameters
|
393
|
+
----------
|
394
|
+
coordinates : NDArray
|
395
|
+
An array representing the coordinates to be mapped [d x N].
|
396
|
+
array_shape : NDArray
|
397
|
+
The shape of the array to which the coordinates are mapped.
|
398
|
+
array_origin : NDArray
|
399
|
+
The origin of the array to which the coordinates are mapped.
|
400
|
+
sampling_rate : NDArray
|
401
|
+
The size of a voxel in the array.
|
402
|
+
coordinates_mask : NDArray, optional
|
403
|
+
An array representing the mask for the coordinates [d x T].
|
404
|
+
|
405
|
+
Returns
|
406
|
+
-------
|
407
|
+
tuple
|
408
|
+
Returns transformed coordinates, transformed coordinates mask,
|
409
|
+
mask for in_volume points, and mask for in_volume points in mask.
|
410
|
+
"""
|
411
|
+
np.divide(
|
412
|
+
coordinates - array_origin[:, None], sampling_rate[:, None], out=coordinates
|
413
|
+
)
|
414
|
+
|
415
|
+
in_volume = np.logical_and(
|
416
|
+
coordinates < np.array(array_shape)[:, None],
|
417
|
+
coordinates >= 0,
|
418
|
+
).min(axis=0)
|
419
|
+
|
420
|
+
in_volume_mask = None
|
421
|
+
if coordinates_mask is not None:
|
422
|
+
np.divide(
|
423
|
+
coordinates_mask - array_origin[:, None],
|
424
|
+
sampling_rate[:, None],
|
425
|
+
out=coordinates_mask,
|
426
|
+
)
|
427
|
+
in_volume_mask = np.logical_and(
|
428
|
+
coordinates_mask < np.array(array_shape)[:, None],
|
429
|
+
coordinates_mask >= 0,
|
430
|
+
).min(axis=0)
|
431
|
+
|
432
|
+
return in_volume, in_volume_mask
|
433
|
+
|
434
|
+
|
435
|
+
class _MatchCoordinatesToCoordinates(_MatchDensityToDensity):
|
436
|
+
"""
|
149
437
|
Parameters
|
150
438
|
----------
|
151
439
|
target_coordinates : NDArray
|
152
|
-
The coordinates of the target.
|
440
|
+
The coordinates of the target with shape [d x N].
|
153
441
|
template_coordinates : NDArray
|
154
|
-
The coordinates of the template.
|
442
|
+
The coordinates of the template with shape [d x T].
|
155
443
|
target_weights : NDArray
|
156
|
-
The weights of the target.
|
444
|
+
The weights of the target with shape [N].
|
157
445
|
template_weights : NDArray
|
158
|
-
The weights of the template.
|
159
|
-
sampling_rate : NDArray
|
160
|
-
The size of the voxel.
|
446
|
+
The weights of the template with shape [T].
|
161
447
|
template_mask_coordinates : NDArray, optional
|
162
|
-
The coordinates of the template mask. Default is None.
|
448
|
+
The coordinates of the template mask with shape [d x T]. Default is None.
|
163
449
|
target_mask_coordinates : NDArray, optional
|
164
|
-
The coordinates of the target mask. Default is None.
|
165
|
-
|
166
|
-
|
450
|
+
The coordinates of the target mask with shape [d X N]. Default is None.
|
451
|
+
negate_score : bool, optional
|
452
|
+
Whether the final score should be multiplied by negative one. Default is True.
|
453
|
+
**kwargs : Dict, optional
|
454
|
+
Keyword arguments propagated to downstream functions.
|
167
455
|
"""
|
168
456
|
|
169
457
|
def __init__(
|
@@ -174,6 +462,7 @@ class MatchCoordinatesToCoordinates(ABC):
|
|
174
462
|
template_weights: NDArray,
|
175
463
|
template_mask_coordinates: NDArray = None,
|
176
464
|
target_mask_coordinates: NDArray = None,
|
465
|
+
negate_score: bool = True,
|
177
466
|
**kwargs,
|
178
467
|
):
|
179
468
|
self.target_weights = target_weights
|
@@ -193,23 +482,27 @@ class MatchCoordinatesToCoordinates(ABC):
|
|
193
482
|
self.template_mask_coordinates_rotated = np.empty(
|
194
483
|
self.template_mask_coordinates.shape, dtype=np.float32
|
195
484
|
)
|
485
|
+
self.score_sign = -1 if negate_score else 1
|
486
|
+
|
487
|
+
if hasattr(self, "_post_init"):
|
488
|
+
self._post_init(**kwargs)
|
196
489
|
|
197
|
-
def
|
490
|
+
def score(self, x: Tuple[float]) -> float:
|
198
491
|
"""
|
199
|
-
|
492
|
+
Compute the matching score for the given transformation parameters.
|
200
493
|
|
201
494
|
Parameters
|
202
495
|
----------
|
203
|
-
x :
|
204
|
-
|
496
|
+
x : tuple of float
|
497
|
+
Even-length tuple where the first half represents translations and the
|
498
|
+
second half Euler angles in zyx convention for each dimension.
|
205
499
|
|
206
500
|
Returns
|
207
501
|
-------
|
208
502
|
float
|
209
|
-
The
|
503
|
+
The matching score obtained for the transformation.
|
210
504
|
"""
|
211
|
-
translation,
|
212
|
-
rotation_matrix = euler_to_rotationmatrix(rotation)
|
505
|
+
translation, rotation_matrix = _format_rigid_transform(x)
|
213
506
|
|
214
507
|
rigid_transform(
|
215
508
|
coordinates=self.template_coordinates,
|
@@ -221,106 +514,161 @@ class MatchCoordinatesToCoordinates(ABC):
|
|
221
514
|
use_geometric_center=False,
|
222
515
|
)
|
223
516
|
|
224
|
-
return
|
517
|
+
return self(
|
225
518
|
transformed_coordinates=self.template_coordinates_rotated,
|
226
519
|
transformed_coordinates_mask=self.template_mask_coordinates_rotated,
|
227
520
|
)
|
228
521
|
|
229
|
-
@abstractmethod
|
230
|
-
def scoring_function(*args, **kwargs):
|
231
|
-
"""
|
232
|
-
Computes a scoring metric for a given set of coordinates.
|
233
522
|
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
523
|
+
class FLC(_MatchDensityToDensity):
|
524
|
+
"""
|
525
|
+
Computes a normalized cross-correlation score of a target f a template g
|
526
|
+
and a mask m:
|
238
527
|
|
528
|
+
.. math::
|
239
529
|
|
240
|
-
|
241
|
-
|
242
|
-
|
530
|
+
\\frac{CC(f, \\frac{g*m - \\overline{g*m}}{\\sigma_{g*m}})}
|
531
|
+
{N_m * \\sqrt{
|
532
|
+
\\frac{CC(f^2, m)}{N_m} - (\\frac{CC(f, m)}{N_m})^2}
|
533
|
+
}
|
243
534
|
|
244
|
-
|
535
|
+
Where:
|
245
536
|
|
246
537
|
.. math::
|
247
538
|
|
248
|
-
|
539
|
+
CC(f,g) = \\mathcal{F}^{-1}(\\mathcal{F}(f) \\cdot \\mathcal{F}(g)^*)
|
540
|
+
|
541
|
+
and Nm is the number of voxels within the template mask m.
|
249
542
|
|
543
|
+
References
|
544
|
+
----------
|
545
|
+
.. [1] W. Wan, S. Khavnekar, J. Wagner, P. Erdmann, and W. Baumeister
|
546
|
+
Microsc. Microanal. 26, 2516 (2020)
|
547
|
+
.. [2] T. Hrabe, Y. Chen, S. Pfeffer, L. Kuhn Cuellar, A.-V. Mangold,
|
548
|
+
and F. Förster, J. Struct. Biol. 178, 177 (2012).
|
250
549
|
"""
|
251
550
|
|
252
|
-
|
253
|
-
super().__init__(**kwargs)
|
254
|
-
self.denominator = 1
|
551
|
+
__doc__ += _MatchDensityToDensity.__doc__
|
255
552
|
|
256
|
-
def
|
257
|
-
self
|
258
|
-
|
259
|
-
transformed_coordinates_mask: NDArray,
|
260
|
-
in_volume: NDArray,
|
261
|
-
in_volume_mask: NDArray,
|
262
|
-
) -> float:
|
263
|
-
"""
|
264
|
-
Compute the Cross-Correlation score.
|
553
|
+
def _post_init(self, **kwargs: Dict):
|
554
|
+
if self.target_mask is not None:
|
555
|
+
backend.multiply(self.target, self.target_mask, out=self.target)
|
265
556
|
|
266
|
-
|
267
|
-
----------
|
268
|
-
transformed_coordinates : NDArray
|
269
|
-
Transformed coordinates.
|
270
|
-
transformed_coordinates_mask : NDArray
|
271
|
-
Mask for the transformed coordinates.
|
272
|
-
in_volume : NDArray
|
273
|
-
Binary mask indicating which ``transformed_coordinates`` are in the
|
274
|
-
target volume.
|
275
|
-
in_volume_mask : NDArray
|
276
|
-
Binary mask indicating which ``transformed_coordinates`` are in the
|
277
|
-
target mask volume.
|
557
|
+
self.target_square = backend.square(self.target)
|
278
558
|
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
"""
|
284
|
-
score = np.dot(
|
285
|
-
self.target_density[tuple(transformed_coordinates[:, in_volume])],
|
286
|
-
self.template_weights[in_volume],
|
559
|
+
normalize_under_mask(
|
560
|
+
template=self.template,
|
561
|
+
mask=self.template_mask,
|
562
|
+
mask_intensity=backend.sum(self.template_mask),
|
287
563
|
)
|
288
|
-
|
564
|
+
|
565
|
+
self.template = backend.reverse(self.template)
|
566
|
+
self.template_mask = backend.reverse(self.template_mask)
|
567
|
+
|
568
|
+
def __call__(self) -> float:
|
569
|
+
"""Returns the score of the current configuration."""
|
570
|
+
n_observations = backend.sum(self.template_mask_rot)
|
571
|
+
|
572
|
+
normalize_under_mask(
|
573
|
+
template=self.template_rot,
|
574
|
+
mask=self.template_mask_rot,
|
575
|
+
mask_intensity=n_observations,
|
576
|
+
)
|
577
|
+
|
578
|
+
ex2 = backend.sum(
|
579
|
+
backend.divide(
|
580
|
+
backend.sum(
|
581
|
+
backend.multiply(self.target_square, self.template_mask_rot),
|
582
|
+
),
|
583
|
+
n_observations,
|
584
|
+
)
|
585
|
+
)
|
586
|
+
e2x = backend.square(
|
587
|
+
backend.divide(
|
588
|
+
backend.sum(backend.multiply(self.target, self.template_mask_rot)),
|
589
|
+
n_observations,
|
590
|
+
)
|
591
|
+
)
|
592
|
+
|
593
|
+
denominator = backend.maximum(backend.subtract(ex2, e2x), 0.0)
|
594
|
+
denominator = backend.sqrt(denominator)
|
595
|
+
denominator = backend.multiply(denominator, n_observations)
|
596
|
+
|
597
|
+
overlap = backend.sum(backend.multiply(self.template_rot, self.target))
|
598
|
+
|
599
|
+
score = backend.divide(overlap, denominator) * self.score_sign
|
289
600
|
return score
|
290
601
|
|
291
602
|
|
292
|
-
class
|
603
|
+
class CrossCorrelation(_MatchCoordinatesToDensity):
|
604
|
+
"""
|
605
|
+
Computes the Cross-Correlation score as:
|
606
|
+
|
607
|
+
.. math::
|
608
|
+
|
609
|
+
\\text{score} = \\text{target_weights} \\cdot \\text{template_weights}
|
293
610
|
"""
|
294
|
-
Class representing the Laplace Cross-Correlation matching score.
|
295
611
|
|
296
|
-
|
297
|
-
|
612
|
+
__doc__ += _MatchCoordinatesToDensity.__doc__
|
613
|
+
|
614
|
+
def __call__(self) -> float:
|
615
|
+
"""Returns the score of the current configuration."""
|
616
|
+
try:
|
617
|
+
score = np.dot(
|
618
|
+
self.target_density[
|
619
|
+
tuple(
|
620
|
+
self.template_coordinates_rotated[:, self.in_volume].astype(int)
|
621
|
+
)
|
622
|
+
],
|
623
|
+
self.template_weights[self.in_volume],
|
624
|
+
)
|
625
|
+
except:
|
626
|
+
print(self.template_coordinates_rotated[:, self.in_volume].astype(int))
|
627
|
+
print(self.target_density.shape)
|
628
|
+
print(self.in_volume)
|
629
|
+
coordinates = self.template_coordinates_rotated[:, self.in_volume].astype(
|
630
|
+
int
|
631
|
+
)
|
632
|
+
in_volume = np.logical_and(
|
633
|
+
coordinates < np.array(self.target_density.shape)[:, None],
|
634
|
+
coordinates >= 0,
|
635
|
+
).min(axis=0)
|
636
|
+
print(in_volume)
|
637
|
+
|
638
|
+
raise ValueError()
|
639
|
+
score /= self.denominator
|
640
|
+
return score * self.score_sign
|
641
|
+
|
642
|
+
|
643
|
+
class LaplaceCrossCorrelation(CrossCorrelation):
|
644
|
+
"""
|
645
|
+
Uses the same formalism as :py:class:`CrossCorrelation` but with Laplace
|
646
|
+
filtered weights (:math:`\\nabla^{2}`):
|
298
647
|
|
299
648
|
.. math::
|
300
649
|
|
301
650
|
\\text{score} = \\nabla^{2} \\text{target_weights} \\cdot
|
302
651
|
\\nabla^{2} \\text{template_weights}
|
303
|
-
|
304
652
|
"""
|
305
653
|
|
306
|
-
|
307
|
-
|
654
|
+
__doc__ += _MatchCoordinatesToDensity.__doc__
|
655
|
+
|
656
|
+
def _post_init(self, **kwargs):
|
308
657
|
self.target_density = laplace(self.target_density)
|
309
658
|
|
310
|
-
arr, positions, _ =
|
311
|
-
self.template_coordinates,
|
659
|
+
arr, positions, _ = self.array_from_coordinates(
|
660
|
+
self.template_coordinates,
|
661
|
+
self.template_weights,
|
662
|
+
np.ones(self.template_coordinates.shape[0]),
|
312
663
|
)
|
313
664
|
self.template_weights = laplace(arr)[tuple(positions)]
|
314
665
|
|
315
666
|
|
316
667
|
class NormalizedCrossCorrelation(CrossCorrelation):
|
317
668
|
"""
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
`template_weights` with the product of their norms. This normalization ensures
|
322
|
-
the score lies between -1 and 1, providing a measure of similarity that's invariant
|
323
|
-
to scale.
|
669
|
+
Computes a normalized version of the :py:class:`CrossCorrelation` score based
|
670
|
+
on the dot product of `target_weights` and `template_weights`, in order to
|
671
|
+
reduce bias to regions of high local energy.
|
324
672
|
|
325
673
|
.. math::
|
326
674
|
|
@@ -338,11 +686,11 @@ class NormalizedCrossCorrelation(CrossCorrelation):
|
|
338
686
|
\\text{template_norm} = ||\\text{template_weights}||
|
339
687
|
|
340
688
|
Here, :math:`||.||` denotes the L2 (Euclidean) norm.
|
341
|
-
|
342
689
|
"""
|
343
690
|
|
344
|
-
|
345
|
-
|
691
|
+
__doc__ += _MatchCoordinatesToDensity.__doc__
|
692
|
+
|
693
|
+
def _post_init(self, **kwargs):
|
346
694
|
target_norm = np.linalg.norm(self.target_density[self.target_density != 0])
|
347
695
|
template_norm = np.linalg.norm(self.template_weights)
|
348
696
|
self.denominator = np.fmax(target_norm * template_norm, np.finfo(float).eps)
|
@@ -350,14 +698,8 @@ class NormalizedCrossCorrelation(CrossCorrelation):
|
|
350
698
|
|
351
699
|
class NormalizedCrossCorrelationMean(NormalizedCrossCorrelation):
|
352
700
|
"""
|
353
|
-
|
354
|
-
|
355
|
-
This class extends the Normalized Cross-Correlation by computing the score
|
356
|
-
after subtracting the mean from both `target_weights` and `template_weights`.
|
357
|
-
This modification enhances the matching score's sensitivity to patterns
|
358
|
-
over flat regions in the data.
|
359
|
-
|
360
|
-
Mathematically, the Mean Normalized Cross-Correlation score is computed as:
|
701
|
+
Computes a similar score than :py:class:`NormalizedCrossCorrelation`, but
|
702
|
+
additionally factors in the mean of template and target.
|
361
703
|
|
362
704
|
.. math::
|
363
705
|
|
@@ -381,21 +723,21 @@ class NormalizedCrossCorrelationMean(NormalizedCrossCorrelation):
|
|
381
723
|
computes the mean of the respective weights.
|
382
724
|
"""
|
383
725
|
|
726
|
+
__doc__ += _MatchCoordinatesToDensity.__doc__
|
727
|
+
|
384
728
|
def __init__(self, **kwargs):
|
385
|
-
|
386
|
-
kwargs["
|
387
|
-
|
729
|
+
kwargs["target"] = np.subtract(kwargs["target"], kwargs["target"].mean())
|
730
|
+
kwargs["template_weights"] = np.subtract(
|
731
|
+
kwargs["template_weights"], kwargs["template_weights"].mean()
|
732
|
+
)
|
388
733
|
super().__init__(**kwargs)
|
389
734
|
|
390
735
|
|
391
|
-
class MaskedCrossCorrelation(
|
736
|
+
class MaskedCrossCorrelation(_MatchCoordinatesToDensity):
|
392
737
|
"""
|
393
|
-
Class representing the Masked Cross-Correlation matching score.
|
394
|
-
|
395
738
|
The Masked Cross-Correlation computes the similarity between `target_weights`
|
396
|
-
and `template_weights` under respective masks. The score
|
397
|
-
|
398
|
-
missing or masked data.
|
739
|
+
and `template_weights` under respective masks. The score provides a measure of
|
740
|
+
similarity even in the presence of missing or masked data.
|
399
741
|
|
400
742
|
The formula for the Masked Cross-Correlation is:
|
401
743
|
|
@@ -437,54 +779,37 @@ class MaskedCrossCorrelation(MatchCoordinatesToDensity):
|
|
437
779
|
.. [1] Masked FFT registration, Dirk Padfield, CVPR 2010 conference
|
438
780
|
"""
|
439
781
|
|
440
|
-
|
441
|
-
super().__init__(**kwargs)
|
442
|
-
|
443
|
-
def scoring_function(
|
444
|
-
self,
|
445
|
-
transformed_coordinates: NDArray,
|
446
|
-
transformed_coordinates_mask: NDArray,
|
447
|
-
in_volume: NDArray,
|
448
|
-
in_volume_mask: NDArray,
|
449
|
-
) -> float:
|
450
|
-
"""
|
451
|
-
Compute the Masked Cross-Correlation score.
|
452
|
-
|
453
|
-
Parameters
|
454
|
-
----------
|
455
|
-
transformed_coordinates : NDArray
|
456
|
-
Transformed coordinates.
|
457
|
-
transformed_coordinates_mask : NDArray
|
458
|
-
Mask for the transformed coordinates.
|
459
|
-
in_volume : NDArray
|
460
|
-
Binary mask indicating which ``transformed_coordinates`` are in the
|
461
|
-
target volume.
|
462
|
-
in_volume_mask : NDArray
|
463
|
-
Binary mask indicating which ``transformed_coordinates`` are in the
|
464
|
-
target mask volume.
|
782
|
+
__doc__ += _MatchCoordinatesToDensity.__doc__
|
465
783
|
|
466
|
-
|
467
|
-
|
468
|
-
float
|
469
|
-
The Masked Cross-Correlation score.
|
470
|
-
"""
|
784
|
+
def __call__(self) -> float:
|
785
|
+
"""Returns the score of the current configuration."""
|
471
786
|
mask_overlap = np.sum(
|
472
787
|
self.target_mask_density[
|
473
|
-
tuple(
|
788
|
+
tuple(
|
789
|
+
self.template_mask_coordinates_rotated[
|
790
|
+
:, self.in_volume_mask
|
791
|
+
].astype(int)
|
792
|
+
)
|
474
793
|
],
|
475
794
|
)
|
476
795
|
mask_overlap = np.fmax(mask_overlap, np.finfo(float).eps)
|
477
796
|
|
478
797
|
mask_target = self.target_density[
|
479
|
-
tuple(
|
798
|
+
tuple(
|
799
|
+
self.template_mask_coordinates_rotated[:, self.in_volume_mask].astype(
|
800
|
+
int
|
801
|
+
)
|
802
|
+
)
|
480
803
|
]
|
481
804
|
denominator1 = np.subtract(
|
482
805
|
np.sum(mask_target**2),
|
483
806
|
np.divide(np.square(np.sum(mask_target)), mask_overlap),
|
484
807
|
)
|
485
808
|
mask_template = np.multiply(
|
486
|
-
self.template_weights[in_volume],
|
487
|
-
self.target_mask_density[
|
809
|
+
self.template_weights[self.in_volume],
|
810
|
+
self.target_mask_density[
|
811
|
+
tuple(self.template_coordinates_rotated[:, self.in_volume].astype(int))
|
812
|
+
],
|
488
813
|
)
|
489
814
|
denominator2 = np.subtract(
|
490
815
|
np.sum(mask_template**2),
|
@@ -496,8 +821,10 @@ class MaskedCrossCorrelation(MatchCoordinatesToDensity):
|
|
496
821
|
denominator = np.sqrt(np.multiply(denominator1, denominator2))
|
497
822
|
|
498
823
|
numerator = np.dot(
|
499
|
-
self.target_density[
|
500
|
-
|
824
|
+
self.target_density[
|
825
|
+
tuple(self.template_coordinates_rotated[:, self.in_volume].astype(int))
|
826
|
+
],
|
827
|
+
self.template_weights[self.in_volume],
|
501
828
|
)
|
502
829
|
|
503
830
|
numerator -= np.divide(
|
@@ -505,16 +832,14 @@ class MaskedCrossCorrelation(MatchCoordinatesToDensity):
|
|
505
832
|
)
|
506
833
|
|
507
834
|
if denominator == 0:
|
508
|
-
return 0
|
835
|
+
return 0.0
|
509
836
|
|
510
837
|
score = numerator / denominator
|
511
|
-
return score
|
838
|
+
return float(score * self.score_sign)
|
512
839
|
|
513
840
|
|
514
|
-
class PartialLeastSquareDifference(
|
841
|
+
class PartialLeastSquareDifference(_MatchCoordinatesToDensity):
|
515
842
|
"""
|
516
|
-
Class representing the Partial Least Square Difference matching score.
|
517
|
-
|
518
843
|
The Partial Least Square Difference (PLSQ) between the target :math:`f` and the
|
519
844
|
template :math:`g` is calculated as:
|
520
845
|
|
@@ -529,109 +854,32 @@ class PartialLeastSquareDifference(MatchCoordinatesToDensity):
|
|
529
854
|
pp. 333--343, 2011. DOI: https://doi.org/10.1016/j.jsb.2011.01.012
|
530
855
|
"""
|
531
856
|
|
532
|
-
|
533
|
-
super().__init__(**kwargs)
|
534
|
-
|
535
|
-
def scoring_function(
|
536
|
-
self,
|
537
|
-
transformed_coordinates: NDArray,
|
538
|
-
transformed_coordinates_mask: NDArray,
|
539
|
-
in_volume: NDArray,
|
540
|
-
in_volume_mask: NDArray,
|
541
|
-
) -> float:
|
542
|
-
"""
|
543
|
-
Compute the Partial Least Square Difference score.
|
544
|
-
|
545
|
-
Given the transformed coordinates and their associated mask, this function
|
546
|
-
computes the difference between target and template densities.
|
857
|
+
__doc__ += _MatchCoordinatesToDensity.__doc__
|
547
858
|
|
548
|
-
|
549
|
-
|
550
|
-
transformed_coordinates : NDArray
|
551
|
-
Transformed coordinates.
|
552
|
-
transformed_coordinates_mask : NDArray
|
553
|
-
Mask for the transformed coordinates.
|
554
|
-
in_volume : NDArray
|
555
|
-
Binary mask indicating which ``transformed_coordinates`` are in the
|
556
|
-
target volume.
|
557
|
-
in_volume_mask : NDArray
|
558
|
-
Binary mask indicating which ``transformed_coordinates`` are in the
|
559
|
-
target mask volume.
|
560
|
-
|
561
|
-
Returns
|
562
|
-
-------
|
563
|
-
float
|
564
|
-
The negative of the Partial Least Square Difference score.
|
565
|
-
"""
|
859
|
+
def __call__(self) -> float:
|
860
|
+
"""Returns the score of the current configuration."""
|
566
861
|
score = np.sum(
|
567
862
|
np.square(
|
568
863
|
np.subtract(
|
569
|
-
self.target_density[
|
570
|
-
|
864
|
+
self.target_density[
|
865
|
+
tuple(
|
866
|
+
self.template_coordinates_rotated[:, self.in_volume].astype(
|
867
|
+
int
|
868
|
+
)
|
869
|
+
)
|
870
|
+
],
|
871
|
+
self.template_weights[self.in_volume],
|
571
872
|
)
|
572
873
|
)
|
573
874
|
)
|
574
|
-
score += np.sum(np.square(self.template_weights[np.invert(in_volume)]))
|
575
|
-
|
576
|
-
return -score
|
577
|
-
|
578
|
-
|
579
|
-
class Chamfer(MatchCoordinatesToCoordinates):
|
580
|
-
"""
|
581
|
-
Class representing the Chamfer matching score.
|
582
|
-
|
583
|
-
The Chamfer distance is computed as:
|
584
|
-
|
585
|
-
.. math::
|
586
|
-
|
587
|
-
\\text{d(f,g)} = \\frac{1}{|X|} \\sum_{\\mathbf{f}_i \\in X}
|
588
|
-
\\inf_{\\mathbf{g} \\in Y} ||\\mathbf{f}_i - \\mathbf{g}||_2
|
875
|
+
score += np.sum(np.square(self.template_weights[np.invert(self.in_volume)]))
|
876
|
+
return score * self.score_sign
|
589
877
|
|
590
|
-
References
|
591
|
-
----------
|
592
|
-
.. [1] Daven Vasishtan and Maya Topf, "Scoring functions for cryoEM density
|
593
|
-
fitting", Journal of Structural Biology, vol. 174, no. 2,
|
594
|
-
pp. 333--343, 2011. DOI: https://doi.org/10.1016/j.jsb.2011.01.012
|
595
|
-
"""
|
596
878
|
|
597
|
-
|
598
|
-
super().__init__(**kwargs)
|
599
|
-
self.target_tree = KDTree(self.target_coordinates.T)
|
600
|
-
|
601
|
-
def scoring_function(
|
602
|
-
self,
|
603
|
-
transformed_coordinates: NDArray,
|
604
|
-
transformed_coordinates_mask: NDArray,
|
605
|
-
**kwargs,
|
606
|
-
) -> float:
|
607
|
-
"""
|
608
|
-
Compute the Chamfer distance score.
|
609
|
-
|
610
|
-
Given the transformed coordinates and their associated mask, this function
|
611
|
-
calculates the average distance between the rotated template coordinates
|
612
|
-
and the nearest target coordinates.
|
613
|
-
|
614
|
-
Parameters
|
615
|
-
----------
|
616
|
-
transformed_coordinates : NDArray
|
617
|
-
Transformed coordinates.
|
618
|
-
|
619
|
-
Returns
|
620
|
-
-------
|
621
|
-
float
|
622
|
-
The negative of the Chamfer distance score.
|
623
|
-
|
624
|
-
"""
|
625
|
-
dist, _ = self.target_tree.query(self.template_coordinates_rotated.T)
|
626
|
-
score = np.mean(dist)
|
627
|
-
return -score
|
628
|
-
|
629
|
-
|
630
|
-
class MutualInformation(MatchCoordinatesToDensity):
|
879
|
+
class MutualInformation(_MatchCoordinatesToDensity):
|
631
880
|
"""
|
632
|
-
|
633
|
-
|
634
|
-
The Mutual Information (MI) score is calculated as:
|
881
|
+
The Mutual Information (MI) score between the target :math:`f` and the
|
882
|
+
template :math:`g` is calculated as:
|
635
883
|
|
636
884
|
.. math::
|
637
885
|
|
@@ -645,43 +893,15 @@ class MutualInformation(MatchCoordinatesToDensity):
|
|
645
893
|
|
646
894
|
"""
|
647
895
|
|
648
|
-
|
649
|
-
super().__init__(**kwargs)
|
650
|
-
|
651
|
-
def scoring_function(
|
652
|
-
self,
|
653
|
-
transformed_coordinates: NDArray,
|
654
|
-
transformed_coordinates_mask: NDArray,
|
655
|
-
in_volume: NDArray,
|
656
|
-
in_volume_mask: NDArray,
|
657
|
-
) -> float:
|
658
|
-
"""
|
659
|
-
Compute the Mutual Information score.
|
660
|
-
|
661
|
-
Given the transformed coordinates and their associated mask, this function
|
662
|
-
computes the mutual information between the target and template densities.
|
896
|
+
__doc__ += _MatchCoordinatesToDensity.__doc__
|
663
897
|
|
664
|
-
|
665
|
-
|
666
|
-
transformed_coordinates : NDArray
|
667
|
-
Transformed coordinates.
|
668
|
-
transformed_coordinates_mask : NDArray
|
669
|
-
Mask for the transformed coordinates.
|
670
|
-
in_volume : NDArray
|
671
|
-
Binary mask indicating which ``transformed_coordinates`` are in the
|
672
|
-
target volume.
|
673
|
-
in_volume_mask : NDArray
|
674
|
-
Binary mask indicating which ``transformed_coordinates`` are in the
|
675
|
-
target mask volume.
|
676
|
-
|
677
|
-
Returns
|
678
|
-
-------
|
679
|
-
float
|
680
|
-
The Mutual Information score.
|
681
|
-
"""
|
898
|
+
def __call__(self) -> float:
|
899
|
+
"""Returns the score of the current configuration."""
|
682
900
|
p_xy, target, template = np.histogram2d(
|
683
|
-
self.target_density[
|
684
|
-
|
901
|
+
self.target_density[
|
902
|
+
tuple(self.template_coordinates_rotated[:, self.in_volume].astype(int))
|
903
|
+
],
|
904
|
+
self.template_weights[self.in_volume],
|
685
905
|
)
|
686
906
|
p_x, p_y = np.sum(p_xy, axis=1), np.sum(p_xy, axis=0)
|
687
907
|
|
@@ -692,14 +912,13 @@ class MutualInformation(MatchCoordinatesToDensity):
|
|
692
912
|
logprob = np.divide(p_xy, p_x[:, None] * p_y[None, :] + np.finfo(float).eps)
|
693
913
|
score = np.nansum(p_xy * logprob)
|
694
914
|
|
695
|
-
return score
|
915
|
+
return score * self.score_sign
|
696
916
|
|
697
917
|
|
698
|
-
class Envelope(
|
918
|
+
class Envelope(_MatchCoordinatesToDensity):
|
699
919
|
"""
|
700
|
-
|
701
|
-
|
702
|
-
The Envelope score (ENV) is calculated as:
|
920
|
+
The Envelope score (ENV) between the target :math:`f` and the
|
921
|
+
template :math:`g` is calculated as:
|
703
922
|
|
704
923
|
.. math::
|
705
924
|
|
@@ -713,59 +932,64 @@ class Envelope(MatchCoordinatesToDensity):
|
|
713
932
|
pp. 333--343, 2011. DOI: https://doi.org/10.1016/j.jsb.2011.01.012
|
714
933
|
"""
|
715
934
|
|
716
|
-
|
935
|
+
__doc__ += _MatchCoordinatesToDensity.__doc__
|
936
|
+
|
937
|
+
def __init__(self, target_threshold: float = None, **kwargs):
|
717
938
|
super().__init__(**kwargs)
|
939
|
+
if target_threshold is None:
|
940
|
+
target_threshold = np.mean(self.target_density)
|
718
941
|
self.target_density = np.where(self.target_density > target_threshold, -1, 1)
|
719
942
|
self.target_density_present = np.sum(self.target_density == -1)
|
720
943
|
self.target_density_absent = np.sum(self.target_density == 1)
|
721
944
|
self.template_weights = np.ones_like(self.template_weights)
|
722
945
|
|
723
|
-
def
|
724
|
-
|
725
|
-
|
726
|
-
|
727
|
-
|
728
|
-
in_volume_mask: NDArray,
|
729
|
-
) -> float:
|
730
|
-
"""
|
731
|
-
Compute the Envelope score.
|
732
|
-
|
733
|
-
Given the transformed coordinates and their associated mask, this function
|
734
|
-
computes the envelope score based on target density thresholds.
|
735
|
-
|
736
|
-
Parameters
|
737
|
-
----------
|
738
|
-
transformed_coordinates : NDArray
|
739
|
-
Transformed coordinates.
|
740
|
-
transformed_coordinates_mask : NDArray
|
741
|
-
Mask for the transformed coordinates.
|
742
|
-
in_volume : NDArray
|
743
|
-
Binary mask indicating which ``transformed_coordinates`` are in the
|
744
|
-
target volume.
|
745
|
-
in_volume_mask : NDArray
|
746
|
-
Binary mask indicating which ``transformed_coordinates`` are in the
|
747
|
-
target mask volume.
|
748
|
-
|
749
|
-
Returns
|
750
|
-
-------
|
751
|
-
float
|
752
|
-
The Envelope score.
|
753
|
-
"""
|
754
|
-
score = self.target_density[tuple(transformed_coordinates[:, in_volume])]
|
946
|
+
def __call__(self) -> float:
|
947
|
+
"""Returns the score of the current configuration."""
|
948
|
+
score = self.target_density[
|
949
|
+
tuple(self.template_coordinates_rotated[:, self.in_volume].astype(int))
|
950
|
+
]
|
755
951
|
unassigned_density = self.target_density_present - (score == -1).sum()
|
756
952
|
|
757
|
-
score = score.sum() - unassigned_density - 2 * np.sum(np.invert(in_volume))
|
953
|
+
score = score.sum() - unassigned_density - 2 * np.sum(np.invert(self.in_volume))
|
758
954
|
min_score = -self.target_density_present - 2 * self.target_density_absent
|
759
955
|
score = (score - 2 * min_score) / (2 * self.target_density_present - min_score)
|
760
956
|
|
761
|
-
return score
|
957
|
+
return score * self.score_sign
|
958
|
+
|
959
|
+
|
960
|
+
class Chamfer(_MatchCoordinatesToCoordinates):
|
961
|
+
"""
|
962
|
+
The Chamfer distance between the target :math:`f` and the template :math:`g`
|
963
|
+
is calculated as:
|
964
|
+
|
965
|
+
.. math::
|
762
966
|
|
967
|
+
\\text{d(f,g)} = \\frac{1}{|X|} \\sum_{\\mathbf{f}_i \\in X}
|
968
|
+
\\inf_{\\mathbf{g} \\in Y} ||\\mathbf{f}_i - \\mathbf{g}||_2
|
763
969
|
|
764
|
-
|
970
|
+
References
|
971
|
+
----------
|
972
|
+
.. [1] Daven Vasishtan and Maya Topf, "Scoring functions for cryoEM density
|
973
|
+
fitting", Journal of Structural Biology, vol. 174, no. 2,
|
974
|
+
pp. 333--343, 2011. DOI: https://doi.org/10.1016/j.jsb.2011.01.012
|
765
975
|
"""
|
766
|
-
Class representing the Normal Vector matching score.
|
767
976
|
|
768
|
-
|
977
|
+
__doc__ += _MatchCoordinatesToDensity.__doc__
|
978
|
+
|
979
|
+
def _post_init(self, **kwargs):
|
980
|
+
self.target_tree = KDTree(self.target_coordinates.T)
|
981
|
+
|
982
|
+
def __call__(self) -> float:
|
983
|
+
"""Returns the score of the current configuration."""
|
984
|
+
dist, _ = self.target_tree.query(self.template_coordinates_rotated.T)
|
985
|
+
score = np.mean(dist)
|
986
|
+
return score * self.score_sign
|
987
|
+
|
988
|
+
|
989
|
+
class NormalVectorScore(_MatchCoordinatesToCoordinates):
|
990
|
+
"""
|
991
|
+
The Normal Vector Score (NVS) between the target's :math:`f` and the template
|
992
|
+
:math:`g`'s normal vectors is calculated as:
|
769
993
|
|
770
994
|
.. math::
|
771
995
|
|
@@ -784,35 +1008,14 @@ class NormalVectorScore(MatchCoordinatesToCoordinates):
|
|
784
1008
|
|
785
1009
|
"""
|
786
1010
|
|
787
|
-
|
788
|
-
super().__init__(**kwargs)
|
1011
|
+
__doc__ += _MatchCoordinatesToDensity.__doc__
|
789
1012
|
|
790
|
-
def
|
791
|
-
|
792
|
-
|
793
|
-
|
794
|
-
|
795
|
-
|
796
|
-
"""
|
797
|
-
Compute the Normal Vector Score.
|
798
|
-
|
799
|
-
Given the template and target vectors, this function computes the average
|
800
|
-
cosine similarity between the two sets of vectors.
|
801
|
-
|
802
|
-
Parameters
|
803
|
-
----------
|
804
|
-
template_vectors : NDArray
|
805
|
-
Normal vectors derived from the template.
|
806
|
-
target_vectors : NDArray
|
807
|
-
Normal vectors derived from the target.
|
808
|
-
|
809
|
-
Returns
|
810
|
-
-------
|
811
|
-
float
|
812
|
-
The Normal Vector Score.
|
813
|
-
"""
|
814
|
-
numerator = np.multiply(transformed_coordinates, self.target_coordinates)
|
815
|
-
denominator = np.linalg.norm(transformed_coordinates)
|
1013
|
+
def __call__(self) -> float:
|
1014
|
+
"""Returns the score of the current configuration."""
|
1015
|
+
numerator = np.multiply(
|
1016
|
+
self.template_coordinates_rotated, self.target_coordinates
|
1017
|
+
)
|
1018
|
+
denominator = np.linalg.norm(self.template_coordinates_rotated)
|
816
1019
|
denominator *= np.linalg.norm(self.target_coordinates)
|
817
1020
|
score = np.mean(numerator / denominator)
|
818
1021
|
return score
|
@@ -829,12 +1032,13 @@ MATCHING_OPTIMIZATION_REGISTER = {
|
|
829
1032
|
"Chamfer": Chamfer,
|
830
1033
|
"MutualInformation": MutualInformation,
|
831
1034
|
"NormalVectorScore": NormalVectorScore,
|
1035
|
+
"FLC": FLC,
|
832
1036
|
}
|
833
1037
|
|
834
1038
|
|
835
1039
|
def register_matching_optimization(match_name: str, match_class: type):
|
836
1040
|
"""
|
837
|
-
Registers a
|
1041
|
+
Registers a new mtaching method.
|
838
1042
|
|
839
1043
|
Parameters
|
840
1044
|
----------
|
@@ -848,7 +1052,7 @@ def register_matching_optimization(match_name: str, match_class: type):
|
|
848
1052
|
ValueError
|
849
1053
|
If any of the required methods is not defined.
|
850
1054
|
"""
|
851
|
-
methods_to_check = ["__init__", "__call__"
|
1055
|
+
methods_to_check = ["__init__", "__call__"]
|
852
1056
|
|
853
1057
|
for method in methods_to_check:
|
854
1058
|
if not hasattr(match_class, method):
|
@@ -858,266 +1062,166 @@ def register_matching_optimization(match_name: str, match_class: type):
|
|
858
1062
|
MATCHING_OPTIMIZATION_REGISTER[match_name] = match_class
|
859
1063
|
|
860
1064
|
|
861
|
-
|
862
|
-
"""
|
863
|
-
A class to refine the fit between target and template coordinates.
|
864
|
-
|
865
|
-
Notes
|
866
|
-
-----
|
867
|
-
By default scipy.optimize.differential_evolution or scipy.optimize.basinhopping
|
868
|
-
are used which can be unreliable if the initial alignment is very poor. Other
|
869
|
-
optimizers can be implemented by subclassing :py:class:`FitRefinement` and
|
870
|
-
overwriting the :py:meth:`FitRefinement.refine` function.
|
871
|
-
|
1065
|
+
def create_score_object(score: str, **kwargs) -> object:
|
872
1066
|
"""
|
1067
|
+
Initialize score object with name ``score`` using `**kwargs``.
|
873
1068
|
|
874
|
-
|
875
|
-
|
876
|
-
|
877
|
-
|
878
|
-
|
879
|
-
|
880
|
-
coordinates_mask: NDArray = None,
|
881
|
-
) -> Tuple[NDArray, NDArray]:
|
882
|
-
"""
|
883
|
-
Map coordinates to a volume based on given voxel size and origin.
|
1069
|
+
Parameters
|
1070
|
+
----------
|
1071
|
+
score: str
|
1072
|
+
Name of the score.
|
1073
|
+
**kwargs: Dict
|
1074
|
+
Keyword arguments passed to the __init__ method of the score object.
|
884
1075
|
|
885
|
-
|
886
|
-
|
887
|
-
|
888
|
-
|
889
|
-
array_shape : NDArray
|
890
|
-
The shape of the array to which the coordinates are mapped.
|
891
|
-
array_origin : NDArray
|
892
|
-
The origin of the array to which the coordinates are mapped.
|
893
|
-
sampling_rate : NDArray
|
894
|
-
The size of a voxel in the array.
|
895
|
-
coordinates_mask : NDArray, optional
|
896
|
-
An array representing the mask for the coordinates [d x T].
|
1076
|
+
Returns
|
1077
|
+
-------
|
1078
|
+
object
|
1079
|
+
Initialized score object.
|
897
1080
|
|
898
|
-
|
899
|
-
|
900
|
-
|
901
|
-
|
902
|
-
mask for in_volume points, and mask for in_volume points in mask.
|
903
|
-
"""
|
904
|
-
coordinates = coordinates.astype(sampling_rate.dtype)
|
905
|
-
np.divide(
|
906
|
-
coordinates - array_origin[:, None], sampling_rate[:, None], out=coordinates
|
907
|
-
)
|
908
|
-
transformed_coordinates = coordinates.astype(int)
|
909
|
-
in_volume = np.logical_and(
|
910
|
-
transformed_coordinates < np.array(array_shape)[:, None],
|
911
|
-
transformed_coordinates >= 0,
|
912
|
-
).min(axis=0)
|
1081
|
+
Raises
|
1082
|
+
------
|
1083
|
+
ValueError
|
1084
|
+
If ``score`` is not a key in MATCHING_OPTIMIZATION_REGISTER.
|
913
1085
|
|
914
|
-
|
1086
|
+
See Also
|
1087
|
+
--------
|
1088
|
+
:py:meth:`register_matching_optimization`
|
1089
|
+
"""
|
915
1090
|
|
916
|
-
|
917
|
-
coordinates_mask = coordinates_mask.astype(sampling_rate.dtype)
|
918
|
-
np.divide(
|
919
|
-
coordinates_mask - array_origin[:, None],
|
920
|
-
sampling_rate[:, None],
|
921
|
-
out=coordinates_mask,
|
922
|
-
)
|
923
|
-
transformed_coordinates_mask = coordinates_mask.astype(int)
|
924
|
-
in_volume_mask = np.logical_and(
|
925
|
-
transformed_coordinates_mask < np.array(array_shape)[:, None],
|
926
|
-
transformed_coordinates_mask >= 0,
|
927
|
-
).min(axis=0)
|
1091
|
+
score_object = MATCHING_OPTIMIZATION_REGISTER.get(score, None)
|
928
1092
|
|
929
|
-
|
930
|
-
|
931
|
-
|
932
|
-
|
933
|
-
in_volume_mask,
|
1093
|
+
if score_object is None:
|
1094
|
+
raise ValueError(
|
1095
|
+
f"{score} is not defined. Please pick from "
|
1096
|
+
f" {', '.join(list(MATCHING_OPTIMIZATION_REGISTER.keys()))}."
|
934
1097
|
)
|
935
1098
|
|
936
|
-
|
937
|
-
|
938
|
-
coordinates: NDArray,
|
939
|
-
weights: NDArray,
|
940
|
-
sampling_rate: NDArray,
|
941
|
-
origin: NDArray = None,
|
942
|
-
shape: NDArray = None,
|
943
|
-
) -> Tuple[NDArray, NDArray, NDArray]:
|
944
|
-
"""
|
945
|
-
Create a volume from coordinates, using given weights and voxel size.
|
946
|
-
|
947
|
-
Parameters
|
948
|
-
----------
|
949
|
-
coordinates : NDArray
|
950
|
-
An array representing the coordinates [d x N].
|
951
|
-
weights : NDArray
|
952
|
-
An array representing the weights for each coordinate [N].
|
953
|
-
sampling_rate : NDArray
|
954
|
-
The size of a voxel in the volume.
|
955
|
-
origin : NDArray, optional
|
956
|
-
The origin of the volume.
|
957
|
-
shape : NDArray, optional
|
958
|
-
The shape of the volume.
|
959
|
-
|
960
|
-
Returns
|
961
|
-
-------
|
962
|
-
tuple
|
963
|
-
Returns the generated volume, positions of coordinates, and origin.
|
964
|
-
"""
|
965
|
-
if origin is None:
|
966
|
-
origin = coordinates.min(axis=1)
|
967
|
-
|
968
|
-
positions = np.divide(coordinates - origin[:, None], sampling_rate[:, None])
|
969
|
-
positions = positions.astype(int)
|
970
|
-
|
971
|
-
if shape is None:
|
972
|
-
shape = positions.max(axis=1) + 1
|
973
|
-
|
974
|
-
arr = np.zeros(shape, dtype=np.float32)
|
975
|
-
np.add.at(arr, tuple(positions), weights)
|
976
|
-
return arr, positions, origin
|
977
|
-
|
978
|
-
def refine(
|
979
|
-
self,
|
980
|
-
target_coordinates: NDArray,
|
981
|
-
target_weights: NDArray,
|
982
|
-
template_coordinates: NDArray,
|
983
|
-
template_weights: NDArray,
|
984
|
-
sampling_rate: float = None,
|
985
|
-
translational_uncertainty: Tuple[float] = None,
|
986
|
-
rotational_uncertainty: Tuple[float] = None,
|
987
|
-
scoring_class: str = "CrossCorrelation",
|
988
|
-
scoring_class_parameters: Dict = dict(),
|
989
|
-
local_optimization: bool = True,
|
990
|
-
maxiter: int = 100,
|
991
|
-
) -> (NDArray, NDArray):
|
992
|
-
"""
|
993
|
-
Refines the alignment of template coordinates to target coordinates.
|
994
|
-
|
995
|
-
Parameters
|
996
|
-
----------
|
997
|
-
target_coordinates : NDArray
|
998
|
-
The coordinates of the target.
|
999
|
-
|
1000
|
-
target_weights : NDArray
|
1001
|
-
The weights of the target.
|
1099
|
+
score_object = score_object(**kwargs)
|
1100
|
+
return score_object
|
1002
1101
|
|
1003
|
-
template_coordinates : NDArray
|
1004
|
-
The coordinates of the template.
|
1005
1102
|
|
1006
|
-
|
1007
|
-
|
1103
|
+
def optimize_match(
|
1104
|
+
score_object: object,
|
1105
|
+
bounds_translation: Tuple[Tuple[float]] = None,
|
1106
|
+
bounds_rotation: Tuple[Tuple[float]] = None,
|
1107
|
+
optimization_method: str = "basinhopping",
|
1108
|
+
maxiter: int = 500,
|
1109
|
+
) -> Tuple[ArrayLike, ArrayLike, float]:
|
1110
|
+
"""
|
1111
|
+
Find the translation and rotation optimizing the score returned by `score_object`
|
1112
|
+
with respect to provided bounds.
|
1008
1113
|
|
1009
|
-
|
1010
|
-
|
1114
|
+
Parameters
|
1115
|
+
----------
|
1116
|
+
score_object: object
|
1117
|
+
Class object that defines a score method, which returns a floating point
|
1118
|
+
value given a tuple of floating points where the first half describes a
|
1119
|
+
translation and the second a rotation. The score will be minimized, i.e.
|
1120
|
+
it has to be negated if similarity should be optimized.
|
1121
|
+
bounds_translation : tuple of tuple float, optional
|
1122
|
+
Bounds on the evaluated translations. Has to be specified per dimension
|
1123
|
+
as tuple of (min, max). Default is None.
|
1124
|
+
bounds_rotation : tuple of tuple float, optional
|
1125
|
+
Bounds on the evaluated zyx Euler angles. Has to be specified per dimension
|
1126
|
+
as tuple of (min, max). Default is None.
|
1127
|
+
optimization_method : str, optional
|
1128
|
+
Optimizer that will be used, by default basinhopping. For further
|
1129
|
+
information refer to :doc:`scipy:reference/optimize`.
|
1130
|
+
|
1131
|
+
+--------------------------+-----------------------------------------+
|
1132
|
+
| 'differential_evolution' | Highest accuracy but long runtime. |
|
1133
|
+
| | Requires bounds on translation. |
|
1134
|
+
+--------------------------+-----------------------------------------+
|
1135
|
+
| 'basinhopping' | Decent accuracy, medium runtime. |
|
1136
|
+
+--------------------------+-----------------------------------------+
|
1137
|
+
| 'minimize' | If initial values are closed to optimum |
|
1138
|
+
| | decent performance, short runtime. |
|
1139
|
+
+--------------------------+-----------------------------------------+
|
1140
|
+
maxiter : int, optional
|
1141
|
+
The maximum number of iterations. Default is 500. Not considered for
|
1142
|
+
`optimization_method` 'minimize'.
|
1143
|
+
|
1144
|
+
Returns
|
1145
|
+
-------
|
1146
|
+
Tuple[ArrayLike, ArrayLike, float]
|
1147
|
+
Translation and rotation matrix yielding final score.
|
1011
1148
|
|
1012
|
-
|
1013
|
-
|
1149
|
+
Raises
|
1150
|
+
------
|
1151
|
+
ValueError
|
1152
|
+
If `optimization_method` is not supported.
|
1014
1153
|
|
1015
|
-
|
1016
|
-
|
1154
|
+
Notes
|
1155
|
+
-----
|
1156
|
+
This function currently only supports three-dimensional optimization and
|
1157
|
+
`score_object` will be modified during this operation.
|
1158
|
+
"""
|
1159
|
+
ndim = 3
|
1160
|
+
_optimization_method = {
|
1161
|
+
"differential_evolution": differential_evolution,
|
1162
|
+
"basinhopping": basinhopping,
|
1163
|
+
"minimize": minimize,
|
1164
|
+
}
|
1165
|
+
if optimization_method not in _optimization_method:
|
1166
|
+
raise ValueError(
|
1167
|
+
f"{optimization_method} is not supported. "
|
1168
|
+
f"Pick from {', '.join(list(_optimization_method.keys()))}"
|
1169
|
+
)
|
1017
1170
|
|
1018
|
-
|
1019
|
-
The scoring class to be used. Default is "CC".
|
1171
|
+
finfo = np.finfo(np.float32)
|
1020
1172
|
|
1021
|
-
|
1022
|
-
|
1173
|
+
# DE always requires bounds
|
1174
|
+
if optimization_method == "differential_evolution" and bounds_translation is None:
|
1175
|
+
bounds_translation = tuple((finfo.min, finfo.max) for _ in range(ndim))
|
1023
1176
|
|
1024
|
-
|
1025
|
-
|
1177
|
+
if bounds_translation is None and bounds_rotation is not None:
|
1178
|
+
bounds_translation = tuple((finfo.min, finfo.max) for _ in range(ndim))
|
1026
1179
|
|
1027
|
-
|
1028
|
-
|
1180
|
+
if bounds_rotation is None and bounds_translation is not None:
|
1181
|
+
bounds_rotation = tuple((-180, 180) for _ in range(ndim))
|
1029
1182
|
|
1030
|
-
|
1031
|
-
|
1032
|
-
|
1033
|
-
|
1034
|
-
|
1035
|
-
|
1036
|
-
|
1037
|
-
|
1038
|
-
|
1039
|
-
If scoring class is not a part of `MATCHING_OPTIMIZATION_REGISTER`.
|
1040
|
-
Individual scores can be added via
|
1041
|
-
:py:meth:`register_matching_optimization`.
|
1042
|
-
|
1043
|
-
See Also
|
1044
|
-
--------
|
1045
|
-
:py:meth:`register_matching_optimization`
|
1046
|
-
"""
|
1047
|
-
if scoring_class not in MATCHING_OPTIMIZATION_REGISTER:
|
1048
|
-
raise NotImplementedError(
|
1049
|
-
f"Parameter score has to be one of "
|
1050
|
-
f"{', '.join(MATCHING_OPTIMIZATION_REGISTER.keys())}."
|
1051
|
-
)
|
1052
|
-
scoring_class = MATCHING_OPTIMIZATION_REGISTER.get(scoring_class, None)
|
1053
|
-
|
1054
|
-
if sampling_rate is None:
|
1055
|
-
sampling_rate = np.ones(1)
|
1056
|
-
sampling_rate = np.repeat(
|
1057
|
-
sampling_rate, target_coordinates.shape[0] // sampling_rate.size
|
1183
|
+
bounds, linear_constraint = None, ()
|
1184
|
+
if bounds_rotation is not None and bounds_translation is not None:
|
1185
|
+
uncertainty = (*bounds_translation, *bounds_rotation)
|
1186
|
+
bounds = [
|
1187
|
+
bound if bound != (0, 0) else (-finfo.resolution, finfo.resolution)
|
1188
|
+
for bound in uncertainty
|
1189
|
+
]
|
1190
|
+
linear_constraint = LinearConstraint(
|
1191
|
+
np.eye(len(bounds)), np.min(bounds, axis=1), np.max(bounds, axis=1)
|
1058
1192
|
)
|
1059
1193
|
|
1060
|
-
|
1061
|
-
|
1062
|
-
|
1063
|
-
|
1064
|
-
|
1065
|
-
|
1066
|
-
|
1194
|
+
initial_score = score_object()
|
1195
|
+
if optimization_method == "basinhopping":
|
1196
|
+
result = basinhopping(
|
1197
|
+
x0=np.zeros(2 * ndim),
|
1198
|
+
func=score_object.score,
|
1199
|
+
niter=maxiter,
|
1200
|
+
minimizer_kwargs={"method": "COBYLA", "constraints": linear_constraint},
|
1067
1201
|
)
|
1068
|
-
|
1069
|
-
|
1070
|
-
|
1071
|
-
|
1072
|
-
|
1073
|
-
|
1074
|
-
mass_center_template /= template_weights.sum()
|
1075
|
-
|
1076
|
-
if translational_uncertainty is None:
|
1077
|
-
mass_center_difference = np.ceil(
|
1078
|
-
np.subtract(mass_center_target, mass_center_template)
|
1079
|
-
).astype(int)
|
1080
|
-
target_range = np.ceil(
|
1081
|
-
np.divide(
|
1082
|
-
np.subtract(
|
1083
|
-
target_coordinates.max(axis=1), target_coordinates.min(axis=1)
|
1084
|
-
),
|
1085
|
-
2,
|
1086
|
-
)
|
1087
|
-
).astype(int)
|
1088
|
-
translational_uncertainty = tuple(
|
1089
|
-
(center - start, center + start)
|
1090
|
-
for center, start in zip(mass_center_difference, target_range)
|
1091
|
-
)
|
1092
|
-
if rotational_uncertainty is None:
|
1093
|
-
rotational_uncertainty = tuple(
|
1094
|
-
(-90, 90) for _ in range(target_coordinates.shape[0])
|
1095
|
-
)
|
1096
|
-
|
1097
|
-
uncertainty = (*translational_uncertainty, *rotational_uncertainty)
|
1098
|
-
bounds = [bound if bound != (0, 0) else (-1e-9, 1e-9) for bound in uncertainty]
|
1099
|
-
linear_constraint = LinearConstraint(
|
1100
|
-
np.eye(len(bounds)), np.min(bounds, axis=1), np.max(bounds, axis=1)
|
1202
|
+
elif optimization_method == "differential_evolution":
|
1203
|
+
result = differential_evolution(
|
1204
|
+
func=score_object.score,
|
1205
|
+
bounds=bounds,
|
1206
|
+
constraints=linear_constraint,
|
1207
|
+
maxiter=maxiter,
|
1101
1208
|
)
|
1209
|
+
elif optimization_method == "minimize":
|
1210
|
+
result = minimize(
|
1211
|
+
x0=np.zeros(2 * ndim),
|
1212
|
+
fun=score_object.score,
|
1213
|
+
bounds=bounds,
|
1214
|
+
constraints=linear_constraint,
|
1215
|
+
)
|
1216
|
+
print(f"Niter: {result.nit}, success : {result.success} ({result.message}).")
|
1217
|
+
print(f"Initial score: {initial_score} - Refined score: {result.fun}")
|
1218
|
+
if initial_score < result.fun:
|
1219
|
+
print("Initial score better than refined score. Returning identity.")
|
1220
|
+
result.x = np.zeros_like(result.x)
|
1221
|
+
translation, rotation = result.x[:ndim], result.x[ndim:]
|
1222
|
+
rotation_matrix = euler_to_rotationmatrix(rotation)
|
1223
|
+
return translation, rotation_matrix, result.fun
|
1102
1224
|
|
1103
|
-
if local_optimization:
|
1104
|
-
result = basinhopping(
|
1105
|
-
x0=np.zeros(6),
|
1106
|
-
func=score,
|
1107
|
-
niter=maxiter,
|
1108
|
-
minimizer_kwargs={"method": "COBYLA", "constraints": linear_constraint},
|
1109
|
-
)
|
1110
|
-
else:
|
1111
|
-
result = differential_evolution(
|
1112
|
-
func=score,
|
1113
|
-
bounds=bounds,
|
1114
|
-
constraints=linear_constraint,
|
1115
|
-
maxiter=maxiter,
|
1116
|
-
)
|
1117
1225
|
|
1118
|
-
|
1119
|
-
|
1120
|
-
result.x = np.zeros_like(result.x)
|
1121
|
-
translation, rotation = result.x[:3], result.x[3:]
|
1122
|
-
rotation_matrix = euler_to_rotationmatrix(rotation)
|
1123
|
-
return translation, rotation_matrix, -result.fun
|
1226
|
+
class FitRefinement:
|
1227
|
+
pass
|