pytme 0.2.9__cp311-cp311-macosx_15_0_arm64.whl

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