pytme 0.2.0b0__cp311-cp311-macosx_14_0_arm64.whl → 0.2.2__cp311-cp311-macosx_14_0_arm64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pytme-0.2.2.data/scripts/match_template.py +1187 -0
- {pytme-0.2.0b0.data → pytme-0.2.2.data}/scripts/postprocess.py +170 -71
- {pytme-0.2.0b0.data → pytme-0.2.2.data}/scripts/preprocessor_gui.py +179 -86
- pytme-0.2.2.dist-info/METADATA +91 -0
- pytme-0.2.2.dist-info/RECORD +74 -0
- {pytme-0.2.0b0.dist-info → pytme-0.2.2.dist-info}/WHEEL +1 -1
- scripts/extract_candidates.py +126 -87
- scripts/match_template.py +596 -209
- scripts/match_template_filters.py +571 -223
- scripts/postprocess.py +170 -71
- scripts/preprocessor_gui.py +179 -86
- scripts/refine_matches.py +567 -159
- tme/__init__.py +0 -1
- tme/__version__.py +1 -1
- tme/analyzer.py +627 -855
- tme/backends/__init__.py +41 -11
- tme/backends/_jax_utils.py +185 -0
- tme/backends/cupy_backend.py +120 -225
- tme/backends/jax_backend.py +282 -0
- tme/backends/matching_backend.py +464 -388
- tme/backends/mlx_backend.py +45 -68
- tme/backends/npfftw_backend.py +256 -514
- tme/backends/pytorch_backend.py +41 -154
- tme/density.py +312 -421
- tme/extensions.cpython-311-darwin.so +0 -0
- tme/matching_data.py +366 -303
- tme/matching_exhaustive.py +279 -1521
- tme/matching_optimization.py +234 -129
- tme/matching_scores.py +884 -0
- tme/matching_utils.py +281 -387
- tme/memory.py +377 -0
- tme/orientations.py +226 -66
- tme/parser.py +3 -4
- tme/preprocessing/__init__.py +2 -0
- tme/preprocessing/_utils.py +217 -0
- tme/preprocessing/composable_filter.py +31 -0
- tme/preprocessing/compose.py +55 -0
- tme/preprocessing/frequency_filters.py +388 -0
- tme/preprocessing/tilt_series.py +1011 -0
- tme/preprocessor.py +574 -530
- tme/structure.py +495 -189
- tme/types.py +5 -3
- pytme-0.2.0b0.data/scripts/match_template.py +0 -800
- pytme-0.2.0b0.dist-info/METADATA +0 -73
- pytme-0.2.0b0.dist-info/RECORD +0 -66
- tme/helpers.py +0 -881
- tme/matching_constrained.py +0 -195
- {pytme-0.2.0b0.data → pytme-0.2.2.data}/scripts/estimate_ram_usage.py +0 -0
- {pytme-0.2.0b0.data → pytme-0.2.2.data}/scripts/preprocess.py +0 -0
- {pytme-0.2.0b0.dist-info → pytme-0.2.2.dist-info}/LICENSE +0 -0
- {pytme-0.2.0b0.dist-info → pytme-0.2.2.dist-info}/entry_points.txt +0 -0
- {pytme-0.2.0b0.dist-info → pytme-0.2.2.dist-info}/top_level.txt +0 -0
tme/preprocessor.py
CHANGED
@@ -4,39 +4,19 @@
|
|
4
4
|
|
5
5
|
Author: Valentin Maurer <valentin.maurer@embl-hamburg.de>
|
6
6
|
"""
|
7
|
-
|
7
|
+
import os
|
8
|
+
import pickle
|
8
9
|
import inspect
|
9
10
|
from typing import Dict, Tuple
|
10
11
|
|
11
12
|
import numpy as np
|
12
|
-
from
|
13
|
-
|
14
|
-
from scipy.
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
zoom,
|
20
|
-
generic_gradient_magnitude,
|
21
|
-
sobel,
|
22
|
-
prewitt,
|
23
|
-
laplace,
|
24
|
-
gaussian_laplace,
|
25
|
-
gaussian_gradient_magnitude,
|
26
|
-
)
|
27
|
-
from scipy.ndimage import mean as ndimean
|
28
|
-
from scipy.signal import convolve, decimate
|
29
|
-
from scipy.optimize import differential_evolution
|
30
|
-
from pywt import wavelist, wavedecn, waverecn
|
31
|
-
from scipy.interpolate import interp1d
|
32
|
-
|
33
|
-
from .density import Density
|
34
|
-
from .helpers import (
|
35
|
-
window_kaiserb,
|
36
|
-
window_blob,
|
37
|
-
apply_window_filter,
|
38
|
-
Ntree,
|
39
|
-
)
|
13
|
+
from scipy import ndimage
|
14
|
+
from scipy.special import iv as bessel
|
15
|
+
from scipy.interpolate import interp1d, splrep, BSpline
|
16
|
+
from scipy.optimize import differential_evolution, minimize
|
17
|
+
|
18
|
+
from .types import NDArray
|
19
|
+
from .backends import NumpyFFTWBackend
|
40
20
|
from .matching_utils import euler_to_rotationmatrix
|
41
21
|
|
42
22
|
|
@@ -47,7 +27,7 @@ class Preprocessor:
|
|
47
27
|
|
48
28
|
def apply_method(self, method: str, parameters: Dict):
|
49
29
|
"""
|
50
|
-
|
30
|
+
Invoke ``Preprocessor.method`` using ``parameters``.
|
51
31
|
|
52
32
|
Parameters
|
53
33
|
----------
|
@@ -69,8 +49,7 @@ class Preprocessor:
|
|
69
49
|
raise NotImplementedError(
|
70
50
|
f"'{method}' is not supported as a filter method on this class."
|
71
51
|
)
|
72
|
-
|
73
|
-
return method_to_call(**parameters)
|
52
|
+
return getattr(self, method)(**parameters)
|
74
53
|
|
75
54
|
def method_to_id(self, method: str, parameters: Dict) -> str:
|
76
55
|
"""
|
@@ -109,59 +88,11 @@ class Preprocessor:
|
|
109
88
|
|
110
89
|
return "-".join([str(default[key]) for key in sorted(default.keys())])
|
111
90
|
|
112
|
-
@staticmethod
|
113
|
-
def _gaussian_fourier(template: NDArray, sigma: NDArray) -> NDArray:
|
114
|
-
"""
|
115
|
-
Apply a Gaussian filter in Fourier space on the provided template.
|
116
|
-
|
117
|
-
Parameters
|
118
|
-
----------
|
119
|
-
template : NDArray
|
120
|
-
The input template on which to apply the filter.
|
121
|
-
sigma : NDArray
|
122
|
-
The standard deviation for Gaussian kernel. The greater the value,
|
123
|
-
the more spread out is the filter.
|
124
|
-
|
125
|
-
Returns
|
126
|
-
-------
|
127
|
-
NDArray
|
128
|
-
The template after applying the Fourier Gaussian filter.
|
129
|
-
"""
|
130
|
-
fourrier_map = fourier_gaussian(np.fft.fftn(template), sigma)
|
131
|
-
template = np.real(np.fft.ifftn(fourrier_map))
|
132
|
-
|
133
|
-
return template
|
134
|
-
|
135
|
-
@staticmethod
|
136
|
-
def _gaussian_real(
|
137
|
-
template: NDArray, sigma: NDArray, cutoff_value: float = 4.0
|
138
|
-
) -> NDArray:
|
139
|
-
"""
|
140
|
-
Apply a Gaussian filter on the provided template in real space.
|
141
|
-
|
142
|
-
Parameters
|
143
|
-
----------
|
144
|
-
template : NDArray
|
145
|
-
The input template on which to apply the filter.
|
146
|
-
sigma : NDArray
|
147
|
-
The standard deviation for Gaussian kernel. The greater the value,
|
148
|
-
the more spread out is the filter.
|
149
|
-
cutoff_value : float, optional
|
150
|
-
The value below which the data should be ignored. Default is 4.0.
|
151
|
-
|
152
|
-
Returns
|
153
|
-
-------
|
154
|
-
NDArray
|
155
|
-
The template after applying the Gaussian filter in real space.
|
156
|
-
"""
|
157
|
-
template = gaussian_filter(template, sigma, cval=cutoff_value)
|
158
|
-
return template
|
159
|
-
|
160
91
|
def gaussian_filter(
|
161
92
|
self,
|
162
93
|
template: NDArray,
|
163
|
-
sigma:
|
164
|
-
|
94
|
+
sigma: Tuple[float],
|
95
|
+
cutoff_value: float = 4.0,
|
165
96
|
) -> NDArray:
|
166
97
|
"""
|
167
98
|
Convolve an atomic structure with a Gaussian kernel.
|
@@ -169,31 +100,19 @@ class Preprocessor:
|
|
169
100
|
Parameters
|
170
101
|
----------
|
171
102
|
template : NDArray
|
172
|
-
|
173
|
-
|
174
|
-
The
|
175
|
-
|
176
|
-
|
177
|
-
The standard deviation for Gaussian kernel. Should either be a scalar
|
178
|
-
or a sequence of scalars.
|
179
|
-
fourier : bool, optional
|
180
|
-
If true, applies a Fourier Gaussian filter; otherwise, applies a
|
181
|
-
real-space Gaussian filter. Default is False.
|
103
|
+
Input data.
|
104
|
+
sigma : float or tuple of floats
|
105
|
+
The standard deviation of the Gaussian kernel along one or all axes.
|
106
|
+
cutoff_value : float, optional
|
107
|
+
Truncates the Gaussian kernel at cutoff_values times sigma.
|
182
108
|
|
183
109
|
Returns
|
184
110
|
-------
|
185
111
|
NDArray
|
186
|
-
|
112
|
+
Gaussian filtered template.
|
187
113
|
"""
|
188
114
|
sigma = 0 if sigma is None else sigma
|
189
|
-
|
190
|
-
if sigma <= 0:
|
191
|
-
return template
|
192
|
-
|
193
|
-
func = self._gaussian_real if not fourier else self._gaussian_fourier
|
194
|
-
template = func(template, sigma)
|
195
|
-
|
196
|
-
return template
|
115
|
+
return ndimage.gaussian_filter(template, sigma, cval=cutoff_value)
|
197
116
|
|
198
117
|
def difference_of_gaussian_filter(
|
199
118
|
self, template: NDArray, low_sigma: NDArray, high_sigma: NDArray
|
@@ -220,8 +139,8 @@ class Preprocessor:
|
|
220
139
|
"""
|
221
140
|
if np.any(low_sigma > high_sigma):
|
222
141
|
print("low_sigma should be smaller than high_sigma.")
|
223
|
-
im1 = self.
|
224
|
-
im2 = self.
|
142
|
+
im1 = self.gaussian_filter(template, low_sigma)
|
143
|
+
im2 = self.gaussian_filter(template, high_sigma)
|
225
144
|
return im1 - im2
|
226
145
|
|
227
146
|
def local_gaussian_alignment_filter(
|
@@ -372,7 +291,6 @@ class Preprocessor:
|
|
372
291
|
+-------------------+------------------------------------------------+
|
373
292
|
| 'gaussian_laplace | See scipy.ndimage.gaussian_laplace |
|
374
293
|
+-------------------+------------------------------------------------+
|
375
|
-
|
376
294
|
reverse : bool, optional
|
377
295
|
If true, the filterring is strong along edges. Default is False.
|
378
296
|
|
@@ -382,15 +300,15 @@ class Preprocessor:
|
|
382
300
|
Simulated electron densities.
|
383
301
|
"""
|
384
302
|
if edge_algorithm == "sobel":
|
385
|
-
edges = generic_gradient_magnitude(template, sobel)
|
303
|
+
edges = ndimage.generic_gradient_magnitude(template, ndimage.sobel)
|
386
304
|
elif edge_algorithm == "prewitt":
|
387
|
-
edges = generic_gradient_magnitude(template, prewitt)
|
305
|
+
edges = ndimage.generic_gradient_magnitude(template, ndimage.prewitt)
|
388
306
|
elif edge_algorithm == "laplace":
|
389
|
-
edges = laplace(template)
|
307
|
+
edges = ndimage.laplace(template)
|
390
308
|
elif edge_algorithm == "gaussian":
|
391
|
-
edges = gaussian_gradient_magnitude(template, sigma / 2)
|
309
|
+
edges = ndimage.gaussian_gradient_magnitude(template, sigma / 2)
|
392
310
|
elif edge_algorithm == "gaussian_laplace":
|
393
|
-
edges = gaussian_laplace(template, sigma / 2)
|
311
|
+
edges = ndimage.gaussian_laplace(template, sigma / 2)
|
394
312
|
else:
|
395
313
|
raise ValueError(
|
396
314
|
"Supported edge_algorithm values are"
|
@@ -399,49 +317,16 @@ class Preprocessor:
|
|
399
317
|
edges[edges != 0] = 1
|
400
318
|
edges /= edges.max()
|
401
319
|
|
402
|
-
edges = gaussian_filter(edges, sigma)
|
403
|
-
|
320
|
+
edges = ndimage.gaussian_filter(edges, sigma)
|
321
|
+
filt = ndimage.gaussian_filter(template, sigma)
|
404
322
|
|
405
323
|
if not reverse:
|
406
|
-
res = template * edges +
|
324
|
+
res = template * edges + filt * (1 - edges)
|
407
325
|
else:
|
408
|
-
res = template * (1 - edges) +
|
326
|
+
res = template * (1 - edges) + filt * (edges)
|
409
327
|
|
410
328
|
return res
|
411
329
|
|
412
|
-
def ntree_filter(
|
413
|
-
self,
|
414
|
-
template: NDArray,
|
415
|
-
sigma_range: Tuple[float, float],
|
416
|
-
target: NDArray = None,
|
417
|
-
) -> NDArray:
|
418
|
-
"""
|
419
|
-
Use dyadic tree to identify volume partitions in *template*
|
420
|
-
and filter them with respect to their occupancy.
|
421
|
-
|
422
|
-
Parameters
|
423
|
-
----------
|
424
|
-
template : NDArray
|
425
|
-
The input atomic structure map.
|
426
|
-
sigma_range : tuple of float
|
427
|
-
Range of sigma values used to filter volume partitions.
|
428
|
-
target : NDArray, optional
|
429
|
-
If provided, dyadic tree is computed on target rather than template.
|
430
|
-
|
431
|
-
Returns
|
432
|
-
-------
|
433
|
-
NDArray
|
434
|
-
Simulated electron densities.
|
435
|
-
"""
|
436
|
-
if target is None:
|
437
|
-
target = template
|
438
|
-
|
439
|
-
tree = Ntree(target)
|
440
|
-
|
441
|
-
filter = tree.filter_chunks(arr=template, sigma_range=sigma_range)
|
442
|
-
|
443
|
-
return filter
|
444
|
-
|
445
330
|
def mean_filter(self, template: NDArray, width: NDArray) -> NDArray:
|
446
331
|
"""
|
447
332
|
Perform mean filtering.
|
@@ -466,7 +351,7 @@ class Preprocessor:
|
|
466
351
|
filter_width = np.repeat(width, template.ndim // width.size)
|
467
352
|
filter_mask = np.ones(filter_width)
|
468
353
|
filter_mask = filter_mask / np.sum(filter_mask)
|
469
|
-
template = convolve(template, filter_mask, mode="
|
354
|
+
template = ndimage.convolve(template, filter_mask, mode="reflect")
|
470
355
|
|
471
356
|
# Sometimes scipy messes up the box sizes ...
|
472
357
|
template = self.interpolate_box(box=interpolation_box, arr=template)
|
@@ -607,7 +492,7 @@ class Preprocessor:
|
|
607
492
|
if size <= 1:
|
608
493
|
size = 3
|
609
494
|
|
610
|
-
template = rank_filter(template, rank=rank, size=size)
|
495
|
+
template = ndimage.rank_filter(template, rank=rank, size=size)
|
611
496
|
template = self.interpolate_box(box=interpolation_box, arr=template)
|
612
497
|
|
613
498
|
return template
|
@@ -630,7 +515,7 @@ class Preprocessor:
|
|
630
515
|
"""
|
631
516
|
interpolation_box = template.shape
|
632
517
|
|
633
|
-
template = median_filter(template, size=size)
|
518
|
+
template = ndimage.median_filter(template, size=size)
|
634
519
|
template = self.interpolate_box(box=interpolation_box, arr=template)
|
635
520
|
|
636
521
|
return template
|
@@ -655,144 +540,13 @@ class Preprocessor:
|
|
655
540
|
interpolation_box = array.shape
|
656
541
|
|
657
542
|
for k in range(template.ndim):
|
658
|
-
array = decimate(array, q=level, axis=k)
|
543
|
+
array = ndimage.decimate(array, q=level, axis=k)
|
659
544
|
|
660
|
-
template = zoom(array, np.divide(template.shape, array.shape))
|
545
|
+
template = ndimage.zoom(array, np.divide(template.shape, array.shape))
|
661
546
|
template = self.interpolate_box(box=interpolation_box, arr=template)
|
662
547
|
|
663
548
|
return template
|
664
549
|
|
665
|
-
def wavelet_filter(
|
666
|
-
self,
|
667
|
-
template: NDArray,
|
668
|
-
level: int,
|
669
|
-
wavelet: str = "bior2.2",
|
670
|
-
) -> NDArray:
|
671
|
-
"""
|
672
|
-
Perform dyadic wavelet decomposition.
|
673
|
-
|
674
|
-
Parameters
|
675
|
-
----------
|
676
|
-
template : NDArray
|
677
|
-
The input atomic structure map.
|
678
|
-
level : int
|
679
|
-
Scale of the wavelet transform.
|
680
|
-
wavelet : str, optional
|
681
|
-
Mother wavelet used for decomposition. Default is 'bior2.2'.
|
682
|
-
|
683
|
-
Returns
|
684
|
-
-------
|
685
|
-
NDArray
|
686
|
-
Simulated electron densities.
|
687
|
-
"""
|
688
|
-
if wavelet not in wavelist(kind="discrete"):
|
689
|
-
raise NotImplementedError(
|
690
|
-
"Print argument wavelet has to be one of the following: %s",
|
691
|
-
", ".join(wavelist(kind="discrete")),
|
692
|
-
)
|
693
|
-
|
694
|
-
template, interpolation_box = template.copy(), template.shape
|
695
|
-
decomp = wavedecn(template, level=level, wavelet=wavelet)
|
696
|
-
|
697
|
-
for i in range(1, level + 1):
|
698
|
-
decomp[i] = {k: np.zeros_like(v) for k, v in decomp[i].items()}
|
699
|
-
|
700
|
-
template = waverecn(coeffs=decomp, wavelet=wavelet)
|
701
|
-
template = self.interpolate_box(box=interpolation_box, arr=template)
|
702
|
-
|
703
|
-
return template
|
704
|
-
|
705
|
-
@staticmethod
|
706
|
-
def molmap(
|
707
|
-
coordinates: NDArray,
|
708
|
-
weights: Tuple[float],
|
709
|
-
resolution: float,
|
710
|
-
sigma_factor: float = 1 / (np.pi * np.sqrt(2)),
|
711
|
-
cutoff_value: float = 5.0,
|
712
|
-
origin: Tuple[float] = None,
|
713
|
-
shape: Tuple[int] = None,
|
714
|
-
sampling_rate: float = None,
|
715
|
-
) -> NDArray:
|
716
|
-
"""
|
717
|
-
Compute the electron densities analogous to Chimera's molmap function.
|
718
|
-
|
719
|
-
Parameters
|
720
|
-
----------
|
721
|
-
coordinates : NDArray
|
722
|
-
A N x 3 array containing atomic coordinates in z, y, x format.
|
723
|
-
weights : [float]
|
724
|
-
The weights to use for the entries in coordinates.
|
725
|
-
resolution : float
|
726
|
-
The product of resolution and sigma_factor gives the sigma used to
|
727
|
-
compute the discretized Gaussian.
|
728
|
-
sigma_factor : float
|
729
|
-
The factor used with resolution to compute sigma. Default is 1 / (π√2).
|
730
|
-
cutoff_value : float
|
731
|
-
The cutoff value for the Gaussian kernel. Default is 5.0.
|
732
|
-
origin : (float,)
|
733
|
-
The origin of the coordinate system used in coordinates. If not specified,
|
734
|
-
the minimum coordinate along each axis is used.
|
735
|
-
shape : (int,)
|
736
|
-
The shape of the output array. If not specified, the function computes the
|
737
|
-
smallest output array that contains all atoms.
|
738
|
-
sampling_rate : float
|
739
|
-
The Ångstrom per voxel of the output array. If not specified, the function
|
740
|
-
sets this value to resolution/3.
|
741
|
-
|
742
|
-
References
|
743
|
-
----------
|
744
|
-
..[1] https://www.cgl.ucsf.edu/chimera/docs/UsersGuide/midas/molmap.html
|
745
|
-
|
746
|
-
Returns
|
747
|
-
-------
|
748
|
-
NDArray
|
749
|
-
A numpy array containing the simulated electron densities.
|
750
|
-
"""
|
751
|
-
if sampling_rate is None:
|
752
|
-
sampling_rate = resolution * (1.0 / 3)
|
753
|
-
|
754
|
-
coordinates = coordinates.copy()
|
755
|
-
if origin is None:
|
756
|
-
origin = coordinates.min(axis=0)
|
757
|
-
if shape is None:
|
758
|
-
positions = (coordinates - origin) / sampling_rate
|
759
|
-
shape = positions.max(axis=0).astype(int)[::-1] + 2
|
760
|
-
|
761
|
-
positions = (coordinates - origin) / sampling_rate
|
762
|
-
positions = positions[:, ::-1]
|
763
|
-
|
764
|
-
out = np.zeros(shape, dtype=np.float32)
|
765
|
-
sigma = sigma_factor * resolution
|
766
|
-
sigma_grid = sigma / sampling_rate
|
767
|
-
sigma_grid2 = sigma_grid * sigma_grid
|
768
|
-
|
769
|
-
starts = np.maximum(np.ceil(positions - cutoff_value * sigma_grid), 0).astype(
|
770
|
-
int
|
771
|
-
)
|
772
|
-
stops = np.minimum(
|
773
|
-
np.floor(positions + cutoff_value * sigma_grid), shape
|
774
|
-
).astype(int)
|
775
|
-
ranges = tuple(tuple(zip(start, stop)) for start, stop in zip(starts, stops))
|
776
|
-
|
777
|
-
positions = positions.reshape(
|
778
|
-
*positions.shape, *tuple(1 for _ in range(positions.shape[1]))
|
779
|
-
)
|
780
|
-
for index in range(positions.shape[0]):
|
781
|
-
grid_index = np.meshgrid(*[range(*coord) for coord in ranges[index]])
|
782
|
-
distances = np.sum(
|
783
|
-
np.square(np.subtract(grid_index, positions[index])),
|
784
|
-
dtype=np.float32,
|
785
|
-
axis=0,
|
786
|
-
)
|
787
|
-
np.add.at(
|
788
|
-
out,
|
789
|
-
tuple(grid_index),
|
790
|
-
weights[index] * np.exp(-0.5 * distances / sigma_grid2),
|
791
|
-
)
|
792
|
-
|
793
|
-
out *= np.power(2 * np.pi, -1.5) * np.power(sigma, -3)
|
794
|
-
return out
|
795
|
-
|
796
550
|
def interpolate_box(
|
797
551
|
self, arr: NDArray, box: Tuple[int], kind: str = "nearest"
|
798
552
|
) -> NDArray:
|
@@ -834,79 +588,11 @@ class Preprocessor:
|
|
834
588
|
|
835
589
|
return arr
|
836
590
|
|
837
|
-
@staticmethod
|
838
|
-
def fftfreqn(shape: NDArray, sampling_rate: NDArray = 1) -> NDArray:
|
839
|
-
"""
|
840
|
-
Calculate the N-dimensional equivalent to the inverse fftshifted
|
841
|
-
absolute of numpy's fftfreq function, supporting anisotropic sampling.
|
842
|
-
|
843
|
-
Parameters
|
844
|
-
----------
|
845
|
-
shape : NDArray
|
846
|
-
The shape of the N-dimensional array.
|
847
|
-
sampling_rate : NDArray
|
848
|
-
The sampling rate in the N-dimensional array.
|
849
|
-
|
850
|
-
Returns
|
851
|
-
-------
|
852
|
-
NDArray
|
853
|
-
A numpy array representing the norm of indices after normalization.
|
854
|
-
|
855
|
-
Examples
|
856
|
-
--------
|
857
|
-
>>> import numpy as np
|
858
|
-
>>> from tme import Preprocessor
|
859
|
-
>>> freq = Preprocessor().fftfreqn((10,), 1)
|
860
|
-
>>> freq_numpy = np.fft.fftfreq(10, 1)
|
861
|
-
>>> np.allclose(freq, np.abs(np.fft.ifftshift(freq_numpy)))
|
862
|
-
"""
|
863
|
-
indices = np.indices(shape).T
|
864
|
-
norm = np.multiply(shape, sampling_rate)
|
865
|
-
indices -= np.divide(shape, 2).astype(int)
|
866
|
-
indices = np.divide(indices, norm)
|
867
|
-
return np.linalg.norm(indices, axis=-1).T
|
868
|
-
|
869
|
-
def _approximate_butterworth(
|
870
|
-
self,
|
871
|
-
radial_frequencies: NDArray,
|
872
|
-
lowcut: float,
|
873
|
-
highcut: float,
|
874
|
-
gaussian_sigma: float,
|
875
|
-
) -> NDArray:
|
876
|
-
"""
|
877
|
-
Approximate a Butterworth band-pass filter for given radial frequencies.
|
878
|
-
The DC component of the filter is at the origin.
|
879
|
-
|
880
|
-
Parameters
|
881
|
-
----------
|
882
|
-
radial_frequencies : NDArray
|
883
|
-
The radial frequencies for which the Butterworth band-pass
|
884
|
-
filter is to be calculated.
|
885
|
-
lowcut : float
|
886
|
-
The lower cutoff frequency for the band-pass filter.
|
887
|
-
highcut : float
|
888
|
-
The upper cutoff frequency for the band-pass filter.
|
889
|
-
gaussian_sigma : float
|
890
|
-
The sigma value for the Gaussian smoothing applied to the filter.
|
891
|
-
|
892
|
-
Returns
|
893
|
-
-------
|
894
|
-
NDArray
|
895
|
-
A numpy array representing the approximate Butterworth
|
896
|
-
band-pass filter applied to the radial frequencies.
|
897
|
-
"""
|
898
|
-
bpf = ((radial_frequencies <= highcut) & (radial_frequencies >= lowcut)) * 1.0
|
899
|
-
bpf = self.gaussian_filter(template=bpf, sigma=gaussian_sigma, fourier=False)
|
900
|
-
bpf[bpf < np.exp(-2)] = 0
|
901
|
-
bpf = np.fft.ifftshift(bpf)
|
902
|
-
|
903
|
-
return bpf
|
904
|
-
|
905
591
|
def bandpass_filter(
|
906
592
|
self,
|
907
593
|
template: NDArray,
|
908
|
-
|
909
|
-
|
594
|
+
lowpass: float,
|
595
|
+
highpass: float,
|
910
596
|
sampling_rate: NDArray = None,
|
911
597
|
gaussian_sigma: float = 0.0,
|
912
598
|
) -> NDArray:
|
@@ -918,10 +604,10 @@ class Preprocessor:
|
|
918
604
|
----------
|
919
605
|
template : NDArray
|
920
606
|
The input numpy array on which the band-pass filter should be applied.
|
921
|
-
|
607
|
+
lowpass : float
|
922
608
|
The lower boundary of the frequency range to be preserved. Lower values will
|
923
609
|
retain broader, more global features.
|
924
|
-
|
610
|
+
highpass : float
|
925
611
|
The upper boundary of the frequency range to be preserved. Higher values
|
926
612
|
will emphasize finer details and potentially noise.
|
927
613
|
sampling_rate : NDarray, optional
|
@@ -936,8 +622,8 @@ class Preprocessor:
|
|
936
622
|
"""
|
937
623
|
bpf = self.bandpass_mask(
|
938
624
|
shape=template.shape,
|
939
|
-
|
940
|
-
|
625
|
+
lowpass=lowpass,
|
626
|
+
highpass=highpass,
|
941
627
|
sampling_rate=sampling_rate,
|
942
628
|
gaussian_sigma=gaussian_sigma,
|
943
629
|
omit_negative_frequencies=False,
|
@@ -951,8 +637,8 @@ class Preprocessor:
|
|
951
637
|
def bandpass_mask(
|
952
638
|
self,
|
953
639
|
shape: Tuple[int],
|
954
|
-
|
955
|
-
|
640
|
+
lowpass: float,
|
641
|
+
highpass: float,
|
956
642
|
sampling_rate: NDArray = None,
|
957
643
|
gaussian_sigma: float = 0.0,
|
958
644
|
omit_negative_frequencies: bool = True,
|
@@ -965,7 +651,7 @@ class Preprocessor:
|
|
965
651
|
----------
|
966
652
|
shape : tuple of ints
|
967
653
|
Shape of the returned bandpass filter.
|
968
|
-
|
654
|
+
lowpass : float
|
969
655
|
The lower boundary of the frequency range to be preserved. Lower values will
|
970
656
|
retain broader, more global features.
|
971
657
|
maximum_frequency : float
|
@@ -984,27 +670,15 @@ class Preprocessor:
|
|
984
670
|
NDArray
|
985
671
|
Bandpass filtered.
|
986
672
|
"""
|
987
|
-
|
988
|
-
sampling_rate = np.ones(len(shape))
|
989
|
-
sampling_rate = np.asarray(sampling_rate, dtype=np.float32)
|
990
|
-
sampling_rate /= sampling_rate.max()
|
991
|
-
|
992
|
-
if minimum_frequency > maximum_frequency:
|
993
|
-
minimum_frequency, maximum_frequency = maximum_frequency, minimum_frequency
|
994
|
-
|
995
|
-
radial_freq = self.fftfreqn(shape, sampling_rate)
|
996
|
-
bpf = self._approximate_butterworth(
|
997
|
-
radial_frequencies=radial_freq,
|
998
|
-
lowcut=minimum_frequency,
|
999
|
-
highcut=maximum_frequency,
|
1000
|
-
gaussian_sigma=gaussian_sigma,
|
1001
|
-
)
|
1002
|
-
|
1003
|
-
if omit_negative_frequencies:
|
1004
|
-
stop = 1 + (shape[-1] // 2)
|
1005
|
-
bpf = bpf[..., :stop]
|
673
|
+
from .preprocessing import BandPassFilter
|
1006
674
|
|
1007
|
-
return
|
675
|
+
return BandPassFilter(
|
676
|
+
sampling_rate=sampling_rate,
|
677
|
+
lowpass=lowpass,
|
678
|
+
highpass=highpass,
|
679
|
+
return_real_fourier=omit_negative_frequencies,
|
680
|
+
use_gaussian=gaussian_sigma == 0.0,
|
681
|
+
)(shape=shape)["data"]
|
1008
682
|
|
1009
683
|
def wedge_mask(
|
1010
684
|
self,
|
@@ -1104,7 +778,7 @@ class Preprocessor:
|
|
1104
778
|
for i in range(plane.ndim)
|
1105
779
|
)
|
1106
780
|
plane[subset] = 1
|
1107
|
-
|
781
|
+
NumpyFFTWBackend().rigid_transform(
|
1108
782
|
arr=plane,
|
1109
783
|
rotation_matrix=rotation_matrix,
|
1110
784
|
out=plane_rotated,
|
@@ -1113,9 +787,7 @@ class Preprocessor:
|
|
1113
787
|
)
|
1114
788
|
wedge_volume += plane_rotated
|
1115
789
|
|
1116
|
-
wedge_volume = self.gaussian_filter(
|
1117
|
-
template=wedge_volume, sigma=sigma, fourier=False
|
1118
|
-
)
|
790
|
+
wedge_volume = self.gaussian_filter(template=wedge_volume, sigma=sigma)
|
1119
791
|
wedge_volume = np.where(wedge_volume > np.exp(-2), 1, 0)
|
1120
792
|
wedge_volume = np.fft.ifftshift(wedge_volume)
|
1121
793
|
|
@@ -1135,6 +807,7 @@ class Preprocessor:
|
|
1135
807
|
opening_axis: int = 0,
|
1136
808
|
tilt_axis: int = 2,
|
1137
809
|
sigma: float = 0,
|
810
|
+
weights: float = 1,
|
1138
811
|
omit_negative_frequencies: bool = True,
|
1139
812
|
) -> NDArray:
|
1140
813
|
"""
|
@@ -1165,6 +838,8 @@ class Preprocessor:
|
|
1165
838
|
- 2 for X-axis
|
1166
839
|
sigma : float, optional
|
1167
840
|
Standard deviation for Gaussian kernel used for smoothing the wedge.
|
841
|
+
weights : float, tuple of float
|
842
|
+
Weight of each element in the wedge. Defaults to one.
|
1168
843
|
omit_negative_frequencies : bool, optional
|
1169
844
|
Whether the wedge mask should omit negative frequencies, i.e. be
|
1170
845
|
applicable to symmetric Fourier transforms (see :obj:`numpy.fft.fftn`)
|
@@ -1188,20 +863,24 @@ class Preprocessor:
|
|
1188
863
|
if tilt_angles is None:
|
1189
864
|
tilt_angles = np.arange(-start_tilt, stop_tilt + tilt_step, tilt_step)
|
1190
865
|
|
866
|
+
shape = tuple(int(x) for x in shape)
|
867
|
+
opening_axis, tilt_axis = int(opening_axis), int(tilt_axis)
|
868
|
+
|
869
|
+
weights = np.asarray(weights)
|
870
|
+
weights = np.repeat(weights, tilt_angles.size // weights.size)
|
1191
871
|
plane = np.zeros((shape[opening_axis], shape[tilt_axis]), dtype=np.float32)
|
1192
872
|
subset = tuple(
|
1193
873
|
slice(None) if i != 0 else slice(x // 2, x // 2 + 1)
|
1194
874
|
for i, x in enumerate(plane.shape)
|
1195
875
|
)
|
1196
|
-
plane[subset] = 1
|
1197
876
|
plane_rotated, wedge_volume = np.zeros_like(plane), np.zeros_like(plane)
|
1198
877
|
for index in range(tilt_angles.shape[0]):
|
1199
878
|
plane_rotated.fill(0)
|
1200
|
-
|
879
|
+
plane[subset] = weights[index]
|
1201
880
|
rotation_matrix = euler_to_rotationmatrix((tilt_angles[index], 0))
|
1202
881
|
rotation_matrix = rotation_matrix[np.ix_((0, 1), (0, 1))]
|
1203
882
|
|
1204
|
-
|
883
|
+
NumpyFFTWBackend().rigid_transform(
|
1205
884
|
arr=plane,
|
1206
885
|
rotation_matrix=rotation_matrix,
|
1207
886
|
out=plane_rotated,
|
@@ -1210,10 +889,11 @@ class Preprocessor:
|
|
1210
889
|
)
|
1211
890
|
wedge_volume += plane_rotated
|
1212
891
|
|
1213
|
-
|
1214
|
-
|
1215
|
-
|
1216
|
-
|
892
|
+
# Ramp filtering would be more accurate
|
893
|
+
np.fmin(wedge_volume, np.max(weights), wedge_volume)
|
894
|
+
|
895
|
+
if sigma > 0:
|
896
|
+
wedge_volume = self.gaussian_filter(template=wedge_volume, sigma=sigma)
|
1217
897
|
|
1218
898
|
if opening_axis > tilt_axis:
|
1219
899
|
wedge_volume = np.moveaxis(wedge_volume, 1, 0)
|
@@ -1331,7 +1011,7 @@ class Preprocessor:
|
|
1331
1011
|
if not infinite_plane:
|
1332
1012
|
np.multiply(wedge, distances <= shape[tilt_axis] // 2, out=wedge)
|
1333
1013
|
|
1334
|
-
wedge = self.gaussian_filter(template=wedge, sigma=sigma
|
1014
|
+
wedge = self.gaussian_filter(template=wedge, sigma=sigma)
|
1335
1015
|
wedge = np.fft.ifftshift(wedge > np.exp(-2))
|
1336
1016
|
|
1337
1017
|
if omit_negative_frequencies:
|
@@ -1340,167 +1020,531 @@ class Preprocessor:
|
|
1340
1020
|
|
1341
1021
|
return wedge
|
1342
1022
|
|
1343
|
-
@staticmethod
|
1344
|
-
def _fourier_crop_mask(old_shape: NDArray, new_shape: NDArray) -> NDArray:
|
1345
|
-
"""
|
1346
|
-
Generate a mask for Fourier cropping.
|
1347
1023
|
|
1348
|
-
|
1349
|
-
|
1350
|
-
|
1351
|
-
|
1352
|
-
|
1353
|
-
|
1024
|
+
def window_kaiserb(width: int, beta: float = 3.2, order: int = 0) -> NDArray:
|
1025
|
+
"""
|
1026
|
+
Create a Kaiser-Bessel window.
|
1027
|
+
|
1028
|
+
Parameters
|
1029
|
+
----------
|
1030
|
+
width : int
|
1031
|
+
Width of the window.
|
1032
|
+
beta : float, optional
|
1033
|
+
Beta parameter of the Kaiser-Bessel window. Default is 3.2.
|
1034
|
+
order : int, optional
|
1035
|
+
Order of the Bessel function. Default is 0.
|
1036
|
+
|
1037
|
+
Returns
|
1038
|
+
-------
|
1039
|
+
NDArray
|
1040
|
+
Kaiser-Bessel window.
|
1041
|
+
|
1042
|
+
References
|
1043
|
+
----------
|
1044
|
+
.. [1] Sorzano, Carlos et al (Mar. 2015). Fast and accurate conversion
|
1045
|
+
of atomic models into electron density maps. AIMS Biophysics
|
1046
|
+
2, 8–20.
|
1047
|
+
"""
|
1048
|
+
window = np.arange(0, width)
|
1049
|
+
alpha = (width - 1) / 2.0
|
1050
|
+
arr = beta * np.sqrt(1 - ((window - alpha) / alpha) ** 2.0)
|
1354
1051
|
|
1355
|
-
|
1356
|
-
-------
|
1357
|
-
NDArray
|
1358
|
-
The mask array for Fourier cropping.
|
1359
|
-
"""
|
1360
|
-
mask = np.zeros(old_shape, dtype=bool)
|
1361
|
-
mask[tuple(np.indices(new_shape))] = 1
|
1362
|
-
box_shift = np.floor(np.divide(new_shape, 2)).astype(int)
|
1363
|
-
mask = np.roll(mask, shift=-box_shift, axis=range(len(old_shape)))
|
1364
|
-
return mask
|
1052
|
+
return bessel(order, arr) / bessel(order, beta)
|
1365
1053
|
|
1366
|
-
def fourier_crop(
|
1367
|
-
self,
|
1368
|
-
template: NDArray,
|
1369
|
-
reciprocal_template_filter: NDArray,
|
1370
|
-
crop_factor: float = 3 / 2,
|
1371
|
-
) -> NDArray:
|
1372
|
-
"""
|
1373
|
-
Perform Fourier uncropping on a given template.
|
1374
1054
|
|
1375
|
-
|
1376
|
-
|
1377
|
-
|
1378
|
-
|
1379
|
-
|
1380
|
-
|
1381
|
-
|
1382
|
-
|
1055
|
+
def window_blob(width: int, beta: float = 3.2, order: int = 2) -> NDArray:
|
1056
|
+
"""
|
1057
|
+
Generate a blob window based on Bessel functions.
|
1058
|
+
|
1059
|
+
Parameters
|
1060
|
+
----------
|
1061
|
+
width : int
|
1062
|
+
Width of the window.
|
1063
|
+
beta : float, optional
|
1064
|
+
Beta parameter. Default is 3.2.
|
1065
|
+
order : int, optional
|
1066
|
+
Order of the Bessel function. Default is 2.
|
1067
|
+
|
1068
|
+
Returns
|
1069
|
+
-------
|
1070
|
+
NDArray
|
1071
|
+
Blob window.
|
1072
|
+
|
1073
|
+
References
|
1074
|
+
----------
|
1075
|
+
.. [1] Sorzano, Carlos et al (Mar. 2015). Fast and accurate conversion
|
1076
|
+
of atomic models into electron density maps. AIMS Biophysics
|
1077
|
+
2, 8–20.
|
1078
|
+
"""
|
1079
|
+
window = np.arange(0, width)
|
1080
|
+
alpha = (width - 1) / 2.0
|
1081
|
+
arr = beta * np.sqrt(1 - ((window - alpha) / alpha) ** 2.0)
|
1383
1082
|
|
1384
|
-
|
1385
|
-
|
1386
|
-
|
1387
|
-
|
1388
|
-
|
1389
|
-
|
1390
|
-
|
1391
|
-
|
1392
|
-
|
1393
|
-
|
1394
|
-
|
1395
|
-
|
1396
|
-
|
1083
|
+
arr = np.divide(np.power(arr, order) * bessel(order, arr), bessel(order, beta))
|
1084
|
+
arr[arr != arr] = 0
|
1085
|
+
return arr
|
1086
|
+
|
1087
|
+
|
1088
|
+
def window_sinckb(omega: float, d: float, dw: float):
|
1089
|
+
"""
|
1090
|
+
Compute the sinc window combined with a Kaiser window.
|
1091
|
+
|
1092
|
+
Parameters
|
1093
|
+
----------
|
1094
|
+
omega : float
|
1095
|
+
Reduction factor.
|
1096
|
+
d : float
|
1097
|
+
Ripple.
|
1098
|
+
dw : float
|
1099
|
+
Delta w.
|
1100
|
+
|
1101
|
+
Returns
|
1102
|
+
-------
|
1103
|
+
ndarray
|
1104
|
+
Impulse response of the low-pass filter.
|
1105
|
+
|
1106
|
+
References
|
1107
|
+
----------
|
1108
|
+
.. [1] Sorzano, Carlos et al (Mar. 2015). Fast and accurate conversion
|
1109
|
+
of atomic models into electron density maps. AIMS Biophysics
|
1110
|
+
2, 8–20.
|
1111
|
+
"""
|
1112
|
+
kaiser = kaiser_mask(d, dw)
|
1113
|
+
sinc_m = sinc_mask(np.zeros(kaiser.shape), omega)
|
1397
1114
|
|
1398
|
-
|
1399
|
-
new_boxsize = np.array(template.shape).copy()
|
1115
|
+
mask = sinc_m * kaiser
|
1400
1116
|
|
1401
|
-
|
1402
|
-
arr_ft = np.fft.fftn(template)
|
1403
|
-
arr_ft *= np.prod(new_boxsize) / np.prod(template.shape)
|
1404
|
-
arr_ft = np.reshape(arr_ft[mask], new_boxsize)
|
1405
|
-
arr_cropped = np.real(np.fft.ifftn(arr_ft))
|
1406
|
-
return arr_cropped
|
1117
|
+
return mask / np.sum(mask)
|
1407
1118
|
|
1408
|
-
def fourier_uncrop(
|
1409
|
-
self, template: NDArray, reciprocal_template_filter: NDArray
|
1410
|
-
) -> NDArray:
|
1411
|
-
"""
|
1412
|
-
Perform an uncrop operation in the Fourier space.
|
1413
1119
|
|
1414
|
-
|
1415
|
-
|
1416
|
-
|
1417
|
-
|
1418
|
-
|
1419
|
-
|
1120
|
+
def apply_window_filter(
|
1121
|
+
arr: NDArray,
|
1122
|
+
filter_window: NDArray,
|
1123
|
+
mode: str = "reflect",
|
1124
|
+
cval: float = 0.0,
|
1125
|
+
origin: int = 0,
|
1126
|
+
):
|
1127
|
+
"""
|
1128
|
+
Apply a window filter on an input array.
|
1129
|
+
|
1130
|
+
Parameters
|
1131
|
+
----------
|
1132
|
+
arr : NDArray,
|
1133
|
+
Input array.
|
1134
|
+
filter_window : NDArray,
|
1135
|
+
Window filter to apply.
|
1136
|
+
mode : str, optional
|
1137
|
+
Mode for the filtering, default is "reflect".
|
1138
|
+
cval : float, optional
|
1139
|
+
Value to fill when mode is "constant", default is 0.0.
|
1140
|
+
origin : int, optional
|
1141
|
+
Origin of the filter window, default is 0.
|
1142
|
+
|
1143
|
+
Returns
|
1144
|
+
-------
|
1145
|
+
NDArray,
|
1146
|
+
Array after filtering.
|
1420
1147
|
|
1421
|
-
|
1422
|
-
|
1423
|
-
|
1424
|
-
|
1425
|
-
|
1426
|
-
|
1427
|
-
|
1148
|
+
"""
|
1149
|
+
filter_window = filter_window[::-1]
|
1150
|
+
for axs in range(arr.ndim):
|
1151
|
+
ndimage.correlate1d(
|
1152
|
+
input=arr,
|
1153
|
+
weights=filter_window,
|
1154
|
+
axis=axs,
|
1155
|
+
output=arr,
|
1156
|
+
mode=mode,
|
1157
|
+
cval=cval,
|
1158
|
+
origin=origin,
|
1159
|
+
)
|
1160
|
+
return arr
|
1161
|
+
|
1162
|
+
|
1163
|
+
def sinc_mask(mask: NDArray, omega: float) -> NDArray:
|
1164
|
+
"""
|
1165
|
+
Create a sinc mask.
|
1166
|
+
|
1167
|
+
Parameters
|
1168
|
+
----------
|
1169
|
+
mask : NDArray
|
1170
|
+
Input mask.
|
1171
|
+
omega : float
|
1172
|
+
Reduction factor.
|
1173
|
+
|
1174
|
+
Returns
|
1175
|
+
-------
|
1176
|
+
NDArray
|
1177
|
+
Sinc mask.
|
1178
|
+
"""
|
1179
|
+
# Move filter origin to the center of the mask
|
1180
|
+
mask_origin = int((mask.size - 1) / 2)
|
1181
|
+
dist = np.arange(-mask_origin, mask_origin + 1)
|
1182
|
+
|
1183
|
+
return np.multiply(omega / np.pi, np.sinc((omega / np.pi) * dist))
|
1184
|
+
|
1185
|
+
|
1186
|
+
def kaiser_mask(d: float, dw: float) -> NDArray:
|
1187
|
+
"""
|
1188
|
+
Create a Kaiser mask.
|
1189
|
+
|
1190
|
+
Parameters
|
1191
|
+
----------
|
1192
|
+
d : float
|
1193
|
+
Ripple.
|
1194
|
+
dw : float
|
1195
|
+
Delta-w.
|
1196
|
+
|
1197
|
+
Returns
|
1198
|
+
-------
|
1199
|
+
NDArray
|
1200
|
+
Kaiser mask.
|
1201
|
+
"""
|
1202
|
+
# convert dw from a frequency normalized to 1 to a frequency normalized to pi
|
1203
|
+
dw *= np.pi
|
1204
|
+
A = -20 * np.log10(d)
|
1205
|
+
M = max(1, np.ceil((A - 8) / (2.285 * dw)))
|
1206
|
+
|
1207
|
+
beta = 0
|
1208
|
+
if A > 50:
|
1209
|
+
beta = 0.1102 * (A - 8.7)
|
1210
|
+
elif A >= 21:
|
1211
|
+
beta = 0.5842 * np.power(A - 21, 0.4) + 0.07886 * (A - 21)
|
1212
|
+
|
1213
|
+
mask_values = np.abs(np.arange(-M, M + 1))
|
1214
|
+
mask = np.sqrt(1 - np.power(mask_values / M, 2))
|
1215
|
+
|
1216
|
+
return np.divide(bessel(0, beta * mask), bessel(0, beta))
|
1217
|
+
|
1218
|
+
|
1219
|
+
def electron_factor(
|
1220
|
+
dist: NDArray, method: str, atom: str, fourier: bool = False
|
1221
|
+
) -> NDArray:
|
1222
|
+
"""
|
1223
|
+
Compute the electron factor.
|
1224
|
+
|
1225
|
+
Parameters
|
1226
|
+
----------
|
1227
|
+
dist : NDArray
|
1228
|
+
Distance.
|
1229
|
+
method : str
|
1230
|
+
Method name.
|
1231
|
+
atom : str
|
1232
|
+
Atom type.
|
1233
|
+
fourier : bool, optional
|
1234
|
+
Whether to compute the electron factor in Fourier space.
|
1235
|
+
|
1236
|
+
Returns
|
1237
|
+
-------
|
1238
|
+
NDArray
|
1239
|
+
Computed electron factor.
|
1240
|
+
"""
|
1241
|
+
data = get_scattering_factors(method)
|
1242
|
+
n_range = len(data.get(atom, [])) // 2
|
1243
|
+
default = np.zeros(n_range * 3)
|
1244
|
+
|
1245
|
+
res = 0.0
|
1246
|
+
a_values = data.get(atom, default)[:n_range]
|
1247
|
+
b_values = data.get(atom, default)[n_range : 2 * n_range]
|
1248
|
+
|
1249
|
+
if method == "dt1969":
|
1250
|
+
b_values = data.get(atom, default)[1 : (n_range + 1)]
|
1251
|
+
|
1252
|
+
for i in range(n_range):
|
1253
|
+
a = a_values[i]
|
1254
|
+
b = b_values[i]
|
1255
|
+
|
1256
|
+
if fourier:
|
1257
|
+
temp = a * np.exp(-b * np.power(dist, 2))
|
1258
|
+
else:
|
1259
|
+
b = b / (4 * np.power(np.pi, 2))
|
1260
|
+
temp = a * np.sqrt(np.pi / b) * np.exp(-np.power(dist, 2) / (4 * b))
|
1261
|
+
|
1262
|
+
if not np.isnan(temp).any():
|
1263
|
+
res += temp
|
1264
|
+
|
1265
|
+
return res / (2 * np.pi)
|
1266
|
+
|
1267
|
+
|
1268
|
+
def optimize_hlfp(profile, M, T, atom, method, filter_method):
|
1269
|
+
"""
|
1270
|
+
Optimize high-low pass filter (HLFP).
|
1271
|
+
|
1272
|
+
Parameters
|
1273
|
+
----------
|
1274
|
+
profile : NDArray
|
1275
|
+
Input profile.
|
1276
|
+
M : int
|
1277
|
+
Scaling factor.
|
1278
|
+
T : float
|
1279
|
+
Time step.
|
1280
|
+
atom : str
|
1281
|
+
Atom type.
|
1282
|
+
method : str
|
1283
|
+
Method name.
|
1284
|
+
filter_method : str
|
1285
|
+
Filter method name.
|
1286
|
+
|
1287
|
+
Returns
|
1288
|
+
-------
|
1289
|
+
float
|
1290
|
+
Fitness value.
|
1291
|
+
|
1292
|
+
References
|
1293
|
+
----------
|
1294
|
+
.. [1] Sorzano, Carlos et al (Mar. 2015). Fast and accurate conversion
|
1295
|
+
of atomic models into electron density maps. AIMS Biophysics
|
1296
|
+
2, 8–20.
|
1297
|
+
"""
|
1298
|
+
# omega, d, dw
|
1299
|
+
initial_params = [1.0, 0.01, 1.0 / 8.0]
|
1300
|
+
if filter_method == "brute":
|
1301
|
+
best_fitness = float("inf")
|
1302
|
+
OMEGA, D, DW = np.meshgrid(
|
1303
|
+
np.arange(0.7, 1.3, 0.015),
|
1304
|
+
np.arange(0.01, 0.2, 0.015),
|
1305
|
+
np.arange(0.05, 0.2, 0.015),
|
1428
1306
|
)
|
1429
|
-
|
1430
|
-
|
1431
|
-
|
1432
|
-
|
1307
|
+
for omega, d, dw in zip(OMEGA.ravel(), D.ravel(), DW.ravel()):
|
1308
|
+
current_fitness = _hlpf_fitness([omega, d, dw], T, M, profile, atom, method)
|
1309
|
+
if current_fitness < best_fitness:
|
1310
|
+
best_fitness = current_fitness
|
1311
|
+
initial_params = [omega, d, dw]
|
1312
|
+
final_params = np.array(initial_params)
|
1313
|
+
else:
|
1314
|
+
res = minimize(
|
1315
|
+
_hlpf_fitness,
|
1316
|
+
initial_params,
|
1317
|
+
args=tuple([T, M, profile, atom, method]),
|
1318
|
+
method="SLSQP",
|
1319
|
+
bounds=([0.2, 2], [1e-3, 2], [1e-3, 1]),
|
1433
1320
|
)
|
1434
|
-
|
1435
|
-
np.
|
1436
|
-
|
1437
|
-
|
1321
|
+
final_params = res.x
|
1322
|
+
if np.any(final_params != final_params):
|
1323
|
+
print(f"Solver returned NAs for atom {atom} at {M}" % (atom, M))
|
1324
|
+
final_params = final_params
|
1438
1325
|
|
1326
|
+
final_params[0] *= np.pi / M
|
1327
|
+
mask = window_sinckb(*final_params)
|
1439
1328
|
|
1440
|
-
|
1441
|
-
|
1442
|
-
|
1443
|
-
shape: Tuple[int],
|
1444
|
-
sampling_rate: NDArray,
|
1445
|
-
omit_negative_frequencies: bool = False,
|
1446
|
-
):
|
1447
|
-
center = np.divide(shape, 2).astype(int)
|
1448
|
-
norm = np.multiply(shape, sampling_rate)
|
1329
|
+
if profile.shape[0] > mask.shape[0]:
|
1330
|
+
profile_origin = int((profile.size - 1) / 2)
|
1331
|
+
mask = window(mask, profile_origin, profile_origin)
|
1449
1332
|
|
1450
|
-
|
1451
|
-
|
1452
|
-
|
1453
|
-
|
1454
|
-
|
1455
|
-
|
1456
|
-
|
1457
|
-
|
1458
|
-
|
1459
|
-
|
1460
|
-
|
1461
|
-
|
1462
|
-
|
1463
|
-
|
1464
|
-
|
1465
|
-
|
1466
|
-
|
1467
|
-
|
1333
|
+
return mask
|
1334
|
+
|
1335
|
+
|
1336
|
+
def _hlpf_fitness(
|
1337
|
+
params: Tuple[float], T: float, M: float, profile: NDArray, atom: str, method: str
|
1338
|
+
) -> float:
|
1339
|
+
"""
|
1340
|
+
Fitness function for high-low pass filter optimization.
|
1341
|
+
|
1342
|
+
Parameters
|
1343
|
+
----------
|
1344
|
+
params : tuple of float
|
1345
|
+
Parameters [omega, d, dw] for optimization.
|
1346
|
+
T : float
|
1347
|
+
Time step.
|
1348
|
+
M : int
|
1349
|
+
Scaling factor.
|
1350
|
+
profile : NDArray
|
1351
|
+
Input profile.
|
1352
|
+
atom : str
|
1353
|
+
Atom type.
|
1354
|
+
method : str
|
1355
|
+
Method name.
|
1356
|
+
|
1357
|
+
Returns
|
1358
|
+
-------
|
1359
|
+
float
|
1360
|
+
Fitness value.
|
1361
|
+
|
1362
|
+
References
|
1363
|
+
----------
|
1364
|
+
.. [1] Sorzano, Carlos et al (Mar. 2015). Fast and accurate conversion
|
1365
|
+
of atomic models into electron density maps. AIMS Biophysics
|
1366
|
+
2, 8–20.
|
1367
|
+
.. [2] https://github.com/I2PC/xmipp/blob/707f921dfd29cacf5a161535034d28153b58215a/src/xmipp/libraries/data/pdb.cpp#L1344
|
1368
|
+
"""
|
1369
|
+
omega, d, dw = params
|
1370
|
+
|
1371
|
+
if not (0.7 <= omega <= 1.3) and (0 <= d <= 0.2) and (1e-3 <= dw <= 0.2):
|
1372
|
+
return 1e38 * np.random.randint(1, 100)
|
1373
|
+
|
1374
|
+
mask = window_sinckb(omega=omega * np.pi / M, d=d, dw=dw)
|
1375
|
+
|
1376
|
+
if profile.shape[0] > mask.shape[0]:
|
1377
|
+
profile_origin = int((profile.size - 1) / 2)
|
1378
|
+
mask = window(mask, profile_origin, profile_origin)
|
1379
|
+
else:
|
1380
|
+
filter_origin = int((mask.size - 1) / 2)
|
1381
|
+
profile = window(profile, filter_origin, filter_origin)
|
1382
|
+
|
1383
|
+
f_mask = ndimage.convolve(profile, mask)
|
1384
|
+
|
1385
|
+
orig = int((f_mask.size - 1) / 2)
|
1386
|
+
dist = np.arange(-orig, orig + 1) * T
|
1387
|
+
t, c, k = splrep(x=dist, y=f_mask, k=3)
|
1388
|
+
i_max = np.ceil(np.divide(f_mask.shape, M)).astype(int)[0]
|
1389
|
+
coarse_mask = np.arange(-i_max, i_max + 1) * M
|
1390
|
+
spline = BSpline(t, c, k)
|
1391
|
+
coarse_values = spline(coarse_mask)
|
1392
|
+
|
1393
|
+
# padding to retain longer fourier response
|
1394
|
+
aux = window(
|
1395
|
+
coarse_values, x0=10 * coarse_values.shape[0], xf=10 * coarse_values.shape[0]
|
1396
|
+
)
|
1397
|
+
f_filter = np.fft.fftn(aux)
|
1398
|
+
f_filter_mag = np.abs(f_filter)
|
1399
|
+
freq = np.fft.fftfreq(f_filter.size)
|
1400
|
+
freq /= M * T
|
1401
|
+
amplitude_f = mask.sum() / coarse_values.sum()
|
1402
|
+
|
1403
|
+
size_f = f_filter_mag.shape[0] * amplitude_f
|
1404
|
+
fourier_form_f = electron_factor(dist=freq, atom=atom, method=method, fourier=True)
|
1405
|
+
|
1406
|
+
valid_freq_mask = freq >= 0
|
1407
|
+
f1_values = np.log10(f_filter_mag[valid_freq_mask] * size_f)
|
1408
|
+
f2_values = np.log10(np.divide(T, fourier_form_f[valid_freq_mask]))
|
1409
|
+
squared_differences = np.square(f1_values - f2_values)
|
1410
|
+
error = np.sum(squared_differences)
|
1411
|
+
error /= np.sum(valid_freq_mask)
|
1412
|
+
|
1413
|
+
return error
|
1414
|
+
|
1415
|
+
|
1416
|
+
def window(arr, x0, xf, constant_values=0):
|
1417
|
+
"""
|
1418
|
+
Window an array by slicing between x0 and xf and padding if required.
|
1419
|
+
|
1420
|
+
Parameters
|
1421
|
+
----------
|
1422
|
+
arr : ndarray
|
1423
|
+
Input array to be windowed.
|
1424
|
+
x0 : int
|
1425
|
+
Start of the window.
|
1426
|
+
xf : int
|
1427
|
+
End of the window.
|
1428
|
+
constant_values : int or float, optional
|
1429
|
+
The constant values to use for padding, by default 0.
|
1430
|
+
|
1431
|
+
Returns
|
1432
|
+
-------
|
1433
|
+
ndarray
|
1434
|
+
Windowed array.
|
1435
|
+
"""
|
1436
|
+
origin = int((arr.size - 1) / 2)
|
1437
|
+
|
1438
|
+
xs = origin - x0
|
1439
|
+
xe = origin - xf
|
1440
|
+
|
1441
|
+
if xs >= 0 and xe <= arr.shape[0]:
|
1442
|
+
if xs <= arr.shape[0] and xe > 0:
|
1443
|
+
arr = arr[xs:xe]
|
1444
|
+
xs = 0
|
1445
|
+
xe = 0
|
1446
|
+
elif xs <= arr.shape[0]:
|
1447
|
+
arr = arr[xs:]
|
1448
|
+
xs = 0
|
1449
|
+
elif xe >= 0 and xe <= arr.shape[0]:
|
1450
|
+
arr = arr[:xe]
|
1451
|
+
xe = 0
|
1452
|
+
|
1453
|
+
xs *= -1
|
1454
|
+
xe *= -1
|
1455
|
+
|
1456
|
+
return np.pad(
|
1457
|
+
arr, (int(xs), int(xe)), mode="constant", constant_values=constant_values
|
1458
|
+
)
|
1459
|
+
|
1460
|
+
|
1461
|
+
def atom_profile(
|
1462
|
+
M, atom, T=0.08333333, method="peng1995", lfilter=True, filter_method="minimize"
|
1463
|
+
):
|
1464
|
+
"""
|
1465
|
+
Generate an atom profile using a variety of methods.
|
1466
|
+
|
1467
|
+
Parameters
|
1468
|
+
----------
|
1469
|
+
M : float
|
1470
|
+
Down sampling factor.
|
1471
|
+
atom : Any
|
1472
|
+
Type or representation of the atom.
|
1473
|
+
T : float, optional
|
1474
|
+
Sampling rate in angstroms/pixel, by default 0.08333333.
|
1475
|
+
method : str, optional
|
1476
|
+
Method to be used for generating the profile, by default "peng1995".
|
1477
|
+
lfilter : bool, optional
|
1478
|
+
Whether to apply filter on the profile, by default True.
|
1479
|
+
filter_method : str, optional
|
1480
|
+
The method for the filter, by default "minimize".
|
1481
|
+
|
1482
|
+
Returns
|
1483
|
+
-------
|
1484
|
+
BSpline
|
1485
|
+
A spline representation of the atom profile.
|
1486
|
+
|
1487
|
+
References
|
1488
|
+
----------
|
1489
|
+
.. [1] Sorzano, Carlos et al (Mar. 2015). Fast and accurate conversion
|
1490
|
+
of atomic models into electron density maps. AIMS Biophysics
|
1491
|
+
2, 8–20.
|
1492
|
+
.. [2] https://github.com/I2PC/xmipp/blob/707f921dfd29cacf5a161535034d28153b58215a/src/xmipp/libraries/data/pdb.cpp#L1344
|
1493
|
+
"""
|
1494
|
+
M = M / T
|
1495
|
+
imax = np.ceil(4 / T * np.sqrt(76.7309 / (2 * np.power(np.pi, 2))))
|
1496
|
+
dist = np.arange(-imax, imax + 1) * T
|
1497
|
+
|
1498
|
+
profile = electron_factor(dist, method, atom)
|
1499
|
+
|
1500
|
+
if lfilter:
|
1501
|
+
window = optimize_hlfp(
|
1502
|
+
profile=profile,
|
1503
|
+
M=M,
|
1504
|
+
T=T,
|
1505
|
+
atom=atom,
|
1506
|
+
method=method,
|
1507
|
+
filter_method=filter_method,
|
1468
1508
|
)
|
1469
|
-
|
1509
|
+
profile = ndimage.convolve(profile, window)
|
1470
1510
|
|
1471
|
-
|
1472
|
-
|
1511
|
+
indices = np.where(profile > 1e-3)
|
1512
|
+
min_indices = np.maximum(np.amin(indices, axis=1), 0)
|
1513
|
+
max_indices = np.minimum(np.amax(indices, axis=1) + 1, profile.shape)
|
1514
|
+
slices = tuple(slice(*coord) for coord in zip(min_indices, max_indices))
|
1515
|
+
profile = profile[slices]
|
1473
1516
|
|
1474
|
-
|
1475
|
-
|
1476
|
-
|
1517
|
+
profile_origin = int((profile.size - 1) / 2)
|
1518
|
+
dist = np.arange(-profile_origin, profile_origin + 1) * T
|
1519
|
+
t, c, k = splrep(x=dist, y=profile, k=3)
|
1477
1520
|
|
1478
|
-
|
1479
|
-
np.reciprocal(radial_averages, out=radial_averages)
|
1480
|
-
np.divide(radial_averages, radial_averages.max(), out=radial_averages)
|
1521
|
+
return BSpline(t, c, k)
|
1481
1522
|
|
1482
|
-
np.multiply(fourier_transform, radial_averages[bins], out=fourier_transform)
|
1483
1523
|
|
1484
|
-
|
1485
|
-
|
1486
|
-
|
1487
|
-
return ret, bin_edges, radial_averages
|
1524
|
+
def get_scattering_factors(method: str) -> Dict:
|
1525
|
+
"""
|
1526
|
+
Retrieve scattering factors from a stored file based on the given method.
|
1488
1527
|
|
1489
|
-
|
1490
|
-
|
1491
|
-
|
1492
|
-
|
1493
|
-
shape=template.shape, sampling_rate=1, omit_negative_frequencies=True
|
1494
|
-
)
|
1495
|
-
frequency_grid = np.linalg.norm(grid, axis=0)
|
1528
|
+
Parameters
|
1529
|
+
----------
|
1530
|
+
method : str
|
1531
|
+
Method name used to get the scattering factors.
|
1496
1532
|
|
1497
|
-
|
1498
|
-
|
1533
|
+
Returns
|
1534
|
+
-------
|
1535
|
+
Dict
|
1536
|
+
Dictionary containing scattering factors for the given method.
|
1499
1537
|
|
1500
|
-
|
1501
|
-
|
1502
|
-
|
1503
|
-
|
1504
|
-
).real
|
1538
|
+
Raises
|
1539
|
+
------
|
1540
|
+
ValueError
|
1541
|
+
If the method is not found in the stored data.
|
1505
1542
|
|
1506
|
-
|
1543
|
+
"""
|
1544
|
+
path = os.path.join(os.path.dirname(__file__), "data", "scattering_factors.pickle")
|
1545
|
+
with open(path, "rb") as infile:
|
1546
|
+
data = pickle.load(infile)
|
1547
|
+
|
1548
|
+
if method not in data:
|
1549
|
+
raise ValueError(f"{method} is not valid. Use {', '.join(data.keys())}.")
|
1550
|
+
return data[method]
|