pytme 0.2.1__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.
Files changed (49) hide show
  1. {pytme-0.2.1.data → pytme-0.2.2.data}/scripts/match_template.py +147 -93
  2. {pytme-0.2.1.data → pytme-0.2.2.data}/scripts/postprocess.py +67 -26
  3. {pytme-0.2.1.data → pytme-0.2.2.data}/scripts/preprocessor_gui.py +175 -85
  4. pytme-0.2.2.dist-info/METADATA +91 -0
  5. pytme-0.2.2.dist-info/RECORD +74 -0
  6. {pytme-0.2.1.dist-info → pytme-0.2.2.dist-info}/WHEEL +1 -1
  7. scripts/extract_candidates.py +20 -13
  8. scripts/match_template.py +147 -93
  9. scripts/match_template_filters.py +154 -95
  10. scripts/postprocess.py +67 -26
  11. scripts/preprocessor_gui.py +175 -85
  12. scripts/refine_matches.py +265 -61
  13. tme/__init__.py +0 -1
  14. tme/__version__.py +1 -1
  15. tme/analyzer.py +451 -809
  16. tme/backends/__init__.py +40 -11
  17. tme/backends/_jax_utils.py +185 -0
  18. tme/backends/cupy_backend.py +111 -223
  19. tme/backends/jax_backend.py +214 -150
  20. tme/backends/matching_backend.py +445 -384
  21. tme/backends/mlx_backend.py +32 -59
  22. tme/backends/npfftw_backend.py +239 -507
  23. tme/backends/pytorch_backend.py +21 -145
  24. tme/density.py +233 -363
  25. tme/extensions.cpython-311-darwin.so +0 -0
  26. tme/matching_data.py +322 -285
  27. tme/matching_exhaustive.py +172 -1493
  28. tme/matching_optimization.py +143 -106
  29. tme/matching_scores.py +884 -0
  30. tme/matching_utils.py +280 -386
  31. tme/memory.py +377 -0
  32. tme/orientations.py +52 -12
  33. tme/parser.py +3 -4
  34. tme/preprocessing/_utils.py +61 -32
  35. tme/preprocessing/compose.py +7 -3
  36. tme/preprocessing/frequency_filters.py +49 -39
  37. tme/preprocessing/tilt_series.py +34 -40
  38. tme/preprocessor.py +560 -526
  39. tme/structure.py +491 -188
  40. tme/types.py +5 -3
  41. pytme-0.2.1.dist-info/METADATA +0 -73
  42. pytme-0.2.1.dist-info/RECORD +0 -73
  43. tme/helpers.py +0 -881
  44. tme/matching_constrained.py +0 -195
  45. {pytme-0.2.1.data → pytme-0.2.2.data}/scripts/estimate_ram_usage.py +0 -0
  46. {pytme-0.2.1.data → pytme-0.2.2.data}/scripts/preprocess.py +0 -0
  47. {pytme-0.2.1.dist-info → pytme-0.2.2.dist-info}/LICENSE +0 -0
  48. {pytme-0.2.1.dist-info → pytme-0.2.2.dist-info}/entry_points.txt +0 -0
  49. {pytme-0.2.1.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 numpy.typing import NDArray
13
-
14
- from scipy.ndimage import (
15
- fourier_gaussian,
16
- gaussian_filter,
17
- rank_filter,
18
- median_filter,
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
- method_to_call = getattr(self, method)
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: NDArray,
164
- fourier: bool = False,
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
- The input atomic structure map.
173
- resolution : float, optional
174
- The resolution. The product of `resolution` and `sigma_coeff` is used
175
- to compute the `sigma` for the discretized Gaussian. Default is None.
176
- sigma : NDArray
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
- The simulated electron densities after applying the Gaussian filter.
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._gaussian_real(template, low_sigma)
224
- im2 = self._gaussian_real(template, high_sigma)
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
- filter = gaussian_filter(template, sigma)
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 + filter * (1 - edges)
324
+ res = template * edges + filt * (1 - edges)
407
325
  else:
408
- res = template * (1 - edges) + filter * (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="same")
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 = waverecn(coeffs=decomp, wavelet=wavelet)
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
- minimum_frequency: float,
909
- maximum_frequency: float,
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
- minimum_frequency : float
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
- maximum_frequency : float
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
- minimum_frequency=minimum_frequency,
940
- maximum_frequency=maximum_frequency,
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
- minimum_frequency: float,
955
- maximum_frequency: float,
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
- minimum_frequency : float
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
- if sampling_rate is None:
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
- if omit_negative_frequencies:
1004
- stop = 1 + (shape[-1] // 2)
1005
- bpf = bpf[..., :stop]
1006
-
1007
- return bpf
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
- Density.rotate_array(
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
- Density.rotate_array(
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, fourier=False)
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
- Parameters
1359
- ----------
1360
- old_shape : NDArray
1361
- The original shape of the array before cropping.
1362
- new_shape : NDArray
1363
- The new desired shape for the array after cropping.
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
- Returns
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
- Parameters
1386
- ----------
1387
- template : NDArray
1388
- The original template to be uncropped.
1389
- reciprocal_template_filter : NDArray
1390
- The filter to be applied in the Fourier space.
1391
- crop_factor : float
1392
- Cropping factor over reeciprocal_template_filter boundary.
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
- Returns
1395
- -------
1396
- NDArray
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
- mask = self._fourier_crop_mask(old_shape=template.shape, new_shape=new_boxsize)
1412
- arr_ft = np.fft.fftn(template)
1413
- arr_ft *= np.prod(new_boxsize) / np.prod(template.shape)
1414
- arr_ft = np.reshape(arr_ft[mask], new_boxsize)
1415
- arr_cropped = np.real(np.fft.ifftn(arr_ft))
1416
- return arr_cropped
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
- def fourier_uncrop(
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
- Parameters
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
- Returns
1432
- -------
1433
- NDArray
1434
- Uncropped template with shape reciprocal_template_filter.
1435
- """
1436
- mask = self._fourier_crop_mask(
1437
- old_shape=reciprocal_template_filter.shape, new_shape=template.shape
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
- ft_vol = np.zeros_like(mask)
1440
- ft_vol[mask] = np.fft.fftn(template).ravel()
1441
- ft_vol *= np.divide(np.prod(mask.shape), np.prod(template.shape)).astype(
1442
- ft_vol.dtype
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
- reciprocal_template_filter = reciprocal_template_filter.astype(ft_vol.dtype)
1445
- np.multiply(ft_vol, reciprocal_template_filter, out=ft_vol)
1446
- ret = np.real(np.fft.ifftn(ft_vol))
1447
- return ret
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
- class LinearWhiteningFilter:
1451
- @staticmethod
1452
- def _fftfreqn(
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
- if omit_negative_frequencies:
1461
- shape = (*shape[:-1], center[-1] + 1)
1462
- center = (*center[:-1], 0)
1463
-
1464
- indices = np.indices(shape).T
1465
- indices -= center
1466
- indices = np.divide(indices, norm)
1467
- return indices.T
1468
-
1469
- def filter(
1470
- self, template: NDArray, n_bins: int = None
1471
- ) -> Tuple[NDArray, NDArray, NDArray]:
1472
- max_bins = np.max(template.shape) // 2 + 1
1473
- n_bins = max_bins if n_bins is None else n_bins
1474
- n_bins = int(min(n_bins, max_bins))
1475
-
1476
- grid = self._fftfreqn(
1477
- shape=template.shape, sampling_rate=1, omit_negative_frequencies=True
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
- frequency_grid = np.linalg.norm(grid, axis=0)
1509
+ profile = ndimage.convolve(profile, window)
1480
1510
 
1481
- _, bin_edges = np.histogram(frequency_grid, bins=n_bins - 1)
1482
- bins = np.digitize(frequency_grid, bins=bin_edges, right=True)
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
- fft_shift_axes = tuple(range(template.ndim - 1))
1485
- fourier_transform = np.fft.fftshift(np.fft.rfftn(template), axes=fft_shift_axes)
1486
- fourier_spectrum = np.abs(fourier_transform)
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
- radial_averages = ndimean(fourier_spectrum, labels=bins, index=np.unique(bins))
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
- ret = np.fft.irfftn(
1495
- np.fft.ifftshift(fourier_transform, axes=fft_shift_axes), s=template.shape
1496
- ).real
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
- def apply(
1500
- self, template: NDArray, bin_edges: NDArray, radial_averages: NDArray
1501
- ) -> NDArray:
1502
- grid = self._fftfreqn(
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
- fft_shift_axes = tuple(range(template.ndim - 1))
1508
- fourier_transform = np.fft.fftshift(np.fft.rfftn(template), axes=fft_shift_axes)
1533
+ Returns
1534
+ -------
1535
+ Dict
1536
+ Dictionary containing scattering factors for the given method.
1509
1537
 
1510
- bins = np.digitize(frequency_grid, bins=bin_edges, right=True)
1511
- np.multiply(fourier_transform, radial_averages[bins], out=fourier_transform)
1512
- ret = np.fft.irfftn(
1513
- np.fft.ifftshift(fourier_transform, axes=fft_shift_axes), s=template.shape
1514
- ).real
1538
+ Raises
1539
+ ------
1540
+ ValueError
1541
+ If the method is not found in the stored data.
1515
1542
 
1516
- return ret
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]