pytme 0.1.5__cp311-cp311-macosx_14_0_arm64.whl

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