pytme 0.2.9__cp311-cp311-macosx_15_0_arm64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pytme-0.2.9.data/scripts/estimate_ram_usage.py +97 -0
- pytme-0.2.9.data/scripts/match_template.py +1135 -0
- pytme-0.2.9.data/scripts/postprocess.py +622 -0
- pytme-0.2.9.data/scripts/preprocess.py +209 -0
- pytme-0.2.9.data/scripts/preprocessor_gui.py +1227 -0
- pytme-0.2.9.dist-info/METADATA +95 -0
- pytme-0.2.9.dist-info/RECORD +119 -0
- pytme-0.2.9.dist-info/WHEEL +5 -0
- pytme-0.2.9.dist-info/entry_points.txt +6 -0
- pytme-0.2.9.dist-info/licenses/LICENSE +153 -0
- pytme-0.2.9.dist-info/top_level.txt +3 -0
- scripts/__init__.py +0 -0
- scripts/estimate_ram_usage.py +97 -0
- scripts/match_template.py +1135 -0
- scripts/postprocess.py +622 -0
- scripts/preprocess.py +209 -0
- scripts/preprocessor_gui.py +1227 -0
- tests/__init__.py +0 -0
- tests/data/Blurring/blob_width18.npy +0 -0
- tests/data/Blurring/edgegaussian_sigma3.npy +0 -0
- tests/data/Blurring/gaussian_sigma2.npy +0 -0
- tests/data/Blurring/hamming_width6.npy +0 -0
- tests/data/Blurring/kaiserb_width18.npy +0 -0
- tests/data/Blurring/localgaussian_sigma0510.npy +0 -0
- tests/data/Blurring/mean_size5.npy +0 -0
- tests/data/Blurring/ntree_sigma0510.npy +0 -0
- tests/data/Blurring/rank_rank3.npy +0 -0
- tests/data/Maps/.DS_Store +0 -0
- tests/data/Maps/emd_8621.mrc.gz +0 -0
- tests/data/README.md +2 -0
- tests/data/Raw/em_map.map +0 -0
- tests/data/Structures/.DS_Store +0 -0
- tests/data/Structures/1pdj.cif +3339 -0
- tests/data/Structures/1pdj.pdb +1429 -0
- tests/data/Structures/5khe.cif +3685 -0
- tests/data/Structures/5khe.ent +2210 -0
- tests/data/Structures/5khe.pdb +2210 -0
- tests/data/Structures/5uz4.cif +70548 -0
- tests/preprocessing/__init__.py +0 -0
- tests/preprocessing/test_compose.py +76 -0
- tests/preprocessing/test_frequency_filters.py +178 -0
- tests/preprocessing/test_preprocessor.py +136 -0
- tests/preprocessing/test_utils.py +79 -0
- tests/test_analyzer.py +216 -0
- tests/test_backends.py +446 -0
- tests/test_density.py +503 -0
- tests/test_extensions.py +130 -0
- tests/test_matching_cli.py +283 -0
- tests/test_matching_data.py +162 -0
- tests/test_matching_exhaustive.py +124 -0
- tests/test_matching_memory.py +30 -0
- tests/test_matching_optimization.py +226 -0
- tests/test_matching_utils.py +189 -0
- tests/test_orientations.py +175 -0
- tests/test_parser.py +33 -0
- tests/test_rotations.py +153 -0
- tests/test_structure.py +247 -0
- tme/__init__.py +6 -0
- tme/__version__.py +1 -0
- tme/analyzer/__init__.py +2 -0
- tme/analyzer/_utils.py +186 -0
- tme/analyzer/aggregation.py +577 -0
- tme/analyzer/peaks.py +953 -0
- tme/backends/__init__.py +171 -0
- tme/backends/_cupy_utils.py +734 -0
- tme/backends/_jax_utils.py +188 -0
- tme/backends/cupy_backend.py +294 -0
- tme/backends/jax_backend.py +314 -0
- tme/backends/matching_backend.py +1270 -0
- tme/backends/mlx_backend.py +241 -0
- tme/backends/npfftw_backend.py +583 -0
- tme/backends/pytorch_backend.py +430 -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 +2263 -0
- tme/extensions.cpython-311-darwin.so +0 -0
- tme/external/bindings.cpp +332 -0
- tme/filters/__init__.py +6 -0
- tme/filters/_utils.py +311 -0
- tme/filters/bandpass.py +230 -0
- tme/filters/compose.py +81 -0
- tme/filters/ctf.py +393 -0
- tme/filters/reconstruction.py +160 -0
- tme/filters/wedge.py +542 -0
- tme/filters/whitening.py +191 -0
- tme/matching_data.py +863 -0
- tme/matching_exhaustive.py +497 -0
- tme/matching_optimization.py +1311 -0
- tme/matching_scores.py +1183 -0
- tme/matching_utils.py +1188 -0
- tme/memory.py +337 -0
- tme/orientations.py +598 -0
- tme/parser.py +685 -0
- tme/preprocessor.py +1329 -0
- tme/rotations.py +350 -0
- tme/structure.py +1864 -0
- tme/types.py +13 -0
tme/preprocessor.py
ADDED
@@ -0,0 +1,1329 @@
|
|
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 os
|
9
|
+
import pickle
|
10
|
+
import inspect
|
11
|
+
from typing import Dict, Tuple
|
12
|
+
|
13
|
+
import numpy as np
|
14
|
+
from scipy import ndimage
|
15
|
+
from scipy.special import iv as bessel
|
16
|
+
from scipy.interpolate import interp1d, splrep, BSpline
|
17
|
+
from scipy.optimize import differential_evolution, minimize
|
18
|
+
|
19
|
+
from .types import NDArray
|
20
|
+
|
21
|
+
|
22
|
+
class Preprocessor:
|
23
|
+
"""
|
24
|
+
Implements filtering operations on density arrays.
|
25
|
+
"""
|
26
|
+
|
27
|
+
def apply_method(self, method: str, parameters: Dict):
|
28
|
+
"""
|
29
|
+
Invoke ``Preprocessor.method`` using ``parameters``.
|
30
|
+
|
31
|
+
Parameters
|
32
|
+
----------
|
33
|
+
method : str
|
34
|
+
The name of the method to be used.
|
35
|
+
parameters : dict
|
36
|
+
The parameters for the specified method.
|
37
|
+
|
38
|
+
Returns
|
39
|
+
-------
|
40
|
+
The output of ``method``.
|
41
|
+
|
42
|
+
Raises
|
43
|
+
------
|
44
|
+
NotImplementedError
|
45
|
+
If ``method`` is not a member of :py:class:`Preprocessor`.
|
46
|
+
"""
|
47
|
+
if not hasattr(self, method):
|
48
|
+
raise NotImplementedError(
|
49
|
+
f"'{method}' is not supported as a filter method on this class."
|
50
|
+
)
|
51
|
+
return getattr(self, method)(**parameters)
|
52
|
+
|
53
|
+
def method_to_id(self, method: str, parameters: Dict) -> str:
|
54
|
+
"""
|
55
|
+
Generate a unique ID for a specific method operation.
|
56
|
+
|
57
|
+
Parameters
|
58
|
+
----------
|
59
|
+
method : str
|
60
|
+
The name of the method.
|
61
|
+
parameters : dict
|
62
|
+
A dictionary containing the parameters used by the method.
|
63
|
+
|
64
|
+
Returns
|
65
|
+
-------
|
66
|
+
str
|
67
|
+
A string representation of the method operation, which can be used
|
68
|
+
as a unique identifier.
|
69
|
+
|
70
|
+
Raises
|
71
|
+
------
|
72
|
+
NotImplementedError
|
73
|
+
If ``method`` is not a member of :py:class:`Preprocessor`.
|
74
|
+
"""
|
75
|
+
if not hasattr(self, method):
|
76
|
+
raise NotImplementedError(
|
77
|
+
f"'{method}' is not supported as a filter method on this class."
|
78
|
+
)
|
79
|
+
signature = inspect.signature(getattr(self, method))
|
80
|
+
default = {
|
81
|
+
k: v.default
|
82
|
+
for k, v in signature.parameters.items()
|
83
|
+
if v.default is not inspect.Parameter.empty
|
84
|
+
}
|
85
|
+
|
86
|
+
default.update(parameters)
|
87
|
+
|
88
|
+
return "-".join([str(default[key]) for key in sorted(default.keys())])
|
89
|
+
|
90
|
+
def gaussian_filter(
|
91
|
+
self,
|
92
|
+
template: NDArray,
|
93
|
+
sigma: Tuple[float],
|
94
|
+
cutoff_value: float = 4.0,
|
95
|
+
) -> NDArray:
|
96
|
+
"""
|
97
|
+
Convolve an atomic structure with a Gaussian kernel.
|
98
|
+
|
99
|
+
Parameters
|
100
|
+
----------
|
101
|
+
template : NDArray
|
102
|
+
Input data.
|
103
|
+
sigma : float or tuple of floats
|
104
|
+
The standard deviation of the Gaussian kernel along one or all axes.
|
105
|
+
cutoff_value : float, optional
|
106
|
+
Truncates the Gaussian kernel at cutoff_values times sigma.
|
107
|
+
|
108
|
+
Returns
|
109
|
+
-------
|
110
|
+
NDArray
|
111
|
+
Gaussian filtered template.
|
112
|
+
"""
|
113
|
+
sigma = 0 if sigma is None else sigma
|
114
|
+
return ndimage.gaussian_filter(template, sigma, cval=cutoff_value)
|
115
|
+
|
116
|
+
def difference_of_gaussian_filter(
|
117
|
+
self, template: NDArray, low_sigma: NDArray, high_sigma: NDArray
|
118
|
+
) -> NDArray:
|
119
|
+
"""
|
120
|
+
Apply the Difference of Gaussian (DoG) bandpass filter on
|
121
|
+
the provided template.
|
122
|
+
|
123
|
+
Parameters
|
124
|
+
----------
|
125
|
+
template : NDArray
|
126
|
+
The input template on which to apply the technique.
|
127
|
+
low_sigma : NDArray
|
128
|
+
The smaller standard deviation for the Gaussian kernel.
|
129
|
+
Should be scalar or sequence of scalars of length template.ndim.
|
130
|
+
high_sigma : NDArray
|
131
|
+
The larger standard deviation for the Gaussian kernel.
|
132
|
+
Should be scalar or sequence of scalars of length template.ndim.
|
133
|
+
|
134
|
+
Returns
|
135
|
+
-------
|
136
|
+
NDArray
|
137
|
+
The result of applying the Difference of Gaussian technique on the template.
|
138
|
+
"""
|
139
|
+
if np.any(low_sigma > high_sigma):
|
140
|
+
print("low_sigma should be smaller than high_sigma.")
|
141
|
+
im1 = self.gaussian_filter(template, low_sigma)
|
142
|
+
im2 = self.gaussian_filter(template, high_sigma)
|
143
|
+
return im1 - im2
|
144
|
+
|
145
|
+
def local_gaussian_alignment_filter(
|
146
|
+
self,
|
147
|
+
target: NDArray,
|
148
|
+
template: NDArray,
|
149
|
+
lbd: float,
|
150
|
+
sigma_range: Tuple[float, float] = (0.1, 20),
|
151
|
+
) -> NDArray:
|
152
|
+
"""
|
153
|
+
Simulate electron density by optimizing a sum of Gaussians.
|
154
|
+
|
155
|
+
For that, the following minimization problem is considered:
|
156
|
+
|
157
|
+
.. math::
|
158
|
+
dl_{\\text{{target}}} = \\frac{\\lambda}{\\sigma_{x}^{2}} + \\epsilon^{2}
|
159
|
+
|
160
|
+
.. math::
|
161
|
+
\\epsilon^{2} = \\| \\text{target} - \\text{template} \\|^{2}
|
162
|
+
|
163
|
+
Parameters
|
164
|
+
----------
|
165
|
+
target : NDArray
|
166
|
+
The target electron density map.
|
167
|
+
template : NDArray
|
168
|
+
The input atomic structure map.
|
169
|
+
lbd : float
|
170
|
+
The lambda hyperparameter.
|
171
|
+
sigma_range : tuple of float, optional
|
172
|
+
The range of sigma values for the optimizer. Default is (0.1, 20).
|
173
|
+
|
174
|
+
Returns
|
175
|
+
-------
|
176
|
+
NDArray
|
177
|
+
Simulated electron densities.
|
178
|
+
|
179
|
+
References
|
180
|
+
----------
|
181
|
+
.. [1] Gomez, G (Jan. 2000). Local Smoothness in terms of Variance:
|
182
|
+
The Adaptive Gaussian Filter. In Procedings of the British
|
183
|
+
Machine Vision Conference 2000.
|
184
|
+
"""
|
185
|
+
|
186
|
+
class _optimizer(Preprocessor):
|
187
|
+
def __init__(self, target, template, lbd):
|
188
|
+
self._target = target
|
189
|
+
self._template = template
|
190
|
+
self._dl = np.full(template.shape, 10**9)
|
191
|
+
self._filter = np.zeros_like(template)
|
192
|
+
self._lbd = lbd
|
193
|
+
|
194
|
+
def __call__(self, x, *args):
|
195
|
+
x = x[0]
|
196
|
+
filter = super().gaussian_filter(sigma=x, template=template)
|
197
|
+
dl = self._lbd / (x**2) + np.power(self._target - filter, 2)
|
198
|
+
ind = dl < self._dl
|
199
|
+
self._dl[ind] = dl[ind]
|
200
|
+
self._filter[ind] = filter[ind]
|
201
|
+
return np.sum(self._dl)
|
202
|
+
|
203
|
+
# This method needs pre normalization
|
204
|
+
template = template.copy()
|
205
|
+
target = target.copy()
|
206
|
+
sd_target = np.std(target)
|
207
|
+
sd_template = np.std(template)
|
208
|
+
m_target = np.mean(target)
|
209
|
+
m_template = np.mean(target)
|
210
|
+
if sd_target != 0:
|
211
|
+
target = (target - m_target) / sd_target
|
212
|
+
|
213
|
+
if sd_template != 0:
|
214
|
+
template = (template - m_template) / sd_template
|
215
|
+
|
216
|
+
temp = _optimizer(target=target, template=template, lbd=lbd)
|
217
|
+
|
218
|
+
_ = differential_evolution(temp, bounds=[sigma_range], seed=2)
|
219
|
+
|
220
|
+
# Make sure there is no negative density
|
221
|
+
temp._filter += np.abs(np.min(temp._filter))
|
222
|
+
|
223
|
+
return temp._filter
|
224
|
+
|
225
|
+
def local_gaussian_filter(
|
226
|
+
self,
|
227
|
+
template: NDArray,
|
228
|
+
lbd: float,
|
229
|
+
sigma_range: Tuple[float, float],
|
230
|
+
gaussian_sigma: float,
|
231
|
+
) -> NDArray:
|
232
|
+
"""
|
233
|
+
Wrapper for `Preprocessor.local_gaussian_alignment_filter` if no
|
234
|
+
target is available.
|
235
|
+
|
236
|
+
Parameters
|
237
|
+
----------
|
238
|
+
template : NDArray
|
239
|
+
The input atomic structure map.
|
240
|
+
apix : float
|
241
|
+
Ångstrom per voxel passed to `Preprocessor.gaussian_filter`.
|
242
|
+
lbd : float
|
243
|
+
The lambda hyperparameter, common values: 2, 5, 20.
|
244
|
+
sigma_range : tuple of float
|
245
|
+
The range of sigma values for the optimizer.
|
246
|
+
gaussian_sigma : float
|
247
|
+
The sigma value passed to `Preprocessor.gaussian_filter` to
|
248
|
+
obtain a target.
|
249
|
+
|
250
|
+
Returns
|
251
|
+
-------
|
252
|
+
NDArray
|
253
|
+
Simulated electron densities.
|
254
|
+
"""
|
255
|
+
filtered_data = self.gaussian_filter(sigma=gaussian_sigma, template=template)
|
256
|
+
return self.local_gaussian_alignment_filter(
|
257
|
+
target=filtered_data,
|
258
|
+
template=template,
|
259
|
+
lbd=lbd,
|
260
|
+
sigma_range=sigma_range,
|
261
|
+
)
|
262
|
+
|
263
|
+
def edge_gaussian_filter(
|
264
|
+
self,
|
265
|
+
template: NDArray,
|
266
|
+
edge_algorithm: str,
|
267
|
+
sigma: float,
|
268
|
+
reverse: bool = False,
|
269
|
+
) -> NDArray:
|
270
|
+
"""
|
271
|
+
Perform Gaussian filterring according to edges in the input template.
|
272
|
+
|
273
|
+
Parameters
|
274
|
+
----------
|
275
|
+
template : NDArray
|
276
|
+
The input atomic structure map.
|
277
|
+
sigma : NDArray
|
278
|
+
The sigma value for the Gaussian filter.
|
279
|
+
edge_algorithm : str
|
280
|
+
The algorithm used to identify edges. Options are:
|
281
|
+
|
282
|
+
+-------------------+------------------------------------------------+
|
283
|
+
| 'sobel' | Applies sobel filter for edge detection. |
|
284
|
+
+-------------------+------------------------------------------------+
|
285
|
+
| 'prewitt' | Applies prewitt filter for edge detection. |
|
286
|
+
+-------------------+------------------------------------------------+
|
287
|
+
| 'laplace' | Computes edges as second derivative. |
|
288
|
+
+-------------------+------------------------------------------------+
|
289
|
+
| 'gaussian' | See scipy.ndimage.gaussian_gradient_magnitude |
|
290
|
+
+-------------------+------------------------------------------------+
|
291
|
+
| 'gaussian_laplace | See scipy.ndimage.gaussian_laplace |
|
292
|
+
+-------------------+------------------------------------------------+
|
293
|
+
reverse : bool, optional
|
294
|
+
If true, the filterring is strong along edges. Default is False.
|
295
|
+
|
296
|
+
Returns
|
297
|
+
-------
|
298
|
+
NDArray
|
299
|
+
Simulated electron densities.
|
300
|
+
"""
|
301
|
+
if edge_algorithm == "sobel":
|
302
|
+
edges = ndimage.generic_gradient_magnitude(template, ndimage.sobel)
|
303
|
+
elif edge_algorithm == "prewitt":
|
304
|
+
edges = ndimage.generic_gradient_magnitude(template, ndimage.prewitt)
|
305
|
+
elif edge_algorithm == "laplace":
|
306
|
+
edges = ndimage.laplace(template)
|
307
|
+
elif edge_algorithm == "gaussian":
|
308
|
+
edges = ndimage.gaussian_gradient_magnitude(template, sigma / 2)
|
309
|
+
elif edge_algorithm == "gaussian_laplace":
|
310
|
+
edges = ndimage.gaussian_laplace(template, sigma / 2)
|
311
|
+
else:
|
312
|
+
raise ValueError(
|
313
|
+
"Supported edge_algorithm values are"
|
314
|
+
"'sobel', 'prewitt', 'laplace', 'gaussian_laplace', 'gaussian'"
|
315
|
+
)
|
316
|
+
edges[edges != 0] = 1
|
317
|
+
edges /= edges.max()
|
318
|
+
|
319
|
+
edges = ndimage.gaussian_filter(edges, sigma)
|
320
|
+
filt = ndimage.gaussian_filter(template, sigma)
|
321
|
+
|
322
|
+
if not reverse:
|
323
|
+
res = template * edges + filt * (1 - edges)
|
324
|
+
else:
|
325
|
+
res = template * (1 - edges) + filt * (edges)
|
326
|
+
|
327
|
+
return res
|
328
|
+
|
329
|
+
def mean_filter(self, template: NDArray, width: NDArray) -> NDArray:
|
330
|
+
"""
|
331
|
+
Perform mean filtering.
|
332
|
+
|
333
|
+
Parameters
|
334
|
+
----------
|
335
|
+
template : NDArray
|
336
|
+
The input atomic structure map.
|
337
|
+
width : NDArray
|
338
|
+
Width of the mean filter along each axis. Can either have length
|
339
|
+
one or template.ndim.
|
340
|
+
|
341
|
+
Returns
|
342
|
+
-------
|
343
|
+
NDArray
|
344
|
+
Simulated electron densities.
|
345
|
+
"""
|
346
|
+
template = template.copy()
|
347
|
+
interpolation_box = template.shape
|
348
|
+
|
349
|
+
width = np.array(width)
|
350
|
+
filter_width = np.repeat(width, template.ndim // width.size)
|
351
|
+
filter_mask = np.ones(filter_width)
|
352
|
+
filter_mask = filter_mask / np.sum(filter_mask)
|
353
|
+
template = ndimage.convolve(template, filter_mask, mode="reflect")
|
354
|
+
|
355
|
+
# Sometimes scipy messes up the box sizes ...
|
356
|
+
template = self.interpolate_box(box=interpolation_box, arr=template)
|
357
|
+
|
358
|
+
return template
|
359
|
+
|
360
|
+
def kaiserb_filter(self, template: NDArray, width: int) -> NDArray:
|
361
|
+
"""
|
362
|
+
Apply Kaiser filter defined as:
|
363
|
+
|
364
|
+
.. math::
|
365
|
+
f_{kaiser} = \\frac{I_{0}(\\beta\\sqrt{1-
|
366
|
+
\\frac{4n^{2}}{(M-1)^{2}}})}{I_{0}(\\beta)}
|
367
|
+
-\\frac{M-1}{2} \\leq n \\leq \\frac{M-1}{2}
|
368
|
+
\\text{With } \\beta=3.2
|
369
|
+
|
370
|
+
Parameters
|
371
|
+
----------
|
372
|
+
template : NDArray
|
373
|
+
The input atomic structure map.
|
374
|
+
width : int
|
375
|
+
Width of the filter window.
|
376
|
+
normalize : bool, optional
|
377
|
+
If true, the output is z-transformed. Default is False.
|
378
|
+
|
379
|
+
Returns
|
380
|
+
-------
|
381
|
+
NDArray
|
382
|
+
Simulated electron densities.
|
383
|
+
|
384
|
+
References
|
385
|
+
----------
|
386
|
+
.. [1] Sorzano, Carlos et al (Mar. 2015). Fast and accurate conversion
|
387
|
+
of atomic models into electron density maps. AIMS Biophysics
|
388
|
+
2, 8–20.
|
389
|
+
"""
|
390
|
+
template, interpolation_box = template.copy(), template.shape
|
391
|
+
|
392
|
+
kaiser_window = window_kaiserb(width=width)
|
393
|
+
template = apply_window_filter(arr=template, filter_window=kaiser_window)
|
394
|
+
|
395
|
+
if not np.all(template.shape == interpolation_box):
|
396
|
+
template = self.interpolate_box(box=interpolation_box, arr=template)
|
397
|
+
|
398
|
+
return template
|
399
|
+
|
400
|
+
def blob_filter(self, template: NDArray, width: int) -> NDArray:
|
401
|
+
"""
|
402
|
+
Apply blob filter defined as:
|
403
|
+
|
404
|
+
.. math::
|
405
|
+
f_{blob} = \\frac{\\sqrt{1-(\\frac{4n^{2}}{(M-1)^{2}})^{m}} I_{m}
|
406
|
+
(\\beta\\sqrt{1-(\\frac{4n^{2}}{(M-1)^{2}})})}
|
407
|
+
{I_{m}(\\beta)}
|
408
|
+
-\\frac{M-1}{2} \\leq n \\leq \\frac{M-1}{2}
|
409
|
+
\\text{With } \\beta=3.2 \\text{ and order=2}
|
410
|
+
|
411
|
+
Parameters
|
412
|
+
----------
|
413
|
+
template : NDArray
|
414
|
+
The input atomic structure map.
|
415
|
+
width : int
|
416
|
+
Width of the filter window.
|
417
|
+
|
418
|
+
Returns
|
419
|
+
-------
|
420
|
+
NDArray
|
421
|
+
Simulated electron densities.
|
422
|
+
|
423
|
+
References
|
424
|
+
----------
|
425
|
+
.. [1] Sorzano, Carlos et al (Mar. 2015). Fast and accurate conversion
|
426
|
+
of atomic models into electron density maps. AIMS Biophysics
|
427
|
+
2, 8–20.
|
428
|
+
"""
|
429
|
+
template, interpolation_box = template.copy(), template.shape
|
430
|
+
|
431
|
+
blob_window = window_blob(width=width)
|
432
|
+
template = apply_window_filter(arr=template, filter_window=blob_window)
|
433
|
+
|
434
|
+
if not np.all(template.shape == interpolation_box):
|
435
|
+
template = self.interpolate_box(box=interpolation_box, arr=template)
|
436
|
+
|
437
|
+
return template
|
438
|
+
|
439
|
+
def hamming_filter(self, template: NDArray, width: int) -> NDArray:
|
440
|
+
"""
|
441
|
+
Apply Hamming filter defined as:
|
442
|
+
|
443
|
+
.. math::
|
444
|
+
f_{hamming} = 0.54 - 0.46\\cos(\\frac{2\\pi n}{M-1})
|
445
|
+
0 \\leq n \\leq M-1
|
446
|
+
|
447
|
+
Parameters
|
448
|
+
----------
|
449
|
+
template : NDArray
|
450
|
+
The input atomic structure map.
|
451
|
+
width : int
|
452
|
+
Width of the filter window.
|
453
|
+
|
454
|
+
Returns
|
455
|
+
-------
|
456
|
+
NDArray
|
457
|
+
Simulated electron densities.
|
458
|
+
"""
|
459
|
+
template, interpolation_box = template.copy(), template.shape
|
460
|
+
|
461
|
+
hamming_window = np.hamming(int(width))
|
462
|
+
hamming_window /= hamming_window.sum()
|
463
|
+
|
464
|
+
template = apply_window_filter(arr=template, filter_window=hamming_window)
|
465
|
+
|
466
|
+
if not np.all(template.shape == interpolation_box):
|
467
|
+
template = self.interpolate_box(box=interpolation_box, arr=template)
|
468
|
+
|
469
|
+
return template
|
470
|
+
|
471
|
+
def rank_filter(self, template: NDArray, rank: int) -> NDArray:
|
472
|
+
"""
|
473
|
+
Perform rank filtering.
|
474
|
+
|
475
|
+
Parameters
|
476
|
+
----------
|
477
|
+
template : NDArray
|
478
|
+
The input atomic structure map.
|
479
|
+
rank : int
|
480
|
+
Footprint value. 0 -> minimum filter, -1 -> maximum filter.
|
481
|
+
|
482
|
+
Returns
|
483
|
+
-------
|
484
|
+
NDArray
|
485
|
+
Simulated electron densities.
|
486
|
+
"""
|
487
|
+
template = template.copy()
|
488
|
+
interpolation_box = template.shape
|
489
|
+
|
490
|
+
size = rank // 2
|
491
|
+
if size <= 1:
|
492
|
+
size = 3
|
493
|
+
|
494
|
+
template = ndimage.rank_filter(template, rank=rank, size=size)
|
495
|
+
template = self.interpolate_box(box=interpolation_box, arr=template)
|
496
|
+
|
497
|
+
return template
|
498
|
+
|
499
|
+
def median_filter(self, template: NDArray, size: int = None) -> NDArray:
|
500
|
+
"""
|
501
|
+
Perform median filtering.
|
502
|
+
|
503
|
+
Parameters
|
504
|
+
----------
|
505
|
+
template : NDArray
|
506
|
+
The template to be filtered.
|
507
|
+
size : int, optional
|
508
|
+
Size of the filter.
|
509
|
+
|
510
|
+
Returns
|
511
|
+
-------
|
512
|
+
NDArray
|
513
|
+
Filtered template.
|
514
|
+
"""
|
515
|
+
interpolation_box = template.shape
|
516
|
+
|
517
|
+
template = ndimage.median_filter(template, size=size)
|
518
|
+
template = self.interpolate_box(box=interpolation_box, arr=template)
|
519
|
+
|
520
|
+
return template
|
521
|
+
|
522
|
+
def mipmap_filter(self, template: NDArray, level: int) -> NDArray:
|
523
|
+
"""
|
524
|
+
Perform mip map antialiasing filtering.
|
525
|
+
|
526
|
+
Parameters
|
527
|
+
----------
|
528
|
+
template : NDArray
|
529
|
+
The input atomic structure map.
|
530
|
+
level : int
|
531
|
+
Pyramid layer. Resolution decreases cubically with level.
|
532
|
+
|
533
|
+
Returns
|
534
|
+
-------
|
535
|
+
NDArray
|
536
|
+
Simulated electron densities.
|
537
|
+
"""
|
538
|
+
array = template.copy()
|
539
|
+
interpolation_box = array.shape
|
540
|
+
|
541
|
+
for k in range(template.ndim):
|
542
|
+
array = ndimage.decimate(array, q=level, axis=k)
|
543
|
+
|
544
|
+
template = ndimage.zoom(array, np.divide(template.shape, array.shape))
|
545
|
+
template = self.interpolate_box(box=interpolation_box, arr=template)
|
546
|
+
|
547
|
+
return template
|
548
|
+
|
549
|
+
def interpolate_box(
|
550
|
+
self, arr: NDArray, box: Tuple[int], kind: str = "nearest"
|
551
|
+
) -> NDArray:
|
552
|
+
"""
|
553
|
+
Resample ``arr`` within ``box`` using ``kind`` interpolation.
|
554
|
+
|
555
|
+
Parameters
|
556
|
+
----------
|
557
|
+
arr : NDArray
|
558
|
+
The input numpy array.
|
559
|
+
box : tuple of int
|
560
|
+
Tuple of integers corresponding to the shape of the output array.
|
561
|
+
kind : str, optional
|
562
|
+
Interpolation method used (see scipy.interpolate.interp1d).
|
563
|
+
Default is 'nearest'.
|
564
|
+
|
565
|
+
Raises
|
566
|
+
------
|
567
|
+
ValueError
|
568
|
+
If the shape of box does not match arr.ndim
|
569
|
+
|
570
|
+
Returns
|
571
|
+
-------
|
572
|
+
NDArray
|
573
|
+
Interpolated numpy array.
|
574
|
+
"""
|
575
|
+
if len(box) != arr.ndim:
|
576
|
+
raise ValueError(f"Expected box of {arr.ndim}, got {len(box)}")
|
577
|
+
|
578
|
+
for axis, size in enumerate(box):
|
579
|
+
f = interp1d(
|
580
|
+
np.linspace(0, 1, arr.shape[axis]),
|
581
|
+
arr,
|
582
|
+
kind=kind,
|
583
|
+
axis=axis,
|
584
|
+
fill_value="extrapolate",
|
585
|
+
)
|
586
|
+
arr = f(np.linspace(0, 1, size))
|
587
|
+
|
588
|
+
return arr
|
589
|
+
|
590
|
+
def bandpass_filter(
|
591
|
+
self,
|
592
|
+
template: NDArray,
|
593
|
+
lowpass: float,
|
594
|
+
highpass: float,
|
595
|
+
sampling_rate: NDArray = None,
|
596
|
+
gaussian_sigma: float = 0.0,
|
597
|
+
) -> NDArray:
|
598
|
+
"""
|
599
|
+
Apply a band-pass filter on the provided template, using a
|
600
|
+
Butterworth approximation.
|
601
|
+
|
602
|
+
Parameters
|
603
|
+
----------
|
604
|
+
template : NDArray
|
605
|
+
The input numpy array on which the band-pass filter should be applied.
|
606
|
+
lowpass : float
|
607
|
+
The lower boundary of the frequency range to be preserved. Lower values will
|
608
|
+
retain broader, more global features.
|
609
|
+
highpass : float
|
610
|
+
The upper boundary of the frequency range to be preserved. Higher values
|
611
|
+
will emphasize finer details and potentially noise.
|
612
|
+
sampling_rate : NDarray, optional
|
613
|
+
The sampling rate along each dimension.
|
614
|
+
gaussian_sigma : float, optional
|
615
|
+
Sigma value for the gaussian smoothing to be applied to the filter.
|
616
|
+
|
617
|
+
Returns
|
618
|
+
-------
|
619
|
+
NDArray
|
620
|
+
Bandpass filtered numpy array.
|
621
|
+
"""
|
622
|
+
bpf = self.bandpass_mask(
|
623
|
+
shape=template.shape,
|
624
|
+
lowpass=lowpass,
|
625
|
+
highpass=highpass,
|
626
|
+
sampling_rate=sampling_rate,
|
627
|
+
gaussian_sigma=gaussian_sigma,
|
628
|
+
omit_negative_frequencies=False,
|
629
|
+
)
|
630
|
+
|
631
|
+
fft_data = np.fft.fftn(template)
|
632
|
+
np.multiply(fft_data, bpf, out=fft_data)
|
633
|
+
ret = np.real(np.fft.ifftn(fft_data))
|
634
|
+
return ret
|
635
|
+
|
636
|
+
def bandpass_mask(
|
637
|
+
self,
|
638
|
+
shape: Tuple[int],
|
639
|
+
lowpass: float,
|
640
|
+
highpass: float,
|
641
|
+
sampling_rate: NDArray = None,
|
642
|
+
gaussian_sigma: float = 0.0,
|
643
|
+
omit_negative_frequencies: bool = True,
|
644
|
+
) -> NDArray:
|
645
|
+
"""
|
646
|
+
Compute an approximate Butterworth bundpass filter. The returned filter
|
647
|
+
has it's DC component at the origin.
|
648
|
+
|
649
|
+
Parameters
|
650
|
+
----------
|
651
|
+
shape : tuple of ints
|
652
|
+
Shape of the returned bandpass filter.
|
653
|
+
lowpass : float
|
654
|
+
The lower boundary of the frequency range to be preserved. Lower values will
|
655
|
+
retain broader, more global features.
|
656
|
+
maximum_frequency : float
|
657
|
+
The upper boundary of the frequency range to be preserved. Higher values
|
658
|
+
will emphasize finer details and potentially noise.
|
659
|
+
sampling_rate : NDarray, optional
|
660
|
+
The sampling rate along each dimension.
|
661
|
+
gaussian_sigma : float, optional
|
662
|
+
Sigma value for the gaussian smoothing to be applied to the filter.
|
663
|
+
omit_negative_frequencies : bool, optional
|
664
|
+
Whether the wedge mask should omit negative frequencies, i.e. be
|
665
|
+
applicable to non hermitian-symmetric fourier transforms.
|
666
|
+
|
667
|
+
Returns
|
668
|
+
-------
|
669
|
+
NDArray
|
670
|
+
Bandpass filtered.
|
671
|
+
"""
|
672
|
+
from .filters import BandPassFilter
|
673
|
+
|
674
|
+
return BandPassFilter(
|
675
|
+
sampling_rate=sampling_rate,
|
676
|
+
lowpass=lowpass,
|
677
|
+
highpass=highpass,
|
678
|
+
return_real_fourier=omit_negative_frequencies,
|
679
|
+
use_gaussian=gaussian_sigma == 0.0,
|
680
|
+
)(shape=shape)["data"]
|
681
|
+
|
682
|
+
def step_wedge_mask(
|
683
|
+
self,
|
684
|
+
shape: Tuple[int],
|
685
|
+
tilt_angles: Tuple[float] = None,
|
686
|
+
opening_axis: int = 0,
|
687
|
+
tilt_axis: int = 2,
|
688
|
+
weights: float = None,
|
689
|
+
infinite_plane: bool = False,
|
690
|
+
omit_negative_frequencies: bool = True,
|
691
|
+
) -> NDArray:
|
692
|
+
"""
|
693
|
+
Create a wedge mask with the same shape as template by rotating a
|
694
|
+
plane according to tilt angles. The DC component of the filter is at the origin.
|
695
|
+
|
696
|
+
Parameters
|
697
|
+
----------
|
698
|
+
tilt_angles : tuple of float
|
699
|
+
Sequence of tilt angles.
|
700
|
+
shape : Tuple of ints
|
701
|
+
Shape of the output wedge array.
|
702
|
+
tilt_axis : int, optional
|
703
|
+
Axis that the plane is tilted over.
|
704
|
+
- 0 for Z-axis
|
705
|
+
- 1 for Y-axis
|
706
|
+
- 2 for X-axis
|
707
|
+
opening_axis : int, optional
|
708
|
+
Axis running through the void defined by the wedge.
|
709
|
+
- 0 for Z-axis
|
710
|
+
- 1 for Y-axis
|
711
|
+
- 2 for X-axis
|
712
|
+
sigma : float, optional
|
713
|
+
Standard deviation for Gaussian kernel used for smoothing the wedge.
|
714
|
+
weights : float, tuple of float
|
715
|
+
Weight of each element in the wedge. Defaults to one.
|
716
|
+
omit_negative_frequencies : bool, optional
|
717
|
+
Whether the wedge mask should omit negative frequencies, i.e. be
|
718
|
+
applicable to symmetric Fourier transforms (see :obj:`numpy.fft.fftn`)
|
719
|
+
|
720
|
+
Returns
|
721
|
+
-------
|
722
|
+
NDArray
|
723
|
+
A numpy array containing the wedge mask.
|
724
|
+
|
725
|
+
See Also
|
726
|
+
--------
|
727
|
+
:py:meth:`Preprocessor.continuous_wedge_mask`
|
728
|
+
"""
|
729
|
+
from .filters import WedgeReconstructed
|
730
|
+
|
731
|
+
return WedgeReconstructed(
|
732
|
+
angles=tilt_angles,
|
733
|
+
tilt_axis=tilt_axis,
|
734
|
+
opening_axis=opening_axis,
|
735
|
+
frequency_cutoff=None if infinite_plane else 0.5,
|
736
|
+
create_continuous_wedge=False,
|
737
|
+
weights=weights,
|
738
|
+
weight_wedge=weights is not None,
|
739
|
+
)(shape=shape, return_real_fourier=omit_negative_frequencies,)["data"]
|
740
|
+
|
741
|
+
def continuous_wedge_mask(
|
742
|
+
self,
|
743
|
+
start_tilt: float,
|
744
|
+
stop_tilt: float,
|
745
|
+
shape: Tuple[int],
|
746
|
+
opening_axis: int = 0,
|
747
|
+
tilt_axis: int = 2,
|
748
|
+
infinite_plane: bool = True,
|
749
|
+
omit_negative_frequencies: bool = True,
|
750
|
+
) -> NDArray:
|
751
|
+
"""
|
752
|
+
Generate a wedge in a given shape based on specified tilt angles and axis.
|
753
|
+
The DC component of the filter is at the origin.
|
754
|
+
|
755
|
+
Parameters
|
756
|
+
----------
|
757
|
+
start_tilt : float
|
758
|
+
Starting tilt angle in degrees, e.g. a stage tilt of 70 degrees
|
759
|
+
would yield a start_tilt value of 70.
|
760
|
+
stop_tilt : float
|
761
|
+
Ending tilt angle in degrees, , e.g. a stage tilt of -70 degrees
|
762
|
+
would yield a stop_tilt value of 70.
|
763
|
+
tilt_axis : int
|
764
|
+
Axis that the plane is tilted over.
|
765
|
+
- 0 for Z-axis
|
766
|
+
- 1 for Y-axis
|
767
|
+
- 2 for X-axis
|
768
|
+
opening_axis : int
|
769
|
+
Axis running through the void defined by the wedge.
|
770
|
+
- 0 for Z-axis
|
771
|
+
- 1 for Y-axis
|
772
|
+
- 2 for X-axis
|
773
|
+
shape : Tuple of ints
|
774
|
+
Shape of the output wedge array.
|
775
|
+
omit_negative_frequencies : bool, optional
|
776
|
+
Whether the wedge mask should omit negative frequencies, i.e. be
|
777
|
+
applicable to symmetric Fourier transforms (see :obj:`numpy.fft.fftn`)
|
778
|
+
infinite_plane : bool, optional
|
779
|
+
Whether the plane should be considered to be larger than the shape. In this
|
780
|
+
case the output wedge mask fill have no spheric component.
|
781
|
+
|
782
|
+
Returns
|
783
|
+
-------
|
784
|
+
NDArray
|
785
|
+
Array of the specified shape with the wedge created based on
|
786
|
+
the tilt angles.
|
787
|
+
|
788
|
+
See Also
|
789
|
+
--------
|
790
|
+
:py:meth:`Preprocessor.step_wedge_mask`
|
791
|
+
"""
|
792
|
+
from .filters import WedgeReconstructed
|
793
|
+
|
794
|
+
return WedgeReconstructed(
|
795
|
+
angles=(start_tilt, stop_tilt),
|
796
|
+
tilt_axis=tilt_axis,
|
797
|
+
opening_axis=opening_axis,
|
798
|
+
frequency_cutoff=None if infinite_plane else 0.5,
|
799
|
+
create_continuous_wedge=True,
|
800
|
+
)(shape=shape, return_real_fourier=omit_negative_frequencies)["data"]
|
801
|
+
|
802
|
+
|
803
|
+
def window_kaiserb(width: int, beta: float = 3.2, order: int = 0) -> NDArray:
|
804
|
+
"""
|
805
|
+
Create a Kaiser-Bessel window.
|
806
|
+
|
807
|
+
Parameters
|
808
|
+
----------
|
809
|
+
width : int
|
810
|
+
Width of the window.
|
811
|
+
beta : float, optional
|
812
|
+
Beta parameter of the Kaiser-Bessel window. Default is 3.2.
|
813
|
+
order : int, optional
|
814
|
+
Order of the Bessel function. Default is 0.
|
815
|
+
|
816
|
+
Returns
|
817
|
+
-------
|
818
|
+
NDArray
|
819
|
+
Kaiser-Bessel window.
|
820
|
+
|
821
|
+
References
|
822
|
+
----------
|
823
|
+
.. [1] Sorzano, Carlos et al (Mar. 2015). Fast and accurate conversion
|
824
|
+
of atomic models into electron density maps. AIMS Biophysics
|
825
|
+
2, 8–20.
|
826
|
+
"""
|
827
|
+
window = np.arange(0, width)
|
828
|
+
alpha = (width - 1) / 2.0
|
829
|
+
arr = beta * np.sqrt(1 - ((window - alpha) / alpha) ** 2.0)
|
830
|
+
|
831
|
+
return bessel(order, arr) / bessel(order, beta)
|
832
|
+
|
833
|
+
|
834
|
+
def window_blob(width: int, beta: float = 3.2, order: int = 2) -> NDArray:
|
835
|
+
"""
|
836
|
+
Generate a blob window based on Bessel functions.
|
837
|
+
|
838
|
+
Parameters
|
839
|
+
----------
|
840
|
+
width : int
|
841
|
+
Width of the window.
|
842
|
+
beta : float, optional
|
843
|
+
Beta parameter. Default is 3.2.
|
844
|
+
order : int, optional
|
845
|
+
Order of the Bessel function. Default is 2.
|
846
|
+
|
847
|
+
Returns
|
848
|
+
-------
|
849
|
+
NDArray
|
850
|
+
Blob window.
|
851
|
+
|
852
|
+
References
|
853
|
+
----------
|
854
|
+
.. [1] Sorzano, Carlos et al (Mar. 2015). Fast and accurate conversion
|
855
|
+
of atomic models into electron density maps. AIMS Biophysics
|
856
|
+
2, 8–20.
|
857
|
+
"""
|
858
|
+
window = np.arange(0, width)
|
859
|
+
alpha = (width - 1) / 2.0
|
860
|
+
arr = beta * np.sqrt(1 - ((window - alpha) / alpha) ** 2.0)
|
861
|
+
|
862
|
+
arr = np.divide(np.power(arr, order) * bessel(order, arr), bessel(order, beta))
|
863
|
+
arr[arr != arr] = 0
|
864
|
+
return arr
|
865
|
+
|
866
|
+
|
867
|
+
def window_sinckb(omega: float, d: float, dw: float):
|
868
|
+
"""
|
869
|
+
Compute the sinc window combined with a Kaiser window.
|
870
|
+
|
871
|
+
Parameters
|
872
|
+
----------
|
873
|
+
omega : float
|
874
|
+
Reduction factor.
|
875
|
+
d : float
|
876
|
+
Ripple.
|
877
|
+
dw : float
|
878
|
+
Delta w.
|
879
|
+
|
880
|
+
Returns
|
881
|
+
-------
|
882
|
+
ndarray
|
883
|
+
Impulse response of the low-pass filter.
|
884
|
+
|
885
|
+
References
|
886
|
+
----------
|
887
|
+
.. [1] Sorzano, Carlos et al (Mar. 2015). Fast and accurate conversion
|
888
|
+
of atomic models into electron density maps. AIMS Biophysics
|
889
|
+
2, 8–20.
|
890
|
+
"""
|
891
|
+
kaiser = kaiser_mask(d, dw)
|
892
|
+
sinc_m = sinc_mask(np.zeros(kaiser.shape), omega)
|
893
|
+
|
894
|
+
mask = sinc_m * kaiser
|
895
|
+
|
896
|
+
return mask / np.sum(mask)
|
897
|
+
|
898
|
+
|
899
|
+
def apply_window_filter(
|
900
|
+
arr: NDArray,
|
901
|
+
filter_window: NDArray,
|
902
|
+
mode: str = "reflect",
|
903
|
+
cval: float = 0.0,
|
904
|
+
origin: int = 0,
|
905
|
+
):
|
906
|
+
"""
|
907
|
+
Apply a window filter on an input array.
|
908
|
+
|
909
|
+
Parameters
|
910
|
+
----------
|
911
|
+
arr : NDArray,
|
912
|
+
Input array.
|
913
|
+
filter_window : NDArray,
|
914
|
+
Window filter to apply.
|
915
|
+
mode : str, optional
|
916
|
+
Mode for the filtering, default is "reflect".
|
917
|
+
cval : float, optional
|
918
|
+
Value to fill when mode is "constant", default is 0.0.
|
919
|
+
origin : int, optional
|
920
|
+
Origin of the filter window, default is 0.
|
921
|
+
|
922
|
+
Returns
|
923
|
+
-------
|
924
|
+
NDArray,
|
925
|
+
Array after filtering.
|
926
|
+
|
927
|
+
"""
|
928
|
+
filter_window = filter_window[::-1]
|
929
|
+
for axs in range(arr.ndim):
|
930
|
+
ndimage.correlate1d(
|
931
|
+
input=arr,
|
932
|
+
weights=filter_window,
|
933
|
+
axis=axs,
|
934
|
+
output=arr,
|
935
|
+
mode=mode,
|
936
|
+
cval=cval,
|
937
|
+
origin=origin,
|
938
|
+
)
|
939
|
+
return arr
|
940
|
+
|
941
|
+
|
942
|
+
def sinc_mask(mask: NDArray, omega: float) -> NDArray:
|
943
|
+
"""
|
944
|
+
Create a sinc mask.
|
945
|
+
|
946
|
+
Parameters
|
947
|
+
----------
|
948
|
+
mask : NDArray
|
949
|
+
Input mask.
|
950
|
+
omega : float
|
951
|
+
Reduction factor.
|
952
|
+
|
953
|
+
Returns
|
954
|
+
-------
|
955
|
+
NDArray
|
956
|
+
Sinc mask.
|
957
|
+
"""
|
958
|
+
# Move filter origin to the center of the mask
|
959
|
+
mask_origin = int((mask.size - 1) / 2)
|
960
|
+
dist = np.arange(-mask_origin, mask_origin + 1)
|
961
|
+
|
962
|
+
return np.multiply(omega / np.pi, np.sinc((omega / np.pi) * dist))
|
963
|
+
|
964
|
+
|
965
|
+
def kaiser_mask(d: float, dw: float) -> NDArray:
|
966
|
+
"""
|
967
|
+
Create a Kaiser mask.
|
968
|
+
|
969
|
+
Parameters
|
970
|
+
----------
|
971
|
+
d : float
|
972
|
+
Ripple.
|
973
|
+
dw : float
|
974
|
+
Delta-w.
|
975
|
+
|
976
|
+
Returns
|
977
|
+
-------
|
978
|
+
NDArray
|
979
|
+
Kaiser mask.
|
980
|
+
"""
|
981
|
+
# convert dw from a frequency normalized to 1 to a frequency normalized to pi
|
982
|
+
dw *= np.pi
|
983
|
+
A = -20 * np.log10(d)
|
984
|
+
M = max(1, np.ceil((A - 8) / (2.285 * dw)))
|
985
|
+
|
986
|
+
beta = 0
|
987
|
+
if A > 50:
|
988
|
+
beta = 0.1102 * (A - 8.7)
|
989
|
+
elif A >= 21:
|
990
|
+
beta = 0.5842 * np.power(A - 21, 0.4) + 0.07886 * (A - 21)
|
991
|
+
|
992
|
+
mask_values = np.abs(np.arange(-M, M + 1))
|
993
|
+
mask = np.sqrt(1 - np.power(mask_values / M, 2))
|
994
|
+
|
995
|
+
return np.divide(bessel(0, beta * mask), bessel(0, beta))
|
996
|
+
|
997
|
+
|
998
|
+
def electron_factor(
|
999
|
+
dist: NDArray, method: str, atom: str, fourier: bool = False
|
1000
|
+
) -> NDArray:
|
1001
|
+
"""
|
1002
|
+
Compute the electron factor.
|
1003
|
+
|
1004
|
+
Parameters
|
1005
|
+
----------
|
1006
|
+
dist : NDArray
|
1007
|
+
Distance.
|
1008
|
+
method : str
|
1009
|
+
Method name.
|
1010
|
+
atom : str
|
1011
|
+
Atom type.
|
1012
|
+
fourier : bool, optional
|
1013
|
+
Whether to compute the electron factor in Fourier space.
|
1014
|
+
|
1015
|
+
Returns
|
1016
|
+
-------
|
1017
|
+
NDArray
|
1018
|
+
Computed electron factor.
|
1019
|
+
"""
|
1020
|
+
data = get_scattering_factors(method)
|
1021
|
+
n_range = len(data.get(atom, [])) // 2
|
1022
|
+
default = np.zeros(n_range * 3)
|
1023
|
+
|
1024
|
+
res = 0.0
|
1025
|
+
a_values = data.get(atom, default)[:n_range]
|
1026
|
+
b_values = data.get(atom, default)[n_range : 2 * n_range]
|
1027
|
+
|
1028
|
+
if method == "dt1969":
|
1029
|
+
b_values = data.get(atom, default)[1 : (n_range + 1)]
|
1030
|
+
|
1031
|
+
for i in range(n_range):
|
1032
|
+
a = a_values[i]
|
1033
|
+
b = b_values[i]
|
1034
|
+
|
1035
|
+
if fourier:
|
1036
|
+
temp = a * np.exp(-b * np.power(dist, 2))
|
1037
|
+
else:
|
1038
|
+
b = b / (4 * np.power(np.pi, 2))
|
1039
|
+
temp = a * np.sqrt(np.pi / b) * np.exp(-np.power(dist, 2) / (4 * b))
|
1040
|
+
|
1041
|
+
if not np.isnan(temp).any():
|
1042
|
+
res += temp
|
1043
|
+
|
1044
|
+
return res / (2 * np.pi)
|
1045
|
+
|
1046
|
+
|
1047
|
+
def optimize_hlfp(profile, M, T, atom, method, filter_method):
|
1048
|
+
"""
|
1049
|
+
Optimize high-low pass filter (HLFP).
|
1050
|
+
|
1051
|
+
Parameters
|
1052
|
+
----------
|
1053
|
+
profile : NDArray
|
1054
|
+
Input profile.
|
1055
|
+
M : int
|
1056
|
+
Scaling factor.
|
1057
|
+
T : float
|
1058
|
+
Time step.
|
1059
|
+
atom : str
|
1060
|
+
Atom type.
|
1061
|
+
method : str
|
1062
|
+
Method name.
|
1063
|
+
filter_method : str
|
1064
|
+
Filter method name.
|
1065
|
+
|
1066
|
+
Returns
|
1067
|
+
-------
|
1068
|
+
float
|
1069
|
+
Fitness value.
|
1070
|
+
|
1071
|
+
References
|
1072
|
+
----------
|
1073
|
+
.. [1] Sorzano, Carlos et al (Mar. 2015). Fast and accurate conversion
|
1074
|
+
of atomic models into electron density maps. AIMS Biophysics
|
1075
|
+
2, 8–20.
|
1076
|
+
"""
|
1077
|
+
# omega, d, dw
|
1078
|
+
initial_params = [1.0, 0.01, 1.0 / 8.0]
|
1079
|
+
if filter_method == "brute":
|
1080
|
+
best_fitness = float("inf")
|
1081
|
+
OMEGA, D, DW = np.meshgrid(
|
1082
|
+
np.arange(0.7, 1.3, 0.015),
|
1083
|
+
np.arange(0.01, 0.2, 0.015),
|
1084
|
+
np.arange(0.05, 0.2, 0.015),
|
1085
|
+
)
|
1086
|
+
for omega, d, dw in zip(OMEGA.ravel(), D.ravel(), DW.ravel()):
|
1087
|
+
current_fitness = _hlpf_fitness([omega, d, dw], T, M, profile, atom, method)
|
1088
|
+
if current_fitness < best_fitness:
|
1089
|
+
best_fitness = current_fitness
|
1090
|
+
initial_params = [omega, d, dw]
|
1091
|
+
final_params = np.array(initial_params)
|
1092
|
+
else:
|
1093
|
+
res = minimize(
|
1094
|
+
_hlpf_fitness,
|
1095
|
+
initial_params,
|
1096
|
+
args=tuple([T, M, profile, atom, method]),
|
1097
|
+
method="SLSQP",
|
1098
|
+
bounds=([0.2, 2], [1e-3, 2], [1e-3, 1]),
|
1099
|
+
)
|
1100
|
+
final_params = res.x
|
1101
|
+
if np.any(final_params != final_params):
|
1102
|
+
print(f"Solver returned NAs for atom {atom} at {M}" % (atom, M))
|
1103
|
+
final_params = final_params
|
1104
|
+
|
1105
|
+
final_params[0] *= np.pi / M
|
1106
|
+
mask = window_sinckb(*final_params)
|
1107
|
+
|
1108
|
+
if profile.shape[0] > mask.shape[0]:
|
1109
|
+
profile_origin = int((profile.size - 1) / 2)
|
1110
|
+
mask = window(mask, profile_origin, profile_origin)
|
1111
|
+
|
1112
|
+
return mask
|
1113
|
+
|
1114
|
+
|
1115
|
+
def _hlpf_fitness(
|
1116
|
+
params: Tuple[float], T: float, M: float, profile: NDArray, atom: str, method: str
|
1117
|
+
) -> float:
|
1118
|
+
"""
|
1119
|
+
Fitness function for high-low pass filter optimization.
|
1120
|
+
|
1121
|
+
Parameters
|
1122
|
+
----------
|
1123
|
+
params : tuple of float
|
1124
|
+
Parameters [omega, d, dw] for optimization.
|
1125
|
+
T : float
|
1126
|
+
Time step.
|
1127
|
+
M : int
|
1128
|
+
Scaling factor.
|
1129
|
+
profile : NDArray
|
1130
|
+
Input profile.
|
1131
|
+
atom : str
|
1132
|
+
Atom type.
|
1133
|
+
method : str
|
1134
|
+
Method name.
|
1135
|
+
|
1136
|
+
Returns
|
1137
|
+
-------
|
1138
|
+
float
|
1139
|
+
Fitness value.
|
1140
|
+
|
1141
|
+
References
|
1142
|
+
----------
|
1143
|
+
.. [1] Sorzano, Carlos et al (Mar. 2015). Fast and accurate conversion
|
1144
|
+
of atomic models into electron density maps. AIMS Biophysics
|
1145
|
+
2, 8–20.
|
1146
|
+
.. [2] https://github.com/I2PC/xmipp/blob/707f921dfd29cacf5a161535034d28153b58215a/src/xmipp/libraries/data/pdb.cpp#L1344
|
1147
|
+
"""
|
1148
|
+
omega, d, dw = params
|
1149
|
+
|
1150
|
+
if not (0.7 <= omega <= 1.3) and (0 <= d <= 0.2) and (1e-3 <= dw <= 0.2):
|
1151
|
+
return 1e38 * np.random.randint(1, 100)
|
1152
|
+
|
1153
|
+
mask = window_sinckb(omega=omega * np.pi / M, d=d, dw=dw)
|
1154
|
+
|
1155
|
+
if profile.shape[0] > mask.shape[0]:
|
1156
|
+
profile_origin = int((profile.size - 1) / 2)
|
1157
|
+
mask = window(mask, profile_origin, profile_origin)
|
1158
|
+
else:
|
1159
|
+
filter_origin = int((mask.size - 1) / 2)
|
1160
|
+
profile = window(profile, filter_origin, filter_origin)
|
1161
|
+
|
1162
|
+
f_mask = ndimage.convolve(profile, mask)
|
1163
|
+
|
1164
|
+
orig = int((f_mask.size - 1) / 2)
|
1165
|
+
dist = np.arange(-orig, orig + 1) * T
|
1166
|
+
t, c, k = splrep(x=dist, y=f_mask, k=3)
|
1167
|
+
i_max = np.ceil(np.divide(f_mask.shape, M)).astype(int)[0]
|
1168
|
+
coarse_mask = np.arange(-i_max, i_max + 1) * M
|
1169
|
+
spline = BSpline(t, c, k)
|
1170
|
+
coarse_values = spline(coarse_mask)
|
1171
|
+
|
1172
|
+
# padding to retain longer fourier response
|
1173
|
+
aux = window(
|
1174
|
+
coarse_values, x0=10 * coarse_values.shape[0], xf=10 * coarse_values.shape[0]
|
1175
|
+
)
|
1176
|
+
f_filter = np.fft.fftn(aux)
|
1177
|
+
f_filter_mag = np.abs(f_filter)
|
1178
|
+
freq = np.fft.fftfreq(f_filter.size)
|
1179
|
+
freq /= M * T
|
1180
|
+
amplitude_f = mask.sum() / coarse_values.sum()
|
1181
|
+
|
1182
|
+
size_f = f_filter_mag.shape[0] * amplitude_f
|
1183
|
+
fourier_form_f = electron_factor(dist=freq, atom=atom, method=method, fourier=True)
|
1184
|
+
|
1185
|
+
valid_freq_mask = freq >= 0
|
1186
|
+
f1_values = np.log10(f_filter_mag[valid_freq_mask] * size_f)
|
1187
|
+
f2_values = np.log10(np.divide(T, fourier_form_f[valid_freq_mask]))
|
1188
|
+
squared_differences = np.square(f1_values - f2_values)
|
1189
|
+
error = np.sum(squared_differences)
|
1190
|
+
error /= np.sum(valid_freq_mask)
|
1191
|
+
|
1192
|
+
return error
|
1193
|
+
|
1194
|
+
|
1195
|
+
def window(arr, x0, xf, constant_values=0):
|
1196
|
+
"""
|
1197
|
+
Window an array by slicing between x0 and xf and padding if required.
|
1198
|
+
|
1199
|
+
Parameters
|
1200
|
+
----------
|
1201
|
+
arr : ndarray
|
1202
|
+
Input array to be windowed.
|
1203
|
+
x0 : int
|
1204
|
+
Start of the window.
|
1205
|
+
xf : int
|
1206
|
+
End of the window.
|
1207
|
+
constant_values : int or float, optional
|
1208
|
+
The constant values to use for padding, by default 0.
|
1209
|
+
|
1210
|
+
Returns
|
1211
|
+
-------
|
1212
|
+
ndarray
|
1213
|
+
Windowed array.
|
1214
|
+
"""
|
1215
|
+
origin = int((arr.size - 1) / 2)
|
1216
|
+
|
1217
|
+
xs = origin - x0
|
1218
|
+
xe = origin - xf
|
1219
|
+
|
1220
|
+
if xs >= 0 and xe <= arr.shape[0]:
|
1221
|
+
if xs <= arr.shape[0] and xe > 0:
|
1222
|
+
arr = arr[xs:xe]
|
1223
|
+
xs = 0
|
1224
|
+
xe = 0
|
1225
|
+
elif xs <= arr.shape[0]:
|
1226
|
+
arr = arr[xs:]
|
1227
|
+
xs = 0
|
1228
|
+
elif xe >= 0 and xe <= arr.shape[0]:
|
1229
|
+
arr = arr[:xe]
|
1230
|
+
xe = 0
|
1231
|
+
|
1232
|
+
xs *= -1
|
1233
|
+
xe *= -1
|
1234
|
+
|
1235
|
+
return np.pad(
|
1236
|
+
arr, (int(xs), int(xe)), mode="constant", constant_values=constant_values
|
1237
|
+
)
|
1238
|
+
|
1239
|
+
|
1240
|
+
def atom_profile(
|
1241
|
+
M, atom, T=0.08333333, method="peng1995", lfilter=True, filter_method="minimize"
|
1242
|
+
):
|
1243
|
+
"""
|
1244
|
+
Generate an atom profile using a variety of methods.
|
1245
|
+
|
1246
|
+
Parameters
|
1247
|
+
----------
|
1248
|
+
M : float
|
1249
|
+
Down sampling factor.
|
1250
|
+
atom : Any
|
1251
|
+
Type or representation of the atom.
|
1252
|
+
T : float, optional
|
1253
|
+
Sampling rate in angstroms/pixel, by default 0.08333333.
|
1254
|
+
method : str, optional
|
1255
|
+
Method to be used for generating the profile, by default "peng1995".
|
1256
|
+
lfilter : bool, optional
|
1257
|
+
Whether to apply filter on the profile, by default True.
|
1258
|
+
filter_method : str, optional
|
1259
|
+
The method for the filter, by default "minimize".
|
1260
|
+
|
1261
|
+
Returns
|
1262
|
+
-------
|
1263
|
+
BSpline
|
1264
|
+
A spline representation of the atom profile.
|
1265
|
+
|
1266
|
+
References
|
1267
|
+
----------
|
1268
|
+
.. [1] Sorzano, Carlos et al (Mar. 2015). Fast and accurate conversion
|
1269
|
+
of atomic models into electron density maps. AIMS Biophysics
|
1270
|
+
2, 8–20.
|
1271
|
+
.. [2] https://github.com/I2PC/xmipp/blob/707f921dfd29cacf5a161535034d28153b58215a/src/xmipp/libraries/data/pdb.cpp#L1344
|
1272
|
+
"""
|
1273
|
+
M = M / T
|
1274
|
+
imax = np.ceil(4 / T * np.sqrt(76.7309 / (2 * np.power(np.pi, 2))))
|
1275
|
+
dist = np.arange(-imax, imax + 1) * T
|
1276
|
+
|
1277
|
+
profile = electron_factor(dist, method, atom)
|
1278
|
+
|
1279
|
+
if lfilter:
|
1280
|
+
window = optimize_hlfp(
|
1281
|
+
profile=profile,
|
1282
|
+
M=M,
|
1283
|
+
T=T,
|
1284
|
+
atom=atom,
|
1285
|
+
method=method,
|
1286
|
+
filter_method=filter_method,
|
1287
|
+
)
|
1288
|
+
profile = ndimage.convolve(profile, window)
|
1289
|
+
|
1290
|
+
indices = np.where(profile > 1e-3)
|
1291
|
+
min_indices = np.maximum(np.amin(indices, axis=1), 0)
|
1292
|
+
max_indices = np.minimum(np.amax(indices, axis=1) + 1, profile.shape)
|
1293
|
+
slices = tuple(slice(*coord) for coord in zip(min_indices, max_indices))
|
1294
|
+
profile = profile[slices]
|
1295
|
+
|
1296
|
+
profile_origin = int((profile.size - 1) / 2)
|
1297
|
+
dist = np.arange(-profile_origin, profile_origin + 1) * T
|
1298
|
+
t, c, k = splrep(x=dist, y=profile, k=3)
|
1299
|
+
|
1300
|
+
return BSpline(t, c, k)
|
1301
|
+
|
1302
|
+
|
1303
|
+
def get_scattering_factors(method: str) -> Dict:
|
1304
|
+
"""
|
1305
|
+
Retrieve scattering factors from a stored file based on the given method.
|
1306
|
+
|
1307
|
+
Parameters
|
1308
|
+
----------
|
1309
|
+
method : str
|
1310
|
+
Method name used to get the scattering factors.
|
1311
|
+
|
1312
|
+
Returns
|
1313
|
+
-------
|
1314
|
+
Dict
|
1315
|
+
Dictionary containing scattering factors for the given method.
|
1316
|
+
|
1317
|
+
Raises
|
1318
|
+
------
|
1319
|
+
ValueError
|
1320
|
+
If the method is not found in the stored data.
|
1321
|
+
|
1322
|
+
"""
|
1323
|
+
path = os.path.join(os.path.dirname(__file__), "data", "scattering_factors.pickle")
|
1324
|
+
with open(path, "rb") as infile:
|
1325
|
+
data = pickle.load(infile)
|
1326
|
+
|
1327
|
+
if method not in data:
|
1328
|
+
raise ValueError(f"{method} is not valid. Use {', '.join(data.keys())}.")
|
1329
|
+
return data[method]
|