pytme 0.2.1__cp311-cp311-macosx_14_0_arm64.whl → 0.2.3__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.1.data → pytme-0.2.3.data}/scripts/match_template.py +219 -216
- {pytme-0.2.1.data → pytme-0.2.3.data}/scripts/postprocess.py +86 -54
- pytme-0.2.3.data/scripts/preprocess.py +132 -0
- {pytme-0.2.1.data → pytme-0.2.3.data}/scripts/preprocessor_gui.py +181 -94
- pytme-0.2.3.dist-info/METADATA +92 -0
- pytme-0.2.3.dist-info/RECORD +75 -0
- {pytme-0.2.1.dist-info → pytme-0.2.3.dist-info}/WHEEL +1 -1
- pytme-0.2.1.data/scripts/preprocess.py → scripts/eval.py +1 -1
- scripts/extract_candidates.py +20 -13
- scripts/match_template.py +219 -216
- scripts/match_template_filters.py +154 -95
- scripts/postprocess.py +86 -54
- scripts/preprocess.py +95 -56
- scripts/preprocessor_gui.py +181 -94
- scripts/refine_matches.py +265 -61
- tme/__init__.py +0 -1
- tme/__version__.py +1 -1
- tme/analyzer.py +458 -813
- tme/backends/__init__.py +40 -11
- tme/backends/_jax_utils.py +187 -0
- tme/backends/cupy_backend.py +109 -226
- tme/backends/jax_backend.py +230 -152
- tme/backends/matching_backend.py +445 -384
- tme/backends/mlx_backend.py +32 -59
- tme/backends/npfftw_backend.py +240 -507
- tme/backends/pytorch_backend.py +30 -151
- tme/density.py +248 -371
- tme/extensions.cpython-311-darwin.so +0 -0
- tme/matching_data.py +328 -284
- tme/matching_exhaustive.py +195 -1499
- tme/matching_optimization.py +143 -106
- tme/matching_scores.py +887 -0
- tme/matching_utils.py +287 -388
- tme/memory.py +377 -0
- tme/orientations.py +78 -21
- tme/parser.py +3 -4
- tme/preprocessing/_utils.py +61 -32
- tme/preprocessing/composable_filter.py +7 -4
- tme/preprocessing/compose.py +7 -3
- tme/preprocessing/frequency_filters.py +49 -39
- tme/preprocessing/tilt_series.py +44 -72
- tme/preprocessor.py +560 -526
- tme/structure.py +491 -188
- tme/types.py +5 -3
- pytme-0.2.1.dist-info/METADATA +0 -73
- pytme-0.2.1.dist-info/RECORD +0 -73
- tme/helpers.py +0 -881
- tme/matching_constrained.py +0 -195
- {pytme-0.2.1.data → pytme-0.2.3.data}/scripts/estimate_ram_usage.py +0 -0
- {pytme-0.2.1.dist-info → pytme-0.2.3.dist-info}/LICENSE +0 -0
- {pytme-0.2.1.dist-info → pytme-0.2.3.dist-info}/entry_points.txt +0 -0
- {pytme-0.2.1.dist-info → pytme-0.2.3.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
|
|
@@ -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)
|
659
|
-
|
660
|
-
template = zoom(array, np.divide(template.shape, array.shape))
|
661
|
-
template = self.interpolate_box(box=interpolation_box, arr=template)
|
662
|
-
|
663
|
-
return template
|
664
|
-
|
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()}
|
543
|
+
array = ndimage.decimate(array, q=level, axis=k)
|
699
544
|
|
700
|
-
template =
|
545
|
+
template = ndimage.zoom(array, np.divide(template.shape, array.shape))
|
701
546
|
template = self.interpolate_box(box=interpolation_box, arr=template)
|
702
547
|
|
703
548
|
return template
|
704
549
|
|
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
|
-
)
|
673
|
+
from .preprocessing import BandPassFilter
|
1002
674
|
|
1003
|
-
|
1004
|
-
|
1005
|
-
|
1006
|
-
|
1007
|
-
|
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
|
|
@@ -1208,7 +880,7 @@ class Preprocessor:
|
|
1208
880
|
rotation_matrix = euler_to_rotationmatrix((tilt_angles[index], 0))
|
1209
881
|
rotation_matrix = rotation_matrix[np.ix_((0, 1), (0, 1))]
|
1210
882
|
|
1211
|
-
|
883
|
+
NumpyFFTWBackend().rigid_transform(
|
1212
884
|
arr=plane,
|
1213
885
|
rotation_matrix=rotation_matrix,
|
1214
886
|
out=plane_rotated,
|
@@ -1221,9 +893,7 @@ class Preprocessor:
|
|
1221
893
|
np.fmin(wedge_volume, np.max(weights), wedge_volume)
|
1222
894
|
|
1223
895
|
if sigma > 0:
|
1224
|
-
wedge_volume = self.gaussian_filter(
|
1225
|
-
template=wedge_volume, sigma=sigma, fourier=False
|
1226
|
-
)
|
896
|
+
wedge_volume = self.gaussian_filter(template=wedge_volume, sigma=sigma)
|
1227
897
|
|
1228
898
|
if opening_axis > tilt_axis:
|
1229
899
|
wedge_volume = np.moveaxis(wedge_volume, 1, 0)
|
@@ -1341,7 +1011,7 @@ class Preprocessor:
|
|
1341
1011
|
if not infinite_plane:
|
1342
1012
|
np.multiply(wedge, distances <= shape[tilt_axis] // 2, out=wedge)
|
1343
1013
|
|
1344
|
-
wedge = self.gaussian_filter(template=wedge, sigma=sigma
|
1014
|
+
wedge = self.gaussian_filter(template=wedge, sigma=sigma)
|
1345
1015
|
wedge = np.fft.ifftshift(wedge > np.exp(-2))
|
1346
1016
|
|
1347
1017
|
if omit_negative_frequencies:
|
@@ -1350,167 +1020,531 @@ class Preprocessor:
|
|
1350
1020
|
|
1351
1021
|
return wedge
|
1352
1022
|
|
1353
|
-
@staticmethod
|
1354
|
-
def _fourier_crop_mask(old_shape: NDArray, new_shape: NDArray) -> NDArray:
|
1355
|
-
"""
|
1356
|
-
Generate a mask for Fourier cropping.
|
1357
1023
|
|
1358
|
-
|
1359
|
-
|
1360
|
-
|
1361
|
-
|
1362
|
-
|
1363
|
-
|
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)
|
1364
1051
|
|
1365
|
-
|
1366
|
-
-------
|
1367
|
-
NDArray
|
1368
|
-
The mask array for Fourier cropping.
|
1369
|
-
"""
|
1370
|
-
mask = np.zeros(old_shape, dtype=bool)
|
1371
|
-
mask[tuple(np.indices(new_shape))] = 1
|
1372
|
-
box_shift = np.floor(np.divide(new_shape, 2)).astype(int)
|
1373
|
-
mask = np.roll(mask, shift=-box_shift, axis=range(len(old_shape)))
|
1374
|
-
return mask
|
1052
|
+
return bessel(order, arr) / bessel(order, beta)
|
1375
1053
|
|
1376
|
-
def fourier_crop(
|
1377
|
-
self,
|
1378
|
-
template: NDArray,
|
1379
|
-
reciprocal_template_filter: NDArray,
|
1380
|
-
crop_factor: float = 3 / 2,
|
1381
|
-
) -> NDArray:
|
1382
|
-
"""
|
1383
|
-
Perform Fourier uncropping on a given template.
|
1384
1054
|
|
1385
|
-
|
1386
|
-
|
1387
|
-
|
1388
|
-
|
1389
|
-
|
1390
|
-
|
1391
|
-
|
1392
|
-
|
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)
|
1393
1082
|
|
1394
|
-
|
1395
|
-
|
1396
|
-
|
1397
|
-
The uncropped template.
|
1398
|
-
"""
|
1399
|
-
new_boxsize = np.zeros(template.ndim, dtype=int)
|
1400
|
-
for i in range(template.ndim):
|
1401
|
-
slices = tuple(
|
1402
|
-
slice(0, 1) if j != i else slice(template.shape[i] // 2)
|
1403
|
-
for j in range(template.ndim)
|
1404
|
-
)
|
1405
|
-
filt = np.squeeze(reciprocal_template_filter[slices])
|
1406
|
-
new_boxsize[i] = np.ceil((np.max(np.where(filt > 0)) + 1) * crop_factor) * 2
|
1083
|
+
arr = np.divide(np.power(arr, order) * bessel(order, arr), bessel(order, beta))
|
1084
|
+
arr[arr != arr] = 0
|
1085
|
+
return arr
|
1407
1086
|
|
1408
|
-
if np.any(np.greater(new_boxsize, template.shape)):
|
1409
|
-
new_boxsize = np.array(template.shape).copy()
|
1410
1087
|
|
1411
|
-
|
1412
|
-
|
1413
|
-
|
1414
|
-
|
1415
|
-
|
1416
|
-
|
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)
|
1417
1114
|
|
1418
|
-
|
1419
|
-
self, template: NDArray, reciprocal_template_filter: NDArray
|
1420
|
-
) -> NDArray:
|
1421
|
-
"""
|
1422
|
-
Perform an uncrop operation in the Fourier space.
|
1115
|
+
mask = sinc_m * kaiser
|
1423
1116
|
|
1424
|
-
|
1425
|
-
----------
|
1426
|
-
template : NDArray
|
1427
|
-
The input array.
|
1428
|
-
reciprocal_template_filter : NDArray
|
1429
|
-
The filter to be applied in the Fourier space.
|
1117
|
+
return mask / np.sum(mask)
|
1430
1118
|
|
1431
|
-
|
1432
|
-
|
1433
|
-
|
1434
|
-
|
1435
|
-
|
1436
|
-
|
1437
|
-
|
1119
|
+
|
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.
|
1147
|
+
|
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,
|
1438
1159
|
)
|
1439
|
-
|
1440
|
-
|
1441
|
-
|
1442
|
-
|
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),
|
1443
1306
|
)
|
1444
|
-
|
1445
|
-
|
1446
|
-
|
1447
|
-
|
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]),
|
1320
|
+
)
|
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
|
1448
1325
|
|
1326
|
+
final_params[0] *= np.pi / M
|
1327
|
+
mask = window_sinckb(*final_params)
|
1449
1328
|
|
1450
|
-
|
1451
|
-
|
1452
|
-
|
1453
|
-
shape: Tuple[int],
|
1454
|
-
sampling_rate: NDArray,
|
1455
|
-
omit_negative_frequencies: bool = False,
|
1456
|
-
):
|
1457
|
-
center = np.divide(shape, 2).astype(int)
|
1458
|
-
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)
|
1459
1332
|
|
1460
|
-
|
1461
|
-
|
1462
|
-
|
1463
|
-
|
1464
|
-
|
1465
|
-
|
1466
|
-
|
1467
|
-
|
1468
|
-
|
1469
|
-
|
1470
|
-
|
1471
|
-
|
1472
|
-
|
1473
|
-
|
1474
|
-
|
1475
|
-
|
1476
|
-
|
1477
|
-
|
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,
|
1478
1508
|
)
|
1479
|
-
|
1509
|
+
profile = ndimage.convolve(profile, window)
|
1480
1510
|
|
1481
|
-
|
1482
|
-
|
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]
|
1483
1516
|
|
1484
|
-
|
1485
|
-
|
1486
|
-
|
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)
|
1487
1520
|
|
1488
|
-
|
1489
|
-
np.reciprocal(radial_averages, out=radial_averages)
|
1490
|
-
np.divide(radial_averages, radial_averages.max(), out=radial_averages)
|
1521
|
+
return BSpline(t, c, k)
|
1491
1522
|
|
1492
|
-
np.multiply(fourier_transform, radial_averages[bins], out=fourier_transform)
|
1493
1523
|
|
1494
|
-
|
1495
|
-
|
1496
|
-
|
1497
|
-
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.
|
1498
1527
|
|
1499
|
-
|
1500
|
-
|
1501
|
-
|
1502
|
-
|
1503
|
-
shape=template.shape, sampling_rate=1, omit_negative_frequencies=True
|
1504
|
-
)
|
1505
|
-
frequency_grid = np.linalg.norm(grid, axis=0)
|
1528
|
+
Parameters
|
1529
|
+
----------
|
1530
|
+
method : str
|
1531
|
+
Method name used to get the scattering factors.
|
1506
1532
|
|
1507
|
-
|
1508
|
-
|
1533
|
+
Returns
|
1534
|
+
-------
|
1535
|
+
Dict
|
1536
|
+
Dictionary containing scattering factors for the given method.
|
1509
1537
|
|
1510
|
-
|
1511
|
-
|
1512
|
-
|
1513
|
-
|
1514
|
-
).real
|
1538
|
+
Raises
|
1539
|
+
------
|
1540
|
+
ValueError
|
1541
|
+
If the method is not found in the stored data.
|
1515
1542
|
|
1516
|
-
|
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]
|