pytme 0.1.5__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.5.data/scripts/estimate_ram_usage.py +81 -0
- pytme-0.1.5.data/scripts/match_template.py +744 -0
- pytme-0.1.5.data/scripts/postprocess.py +279 -0
- pytme-0.1.5.data/scripts/preprocess.py +93 -0
- pytme-0.1.5.data/scripts/preprocessor_gui.py +729 -0
- pytme-0.1.5.dist-info/LICENSE +153 -0
- pytme-0.1.5.dist-info/METADATA +69 -0
- pytme-0.1.5.dist-info/RECORD +63 -0
- pytme-0.1.5.dist-info/WHEEL +5 -0
- pytme-0.1.5.dist-info/entry_points.txt +6 -0
- pytme-0.1.5.dist-info/top_level.txt +2 -0
- scripts/__init__.py +0 -0
- scripts/estimate_ram_usage.py +81 -0
- scripts/match_template.py +744 -0
- scripts/match_template_devel.py +788 -0
- scripts/postprocess.py +279 -0
- scripts/preprocess.py +93 -0
- scripts/preprocessor_gui.py +729 -0
- tme/__init__.py +6 -0
- tme/__version__.py +1 -0
- tme/analyzer.py +1144 -0
- tme/backends/__init__.py +134 -0
- tme/backends/cupy_backend.py +309 -0
- tme/backends/matching_backend.py +1154 -0
- tme/backends/npfftw_backend.py +763 -0
- tme/backends/pytorch_backend.py +526 -0
- tme/data/__init__.py +0 -0
- tme/data/c48n309.npy +0 -0
- tme/data/c48n527.npy +0 -0
- tme/data/c48n9.npy +0 -0
- tme/data/c48u1.npy +0 -0
- tme/data/c48u1153.npy +0 -0
- tme/data/c48u1201.npy +0 -0
- tme/data/c48u1641.npy +0 -0
- tme/data/c48u181.npy +0 -0
- tme/data/c48u2219.npy +0 -0
- tme/data/c48u27.npy +0 -0
- tme/data/c48u2947.npy +0 -0
- tme/data/c48u3733.npy +0 -0
- tme/data/c48u4749.npy +0 -0
- tme/data/c48u5879.npy +0 -0
- tme/data/c48u7111.npy +0 -0
- tme/data/c48u815.npy +0 -0
- tme/data/c48u83.npy +0 -0
- tme/data/c48u8649.npy +0 -0
- tme/data/c600v.npy +0 -0
- tme/data/c600vc.npy +0 -0
- tme/data/metadata.yaml +80 -0
- tme/data/quat_to_numpy.py +42 -0
- tme/data/scattering_factors.pickle +0 -0
- tme/density.py +2314 -0
- tme/extensions.cpython-311-darwin.so +0 -0
- tme/helpers.py +881 -0
- tme/matching_data.py +377 -0
- tme/matching_exhaustive.py +1553 -0
- tme/matching_memory.py +382 -0
- tme/matching_optimization.py +1123 -0
- tme/matching_utils.py +1180 -0
- tme/parser.py +429 -0
- tme/preprocessor.py +1291 -0
- tme/scoring.py +866 -0
- tme/structure.py +1428 -0
- tme/types.py +10 -0
tme/preprocessor.py
ADDED
@@ -0,0 +1,1291 @@
|
|
1
|
+
""" Implements Preprocessor class for filtering operations.
|
2
|
+
|
3
|
+
Copyright (c) 2023 European Molecular Biology Laboratory
|
4
|
+
|
5
|
+
Author: Valentin Maurer <valentin.maurer@embl-hamburg.de>
|
6
|
+
"""
|
7
|
+
|
8
|
+
import inspect
|
9
|
+
from typing import Dict, Tuple
|
10
|
+
|
11
|
+
import numpy as np
|
12
|
+
from numpy.typing import NDArray
|
13
|
+
|
14
|
+
from scipy.ndimage import (
|
15
|
+
fourier_gaussian,
|
16
|
+
gaussian_filter,
|
17
|
+
rank_filter,
|
18
|
+
zoom,
|
19
|
+
generic_gradient_magnitude,
|
20
|
+
sobel,
|
21
|
+
prewitt,
|
22
|
+
laplace,
|
23
|
+
gaussian_laplace,
|
24
|
+
gaussian_gradient_magnitude,
|
25
|
+
)
|
26
|
+
from scipy.signal import convolve, decimate
|
27
|
+
from scipy.optimize import differential_evolution
|
28
|
+
from pywt import wavelist, wavedecn, waverecn
|
29
|
+
from scipy.interpolate import interp1d
|
30
|
+
|
31
|
+
from .density import Density
|
32
|
+
from .helpers import (
|
33
|
+
window_kaiserb,
|
34
|
+
window_blob,
|
35
|
+
apply_window_filter,
|
36
|
+
Ntree,
|
37
|
+
)
|
38
|
+
from .matching_utils import euler_to_rotationmatrix
|
39
|
+
|
40
|
+
|
41
|
+
class Preprocessor:
|
42
|
+
"""
|
43
|
+
Implements filtering operations on density arrays.
|
44
|
+
"""
|
45
|
+
|
46
|
+
def apply_method(self, method: str, parameters: Dict):
|
47
|
+
"""
|
48
|
+
Apply a method on the atomic structure.
|
49
|
+
|
50
|
+
Parameters
|
51
|
+
----------
|
52
|
+
method : str
|
53
|
+
The name of the method to be used.
|
54
|
+
parameters : dict
|
55
|
+
The parameters for the specified method.
|
56
|
+
|
57
|
+
Returns
|
58
|
+
-------
|
59
|
+
The output of ``method``.
|
60
|
+
|
61
|
+
Raises
|
62
|
+
------
|
63
|
+
NotImplementedError
|
64
|
+
If ``method`` is not a member of :py:class:`Preprocessor`.
|
65
|
+
"""
|
66
|
+
if not hasattr(self, method):
|
67
|
+
raise NotImplementedError(
|
68
|
+
f"'{method}' is not supported as a filter method on this class."
|
69
|
+
)
|
70
|
+
method_to_call = getattr(self, method)
|
71
|
+
return method_to_call(**parameters)
|
72
|
+
|
73
|
+
def method_to_id(self, method: str, parameters: Dict) -> str:
|
74
|
+
"""
|
75
|
+
Generate a unique ID for a specific method operation.
|
76
|
+
|
77
|
+
Parameters
|
78
|
+
----------
|
79
|
+
method : str
|
80
|
+
The name of the method.
|
81
|
+
parameters : dict
|
82
|
+
A dictionary containing the parameters used by the method.
|
83
|
+
|
84
|
+
Returns
|
85
|
+
-------
|
86
|
+
str
|
87
|
+
A string representation of the method operation, which can be used
|
88
|
+
as a unique identifier.
|
89
|
+
|
90
|
+
Raises
|
91
|
+
------
|
92
|
+
NotImplementedError
|
93
|
+
If ``method`` is not a member of :py:class:`Preprocessor`.
|
94
|
+
"""
|
95
|
+
if not hasattr(self, method):
|
96
|
+
raise NotImplementedError(
|
97
|
+
f"'{method}' is not supported as a filter method on this class."
|
98
|
+
)
|
99
|
+
signature = inspect.signature(getattr(self, method))
|
100
|
+
default = {
|
101
|
+
k: v.default
|
102
|
+
for k, v in signature.parameters.items()
|
103
|
+
if v.default is not inspect.Parameter.empty
|
104
|
+
}
|
105
|
+
|
106
|
+
default.update(parameters)
|
107
|
+
|
108
|
+
return "-".join([str(default[key]) for key in sorted(default.keys())])
|
109
|
+
|
110
|
+
@staticmethod
|
111
|
+
def _gaussian_fourier(template: NDArray, sigma: NDArray) -> NDArray:
|
112
|
+
"""
|
113
|
+
Apply a Gaussian filter in Fourier space on the provided template.
|
114
|
+
|
115
|
+
Parameters
|
116
|
+
----------
|
117
|
+
template : NDArray
|
118
|
+
The input template on which to apply the filter.
|
119
|
+
sigma : NDArray
|
120
|
+
The standard deviation for Gaussian kernel. The greater the value,
|
121
|
+
the more spread out is the filter.
|
122
|
+
|
123
|
+
Returns
|
124
|
+
-------
|
125
|
+
NDArray
|
126
|
+
The template after applying the Fourier Gaussian filter.
|
127
|
+
"""
|
128
|
+
fourrier_map = fourier_gaussian(np.fft.fftn(template), sigma)
|
129
|
+
template = np.real(np.fft.ifftn(fourrier_map))
|
130
|
+
|
131
|
+
return template
|
132
|
+
|
133
|
+
@staticmethod
|
134
|
+
def _gaussian_real(
|
135
|
+
template: NDArray, sigma: NDArray, cutoff_value: float = 4.0
|
136
|
+
) -> NDArray:
|
137
|
+
"""
|
138
|
+
Apply a Gaussian filter on the provided template in real space.
|
139
|
+
|
140
|
+
Parameters
|
141
|
+
----------
|
142
|
+
template : NDArray
|
143
|
+
The input template on which to apply the filter.
|
144
|
+
sigma : NDArray
|
145
|
+
The standard deviation for Gaussian kernel. The greater the value,
|
146
|
+
the more spread out is the filter.
|
147
|
+
cutoff_value : float, optional
|
148
|
+
The value below which the data should be ignored. Default is 4.0.
|
149
|
+
|
150
|
+
Returns
|
151
|
+
-------
|
152
|
+
NDArray
|
153
|
+
The template after applying the Gaussian filter in real space.
|
154
|
+
"""
|
155
|
+
template = gaussian_filter(template, sigma, cval=cutoff_value)
|
156
|
+
return template
|
157
|
+
|
158
|
+
def gaussian_filter(
|
159
|
+
self,
|
160
|
+
template: NDArray,
|
161
|
+
sigma: NDArray,
|
162
|
+
fourier: bool = False,
|
163
|
+
) -> NDArray:
|
164
|
+
"""
|
165
|
+
Convolve an atomic structure with a Gaussian kernel.
|
166
|
+
|
167
|
+
Parameters
|
168
|
+
----------
|
169
|
+
template : NDArray
|
170
|
+
The input atomic structure map.
|
171
|
+
resolution : float, optional
|
172
|
+
The resolution. The product of `resolution` and `sigma_coeff` is used
|
173
|
+
to compute the `sigma` for the discretized Gaussian. Default is None.
|
174
|
+
sigma : NDArray
|
175
|
+
The standard deviation for Gaussian kernel. Should either be a scalar
|
176
|
+
or a sequence of scalars.
|
177
|
+
fourier : bool, optional
|
178
|
+
If true, applies a Fourier Gaussian filter; otherwise, applies a
|
179
|
+
real-space Gaussian filter. Default is False.
|
180
|
+
|
181
|
+
Returns
|
182
|
+
-------
|
183
|
+
NDArray
|
184
|
+
The simulated electron densities after applying the Gaussian filter.
|
185
|
+
"""
|
186
|
+
sigma = 0 if sigma is None else sigma
|
187
|
+
|
188
|
+
if sigma <= 0:
|
189
|
+
return template
|
190
|
+
|
191
|
+
func = self._gaussian_real if not fourier else self._gaussian_fourier
|
192
|
+
template = func(template, sigma)
|
193
|
+
|
194
|
+
return template
|
195
|
+
|
196
|
+
def difference_of_gaussian_filter(
|
197
|
+
self, template: NDArray, low_sigma: NDArray, high_sigma: NDArray
|
198
|
+
) -> NDArray:
|
199
|
+
"""
|
200
|
+
Apply the Difference of Gaussian (DoG) bandpass filter on
|
201
|
+
the provided template.
|
202
|
+
|
203
|
+
Parameters
|
204
|
+
----------
|
205
|
+
template : NDArray
|
206
|
+
The input template on which to apply the technique.
|
207
|
+
low_sigma : NDArray
|
208
|
+
The smaller standard deviation for the Gaussian kernel.
|
209
|
+
Should be scalar or sequence of scalars of length template.ndim.
|
210
|
+
high_sigma : NDArray
|
211
|
+
The larger standard deviation for the Gaussian kernel.
|
212
|
+
Should be scalar or sequence of scalars of length template.ndim.
|
213
|
+
|
214
|
+
Returns
|
215
|
+
-------
|
216
|
+
NDArray
|
217
|
+
The result of applying the Difference of Gaussian technique on the template.
|
218
|
+
"""
|
219
|
+
if np.any(low_sigma > high_sigma):
|
220
|
+
print("low_sigma should be smaller than high_sigma.")
|
221
|
+
im1 = self._gaussian_real(template, low_sigma)
|
222
|
+
im2 = self._gaussian_real(template, high_sigma)
|
223
|
+
return im1 - im2
|
224
|
+
|
225
|
+
def local_gaussian_alignment_filter(
|
226
|
+
self,
|
227
|
+
target: NDArray,
|
228
|
+
template: NDArray,
|
229
|
+
lbd: float,
|
230
|
+
sigma_range: Tuple[float, float] = (0.1, 20),
|
231
|
+
) -> NDArray:
|
232
|
+
"""
|
233
|
+
Simulate electron density by optimizing a sum of Gaussians.
|
234
|
+
|
235
|
+
For that, the following minimization problem is considered:
|
236
|
+
|
237
|
+
.. math::
|
238
|
+
dl_{\\text{{target}}} = \\frac{\\lambda}{\\sigma_{x}^{2}} + \\epsilon^{2}
|
239
|
+
|
240
|
+
.. math::
|
241
|
+
\\epsilon^{2} = \\| \\text{target} - \\text{template} \\|^{2}
|
242
|
+
|
243
|
+
Parameters
|
244
|
+
----------
|
245
|
+
target : NDArray
|
246
|
+
The target electron density map.
|
247
|
+
template : NDArray
|
248
|
+
The input atomic structure map.
|
249
|
+
lbd : float
|
250
|
+
The lambda hyperparameter.
|
251
|
+
sigma_range : tuple of float, optional
|
252
|
+
The range of sigma values for the optimizer. Default is (0.1, 20).
|
253
|
+
|
254
|
+
Returns
|
255
|
+
-------
|
256
|
+
NDArray
|
257
|
+
Simulated electron densities.
|
258
|
+
|
259
|
+
References
|
260
|
+
----------
|
261
|
+
.. [1] Gomez, G (Jan. 2000). Local Smoothness in terms of Variance:
|
262
|
+
The Adaptive Gaussian Filter. In Procedings of the British
|
263
|
+
Machine Vision Conference 2000.
|
264
|
+
"""
|
265
|
+
|
266
|
+
class _optimizer(Preprocessor):
|
267
|
+
def __init__(self, target, template, lbd):
|
268
|
+
self._target = target
|
269
|
+
self._template = template
|
270
|
+
self._dl = np.full(template.shape, 10**9)
|
271
|
+
self._filter = np.zeros_like(template)
|
272
|
+
self._lbd = lbd
|
273
|
+
|
274
|
+
def __call__(self, x, *args):
|
275
|
+
x = x[0]
|
276
|
+
filter = super().gaussian_filter(sigma=x, template=template)
|
277
|
+
dl = self._lbd / (x**2) + np.power(self._target - filter, 2)
|
278
|
+
ind = dl < self._dl
|
279
|
+
self._dl[ind] = dl[ind]
|
280
|
+
self._filter[ind] = filter[ind]
|
281
|
+
return np.sum(self._dl)
|
282
|
+
|
283
|
+
# This method needs pre normalization
|
284
|
+
template = template.copy()
|
285
|
+
target = target.copy()
|
286
|
+
sd_target = np.std(target)
|
287
|
+
sd_template = np.std(template)
|
288
|
+
m_target = np.mean(target)
|
289
|
+
m_template = np.mean(target)
|
290
|
+
if sd_target != 0:
|
291
|
+
target = (target - m_target) / sd_target
|
292
|
+
|
293
|
+
if sd_template != 0:
|
294
|
+
template = (template - m_template) / sd_template
|
295
|
+
|
296
|
+
temp = _optimizer(target=target, template=template, lbd=lbd)
|
297
|
+
|
298
|
+
_ = differential_evolution(temp, bounds=[sigma_range], seed=2)
|
299
|
+
|
300
|
+
# Make sure there is no negative density
|
301
|
+
temp._filter += np.abs(np.min(temp._filter))
|
302
|
+
|
303
|
+
return temp._filter
|
304
|
+
|
305
|
+
def local_gaussian_filter(
|
306
|
+
self,
|
307
|
+
template: NDArray,
|
308
|
+
lbd: float,
|
309
|
+
sigma_range: Tuple[float, float],
|
310
|
+
gaussian_sigma: float,
|
311
|
+
) -> NDArray:
|
312
|
+
"""
|
313
|
+
Wrapper for `Preprocessor.local_gaussian_alignment_filter` if no
|
314
|
+
target is available.
|
315
|
+
|
316
|
+
Parameters
|
317
|
+
----------
|
318
|
+
template : NDArray
|
319
|
+
The input atomic structure map.
|
320
|
+
apix : float
|
321
|
+
Ångstrom per voxel passed to `Preprocessor.gaussian_filter`.
|
322
|
+
lbd : float
|
323
|
+
The lambda hyperparameter, common values: 2, 5, 20.
|
324
|
+
sigma_range : tuple of float
|
325
|
+
The range of sigma values for the optimizer.
|
326
|
+
gaussian_sigma : float
|
327
|
+
The sigma value passed to `Preprocessor.gaussian_filter` to
|
328
|
+
obtain a target.
|
329
|
+
|
330
|
+
Returns
|
331
|
+
-------
|
332
|
+
NDArray
|
333
|
+
Simulated electron densities.
|
334
|
+
"""
|
335
|
+
filtered_data = self.gaussian_filter(sigma=gaussian_sigma, template=template)
|
336
|
+
return self.local_gaussian_alignment_filter(
|
337
|
+
target=filtered_data,
|
338
|
+
template=template,
|
339
|
+
lbd=lbd,
|
340
|
+
sigma_range=sigma_range,
|
341
|
+
)
|
342
|
+
|
343
|
+
def edge_gaussian_filter(
|
344
|
+
self,
|
345
|
+
template: NDArray,
|
346
|
+
edge_algorithm: str,
|
347
|
+
sigma: float,
|
348
|
+
reverse: bool = False,
|
349
|
+
) -> NDArray:
|
350
|
+
"""
|
351
|
+
Perform Gaussian filterring according to edges in the input template.
|
352
|
+
|
353
|
+
Parameters
|
354
|
+
----------
|
355
|
+
template : NDArray
|
356
|
+
The input atomic structure map.
|
357
|
+
sigma : NDArray
|
358
|
+
The sigma value for the Gaussian filter.
|
359
|
+
edge_algorithm : str
|
360
|
+
The algorithm used to identify edges. Options are:
|
361
|
+
|
362
|
+
+-------------------+------------------------------------------------+
|
363
|
+
| 'sobel' | Applies sobel filter for edge detection. |
|
364
|
+
+-------------------+------------------------------------------------+
|
365
|
+
| 'prewitt' | Applies prewitt filter for edge detection. |
|
366
|
+
+-------------------+------------------------------------------------+
|
367
|
+
| 'laplace' | Computes edges as second derivative. |
|
368
|
+
+-------------------+------------------------------------------------+
|
369
|
+
| 'gaussian' | See scipy.ndimage.gaussian_gradient_magnitude |
|
370
|
+
+-------------------+------------------------------------------------+
|
371
|
+
| 'gaussian_laplace | See scipy.ndimage.gaussian_laplace |
|
372
|
+
+-------------------+------------------------------------------------+
|
373
|
+
|
374
|
+
reverse : bool, optional
|
375
|
+
If true, the filterring is strong along edges. Default is False.
|
376
|
+
|
377
|
+
Returns
|
378
|
+
-------
|
379
|
+
NDArray
|
380
|
+
Simulated electron densities.
|
381
|
+
"""
|
382
|
+
if edge_algorithm == "sobel":
|
383
|
+
edges = generic_gradient_magnitude(template, sobel)
|
384
|
+
elif edge_algorithm == "prewitt":
|
385
|
+
edges = generic_gradient_magnitude(template, prewitt)
|
386
|
+
elif edge_algorithm == "laplace":
|
387
|
+
edges = laplace(template)
|
388
|
+
elif edge_algorithm == "gaussian":
|
389
|
+
edges = gaussian_gradient_magnitude(template, sigma / 2)
|
390
|
+
elif edge_algorithm == "gaussian_laplace":
|
391
|
+
edges = gaussian_laplace(template, sigma / 2)
|
392
|
+
else:
|
393
|
+
raise ValueError(
|
394
|
+
"Supported edge_algorithm values are"
|
395
|
+
"'sobel', 'prewitt', 'laplace', 'gaussian_laplace', 'gaussian'"
|
396
|
+
)
|
397
|
+
edges[edges != 0] = 1
|
398
|
+
edges /= edges.max()
|
399
|
+
|
400
|
+
edges = gaussian_filter(edges, sigma)
|
401
|
+
filter = gaussian_filter(template, sigma)
|
402
|
+
|
403
|
+
if not reverse:
|
404
|
+
res = template * edges + filter * (1 - edges)
|
405
|
+
else:
|
406
|
+
res = template * (1 - edges) + filter * (edges)
|
407
|
+
|
408
|
+
return res
|
409
|
+
|
410
|
+
def ntree_filter(
|
411
|
+
self,
|
412
|
+
template: NDArray,
|
413
|
+
sigma_range: Tuple[float, float],
|
414
|
+
target: NDArray = None,
|
415
|
+
) -> NDArray:
|
416
|
+
"""
|
417
|
+
Use dyadic tree to identify volume partitions in *template*
|
418
|
+
and filter them with respect to their occupancy.
|
419
|
+
|
420
|
+
Parameters
|
421
|
+
----------
|
422
|
+
template : NDArray
|
423
|
+
The input atomic structure map.
|
424
|
+
sigma_range : tuple of float
|
425
|
+
Range of sigma values used to filter volume partitions.
|
426
|
+
target : NDArray, optional
|
427
|
+
If provided, dyadic tree is computed on target rather than template.
|
428
|
+
|
429
|
+
Returns
|
430
|
+
-------
|
431
|
+
NDArray
|
432
|
+
Simulated electron densities.
|
433
|
+
"""
|
434
|
+
if target is None:
|
435
|
+
target = template
|
436
|
+
|
437
|
+
tree = Ntree(target)
|
438
|
+
|
439
|
+
filter = tree.filter_chunks(arr=template, sigma_range=sigma_range)
|
440
|
+
|
441
|
+
return filter
|
442
|
+
|
443
|
+
def mean_filter(self, template: NDArray, width: NDArray) -> NDArray:
|
444
|
+
"""
|
445
|
+
Perform mean filtering.
|
446
|
+
|
447
|
+
Parameters
|
448
|
+
----------
|
449
|
+
template : NDArray
|
450
|
+
The input atomic structure map.
|
451
|
+
width : NDArray
|
452
|
+
Width of the mean filter along each axis. Can either have length
|
453
|
+
one or template.ndim.
|
454
|
+
|
455
|
+
Returns
|
456
|
+
-------
|
457
|
+
NDArray
|
458
|
+
Simulated electron densities.
|
459
|
+
"""
|
460
|
+
template = template.copy()
|
461
|
+
interpolation_box = template.shape
|
462
|
+
|
463
|
+
width = np.array(width)
|
464
|
+
filter_width = np.repeat(width, template.ndim // width.size)
|
465
|
+
filter_mask = np.ones(filter_width)
|
466
|
+
filter_mask = filter_mask / np.sum(filter_mask)
|
467
|
+
template = convolve(template, filter_mask, mode="same")
|
468
|
+
|
469
|
+
# Sometimes scipy messes up the box sizes ...
|
470
|
+
template = self.interpolate_box(box=interpolation_box, arr=template)
|
471
|
+
|
472
|
+
return template
|
473
|
+
|
474
|
+
def kaiserb_filter(self, template: NDArray, width: int) -> NDArray:
|
475
|
+
"""
|
476
|
+
Apply Kaiser filter defined as:
|
477
|
+
|
478
|
+
.. math::
|
479
|
+
f_{kaiser} = \\frac{I_{0}(\\beta\\sqrt{1-
|
480
|
+
\\frac{4n^{2}}{(M-1)^{2}}})}{I_{0}(\\beta)}
|
481
|
+
-\\frac{M-1}{2} \\leq n \\leq \\frac{M-1}{2}
|
482
|
+
\\text{With } \\beta=3.2
|
483
|
+
|
484
|
+
Parameters
|
485
|
+
----------
|
486
|
+
template : NDArray
|
487
|
+
The input atomic structure map.
|
488
|
+
width : int
|
489
|
+
Width of the filter window.
|
490
|
+
normalize : bool, optional
|
491
|
+
If true, the output is z-transformed. Default is False.
|
492
|
+
|
493
|
+
Returns
|
494
|
+
-------
|
495
|
+
NDArray
|
496
|
+
Simulated electron densities.
|
497
|
+
|
498
|
+
References
|
499
|
+
----------
|
500
|
+
.. [1] Sorzano, Carlos et al (Mar. 2015). Fast and accurate conversion
|
501
|
+
of atomic models into electron density maps. AIMS Biophysics
|
502
|
+
2, 8–20.
|
503
|
+
"""
|
504
|
+
template, interpolation_box = template.copy(), template.shape
|
505
|
+
|
506
|
+
kaiser_window = window_kaiserb(width=width)
|
507
|
+
template = apply_window_filter(arr=template, filter_window=kaiser_window)
|
508
|
+
|
509
|
+
if not np.all(template.shape == interpolation_box):
|
510
|
+
template = self.interpolate_box(box=interpolation_box, arr=template)
|
511
|
+
|
512
|
+
return template
|
513
|
+
|
514
|
+
def blob_filter(self, template: NDArray, width: int) -> NDArray:
|
515
|
+
"""
|
516
|
+
Apply blob filter defined as:
|
517
|
+
|
518
|
+
.. math::
|
519
|
+
f_{blob} = \\frac{\\sqrt{1-(\\frac{4n^{2}}{(M-1)^{2}})^{m}} I_{m}
|
520
|
+
(\\beta\\sqrt{1-(\\frac{4n^{2}}{(M-1)^{2}})})}
|
521
|
+
{I_{m}(\\beta)}
|
522
|
+
-\\frac{M-1}{2} \\leq n \\leq \\frac{M-1}{2}
|
523
|
+
\\text{With } \\beta=3.2 \\text{ and order=2}
|
524
|
+
|
525
|
+
Parameters
|
526
|
+
----------
|
527
|
+
template : NDArray
|
528
|
+
The input atomic structure map.
|
529
|
+
width : int
|
530
|
+
Width of the filter window.
|
531
|
+
|
532
|
+
Returns
|
533
|
+
-------
|
534
|
+
NDArray
|
535
|
+
Simulated electron densities.
|
536
|
+
|
537
|
+
References
|
538
|
+
----------
|
539
|
+
.. [1] Sorzano, Carlos et al (Mar. 2015). Fast and accurate conversion
|
540
|
+
of atomic models into electron density maps. AIMS Biophysics
|
541
|
+
2, 8–20.
|
542
|
+
"""
|
543
|
+
template, interpolation_box = template.copy(), template.shape
|
544
|
+
|
545
|
+
blob_window = window_blob(width=width)
|
546
|
+
template = apply_window_filter(arr=template, filter_window=blob_window)
|
547
|
+
|
548
|
+
if not np.all(template.shape == interpolation_box):
|
549
|
+
template = self.interpolate_box(box=interpolation_box, arr=template)
|
550
|
+
|
551
|
+
return template
|
552
|
+
|
553
|
+
def hamming_filter(self, template: NDArray, width: int) -> NDArray:
|
554
|
+
"""
|
555
|
+
Apply Hamming filter defined as:
|
556
|
+
|
557
|
+
.. math::
|
558
|
+
f_{hamming} = 0.54 - 0.46\\cos(\\frac{2\\pi n}{M-1})
|
559
|
+
0 \\leq n \\leq M-1
|
560
|
+
|
561
|
+
Parameters
|
562
|
+
----------
|
563
|
+
template : NDArray
|
564
|
+
The input atomic structure map.
|
565
|
+
width : int
|
566
|
+
Width of the filter window.
|
567
|
+
|
568
|
+
Returns
|
569
|
+
-------
|
570
|
+
NDArray
|
571
|
+
Simulated electron densities.
|
572
|
+
"""
|
573
|
+
template, interpolation_box = template.copy(), template.shape
|
574
|
+
|
575
|
+
hamming_window = np.hamming(int(width))
|
576
|
+
hamming_window /= hamming_window.sum()
|
577
|
+
|
578
|
+
template = apply_window_filter(arr=template, filter_window=hamming_window)
|
579
|
+
|
580
|
+
if not np.all(template.shape == interpolation_box):
|
581
|
+
template = self.interpolate_box(box=interpolation_box, arr=template)
|
582
|
+
|
583
|
+
return template
|
584
|
+
|
585
|
+
def rank_filter(self, template: NDArray, rank: int) -> NDArray:
|
586
|
+
"""
|
587
|
+
Perform rank filtering.
|
588
|
+
|
589
|
+
Parameters
|
590
|
+
----------
|
591
|
+
template : NDArray
|
592
|
+
The input atomic structure map.
|
593
|
+
rank : int
|
594
|
+
Footprint value. 0 -> minimum filter, -1 -> maximum filter.
|
595
|
+
|
596
|
+
Returns
|
597
|
+
-------
|
598
|
+
NDArray
|
599
|
+
Simulated electron densities.
|
600
|
+
"""
|
601
|
+
if template is None:
|
602
|
+
raise ValueError("Argument template missing")
|
603
|
+
template = template.copy()
|
604
|
+
interpolation_box = template.shape
|
605
|
+
|
606
|
+
size = rank // 2
|
607
|
+
if size <= 1:
|
608
|
+
size = 3
|
609
|
+
|
610
|
+
template = rank_filter(template, rank=rank, size=size)
|
611
|
+
template = self.interpolate_box(box=interpolation_box, arr=template)
|
612
|
+
|
613
|
+
return template
|
614
|
+
|
615
|
+
def mipmap_filter(self, template: NDArray, level: int) -> NDArray:
|
616
|
+
"""
|
617
|
+
Perform mip map antialiasing filtering.
|
618
|
+
|
619
|
+
Parameters
|
620
|
+
----------
|
621
|
+
template : NDArray
|
622
|
+
The input atomic structure map.
|
623
|
+
level : int
|
624
|
+
Pyramid layer. Resolution decreases cubically with level.
|
625
|
+
|
626
|
+
Returns
|
627
|
+
-------
|
628
|
+
NDArray
|
629
|
+
Simulated electron densities.
|
630
|
+
"""
|
631
|
+
array = template.copy()
|
632
|
+
interpolation_box = array.shape
|
633
|
+
|
634
|
+
print(array.shape)
|
635
|
+
|
636
|
+
for k in range(template.ndim):
|
637
|
+
array = decimate(array, q=level, axis=k)
|
638
|
+
|
639
|
+
print(array.shape)
|
640
|
+
template = zoom(array, np.divide(template.shape, array.shape))
|
641
|
+
template = self.interpolate_box(box=interpolation_box, arr=template)
|
642
|
+
|
643
|
+
return template
|
644
|
+
|
645
|
+
def wavelet_filter(
|
646
|
+
self,
|
647
|
+
template: NDArray,
|
648
|
+
level: int,
|
649
|
+
wavelet: str = "bior2.2",
|
650
|
+
) -> NDArray:
|
651
|
+
"""
|
652
|
+
Perform dyadic wavelet decomposition.
|
653
|
+
|
654
|
+
Parameters
|
655
|
+
----------
|
656
|
+
template : NDArray
|
657
|
+
The input atomic structure map.
|
658
|
+
level : int
|
659
|
+
Scale of the wavelet transform.
|
660
|
+
wavelet : str, optional
|
661
|
+
Mother wavelet used for decomposition. Default is 'bior2.2'.
|
662
|
+
|
663
|
+
Returns
|
664
|
+
-------
|
665
|
+
NDArray
|
666
|
+
Simulated electron densities.
|
667
|
+
"""
|
668
|
+
if wavelet not in wavelist(kind="discrete"):
|
669
|
+
raise NotImplementedError(
|
670
|
+
"Print argument wavelet has to be one of the following: %s",
|
671
|
+
", ".join(wavelist(kind="discrete")),
|
672
|
+
)
|
673
|
+
|
674
|
+
template, interpolation_box = template.copy(), template.shape
|
675
|
+
decomp = wavedecn(template, level=level, wavelet=wavelet)
|
676
|
+
|
677
|
+
for i in range(1, level + 1):
|
678
|
+
decomp[i] = {k: np.zeros_like(v) for k, v in decomp[i].items()}
|
679
|
+
|
680
|
+
template = waverecn(coeffs=decomp, wavelet=wavelet)
|
681
|
+
template = self.interpolate_box(box=interpolation_box, arr=template)
|
682
|
+
|
683
|
+
return template
|
684
|
+
|
685
|
+
@staticmethod
|
686
|
+
def molmap(
|
687
|
+
coordinates: NDArray,
|
688
|
+
weights: Tuple[float],
|
689
|
+
resolution: float,
|
690
|
+
sigma_factor: float = 1 / (np.pi * np.sqrt(2)),
|
691
|
+
cutoff_value: float = 5.0,
|
692
|
+
origin: Tuple[float] = None,
|
693
|
+
shape: Tuple[int] = None,
|
694
|
+
sampling_rate: float = None,
|
695
|
+
) -> NDArray:
|
696
|
+
"""
|
697
|
+
Compute the electron densities analogous to Chimera's molmap function.
|
698
|
+
|
699
|
+
Parameters
|
700
|
+
----------
|
701
|
+
coordinates : NDArray
|
702
|
+
A N x 3 array containing atomic coordinates in z, y, x format.
|
703
|
+
weights : [float]
|
704
|
+
The weights to use for the entries in coordinates.
|
705
|
+
resolution : float
|
706
|
+
The product of resolution and sigma_factor gives the sigma used to
|
707
|
+
compute the discretized Gaussian.
|
708
|
+
sigma_factor : float
|
709
|
+
The factor used with resolution to compute sigma. Default is 1 / (π√2).
|
710
|
+
cutoff_value : float
|
711
|
+
The cutoff value for the Gaussian kernel. Default is 5.0.
|
712
|
+
origin : (float,)
|
713
|
+
The origin of the coordinate system used in coordinates. If not specified,
|
714
|
+
the minimum coordinate along each axis is used.
|
715
|
+
shape : (int,)
|
716
|
+
The shape of the output array. If not specified, the function computes the
|
717
|
+
smallest output array that contains all atoms.
|
718
|
+
sampling_rate : float
|
719
|
+
The Ångstrom per voxel of the output array. If not specified, the function
|
720
|
+
sets this value to resolution/3.
|
721
|
+
|
722
|
+
References
|
723
|
+
----------
|
724
|
+
..[1] https://www.cgl.ucsf.edu/chimera/docs/UsersGuide/midas/molmap.html
|
725
|
+
|
726
|
+
Returns
|
727
|
+
-------
|
728
|
+
NDArray
|
729
|
+
A numpy array containing the simulated electron densities.
|
730
|
+
"""
|
731
|
+
if sampling_rate is None:
|
732
|
+
sampling_rate = resolution * (1.0 / 3)
|
733
|
+
|
734
|
+
coordinates = coordinates.copy()
|
735
|
+
if origin is None:
|
736
|
+
origin = coordinates.min(axis=0)
|
737
|
+
if shape is None:
|
738
|
+
positions = (coordinates - origin) / sampling_rate
|
739
|
+
shape = positions.max(axis=0).astype(int)[::-1] + 2
|
740
|
+
|
741
|
+
positions = (coordinates - origin) / sampling_rate
|
742
|
+
positions = positions[:, ::-1]
|
743
|
+
|
744
|
+
out = np.zeros(shape, dtype=np.float32)
|
745
|
+
sigma = sigma_factor * resolution
|
746
|
+
sigma_grid = sigma / sampling_rate
|
747
|
+
sigma_grid2 = sigma_grid * sigma_grid
|
748
|
+
for index, point in enumerate(np.rollaxis(positions, 0)):
|
749
|
+
starts = np.maximum(np.ceil(point - cutoff_value * sigma_grid), 0).astype(
|
750
|
+
int
|
751
|
+
)
|
752
|
+
stops = np.minimum(
|
753
|
+
np.floor(point + cutoff_value * sigma_grid), shape
|
754
|
+
).astype(int)
|
755
|
+
|
756
|
+
grid_index = np.meshgrid(
|
757
|
+
*[range(start, stop) for start, stop in zip(starts, stops)]
|
758
|
+
)
|
759
|
+
distances = np.einsum(
|
760
|
+
"aijk->ijk",
|
761
|
+
np.array([(grid_index[i] - point[i]) ** 2 for i in range(len(point))]),
|
762
|
+
dtype=np.float64,
|
763
|
+
)
|
764
|
+
np.add.at(
|
765
|
+
out,
|
766
|
+
tuple(grid_index),
|
767
|
+
weights[index] * np.exp(-0.5 * distances / sigma_grid2),
|
768
|
+
)
|
769
|
+
|
770
|
+
out *= np.power(2 * np.pi, -1.5) * np.power(sigma, -3)
|
771
|
+
return out
|
772
|
+
|
773
|
+
def interpolate_box(
|
774
|
+
self, arr: NDArray, box: Tuple[int], kind: str = "nearest"
|
775
|
+
) -> NDArray:
|
776
|
+
"""
|
777
|
+
Resample ``arr`` within ``box`` using ``kind`` interpolation.
|
778
|
+
|
779
|
+
Parameters
|
780
|
+
----------
|
781
|
+
arr : NDArray
|
782
|
+
The input numpy array.
|
783
|
+
box : tuple of int
|
784
|
+
Tuple of integers corresponding to the shape of the output array.
|
785
|
+
kind : str, optional
|
786
|
+
Interpolation method used (see scipy.interpolate.interp1d).
|
787
|
+
Default is 'nearest'.
|
788
|
+
|
789
|
+
Raises
|
790
|
+
------
|
791
|
+
ValueError
|
792
|
+
If the shape of box does not match arr.ndim
|
793
|
+
|
794
|
+
Returns
|
795
|
+
-------
|
796
|
+
NDArray
|
797
|
+
Interpolated numpy array.
|
798
|
+
"""
|
799
|
+
if len(box) != arr.ndim:
|
800
|
+
raise ValueError(f"Expected box of {arr.ndim}, got {len(box)}")
|
801
|
+
|
802
|
+
for axis, size in enumerate(box):
|
803
|
+
f = interp1d(
|
804
|
+
np.linspace(0, 1, arr.shape[axis]),
|
805
|
+
arr,
|
806
|
+
kind=kind,
|
807
|
+
axis=axis,
|
808
|
+
fill_value="extrapolate",
|
809
|
+
)
|
810
|
+
arr = f(np.linspace(0, 1, size))
|
811
|
+
|
812
|
+
return arr
|
813
|
+
|
814
|
+
@staticmethod
|
815
|
+
def fftfreqn(shape: NDArray, sampling_rate: NDArray) -> NDArray:
|
816
|
+
"""
|
817
|
+
Calculate the N-dimensional equivalent to the inverse fftshifted
|
818
|
+
absolute of numpy's fftfreq function, supporting anisotropic sampling.
|
819
|
+
|
820
|
+
Parameters
|
821
|
+
----------
|
822
|
+
shape : NDArray
|
823
|
+
The shape of the N-dimensional array.
|
824
|
+
sampling_rate : NDArray
|
825
|
+
The sampling rate in the N-dimensional array.
|
826
|
+
|
827
|
+
Returns
|
828
|
+
-------
|
829
|
+
NDArray
|
830
|
+
A numpy array representing the norm of indices after normalization.
|
831
|
+
|
832
|
+
Examples
|
833
|
+
--------
|
834
|
+
>>> import numpy as np
|
835
|
+
>>> from dge import Preprocessor
|
836
|
+
>>> freq = Preprocessor().fftfreqn((10,), 1)
|
837
|
+
>>> freq_numpy = np.fft.fftfreq(10, 1)
|
838
|
+
>>> np.allclose(freq, np.abs(np.fft.ifftshift(freq_numpy)))
|
839
|
+
"""
|
840
|
+
indices = np.indices(shape).T
|
841
|
+
norm = np.multiply(shape, sampling_rate)
|
842
|
+
indices -= np.divide(shape, 2).astype(int)
|
843
|
+
indices = np.divide(indices, norm)
|
844
|
+
return np.linalg.norm(indices, axis=-1).T
|
845
|
+
|
846
|
+
def _approximate_butterworth(
|
847
|
+
self,
|
848
|
+
radial_frequencies: NDArray,
|
849
|
+
lowcut: float,
|
850
|
+
highcut: float,
|
851
|
+
gaussian_sigma: float,
|
852
|
+
) -> NDArray:
|
853
|
+
"""
|
854
|
+
Approximate a Butterworth band-pass filter for given radial frequencies.
|
855
|
+
The DC component of the filter is at the origin.
|
856
|
+
|
857
|
+
Parameters
|
858
|
+
----------
|
859
|
+
radial_frequencies : NDArray
|
860
|
+
The radial frequencies for which the Butterworth band-pass
|
861
|
+
filter is to be calculated.
|
862
|
+
lowcut : float
|
863
|
+
The lower cutoff frequency for the band-pass filter.
|
864
|
+
highcut : float
|
865
|
+
The upper cutoff frequency for the band-pass filter.
|
866
|
+
gaussian_sigma : float
|
867
|
+
The sigma value for the Gaussian smoothing applied to the filter.
|
868
|
+
|
869
|
+
Returns
|
870
|
+
-------
|
871
|
+
NDArray
|
872
|
+
A numpy array representing the approximate Butterworth
|
873
|
+
band-pass filter applied to the radial frequencies.
|
874
|
+
"""
|
875
|
+
bpf = ((radial_frequencies <= highcut) & (radial_frequencies >= lowcut)) * 1.0
|
876
|
+
bpf = self.gaussian_filter(template=bpf, sigma=gaussian_sigma, fourier=False)
|
877
|
+
bpf[bpf < np.exp(-2)] = 0
|
878
|
+
bpf = np.fft.ifftshift(bpf)
|
879
|
+
|
880
|
+
return bpf
|
881
|
+
|
882
|
+
def bandpass_filter(
|
883
|
+
self,
|
884
|
+
template: NDArray,
|
885
|
+
minimum_frequency: float,
|
886
|
+
maximum_frequency: float,
|
887
|
+
sampling_rate: NDArray = None,
|
888
|
+
gaussian_sigma: float = 0.0,
|
889
|
+
) -> NDArray:
|
890
|
+
"""
|
891
|
+
Apply a band-pass filter on the provided template, using a
|
892
|
+
Butterworth approximation.
|
893
|
+
|
894
|
+
Parameters
|
895
|
+
----------
|
896
|
+
template : NDArray
|
897
|
+
The input numpy array on which the band-pass filter should be applied.
|
898
|
+
minimum_frequency : float
|
899
|
+
The lower boundary of the frequency range to be preserved. Lower values will
|
900
|
+
retain broader, more global features.
|
901
|
+
maximum_frequency : float
|
902
|
+
The upper boundary of the frequency range to be preserved. Higher values
|
903
|
+
will emphasize finer details and potentially noise.
|
904
|
+
|
905
|
+
sampling_rate : NDarray, optional
|
906
|
+
The sampling rate along each dimension.
|
907
|
+
gaussian_sigma : float, optional
|
908
|
+
Sigma value for the gaussian smoothing to be applied to the filter.
|
909
|
+
|
910
|
+
Returns
|
911
|
+
-------
|
912
|
+
NDArray
|
913
|
+
Bandpass filtered numpy array.
|
914
|
+
"""
|
915
|
+
bpf = self.bandpass_mask(
|
916
|
+
shape=template.shape,
|
917
|
+
minimum_frequency=minimum_frequency,
|
918
|
+
maximum_frequency=maximum_frequency,
|
919
|
+
sampling_rate=sampling_rate,
|
920
|
+
gaussian_sigma=gaussian_sigma,
|
921
|
+
omit_negative_frequencies=False,
|
922
|
+
)
|
923
|
+
|
924
|
+
fft_data = np.fft.fftn(template)
|
925
|
+
np.multiply(fft_data, bpf, out=fft_data)
|
926
|
+
ret = np.real(np.fft.ifftn(fft_data))
|
927
|
+
return ret
|
928
|
+
|
929
|
+
def bandpass_mask(
|
930
|
+
self,
|
931
|
+
shape: Tuple[int],
|
932
|
+
minimum_frequency: float,
|
933
|
+
maximum_frequency: float,
|
934
|
+
sampling_rate: NDArray = None,
|
935
|
+
gaussian_sigma: float = 0.0,
|
936
|
+
omit_negative_frequencies: bool = True,
|
937
|
+
) -> NDArray:
|
938
|
+
"""
|
939
|
+
Compute an approximate Butterworth bundpass filter. The returned filter
|
940
|
+
has it's DC component at the origin.
|
941
|
+
|
942
|
+
Parameters
|
943
|
+
----------
|
944
|
+
shape : tuple of ints
|
945
|
+
Shape of the returned bandpass filter.
|
946
|
+
minimum_frequency : float
|
947
|
+
The lower boundary of the frequency range to be preserved. Lower values will
|
948
|
+
retain broader, more global features.
|
949
|
+
maximum_frequency : float
|
950
|
+
The upper boundary of the frequency range to be preserved. Higher values
|
951
|
+
will emphasize finer details and potentially noise.
|
952
|
+
sampling_rate : NDarray, optional
|
953
|
+
The sampling rate along each dimension.
|
954
|
+
gaussian_sigma : float, optional
|
955
|
+
Sigma value for the gaussian smoothing to be applied to the filter.
|
956
|
+
omit_negative_frequencies : bool, optional
|
957
|
+
Whether the wedge mask should omit negative frequencies, i.e. be
|
958
|
+
applicable to non hermitian-symmetric fourier transforms.
|
959
|
+
|
960
|
+
Returns
|
961
|
+
-------
|
962
|
+
NDArray
|
963
|
+
Bandpass filtered.
|
964
|
+
"""
|
965
|
+
if sampling_rate is None:
|
966
|
+
sampling_rate = np.ones(len(shape))
|
967
|
+
sampling_rate = np.asarray(sampling_rate, dtype=np.float32)
|
968
|
+
sampling_rate /= sampling_rate.max()
|
969
|
+
|
970
|
+
if minimum_frequency > maximum_frequency:
|
971
|
+
minimum_frequency, maximum_frequency = maximum_frequency, minimum_frequency
|
972
|
+
|
973
|
+
radial_freq = self.fftfreqn(shape, sampling_rate)
|
974
|
+
bpf = self._approximate_butterworth(
|
975
|
+
radial_frequencies=radial_freq,
|
976
|
+
lowcut=minimum_frequency,
|
977
|
+
highcut=maximum_frequency,
|
978
|
+
gaussian_sigma=gaussian_sigma,
|
979
|
+
)
|
980
|
+
|
981
|
+
if omit_negative_frequencies:
|
982
|
+
stop = 1 + (shape[-1] // 2)
|
983
|
+
bpf = bpf[..., :stop]
|
984
|
+
|
985
|
+
return bpf
|
986
|
+
|
987
|
+
def wedge_mask(
|
988
|
+
self,
|
989
|
+
shape: Tuple[int],
|
990
|
+
tilt_angles: NDArray,
|
991
|
+
sigma: float = 0,
|
992
|
+
omit_negative_frequencies: bool = True,
|
993
|
+
) -> NDArray:
|
994
|
+
"""
|
995
|
+
Create a wedge mask with the same shape as template by rotating a
|
996
|
+
plane according to tilt angles. The DC component of the filter is at the origin.
|
997
|
+
|
998
|
+
Parameters
|
999
|
+
----------
|
1000
|
+
shape : Tuple of ints
|
1001
|
+
Shape of the output wedge array.
|
1002
|
+
tilt_angles : NDArray
|
1003
|
+
Tilt angles in format d dimensions N tilts [d x N].
|
1004
|
+
sigma : float, optional
|
1005
|
+
Standard deviation for Gaussian kernel used for smoothing the wedge.
|
1006
|
+
omit_negative_frequencies : bool, optional
|
1007
|
+
Whether the wedge mask should omit negative frequencies, i.e. be
|
1008
|
+
applicable to non hermitian-symmetric fourier transforms.
|
1009
|
+
|
1010
|
+
Returns
|
1011
|
+
-------
|
1012
|
+
NDArray
|
1013
|
+
A numpy array containing the wedge mask.
|
1014
|
+
|
1015
|
+
Notes
|
1016
|
+
-----
|
1017
|
+
The axis perpendicular to the tilts is the leftmost closest axis
|
1018
|
+
with minimal tilt.
|
1019
|
+
|
1020
|
+
Examples
|
1021
|
+
--------
|
1022
|
+
>>> import numpy as np
|
1023
|
+
>>> from tme import Preprocessor
|
1024
|
+
>>> angles = np.zeros((3, 10))
|
1025
|
+
>>> angles[2, :] = np.linspace(-50, 55, 10)
|
1026
|
+
>>> wedge = Preprocessor().wedge_mask(
|
1027
|
+
>>> shape = (50,50,50),
|
1028
|
+
>>> tilt_angles = angles,
|
1029
|
+
>>> omit_negative_frequencies = True
|
1030
|
+
>>> )
|
1031
|
+
>>> wedge = np.fft.fftshift(wedge)
|
1032
|
+
|
1033
|
+
This will create a wedge that is open along axis 1, tilted
|
1034
|
+
around axis 2 and propagated along axis 0. The code above would
|
1035
|
+
be equivalent to the following
|
1036
|
+
|
1037
|
+
>>> wedge = Preprocessor().continuous_wedge_mask(
|
1038
|
+
>>> shape = (50,50,50),
|
1039
|
+
>>> start_tilt = 50,
|
1040
|
+
>>> stop_tilt=55,
|
1041
|
+
>>> tilt_axis=1,
|
1042
|
+
>>> omit_negative_frequencies=False,
|
1043
|
+
>>> infinite_plane=False
|
1044
|
+
>>> )
|
1045
|
+
>>> wedge = np.fft.fftshift(wedge)
|
1046
|
+
|
1047
|
+
with the difference being that :py:meth:`Preprocessor.continuous_wedge_mask`
|
1048
|
+
does not consider individual plane tilts.
|
1049
|
+
|
1050
|
+
See Also
|
1051
|
+
--------
|
1052
|
+
:py:meth:`Preprocessor.continuous_wedge_mask`
|
1053
|
+
"""
|
1054
|
+
plane = np.zeros(shape, dtype=np.float32)
|
1055
|
+
opening_axis = np.argmax(np.abs(tilt_angles), axis=0)
|
1056
|
+
slices = tuple(slice(a, a + 1) for a in np.divide(shape, 2).astype(int))
|
1057
|
+
plane_rotated = np.zeros_like(plane)
|
1058
|
+
wedge_volume = np.zeros_like(plane)
|
1059
|
+
for index in range(tilt_angles.shape[1]):
|
1060
|
+
potential_axes, *_ = np.where(
|
1061
|
+
np.abs(tilt_angles[:, index]) == np.abs(tilt_angles[:, index]).min()
|
1062
|
+
)
|
1063
|
+
largest_tilt = np.argmax(np.abs(tilt_angles[:, index]))
|
1064
|
+
opening_axis_index = np.argmin(np.abs(potential_axes - largest_tilt))
|
1065
|
+
opening_axis = potential_axes[opening_axis_index]
|
1066
|
+
rotation_matrix = euler_to_rotationmatrix(tilt_angles[:, index])
|
1067
|
+
plane_rotated.fill(0)
|
1068
|
+
plane.fill(0)
|
1069
|
+
subset = tuple(
|
1070
|
+
slice(None) if i != opening_axis else slices[opening_axis]
|
1071
|
+
for i in range(plane.ndim)
|
1072
|
+
)
|
1073
|
+
plane[subset] = 1
|
1074
|
+
Density.rotate_array(
|
1075
|
+
arr=plane,
|
1076
|
+
rotation_matrix=rotation_matrix,
|
1077
|
+
out=plane_rotated,
|
1078
|
+
use_geometric_center=True,
|
1079
|
+
order=1,
|
1080
|
+
)
|
1081
|
+
wedge_volume += plane_rotated
|
1082
|
+
|
1083
|
+
wedge_volume = self.gaussian_filter(
|
1084
|
+
template=wedge_volume, sigma=sigma, fourier=False
|
1085
|
+
)
|
1086
|
+
wedge_volume = np.where(wedge_volume > np.exp(-2), 1, 0)
|
1087
|
+
wedge_volume = np.fft.ifftshift(wedge_volume)
|
1088
|
+
|
1089
|
+
if omit_negative_frequencies:
|
1090
|
+
stop = 1 + (wedge_volume.shape[-1] // 2)
|
1091
|
+
wedge_volume = wedge_volume[..., :stop]
|
1092
|
+
|
1093
|
+
return wedge_volume
|
1094
|
+
|
1095
|
+
def continuous_wedge_mask(
|
1096
|
+
self,
|
1097
|
+
start_tilt: float,
|
1098
|
+
stop_tilt: float,
|
1099
|
+
shape: Tuple[int],
|
1100
|
+
tilt_axis: int = 1,
|
1101
|
+
sigma: float = 0,
|
1102
|
+
extrude_plane: bool = True,
|
1103
|
+
infinite_plane: bool = True,
|
1104
|
+
omit_negative_frequencies: bool = True,
|
1105
|
+
) -> NDArray:
|
1106
|
+
"""
|
1107
|
+
Generate a wedge in a given shape based on specified tilt angles and axis.
|
1108
|
+
The DC component of the filter is at the origin.
|
1109
|
+
|
1110
|
+
Parameters
|
1111
|
+
----------
|
1112
|
+
start_tilt : float
|
1113
|
+
Starting tilt angle in degrees, e.g. a stage tilt of 70 degrees
|
1114
|
+
would yield a start_tilt value of 70.
|
1115
|
+
stop_tilt : float
|
1116
|
+
Ending tilt angle in degrees, , e.g. a stage tilt of -70 degrees
|
1117
|
+
would yield a stop_tilt value of 70.
|
1118
|
+
tilt_axis : int
|
1119
|
+
Axis that runs through the empty part of the wedge.
|
1120
|
+
- 0 for X-axis
|
1121
|
+
- 1 for Y-axis
|
1122
|
+
- 2 for Z-axis
|
1123
|
+
shape : Tuple of ints
|
1124
|
+
Shape of the output wedge array.
|
1125
|
+
sigma : float, optional
|
1126
|
+
Standard deviation for Gaussian kernel used for smoothing the wedge.
|
1127
|
+
extrude_plane : bool, optional
|
1128
|
+
Whether the tilted plane is extruded to 3D. By default, this represents
|
1129
|
+
the effect of rotating a plane in 3D yielding a cylinder with wedge
|
1130
|
+
insertion. If set to False, the returned mask has spherical shape,
|
1131
|
+
analogous to rotating a line in 3D.
|
1132
|
+
omit_negative_frequencies : bool, optional
|
1133
|
+
Whether the wedge mask should omit negative frequencies, i.e. be
|
1134
|
+
applicable to non hermitian-symmetric fourier transforms.
|
1135
|
+
infinite_plane : bool, optional
|
1136
|
+
Whether the plane should be considered to be larger than the shape. In this
|
1137
|
+
case the output wedge mask fill have no spheric component.
|
1138
|
+
|
1139
|
+
Returns
|
1140
|
+
-------
|
1141
|
+
NDArray
|
1142
|
+
Array of the specified shape with the wedge created based on
|
1143
|
+
the tilt angles.
|
1144
|
+
|
1145
|
+
Examples
|
1146
|
+
--------
|
1147
|
+
>>> wedge = create_wedge(30, 60, 1, (64, 64, 64))
|
1148
|
+
|
1149
|
+
Notes
|
1150
|
+
-----
|
1151
|
+
The rotation plane is spanned by the tilt axis and the leftmost dimension
|
1152
|
+
that is not the tilt axis.
|
1153
|
+
|
1154
|
+
See Also
|
1155
|
+
--------
|
1156
|
+
:py:meth:`Preprocessor.wedge_mask`
|
1157
|
+
"""
|
1158
|
+
shape_center = np.divide(shape, 2).astype(int)
|
1159
|
+
|
1160
|
+
opening_axis = tilt_axis
|
1161
|
+
base_axis = (tilt_axis + 1) % len(shape)
|
1162
|
+
|
1163
|
+
grid = (np.indices(shape).T - shape_center).T
|
1164
|
+
|
1165
|
+
start_radians = np.tan(np.radians(90 - start_tilt))
|
1166
|
+
stop_radians = np.tan(np.radians(-1 * (90 - stop_tilt)))
|
1167
|
+
max_tan_value = np.tan(np.radians(90)) + 1
|
1168
|
+
|
1169
|
+
with np.errstate(divide="ignore", invalid="ignore"):
|
1170
|
+
ratios = np.where(
|
1171
|
+
grid[opening_axis] == 0,
|
1172
|
+
max_tan_value,
|
1173
|
+
grid[base_axis] / grid[opening_axis],
|
1174
|
+
)
|
1175
|
+
|
1176
|
+
wedge = np.logical_or(start_radians <= ratios, stop_radians >= ratios).astype(
|
1177
|
+
np.float32
|
1178
|
+
)
|
1179
|
+
|
1180
|
+
if extrude_plane:
|
1181
|
+
distances = np.sqrt(grid[base_axis] ** 2 + grid[opening_axis] ** 2)
|
1182
|
+
else:
|
1183
|
+
distances = np.linalg.norm(grid, axis=0)
|
1184
|
+
|
1185
|
+
if not infinite_plane:
|
1186
|
+
np.multiply(wedge, distances <= shape[opening_axis] // 2, out=wedge)
|
1187
|
+
|
1188
|
+
wedge = self.gaussian_filter(template=wedge, sigma=sigma, fourier=False)
|
1189
|
+
wedge = np.fft.ifftshift(wedge > np.exp(-2))
|
1190
|
+
|
1191
|
+
if omit_negative_frequencies:
|
1192
|
+
stop = 1 + (wedge.shape[-1] // 2)
|
1193
|
+
wedge = wedge[..., :stop]
|
1194
|
+
|
1195
|
+
return wedge
|
1196
|
+
|
1197
|
+
@staticmethod
|
1198
|
+
def _fourier_crop_mask(old_shape: NDArray, new_shape: NDArray) -> NDArray:
|
1199
|
+
"""
|
1200
|
+
Generate a mask for Fourier cropping.
|
1201
|
+
|
1202
|
+
Parameters
|
1203
|
+
----------
|
1204
|
+
old_shape : NDArray
|
1205
|
+
The original shape of the array before cropping.
|
1206
|
+
new_shape : NDArray
|
1207
|
+
The new desired shape for the array after cropping.
|
1208
|
+
|
1209
|
+
Returns
|
1210
|
+
-------
|
1211
|
+
NDArray
|
1212
|
+
The mask array for Fourier cropping.
|
1213
|
+
"""
|
1214
|
+
mask = np.zeros(old_shape, dtype=bool)
|
1215
|
+
mask[tuple(np.indices(new_shape))] = 1
|
1216
|
+
box_shift = np.floor(np.divide(new_shape, 2)).astype(int)
|
1217
|
+
mask = np.roll(mask, shift=-box_shift, axis=range(len(old_shape)))
|
1218
|
+
return mask
|
1219
|
+
|
1220
|
+
def fourier_crop(
|
1221
|
+
self,
|
1222
|
+
template: NDArray,
|
1223
|
+
reciprocal_template_filter: NDArray,
|
1224
|
+
crop_factor: float = 3 / 2,
|
1225
|
+
) -> NDArray:
|
1226
|
+
"""
|
1227
|
+
Perform Fourier uncropping on a given template.
|
1228
|
+
|
1229
|
+
Parameters
|
1230
|
+
----------
|
1231
|
+
template : NDArray
|
1232
|
+
The original template to be uncropped.
|
1233
|
+
reciprocal_template_filter : NDArray
|
1234
|
+
The filter to be applied in the Fourier space.
|
1235
|
+
crop_factor : float
|
1236
|
+
Cropping factor over reeciprocal_template_filter boundary.
|
1237
|
+
|
1238
|
+
Returns
|
1239
|
+
-------
|
1240
|
+
NDArray
|
1241
|
+
The uncropped template.
|
1242
|
+
"""
|
1243
|
+
new_boxsize = np.zeros(template.ndim, dtype=int)
|
1244
|
+
for i in range(template.ndim):
|
1245
|
+
slices = tuple(
|
1246
|
+
slice(0, 1) if j != i else slice(template.shape[i] // 2)
|
1247
|
+
for j in range(template.ndim)
|
1248
|
+
)
|
1249
|
+
filt = np.squeeze(reciprocal_template_filter[slices])
|
1250
|
+
new_boxsize[i] = np.ceil((np.max(np.where(filt > 0)) + 1) * crop_factor) * 2
|
1251
|
+
|
1252
|
+
if np.any(np.greater(new_boxsize, template.shape)):
|
1253
|
+
new_boxsize = np.array(template.shape).copy()
|
1254
|
+
|
1255
|
+
mask = self._fourier_crop_mask(old_shape=template.shape, new_shape=new_boxsize)
|
1256
|
+
arr_ft = np.fft.fftn(template)
|
1257
|
+
arr_ft *= np.prod(new_boxsize) / np.prod(template.shape)
|
1258
|
+
arr_ft = np.reshape(arr_ft[mask], new_boxsize)
|
1259
|
+
arr_cropped = np.real(np.fft.ifftn(arr_ft))
|
1260
|
+
return arr_cropped
|
1261
|
+
|
1262
|
+
def fourier_uncrop(
|
1263
|
+
self, template: NDArray, reciprocal_template_filter: NDArray
|
1264
|
+
) -> NDArray:
|
1265
|
+
"""
|
1266
|
+
Perform an uncrop operation in the Fourier space.
|
1267
|
+
|
1268
|
+
Parameters
|
1269
|
+
----------
|
1270
|
+
template : NDArray
|
1271
|
+
The input array.
|
1272
|
+
reciprocal_template_filter : NDArray
|
1273
|
+
The filter to be applied in the Fourier space.
|
1274
|
+
|
1275
|
+
Returns
|
1276
|
+
-------
|
1277
|
+
NDArray
|
1278
|
+
Uncropped template with shape reciprocal_template_filter.
|
1279
|
+
"""
|
1280
|
+
mask = self._fourier_crop_mask(
|
1281
|
+
old_shape=reciprocal_template_filter.shape, new_shape=template.shape
|
1282
|
+
)
|
1283
|
+
ft_vol = np.zeros_like(mask)
|
1284
|
+
ft_vol[mask] = np.fft.fftn(template).ravel()
|
1285
|
+
ft_vol *= np.divide(np.prod(mask.shape), np.prod(template.shape)).astype(
|
1286
|
+
ft_vol.dtype
|
1287
|
+
)
|
1288
|
+
reciprocal_template_filter = reciprocal_template_filter.astype(ft_vol.dtype)
|
1289
|
+
np.multiply(ft_vol, reciprocal_template_filter, out=ft_vol)
|
1290
|
+
ret = np.real(np.fft.ifftn(ft_vol))
|
1291
|
+
return ret
|