pytme 0.1.9__cp311-cp311-macosx_14_0_arm64.whl → 0.2.0b0__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.1.9.data → pytme-0.2.0b0.data}/scripts/match_template.py +148 -126
- pytme-0.2.0b0.data/scripts/postprocess.py +570 -0
- {pytme-0.1.9.data → pytme-0.2.0b0.data}/scripts/preprocessor_gui.py +244 -60
- {pytme-0.1.9.dist-info → pytme-0.2.0b0.dist-info}/METADATA +3 -1
- pytme-0.2.0b0.dist-info/RECORD +66 -0
- {pytme-0.1.9.dist-info → pytme-0.2.0b0.dist-info}/WHEEL +1 -1
- scripts/extract_candidates.py +218 -0
- scripts/match_template.py +148 -126
- scripts/match_template_filters.py +852 -0
- 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 +545 -78
- tme/backends/cupy_backend.py +80 -15
- tme/backends/npfftw_backend.py +33 -2
- tme/backends/pytorch_backend.py +15 -7
- tme/density.py +156 -63
- tme/extensions.cpython-311-darwin.so +0 -0
- tme/matching_constrained.py +195 -0
- tme/matching_data.py +74 -33
- tme/matching_exhaustive.py +351 -208
- tme/matching_memory.py +1 -0
- tme/matching_optimization.py +728 -651
- tme/matching_utils.py +152 -8
- tme/orientations.py +561 -0
- tme/preprocessor.py +21 -18
- tme/structure.py +2 -37
- pytme-0.1.9.data/scripts/postprocess.py +0 -625
- pytme-0.1.9.dist-info/RECORD +0 -61
- {pytme-0.1.9.data → pytme-0.2.0b0.data}/scripts/estimate_ram_usage.py +0 -0
- {pytme-0.1.9.data → pytme-0.2.0b0.data}/scripts/preprocess.py +0 -0
- {pytme-0.1.9.dist-info → pytme-0.2.0b0.dist-info}/LICENSE +0 -0
- {pytme-0.1.9.dist-info → pytme-0.2.0b0.dist-info}/entry_points.txt +0 -0
- {pytme-0.1.9.dist-info → pytme-0.2.0b0.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.
|
138
349
|
|
139
|
-
|
140
|
-
|
141
|
-
|
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.
|
362
|
+
|
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
|
-
|
374
|
+
if shape is None:
|
375
|
+
shape = positions.max(axis=1) + 1
|
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
|
148
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
|
196
486
|
|
197
|
-
|
487
|
+
if hasattr(self, "_post_init"):
|
488
|
+
self._post_init(**kwargs)
|
489
|
+
|
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,134 @@ 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
|
-
to parse a given file format.
|
237
|
-
"""
|
523
|
+
class FLC(_MatchDensityToDensity):
|
524
|
+
__doc__ += _MatchDensityToDensity.__doc__
|
238
525
|
|
526
|
+
def _post_init(self, **kwargs: Dict):
|
527
|
+
if self.target_mask is not None:
|
528
|
+
backend.multiply(self.target, self.target_mask, out=self.target)
|
239
529
|
|
240
|
-
|
241
|
-
|
242
|
-
|
530
|
+
self.target_square = backend.square(self.target)
|
531
|
+
|
532
|
+
normalize_under_mask(
|
533
|
+
template=self.template,
|
534
|
+
mask=self.template_mask,
|
535
|
+
mask_intensity=backend.sum(self.template_mask),
|
536
|
+
)
|
243
537
|
|
244
|
-
|
538
|
+
self.template = backend.reverse(self.template)
|
539
|
+
self.template_mask = backend.reverse(self.template_mask)
|
245
540
|
|
246
|
-
|
541
|
+
def __call__(self) -> float:
|
542
|
+
"""Returns the score of the current configuration."""
|
543
|
+
n_observations = backend.sum(self.template_mask_rot)
|
247
544
|
|
248
|
-
|
545
|
+
normalize_under_mask(
|
546
|
+
template=self.template_rot,
|
547
|
+
mask=self.template_mask_rot,
|
548
|
+
mask_intensity=n_observations,
|
549
|
+
)
|
249
550
|
|
551
|
+
ex2 = backend.sum(
|
552
|
+
backend.divide(
|
553
|
+
backend.sum(
|
554
|
+
backend.multiply(self.target_square, self.template_mask_rot),
|
555
|
+
),
|
556
|
+
n_observations,
|
557
|
+
)
|
558
|
+
)
|
559
|
+
e2x = backend.square(
|
560
|
+
backend.divide(
|
561
|
+
backend.sum(backend.multiply(self.target, self.template_mask_rot)),
|
562
|
+
n_observations,
|
563
|
+
)
|
564
|
+
)
|
565
|
+
|
566
|
+
denominator = backend.maximum(backend.subtract(ex2, e2x), 0.0)
|
567
|
+
denominator = backend.sqrt(denominator)
|
568
|
+
denominator = backend.multiply(denominator, n_observations)
|
569
|
+
|
570
|
+
overlap = backend.sum(backend.multiply(self.template_rot, self.target))
|
571
|
+
|
572
|
+
score = backend.divide(overlap, denominator) * self.score_sign
|
573
|
+
return score
|
574
|
+
|
575
|
+
|
576
|
+
class CrossCorrelation(_MatchCoordinatesToDensity):
|
250
577
|
"""
|
578
|
+
Computes the Cross-Correlation score as:
|
251
579
|
|
252
|
-
|
253
|
-
super().__init__(**kwargs)
|
254
|
-
self.denominator = 1
|
580
|
+
.. math::
|
255
581
|
|
256
|
-
|
257
|
-
|
258
|
-
transformed_coordinates: NDArray,
|
259
|
-
transformed_coordinates_mask: NDArray,
|
260
|
-
in_volume: NDArray,
|
261
|
-
in_volume_mask: NDArray,
|
262
|
-
) -> float:
|
263
|
-
"""
|
264
|
-
Compute the Cross-Correlation score.
|
582
|
+
\\text{score} = \\text{target_weights} \\cdot \\text{template_weights}
|
583
|
+
"""
|
265
584
|
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
585
|
+
__doc__ += _MatchCoordinatesToDensity.__doc__
|
586
|
+
|
587
|
+
def __call__(self) -> float:
|
588
|
+
"""Returns the score of the current configuration."""
|
589
|
+
try:
|
590
|
+
score = np.dot(
|
591
|
+
self.target_density[
|
592
|
+
tuple(
|
593
|
+
self.template_coordinates_rotated[:, self.in_volume].astype(int)
|
594
|
+
)
|
595
|
+
],
|
596
|
+
self.template_weights[self.in_volume],
|
597
|
+
)
|
598
|
+
except:
|
599
|
+
print(self.template_coordinates_rotated[:, self.in_volume].astype(int))
|
600
|
+
print(self.target_density.shape)
|
601
|
+
print(self.in_volume)
|
602
|
+
coordinates = self.template_coordinates_rotated[:, self.in_volume].astype(
|
603
|
+
int
|
604
|
+
)
|
605
|
+
in_volume = np.logical_and(
|
606
|
+
coordinates < np.array(self.target_density.shape)[:, None],
|
607
|
+
coordinates >= 0,
|
608
|
+
).min(axis=0)
|
609
|
+
print(in_volume)
|
278
610
|
|
279
|
-
|
280
|
-
-------
|
281
|
-
float
|
282
|
-
The Cross-Correlation score.
|
283
|
-
"""
|
284
|
-
score = np.dot(
|
285
|
-
self.target_density[tuple(transformed_coordinates[:, in_volume])],
|
286
|
-
self.template_weights[in_volume],
|
287
|
-
)
|
611
|
+
raise ValueError()
|
288
612
|
score /= self.denominator
|
289
|
-
return score
|
613
|
+
return score * self.score_sign
|
290
614
|
|
291
615
|
|
292
616
|
class LaplaceCrossCorrelation(CrossCorrelation):
|
293
617
|
"""
|
294
|
-
|
295
|
-
|
296
|
-
The score is computed like CrossCorrelation, but with Laplace filtered
|
297
|
-
weights, indicated by the Laplace operator :math:`\\nabla^{2}`.
|
618
|
+
Uses the same formalism as :py:class:`CrossCorrelation` but with Laplace
|
619
|
+
filtered weights (:math:`\\nabla^{2}`):
|
298
620
|
|
299
621
|
.. math::
|
300
622
|
|
301
623
|
\\text{score} = \\nabla^{2} \\text{target_weights} \\cdot
|
302
624
|
\\nabla^{2} \\text{template_weights}
|
303
|
-
|
304
625
|
"""
|
305
626
|
|
306
|
-
|
307
|
-
|
627
|
+
__doc__ += _MatchCoordinatesToDensity.__doc__
|
628
|
+
|
629
|
+
def _post_init(self, **kwargs):
|
308
630
|
self.target_density = laplace(self.target_density)
|
309
631
|
|
310
|
-
arr, positions, _ =
|
311
|
-
self.template_coordinates,
|
632
|
+
arr, positions, _ = self.array_from_coordinates(
|
633
|
+
self.template_coordinates,
|
634
|
+
self.template_weights,
|
635
|
+
np.ones(self.template_coordinates.shape[0]),
|
312
636
|
)
|
313
637
|
self.template_weights = laplace(arr)[tuple(positions)]
|
314
638
|
|
315
639
|
|
316
640
|
class NormalizedCrossCorrelation(CrossCorrelation):
|
317
641
|
"""
|
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.
|
642
|
+
Computes a normalized version of the :py:class:`CrossCorrelation` score based
|
643
|
+
on the dot product of `target_weights` and `template_weights`, in order to
|
644
|
+
reduce bias to regions of high local energy.
|
324
645
|
|
325
646
|
.. math::
|
326
647
|
|
@@ -338,11 +659,11 @@ class NormalizedCrossCorrelation(CrossCorrelation):
|
|
338
659
|
\\text{template_norm} = ||\\text{template_weights}||
|
339
660
|
|
340
661
|
Here, :math:`||.||` denotes the L2 (Euclidean) norm.
|
341
|
-
|
342
662
|
"""
|
343
663
|
|
344
|
-
|
345
|
-
|
664
|
+
__doc__ += _MatchCoordinatesToDensity.__doc__
|
665
|
+
|
666
|
+
def _post_init(self, **kwargs):
|
346
667
|
target_norm = np.linalg.norm(self.target_density[self.target_density != 0])
|
347
668
|
template_norm = np.linalg.norm(self.template_weights)
|
348
669
|
self.denominator = np.fmax(target_norm * template_norm, np.finfo(float).eps)
|
@@ -350,14 +671,8 @@ class NormalizedCrossCorrelation(CrossCorrelation):
|
|
350
671
|
|
351
672
|
class NormalizedCrossCorrelationMean(NormalizedCrossCorrelation):
|
352
673
|
"""
|
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:
|
674
|
+
Computes a similar score than :py:class:`NormalizedCrossCorrelation`, but
|
675
|
+
additionally factors in the mean of template and target.
|
361
676
|
|
362
677
|
.. math::
|
363
678
|
|
@@ -381,21 +696,21 @@ class NormalizedCrossCorrelationMean(NormalizedCrossCorrelation):
|
|
381
696
|
computes the mean of the respective weights.
|
382
697
|
"""
|
383
698
|
|
699
|
+
__doc__ += _MatchCoordinatesToDensity.__doc__
|
700
|
+
|
384
701
|
def __init__(self, **kwargs):
|
385
|
-
|
386
|
-
kwargs["
|
387
|
-
|
702
|
+
kwargs["target"] = np.subtract(kwargs["target"], kwargs["target"].mean())
|
703
|
+
kwargs["template_weights"] = np.subtract(
|
704
|
+
kwargs["template_weights"], kwargs["template_weights"].mean()
|
705
|
+
)
|
388
706
|
super().__init__(**kwargs)
|
389
707
|
|
390
708
|
|
391
|
-
class MaskedCrossCorrelation(
|
709
|
+
class MaskedCrossCorrelation(_MatchCoordinatesToDensity):
|
392
710
|
"""
|
393
|
-
Class representing the Masked Cross-Correlation matching score.
|
394
|
-
|
395
711
|
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.
|
712
|
+
and `template_weights` under respective masks. The score provides a measure of
|
713
|
+
similarity even in the presence of missing or masked data.
|
399
714
|
|
400
715
|
The formula for the Masked Cross-Correlation is:
|
401
716
|
|
@@ -437,54 +752,37 @@ class MaskedCrossCorrelation(MatchCoordinatesToDensity):
|
|
437
752
|
.. [1] Masked FFT registration, Dirk Padfield, CVPR 2010 conference
|
438
753
|
"""
|
439
754
|
|
440
|
-
|
441
|
-
super().__init__(**kwargs)
|
755
|
+
__doc__ += _MatchCoordinatesToDensity.__doc__
|
442
756
|
|
443
|
-
def
|
444
|
-
|
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.
|
465
|
-
|
466
|
-
Returns
|
467
|
-
-------
|
468
|
-
float
|
469
|
-
The Masked Cross-Correlation score.
|
470
|
-
"""
|
757
|
+
def __call__(self) -> float:
|
758
|
+
"""Returns the score of the current configuration."""
|
471
759
|
mask_overlap = np.sum(
|
472
760
|
self.target_mask_density[
|
473
|
-
tuple(
|
761
|
+
tuple(
|
762
|
+
self.template_mask_coordinates_rotated[
|
763
|
+
:, self.in_volume_mask
|
764
|
+
].astype(int)
|
765
|
+
)
|
474
766
|
],
|
475
767
|
)
|
476
768
|
mask_overlap = np.fmax(mask_overlap, np.finfo(float).eps)
|
477
769
|
|
478
770
|
mask_target = self.target_density[
|
479
|
-
tuple(
|
771
|
+
tuple(
|
772
|
+
self.template_mask_coordinates_rotated[:, self.in_volume_mask].astype(
|
773
|
+
int
|
774
|
+
)
|
775
|
+
)
|
480
776
|
]
|
481
777
|
denominator1 = np.subtract(
|
482
778
|
np.sum(mask_target**2),
|
483
779
|
np.divide(np.square(np.sum(mask_target)), mask_overlap),
|
484
780
|
)
|
485
781
|
mask_template = np.multiply(
|
486
|
-
self.template_weights[in_volume],
|
487
|
-
self.target_mask_density[
|
782
|
+
self.template_weights[self.in_volume],
|
783
|
+
self.target_mask_density[
|
784
|
+
tuple(self.template_coordinates_rotated[:, self.in_volume].astype(int))
|
785
|
+
],
|
488
786
|
)
|
489
787
|
denominator2 = np.subtract(
|
490
788
|
np.sum(mask_template**2),
|
@@ -496,8 +794,10 @@ class MaskedCrossCorrelation(MatchCoordinatesToDensity):
|
|
496
794
|
denominator = np.sqrt(np.multiply(denominator1, denominator2))
|
497
795
|
|
498
796
|
numerator = np.dot(
|
499
|
-
self.target_density[
|
500
|
-
|
797
|
+
self.target_density[
|
798
|
+
tuple(self.template_coordinates_rotated[:, self.in_volume].astype(int))
|
799
|
+
],
|
800
|
+
self.template_weights[self.in_volume],
|
501
801
|
)
|
502
802
|
|
503
803
|
numerator -= np.divide(
|
@@ -505,16 +805,14 @@ class MaskedCrossCorrelation(MatchCoordinatesToDensity):
|
|
505
805
|
)
|
506
806
|
|
507
807
|
if denominator == 0:
|
508
|
-
return 0
|
808
|
+
return 0.0
|
509
809
|
|
510
810
|
score = numerator / denominator
|
511
|
-
return score
|
811
|
+
return float(score * self.score_sign)
|
512
812
|
|
513
813
|
|
514
|
-
class PartialLeastSquareDifference(
|
814
|
+
class PartialLeastSquareDifference(_MatchCoordinatesToDensity):
|
515
815
|
"""
|
516
|
-
Class representing the Partial Least Square Difference matching score.
|
517
|
-
|
518
816
|
The Partial Least Square Difference (PLSQ) between the target :math:`f` and the
|
519
817
|
template :math:`g` is calculated as:
|
520
818
|
|
@@ -529,109 +827,32 @@ class PartialLeastSquareDifference(MatchCoordinatesToDensity):
|
|
529
827
|
pp. 333--343, 2011. DOI: https://doi.org/10.1016/j.jsb.2011.01.012
|
530
828
|
"""
|
531
829
|
|
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.
|
547
|
-
|
548
|
-
Parameters
|
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.
|
830
|
+
__doc__ += _MatchCoordinatesToDensity.__doc__
|
560
831
|
|
561
|
-
|
562
|
-
|
563
|
-
float
|
564
|
-
The negative of the Partial Least Square Difference score.
|
565
|
-
"""
|
832
|
+
def __call__(self) -> float:
|
833
|
+
"""Returns the score of the current configuration."""
|
566
834
|
score = np.sum(
|
567
835
|
np.square(
|
568
836
|
np.subtract(
|
569
|
-
self.target_density[
|
570
|
-
|
837
|
+
self.target_density[
|
838
|
+
tuple(
|
839
|
+
self.template_coordinates_rotated[:, self.in_volume].astype(
|
840
|
+
int
|
841
|
+
)
|
842
|
+
)
|
843
|
+
],
|
844
|
+
self.template_weights[self.in_volume],
|
571
845
|
)
|
572
846
|
)
|
573
847
|
)
|
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
|
589
|
-
|
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
|
-
|
597
|
-
def __init__(self, **kwargs):
|
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
|
848
|
+
score += np.sum(np.square(self.template_weights[np.invert(self.in_volume)]))
|
849
|
+
return score * self.score_sign
|
628
850
|
|
629
851
|
|
630
|
-
class MutualInformation(
|
852
|
+
class MutualInformation(_MatchCoordinatesToDensity):
|
631
853
|
"""
|
632
|
-
|
633
|
-
|
634
|
-
The Mutual Information (MI) score is calculated as:
|
854
|
+
The Mutual Information (MI) score between the target :math:`f` and the
|
855
|
+
template :math:`g` is calculated as:
|
635
856
|
|
636
857
|
.. math::
|
637
858
|
|
@@ -645,43 +866,15 @@ class MutualInformation(MatchCoordinatesToDensity):
|
|
645
866
|
|
646
867
|
"""
|
647
868
|
|
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.
|
663
|
-
|
664
|
-
Parameters
|
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.
|
869
|
+
__doc__ += _MatchCoordinatesToDensity.__doc__
|
676
870
|
|
677
|
-
|
678
|
-
|
679
|
-
float
|
680
|
-
The Mutual Information score.
|
681
|
-
"""
|
871
|
+
def __call__(self) -> float:
|
872
|
+
"""Returns the score of the current configuration."""
|
682
873
|
p_xy, target, template = np.histogram2d(
|
683
|
-
self.target_density[
|
684
|
-
|
874
|
+
self.target_density[
|
875
|
+
tuple(self.template_coordinates_rotated[:, self.in_volume].astype(int))
|
876
|
+
],
|
877
|
+
self.template_weights[self.in_volume],
|
685
878
|
)
|
686
879
|
p_x, p_y = np.sum(p_xy, axis=1), np.sum(p_xy, axis=0)
|
687
880
|
|
@@ -692,14 +885,13 @@ class MutualInformation(MatchCoordinatesToDensity):
|
|
692
885
|
logprob = np.divide(p_xy, p_x[:, None] * p_y[None, :] + np.finfo(float).eps)
|
693
886
|
score = np.nansum(p_xy * logprob)
|
694
887
|
|
695
|
-
return score
|
888
|
+
return score * self.score_sign
|
696
889
|
|
697
890
|
|
698
|
-
class Envelope(
|
891
|
+
class Envelope(_MatchCoordinatesToDensity):
|
699
892
|
"""
|
700
|
-
|
701
|
-
|
702
|
-
The Envelope score (ENV) is calculated as:
|
893
|
+
The Envelope score (ENV) between the target :math:`f` and the
|
894
|
+
template :math:`g` is calculated as:
|
703
895
|
|
704
896
|
.. math::
|
705
897
|
|
@@ -713,59 +905,64 @@ class Envelope(MatchCoordinatesToDensity):
|
|
713
905
|
pp. 333--343, 2011. DOI: https://doi.org/10.1016/j.jsb.2011.01.012
|
714
906
|
"""
|
715
907
|
|
716
|
-
|
908
|
+
__doc__ += _MatchCoordinatesToDensity.__doc__
|
909
|
+
|
910
|
+
def __init__(self, target_threshold: float = None, **kwargs):
|
717
911
|
super().__init__(**kwargs)
|
912
|
+
if target_threshold is None:
|
913
|
+
target_threshold = np.mean(self.target_density)
|
718
914
|
self.target_density = np.where(self.target_density > target_threshold, -1, 1)
|
719
915
|
self.target_density_present = np.sum(self.target_density == -1)
|
720
916
|
self.target_density_absent = np.sum(self.target_density == 1)
|
721
917
|
self.template_weights = np.ones_like(self.template_weights)
|
722
918
|
|
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])]
|
919
|
+
def __call__(self) -> float:
|
920
|
+
"""Returns the score of the current configuration."""
|
921
|
+
score = self.target_density[
|
922
|
+
tuple(self.template_coordinates_rotated[:, self.in_volume].astype(int))
|
923
|
+
]
|
755
924
|
unassigned_density = self.target_density_present - (score == -1).sum()
|
756
925
|
|
757
|
-
score = score.sum() - unassigned_density - 2 * np.sum(np.invert(in_volume))
|
926
|
+
score = score.sum() - unassigned_density - 2 * np.sum(np.invert(self.in_volume))
|
758
927
|
min_score = -self.target_density_present - 2 * self.target_density_absent
|
759
928
|
score = (score - 2 * min_score) / (2 * self.target_density_present - min_score)
|
760
929
|
|
761
|
-
return score
|
930
|
+
return score * self.score_sign
|
931
|
+
|
932
|
+
|
933
|
+
class Chamfer(_MatchCoordinatesToCoordinates):
|
934
|
+
"""
|
935
|
+
The Chamfer distance between the target :math:`f` and the template :math:`g`
|
936
|
+
is calculated as:
|
937
|
+
|
938
|
+
.. math::
|
762
939
|
|
940
|
+
\\text{d(f,g)} = \\frac{1}{|X|} \\sum_{\\mathbf{f}_i \\in X}
|
941
|
+
\\inf_{\\mathbf{g} \\in Y} ||\\mathbf{f}_i - \\mathbf{g}||_2
|
763
942
|
|
764
|
-
|
943
|
+
References
|
944
|
+
----------
|
945
|
+
.. [1] Daven Vasishtan and Maya Topf, "Scoring functions for cryoEM density
|
946
|
+
fitting", Journal of Structural Biology, vol. 174, no. 2,
|
947
|
+
pp. 333--343, 2011. DOI: https://doi.org/10.1016/j.jsb.2011.01.012
|
765
948
|
"""
|
766
|
-
Class representing the Normal Vector matching score.
|
767
949
|
|
768
|
-
|
950
|
+
__doc__ += _MatchCoordinatesToDensity.__doc__
|
951
|
+
|
952
|
+
def _post_init(self, **kwargs):
|
953
|
+
self.target_tree = KDTree(self.target_coordinates.T)
|
954
|
+
|
955
|
+
def __call__(self) -> float:
|
956
|
+
"""Returns the score of the current configuration."""
|
957
|
+
dist, _ = self.target_tree.query(self.template_coordinates_rotated.T)
|
958
|
+
score = np.mean(dist)
|
959
|
+
return score * self.score_sign
|
960
|
+
|
961
|
+
|
962
|
+
class NormalVectorScore(_MatchCoordinatesToCoordinates):
|
963
|
+
"""
|
964
|
+
The Normal Vector Score (NVS) between the target's :math:`f` and the template
|
965
|
+
:math:`g`'s normal vectors is calculated as:
|
769
966
|
|
770
967
|
.. math::
|
771
968
|
|
@@ -784,35 +981,14 @@ class NormalVectorScore(MatchCoordinatesToCoordinates):
|
|
784
981
|
|
785
982
|
"""
|
786
983
|
|
787
|
-
|
788
|
-
super().__init__(**kwargs)
|
789
|
-
|
790
|
-
def scoring_function(
|
791
|
-
self,
|
792
|
-
transformed_coordinates: NDArray,
|
793
|
-
transformed_coordinates_mask: NDArray,
|
794
|
-
**kwargs,
|
795
|
-
) -> float:
|
796
|
-
"""
|
797
|
-
Compute the Normal Vector Score.
|
984
|
+
__doc__ += _MatchCoordinatesToDensity.__doc__
|
798
985
|
|
799
|
-
|
800
|
-
|
801
|
-
|
802
|
-
|
803
|
-
|
804
|
-
|
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)
|
986
|
+
def __call__(self) -> float:
|
987
|
+
"""Returns the score of the current configuration."""
|
988
|
+
numerator = np.multiply(
|
989
|
+
self.template_coordinates_rotated, self.target_coordinates
|
990
|
+
)
|
991
|
+
denominator = np.linalg.norm(self.template_coordinates_rotated)
|
816
992
|
denominator *= np.linalg.norm(self.target_coordinates)
|
817
993
|
score = np.mean(numerator / denominator)
|
818
994
|
return score
|
@@ -829,12 +1005,13 @@ MATCHING_OPTIMIZATION_REGISTER = {
|
|
829
1005
|
"Chamfer": Chamfer,
|
830
1006
|
"MutualInformation": MutualInformation,
|
831
1007
|
"NormalVectorScore": NormalVectorScore,
|
1008
|
+
"FLC": FLC,
|
832
1009
|
}
|
833
1010
|
|
834
1011
|
|
835
1012
|
def register_matching_optimization(match_name: str, match_class: type):
|
836
1013
|
"""
|
837
|
-
Registers a
|
1014
|
+
Registers a new mtaching method.
|
838
1015
|
|
839
1016
|
Parameters
|
840
1017
|
----------
|
@@ -848,7 +1025,7 @@ def register_matching_optimization(match_name: str, match_class: type):
|
|
848
1025
|
ValueError
|
849
1026
|
If any of the required methods is not defined.
|
850
1027
|
"""
|
851
|
-
methods_to_check = ["__init__", "__call__"
|
1028
|
+
methods_to_check = ["__init__", "__call__"]
|
852
1029
|
|
853
1030
|
for method in methods_to_check:
|
854
1031
|
if not hasattr(match_class, method):
|
@@ -858,266 +1035,166 @@ def register_matching_optimization(match_name: str, match_class: type):
|
|
858
1035
|
MATCHING_OPTIMIZATION_REGISTER[match_name] = match_class
|
859
1036
|
|
860
1037
|
|
861
|
-
|
1038
|
+
def create_score_object(score: str, **kwargs) -> object:
|
862
1039
|
"""
|
863
|
-
|
1040
|
+
Initialize score object with name ``score`` using `**kwargs``.
|
864
1041
|
|
865
|
-
|
866
|
-
|
867
|
-
|
868
|
-
|
869
|
-
|
870
|
-
|
871
|
-
|
872
|
-
"""
|
873
|
-
|
874
|
-
@staticmethod
|
875
|
-
def map_coordinates_to_array(
|
876
|
-
coordinates: NDArray,
|
877
|
-
array_shape: NDArray,
|
878
|
-
array_origin: NDArray,
|
879
|
-
sampling_rate: NDArray,
|
880
|
-
coordinates_mask: NDArray = None,
|
881
|
-
) -> Tuple[NDArray, NDArray]:
|
882
|
-
"""
|
883
|
-
Map coordinates to a volume based on given voxel size and origin.
|
1042
|
+
Parameters
|
1043
|
+
----------
|
1044
|
+
score: str
|
1045
|
+
Name of the score.
|
1046
|
+
**kwargs: Dict
|
1047
|
+
Keyword arguments passed to the __init__ method of the score object.
|
884
1048
|
|
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].
|
1049
|
+
Returns
|
1050
|
+
-------
|
1051
|
+
object
|
1052
|
+
Initialized score object.
|
897
1053
|
|
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)
|
1054
|
+
Raises
|
1055
|
+
------
|
1056
|
+
ValueError
|
1057
|
+
If ``score`` is not a key in MATCHING_OPTIMIZATION_REGISTER.
|
913
1058
|
|
914
|
-
|
1059
|
+
See Also
|
1060
|
+
--------
|
1061
|
+
:py:meth:`register_matching_optimization`
|
1062
|
+
"""
|
915
1063
|
|
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)
|
1064
|
+
score_object = MATCHING_OPTIMIZATION_REGISTER.get(score, None)
|
928
1065
|
|
929
|
-
|
930
|
-
|
931
|
-
|
932
|
-
|
933
|
-
in_volume_mask,
|
1066
|
+
if score_object is None:
|
1067
|
+
raise ValueError(
|
1068
|
+
f"{score} is not defined. Please pick from "
|
1069
|
+
f" {', '.join(list(MATCHING_OPTIMIZATION_REGISTER.keys()))}."
|
934
1070
|
)
|
935
1071
|
|
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.
|
1072
|
+
score_object = score_object(**kwargs)
|
1073
|
+
return score_object
|
946
1074
|
|
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.
|
1002
1075
|
|
1003
|
-
|
1004
|
-
|
1005
|
-
|
1006
|
-
|
1007
|
-
|
1076
|
+
def optimize_match(
|
1077
|
+
score_object: object,
|
1078
|
+
bounds_translation: Tuple[Tuple[float]] = None,
|
1079
|
+
bounds_rotation: Tuple[Tuple[float]] = None,
|
1080
|
+
optimization_method: str = "basinhopping",
|
1081
|
+
maxiter: int = 500,
|
1082
|
+
) -> Tuple[ArrayLike, ArrayLike, float]:
|
1083
|
+
"""
|
1084
|
+
Find the translation and rotation optimizing the score returned by `score_object`
|
1085
|
+
with respect to provided bounds.
|
1008
1086
|
|
1009
|
-
|
1010
|
-
|
1087
|
+
Parameters
|
1088
|
+
----------
|
1089
|
+
score_object: object
|
1090
|
+
Class object that defines a score method, which returns a floating point
|
1091
|
+
value given a tuple of floating points where the first half describes a
|
1092
|
+
translation and the second a rotation. The score will be minimized, i.e.
|
1093
|
+
it has to be negated if similarity should be optimized.
|
1094
|
+
bounds_translation : tuple of tuple float, optional
|
1095
|
+
Bounds on the evaluated translations. Has to be specified per dimension
|
1096
|
+
as tuple of (min, max). Default is None.
|
1097
|
+
bounds_rotation : tuple of tuple float, optional
|
1098
|
+
Bounds on the evaluated zyx Euler angles. Has to be specified per dimension
|
1099
|
+
as tuple of (min, max). Default is None.
|
1100
|
+
optimization_method : str, optional
|
1101
|
+
Optimizer that will be used, by default basinhopping. For further
|
1102
|
+
information refer to :doc:`scipy:reference/optimize`.
|
1103
|
+
|
1104
|
+
+--------------------------+-----------------------------------------+
|
1105
|
+
| 'differential_evolution' | Highest accuracy but long runtime. |
|
1106
|
+
| | Requires bounds on translation. |
|
1107
|
+
+--------------------------+-----------------------------------------+
|
1108
|
+
| 'basinhopping' | Decent accuracy, medium runtime. |
|
1109
|
+
+--------------------------+-----------------------------------------+
|
1110
|
+
| 'minimize' | If initial values are closed to optimum |
|
1111
|
+
| | decent performance, short runtime. |
|
1112
|
+
+--------------------------+-----------------------------------------+
|
1113
|
+
maxiter : int, optional
|
1114
|
+
The maximum number of iterations. Default is 500. Not considered for
|
1115
|
+
`optimization_method` 'minimize'.
|
1116
|
+
|
1117
|
+
Returns
|
1118
|
+
-------
|
1119
|
+
Tuple[ArrayLike, ArrayLike, float]
|
1120
|
+
Translation and rotation matrix yielding final score.
|
1011
1121
|
|
1012
|
-
|
1013
|
-
|
1122
|
+
Raises
|
1123
|
+
------
|
1124
|
+
ValueError
|
1125
|
+
If `optimization_method` is not supported.
|
1014
1126
|
|
1015
|
-
|
1016
|
-
|
1127
|
+
Notes
|
1128
|
+
-----
|
1129
|
+
This function currently only supports three-dimensional optimization and
|
1130
|
+
`score_object` will be modified during this operation.
|
1131
|
+
"""
|
1132
|
+
ndim = 3
|
1133
|
+
_optimization_method = {
|
1134
|
+
"differential_evolution": differential_evolution,
|
1135
|
+
"basinhopping": basinhopping,
|
1136
|
+
"minimize": minimize,
|
1137
|
+
}
|
1138
|
+
if optimization_method not in _optimization_method:
|
1139
|
+
raise ValueError(
|
1140
|
+
f"{optimization_method} is not supported. "
|
1141
|
+
f"Pick from {', '.join(list(_optimization_method.keys()))}"
|
1142
|
+
)
|
1017
1143
|
|
1018
|
-
|
1019
|
-
The scoring class to be used. Default is "CC".
|
1144
|
+
finfo = np.finfo(np.float32)
|
1020
1145
|
|
1021
|
-
|
1022
|
-
|
1146
|
+
# DE always requires bounds
|
1147
|
+
if optimization_method == "differential_evolution" and bounds_translation is None:
|
1148
|
+
bounds_translation = tuple((finfo.min, finfo.max) for _ in range(ndim))
|
1023
1149
|
|
1024
|
-
|
1025
|
-
|
1150
|
+
if bounds_translation is None and bounds_rotation is not None:
|
1151
|
+
bounds_translation = tuple((finfo.min, finfo.max) for _ in range(ndim))
|
1026
1152
|
|
1027
|
-
|
1028
|
-
|
1153
|
+
if bounds_rotation is None and bounds_translation is not None:
|
1154
|
+
bounds_rotation = tuple((-180, 180) for _ in range(ndim))
|
1029
1155
|
|
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
|
1156
|
+
bounds, linear_constraint = None, ()
|
1157
|
+
if bounds_rotation is not None and bounds_translation is not None:
|
1158
|
+
uncertainty = (*bounds_translation, *bounds_rotation)
|
1159
|
+
bounds = [
|
1160
|
+
bound if bound != (0, 0) else (-finfo.resolution, finfo.resolution)
|
1161
|
+
for bound in uncertainty
|
1162
|
+
]
|
1163
|
+
linear_constraint = LinearConstraint(
|
1164
|
+
np.eye(len(bounds)), np.min(bounds, axis=1), np.max(bounds, axis=1)
|
1058
1165
|
)
|
1059
1166
|
|
1060
|
-
|
1061
|
-
|
1062
|
-
|
1063
|
-
|
1064
|
-
|
1065
|
-
|
1066
|
-
|
1167
|
+
initial_score = score_object()
|
1168
|
+
if optimization_method == "basinhopping":
|
1169
|
+
result = basinhopping(
|
1170
|
+
x0=np.zeros(2 * ndim),
|
1171
|
+
func=score_object.score,
|
1172
|
+
niter=maxiter,
|
1173
|
+
minimizer_kwargs={"method": "COBYLA", "constraints": linear_constraint},
|
1067
1174
|
)
|
1068
|
-
|
1069
|
-
|
1070
|
-
|
1071
|
-
|
1072
|
-
|
1073
|
-
|
1074
|
-
|
1075
|
-
|
1076
|
-
|
1077
|
-
|
1078
|
-
|
1079
|
-
|
1080
|
-
|
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)
|
1175
|
+
elif optimization_method == "differential_evolution":
|
1176
|
+
result = differential_evolution(
|
1177
|
+
func=score_object.score,
|
1178
|
+
bounds=bounds,
|
1179
|
+
constraints=linear_constraint,
|
1180
|
+
maxiter=maxiter,
|
1181
|
+
)
|
1182
|
+
elif optimization_method == "minimize":
|
1183
|
+
result = minimize(
|
1184
|
+
x0=np.zeros(2 * ndim),
|
1185
|
+
fun=score_object.score,
|
1186
|
+
bounds=bounds,
|
1187
|
+
constraints=linear_constraint,
|
1101
1188
|
)
|
1189
|
+
print(f"Niter: {result.nit}, success : {result.success} ({result.message}).")
|
1190
|
+
print(f"Initial score: {initial_score} - Refined score: {result.fun}")
|
1191
|
+
if initial_score < result.fun:
|
1192
|
+
print("Initial score better than refined score. Returning identity.")
|
1193
|
+
result.x = np.zeros_like(result.x)
|
1194
|
+
translation, rotation = result.x[:ndim], result.x[ndim:]
|
1195
|
+
rotation_matrix = euler_to_rotationmatrix(rotation)
|
1196
|
+
return translation, rotation_matrix, result.fun
|
1102
1197
|
|
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
1198
|
|
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
|
1199
|
+
class FitRefinement:
|
1200
|
+
pass
|