pytme 0.3.1.post1__cp311-cp311-macosx_15_0_arm64.whl → 0.3.2__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 (68) hide show
  1. pytme-0.3.2.data/scripts/estimate_ram_usage.py +97 -0
  2. {pytme-0.3.1.post1.data → pytme-0.3.2.data}/scripts/match_template.py +213 -196
  3. {pytme-0.3.1.post1.data → pytme-0.3.2.data}/scripts/postprocess.py +40 -78
  4. {pytme-0.3.1.post1.data → pytme-0.3.2.data}/scripts/preprocess.py +4 -5
  5. {pytme-0.3.1.post1.data → pytme-0.3.2.data}/scripts/preprocessor_gui.py +50 -103
  6. {pytme-0.3.1.post1.data → pytme-0.3.2.data}/scripts/pytme_runner.py +46 -69
  7. {pytme-0.3.1.post1.dist-info → pytme-0.3.2.dist-info}/METADATA +3 -2
  8. {pytme-0.3.1.post1.dist-info → pytme-0.3.2.dist-info}/RECORD +68 -65
  9. scripts/estimate_ram_usage.py +97 -0
  10. scripts/match_template.py +213 -196
  11. scripts/match_template_devel.py +1339 -0
  12. scripts/postprocess.py +40 -78
  13. scripts/preprocess.py +4 -5
  14. scripts/preprocessor_gui.py +50 -103
  15. scripts/pytme_runner.py +46 -69
  16. scripts/refine_matches.py +5 -7
  17. tests/preprocessing/test_compose.py +31 -30
  18. tests/preprocessing/test_frequency_filters.py +17 -32
  19. tests/preprocessing/test_preprocessor.py +0 -19
  20. tests/preprocessing/test_utils.py +13 -1
  21. tests/test_analyzer.py +2 -10
  22. tests/test_backends.py +47 -18
  23. tests/test_density.py +72 -13
  24. tests/test_extensions.py +1 -0
  25. tests/test_matching_cli.py +23 -9
  26. tests/test_matching_exhaustive.py +5 -5
  27. tests/test_matching_utils.py +3 -3
  28. tests/test_rotations.py +13 -23
  29. tests/test_structure.py +1 -7
  30. tme/__version__.py +1 -1
  31. tme/analyzer/aggregation.py +47 -16
  32. tme/analyzer/base.py +34 -0
  33. tme/analyzer/peaks.py +26 -13
  34. tme/analyzer/proxy.py +14 -0
  35. tme/backends/_jax_utils.py +124 -71
  36. tme/backends/cupy_backend.py +6 -19
  37. tme/backends/jax_backend.py +110 -105
  38. tme/backends/matching_backend.py +0 -17
  39. tme/backends/mlx_backend.py +0 -29
  40. tme/backends/npfftw_backend.py +100 -97
  41. tme/backends/pytorch_backend.py +65 -78
  42. tme/cli.py +2 -2
  43. tme/density.py +102 -58
  44. tme/extensions.cpython-311-darwin.so +0 -0
  45. tme/filters/_utils.py +52 -24
  46. tme/filters/bandpass.py +99 -105
  47. tme/filters/compose.py +133 -39
  48. tme/filters/ctf.py +51 -102
  49. tme/filters/reconstruction.py +67 -122
  50. tme/filters/wedge.py +296 -325
  51. tme/filters/whitening.py +39 -75
  52. tme/mask.py +2 -2
  53. tme/matching_data.py +87 -15
  54. tme/matching_exhaustive.py +70 -120
  55. tme/matching_optimization.py +9 -63
  56. tme/matching_scores.py +261 -100
  57. tme/matching_utils.py +150 -91
  58. tme/memory.py +1 -0
  59. tme/orientations.py +28 -8
  60. tme/preprocessor.py +0 -239
  61. tme/rotations.py +102 -70
  62. tme/structure.py +601 -631
  63. tme/types.py +1 -0
  64. {pytme-0.3.1.post1.data → pytme-0.3.2.data}/scripts/estimate_memory_usage.py +0 -0
  65. {pytme-0.3.1.post1.dist-info → pytme-0.3.2.dist-info}/WHEEL +0 -0
  66. {pytme-0.3.1.post1.dist-info → pytme-0.3.2.dist-info}/entry_points.txt +0 -0
  67. {pytme-0.3.1.post1.dist-info → pytme-0.3.2.dist-info}/licenses/LICENSE +0 -0
  68. {pytme-0.3.1.post1.dist-info → pytme-0.3.2.dist-info}/top_level.txt +0 -0
tme/filters/wedge.py CHANGED
@@ -1,6 +1,5 @@
1
1
  """
2
- Implements class Wedge and WedgeReconstructed to create Fourier
3
- filter representations.
2
+ Implements class Wedge and WedgeReconstructed.
4
3
 
5
4
  Copyright (c) 2024 European Molecular Biology Laboratory
6
5
 
@@ -16,16 +15,14 @@ from ..types import NDArray
16
15
  from ..backends import backend as be
17
16
  from .compose import ComposableFilter
18
17
  from ..matching_utils import center_slice
19
- from ..rotations import euler_to_rotationmatrix
20
18
  from ..parser import XMLParser, StarParser, MDOCParser
21
19
  from ._utils import (
22
20
  centered_grid,
23
21
  frequency_grid_at_angle,
24
22
  compute_tilt_shape,
25
- crop_real_fourier,
26
23
  fftfreqn,
27
- shift_fourier,
28
24
  create_reconstruction_filter,
25
+ shift_fourier,
29
26
  )
30
27
 
31
28
  __all__ = ["Wedge", "WedgeReconstructed"]
@@ -34,12 +31,10 @@ __all__ = ["Wedge", "WedgeReconstructed"]
34
31
  @dataclass
35
32
  class Wedge(ComposableFilter):
36
33
  """
37
- Generate wedge mask for tomographic data.
34
+ Create per-tilt wedge mask for tomographic data.
38
35
  """
39
36
 
40
- #: The shape of the reconstruction volume.
41
- shape: Tuple[int] = None
42
- #: The tilt angles.
37
+ #: Tilt angles in degrees.
43
38
  angles: Tuple[float] = None
44
39
  #: The weights corresponding to each tilt angle.
45
40
  weights: Tuple[float] = None
@@ -55,7 +50,7 @@ class Wedge(ComposableFilter):
55
50
  sampling_rate: Tuple[float] = 1
56
51
 
57
52
  @classmethod
58
- def from_file(cls, filename: str) -> "Wedge":
53
+ def from_file(cls, filename: str, **kwargs) -> "Wedge":
59
54
  """
60
55
  Generate a :py:class:`Wedge` instance by reading tilt angles and weights.
61
56
  Supported extensions are:
@@ -100,180 +95,56 @@ class Wedge(ComposableFilter):
100
95
  raise ValueError("Length of weights and angles differ.")
101
96
 
102
97
  return cls(
103
- shape=None,
104
98
  tilt_axis=0,
105
99
  opening_axis=2,
106
100
  angles=np.array(angles, dtype=np.float32),
107
101
  weights=np.array(weights, dtype=np.float32),
102
+ **kwargs,
108
103
  )
109
104
 
110
- def __call__(self, **kwargs: Dict) -> NDArray:
111
- """
112
- Returns a Wedge stack of chosen parameters with DC component in the center.
113
- """
114
- func_args = vars(self).copy()
115
- func_args.update(kwargs)
116
-
105
+ def _evaluate(self, shape: Tuple[int, ...], **kwargs: Dict) -> NDArray:
106
+ """Returns a Wedge stack of chosen parameters."""
117
107
  weight_types = {
118
- None: self.weight_angle,
119
- "angle": self.weight_angle,
120
- "relion": self.weight_relion,
121
- "grigorieff": self.weight_grigorieff,
108
+ None: weight_uniform,
109
+ "angle": weight_angle,
110
+ "relion": weight_relion,
111
+ "grigorieff": weight_grigorieff,
122
112
  }
123
113
 
124
- weight_type = func_args.get("weight_type", None)
114
+ weight_type = kwargs.get("weight_type", None)
125
115
  if weight_type not in weight_types:
126
116
  raise ValueError(
127
117
  f"Supported weight_types are {','.join(list(weight_types.keys()))}"
128
118
  )
129
119
 
130
120
  if weight_type == "angle":
131
- func_args["weights"] = np.cos(np.radians(self.angles))
121
+ kwargs["weights"] = np.cos(np.radians(self.angles))
132
122
 
133
- ret = weight_types[weight_type](**func_args)
123
+ ret = weight_types[weight_type](shape=shape, **kwargs)
134
124
 
135
- frequency_cutoff = func_args.get("frequency_cutoff", None)
125
+ frequency_cutoff = kwargs.get("frequency_cutoff", None)
136
126
  if frequency_cutoff is not None:
137
- for index, angle in enumerate(func_args["angles"]):
127
+ for index, angle in enumerate(kwargs["angles"]):
138
128
  frequency_grid = frequency_grid_at_angle(
139
- shape=func_args["shape"],
140
- opening_axis=self.opening_axis,
141
- tilt_axis=self.tilt_axis,
129
+ shape=shape,
130
+ opening_axis=kwargs["opening_axis"],
131
+ tilt_axis=kwargs["tilt_axis"],
142
132
  angle=angle,
143
133
  sampling_rate=1,
144
134
  )
145
135
  ret[index] = np.multiply(ret[index], frequency_grid <= frequency_cutoff)
146
136
 
147
137
  ret = be.astype(be.to_backend_array(ret), be._float_dtype)
148
-
149
- return {
150
- "data": ret,
151
- "shape": func_args["shape"],
152
- "return_real_fourier": func_args.get("return_real_fourier", False),
153
- "is_multiplicative_filter": True,
154
- }
155
-
156
- @staticmethod
157
- def weight_angle(
158
- shape: Tuple[int],
159
- weights: Tuple[float],
160
- angles: Tuple[float],
161
- opening_axis: int,
162
- tilt_axis: int,
163
- **kwargs,
164
- ) -> NDArray:
165
- """
166
- Generate weighted wedges based on the cosine of the current angle.
167
- """
168
- tilt_shape = compute_tilt_shape(
169
- shape=shape, opening_axis=opening_axis, reduce_dim=True
170
- )
171
- wedge, wedges = np.ones(tilt_shape), np.zeros((len(angles), *tilt_shape))
172
- for index, angle in enumerate(angles):
173
- wedge.fill(weights[index])
174
- wedges[index] = wedge
175
-
176
- return wedges
177
-
178
- def weight_relion(
179
- self,
180
- shape: Tuple[int],
181
- opening_axis: int,
182
- tilt_axis: int,
183
- sampling_rate: float = 1.0,
184
- **kwargs,
185
- ) -> NDArray:
186
- """
187
- Generate weighted wedges based on the RELION 1.4 formalism, weighting each
188
- angle using the cosine of the angle and a Gaussian lowpass filter computed
189
- with respect to the exposure per angstrom.
190
-
191
- Returns
192
- -------
193
- NDArray
194
- Weighted wedges.
195
- """
196
- tilt_shape = compute_tilt_shape(
197
- shape=shape, opening_axis=opening_axis, reduce_dim=True
198
- )
199
- wedges = np.zeros((len(self.angles), *tilt_shape))
200
- for index, angle in enumerate(self.angles):
201
- frequency_grid = frequency_grid_at_angle(
202
- shape=shape,
203
- opening_axis=opening_axis,
204
- tilt_axis=tilt_axis,
205
- angle=angle,
206
- sampling_rate=sampling_rate,
207
- )
208
- sigma = np.sqrt(self.weights[index] * 4 / (8 * np.pi**2))
209
- sigma = -2 * np.pi**2 * sigma**2
210
- frequency_grid = np.square(frequency_grid, out=frequency_grid)
211
- frequency_grid = np.multiply(sigma, frequency_grid, out=frequency_grid)
212
- frequency_grid = np.exp(frequency_grid, out=frequency_grid)
213
- wedges[index] = np.multiply(frequency_grid, np.cos(np.radians(angle)))
214
-
215
- return wedges
216
-
217
- def weight_grigorieff(
218
- self,
219
- shape: Tuple[int],
220
- opening_axis: int,
221
- tilt_axis: int,
222
- amplitude: float = 0.245,
223
- power: float = -1.665,
224
- offset: float = 2.81,
225
- sampling_rate: float = 1.0,
226
- **kwargs,
227
- ) -> NDArray:
228
- """
229
- Generate weighted wedges based on the formalism introduced in [1]_.
230
-
231
- Returns
232
- -------
233
- NDArray
234
- Weighted wedges.
235
-
236
- References
237
- ----------
238
- .. [1] Timothy Grant, Nikolaus Grigorieff (2015), eLife 4:e06980.
239
- """
240
- tilt_shape = compute_tilt_shape(
241
- shape=shape, opening_axis=opening_axis, reduce_dim=True
242
- )
243
-
244
- wedges = np.zeros((len(self.angles), *tilt_shape), dtype=be._float_dtype)
245
- for index, angle in enumerate(self.angles):
246
- frequency_grid = frequency_grid_at_angle(
247
- shape=shape,
248
- opening_axis=opening_axis,
249
- tilt_axis=tilt_axis,
250
- angle=angle,
251
- sampling_rate=sampling_rate,
252
- )
253
-
254
- with np.errstate(divide="ignore"):
255
- np.power(frequency_grid, power, out=frequency_grid)
256
- np.multiply(amplitude, frequency_grid, out=frequency_grid)
257
- np.add(frequency_grid, offset, out=frequency_grid)
258
- np.multiply(-2, frequency_grid, out=frequency_grid)
259
- np.divide(
260
- self.weights[index],
261
- frequency_grid,
262
- out=frequency_grid,
263
- )
264
-
265
- wedges[index] = np.exp(frequency_grid)
266
-
267
- return wedges
138
+ return {"data": ret, "shape": shape}
268
139
 
269
140
 
270
141
  @dataclass
271
- class WedgeReconstructed:
142
+ class WedgeReconstructed(Wedge):
272
143
  """
273
- Initialize :py:class:`WedgeReconstructed`.
144
+ Create wedge mask for tomographic reconstructions.
274
145
  """
275
146
 
276
- #: The tilt angles, defaults to None.
147
+ #: Tilt angles in degrees.
277
148
  angles: Tuple[float] = None
278
149
  #: Weights to assign to individual wedge components. Not considered for continuous wedge
279
150
  weights: Tuple[float] = None
@@ -290,22 +161,14 @@ class WedgeReconstructed:
290
161
  #: Filter window applied during reconstruction.
291
162
  reconstruction_filter: str = None
292
163
 
293
- def __post_init__(self):
294
- if self.create_continuous_wedge:
295
- self.angles = (min(self.angles), max(self.angles))
296
-
297
- def __call__(
298
- self, shape: Tuple[int], return_real_fourier: bool = False, **kwargs
299
- ) -> Dict:
164
+ def _evaluate(self, shape: Tuple[int, ...], **kwargs) -> Dict:
300
165
  """
301
- Generate the reconstructed wedge.
166
+ Generate a reconstructed wedge.
302
167
 
303
168
  Parameters
304
169
  ----------
305
170
  shape : tuple of int
306
- The shape of the reconstruction volume.
307
- return_real_fourier : tuple of int
308
- Return a shape compliant with rfftn. Defaults to False.
171
+ The shape to build the filter for.
309
172
  **kwargs : dict
310
173
  Additional keyword arguments.
311
174
 
@@ -316,198 +179,306 @@ class WedgeReconstructed:
316
179
  The filter mask.
317
180
  shape: tuple of ints
318
181
  The requested filter shape
319
- return_real_fourier: bool
320
- Whether data is compliant with rfftn.
321
- is_multiplicative_filter: bool
322
- Whether the filter is multiplicative in Fourier space.
323
182
  """
324
- func_args = vars(self).copy()
325
- func_args.update(kwargs)
326
-
327
- func = self.step_wedge
328
- if func_args.get("create_continuous_wedge", False):
329
- func = self.continuous_wedge
330
-
331
- weight_wedge = func_args.get("weight_wedge", False)
332
- if func_args.get("wedge_weights") is None and weight_wedge:
333
- func_args["weights"] = np.cos(
334
- np.radians(be.to_numpy_array(func_args.get("angles", (0,))))
335
- )
336
-
337
- ret = func(shape=shape, **func_args)
338
- frequency_cutoff = func_args.get("frequency_cutoff", None)
183
+ func = step_wedge
184
+ angles = kwargs.pop("angles", (0,))
185
+ if kwargs.get("create_continuous_wedge", False):
186
+ func = continuous_wedge
187
+ if len(angles) != 2:
188
+ angles = (min(angles), max(angles))
189
+
190
+ weight_wedge = kwargs.get("weight_wedge", False)
191
+ if kwargs.get("wedge_weights") is None and weight_wedge:
192
+ kwargs["weights"] = np.cos(np.radians(be.to_numpy_array(angles)))
193
+ ret = func(shape=shape, angles=angles, **kwargs)
194
+
195
+ # Move DC component to origin
196
+ ret = shift_fourier(ret, shape_is_real_fourier=False)
197
+ frequency_cutoff = kwargs.get("frequency_cutoff", None)
339
198
  if frequency_cutoff is not None:
340
- frequency_mask = fftfreqn(
341
- shape=shape,
342
- sampling_rate=1,
343
- compute_euclidean_norm=True,
344
- shape_is_real_fourier=False,
199
+ frequency_mask = (
200
+ fftfreqn(
201
+ shape=shape,
202
+ sampling_rate=1,
203
+ compute_euclidean_norm=True,
204
+ shape_is_real_fourier=False,
205
+ fftshift=False,
206
+ )
207
+ <= frequency_cutoff
345
208
  )
346
- ret = np.multiply(ret, frequency_mask <= frequency_cutoff, out=ret)
209
+ ret = np.multiply(ret, frequency_mask, out=ret)
347
210
 
348
211
  if not weight_wedge:
349
212
  ret = (ret > 0) * 1.0
350
-
351
213
  ret = be.astype(be.to_backend_array(ret), be._float_dtype)
214
+ return {"data": ret, "shape": shape}
352
215
 
353
- ret = shift_fourier(data=ret, shape_is_real_fourier=False)
354
216
 
355
- if return_real_fourier:
356
- ret = crop_real_fourier(ret)
217
+ def continuous_wedge(
218
+ shape: Tuple[int, ...],
219
+ angles: Tuple[float, float],
220
+ opening_axis: int,
221
+ tilt_axis: int,
222
+ **kwargs: Dict,
223
+ ) -> NDArray:
224
+ """
225
+ Generate a continous wedge mask with DC component at the center.
357
226
 
358
- return {
359
- "data": ret,
360
- "shape": shape,
361
- "return_real_fourier": return_real_fourier,
362
- "is_multiplicative_filter": True,
363
- }
227
+ Parameters
228
+ ----------
229
+ shape : tuple of int
230
+ The shape of the reconstruction volume.
231
+ angles : tuple of float
232
+ Start and stop tilt angle in degrees.
233
+ opening_axis : int
234
+ The axis around which the wedge is opened.
235
+ tilt_axis : int
236
+ The axis along which the tilt is applied.
364
237
 
365
- @staticmethod
366
- def continuous_wedge(
367
- shape: Tuple[int],
368
- angles: Tuple[float],
369
- opening_axis: int,
370
- tilt_axis: int,
371
- **kwargs: Dict,
372
- ) -> NDArray:
373
- """
374
- Generate a continous wedge mask with DC component at the center.
238
+ Returns
239
+ -------
240
+ NDArray
241
+ Wedge mask.
242
+ """
243
+ angles = np.abs(np.asarray(angles))
244
+ aspect_ratio = shape[opening_axis] / shape[tilt_axis]
245
+ angles = np.degrees(np.arctan(np.tan(np.radians(angles)) * aspect_ratio))
246
+
247
+ start_radians = np.tan(np.radians(90 - angles[0]))
248
+ stop_radians = np.tan(np.radians(-1 * (90 - angles[1])))
249
+
250
+ grid = centered_grid(shape)
251
+ with np.errstate(divide="ignore", invalid="ignore"):
252
+ ratios = np.where(
253
+ grid[opening_axis] == 0,
254
+ np.tan(np.radians(90)) + 1,
255
+ grid[tilt_axis] / grid[opening_axis],
256
+ )
375
257
 
376
- Parameters
377
- ----------
378
- shape : tuple of int
379
- The shape of the reconstruction volume.
380
- angles : tuple of float
381
- Start and stop tilt angle.
382
- opening_axis : int
383
- The axis around which the wedge is opened.
384
- tilt_axis : int
385
- The axis along which the tilt is applied.
258
+ wedge = np.logical_or(start_radians <= ratios, stop_radians >= ratios).astype(
259
+ np.float32
260
+ )
386
261
 
387
- Returns
388
- -------
389
- NDArray
390
- Wedge mask.
391
- """
392
- angles = np.abs(np.asarray(angles))
393
- aspect_ratio = shape[opening_axis] / shape[tilt_axis]
394
- angles = np.degrees(np.arctan(np.tan(np.radians(angles)) * aspect_ratio))
395
-
396
- start_radians = np.tan(np.radians(90 - angles[0]))
397
- stop_radians = np.tan(np.radians(-1 * (90 - angles[1])))
398
-
399
- grid = centered_grid(shape)
400
- with np.errstate(divide="ignore", invalid="ignore"):
401
- ratios = np.where(
402
- grid[opening_axis] == 0,
403
- np.tan(np.radians(90)) + 1,
404
- grid[tilt_axis] / grid[opening_axis],
405
- )
262
+ return wedge
406
263
 
407
- wedge = np.logical_or(start_radians <= ratios, stop_radians >= ratios).astype(
408
- np.float32
409
- )
410
264
 
411
- return wedge
412
-
413
- @staticmethod
414
- def step_wedge(
415
- shape: Tuple[int],
416
- angles: Tuple[float],
417
- opening_axis: int,
418
- tilt_axis: int,
419
- weights: Tuple[float] = None,
420
- reconstruction_filter: str = None,
421
- **kwargs: Dict,
422
- ) -> NDArray:
423
- """
424
- Generate a per-angle wedge shape with DC component at the center.
265
+ def step_wedge(
266
+ shape: Tuple[int, ...],
267
+ angles: Tuple[float, ...],
268
+ opening_axis: int,
269
+ tilt_axis: int,
270
+ weights: Tuple[float, ...] = None,
271
+ reconstruction_filter: str = None,
272
+ **kwargs: Dict,
273
+ ) -> NDArray:
274
+ """
275
+ Generate a per-angle wedge shape with DC component at the center.
425
276
 
426
- Parameters
427
- ----------
428
- shape : tuple of int
429
- The shape of the reconstruction volume.
430
- angles : tuple of float
431
- The tilt angles.
432
- opening_axis : int
433
- The axis around which the wedge is opened.
434
- tilt_axis : int
435
- The axis along which the tilt is applied.
436
- reconstruction_filter : str
437
- Filter used during reconstruction.
438
- weights : tuple of float, optional
439
- Weights to assign to individual tilts. Defaults to 1.
277
+ Parameters
278
+ ----------
279
+ shape : tuple of int
280
+ The shape of the reconstruction volume.
281
+ angles : tuple of float
282
+ The tilt angles in degrees.
283
+ opening_axis : int
284
+ The axis around which the wedge is opened.
285
+ tilt_axis : int
286
+ The axis along which the tilt is applied.
287
+ reconstruction_filter : str
288
+ Filter used during reconstruction.
289
+ weights : tuple of float, optional
290
+ Weights to assign to individual tilts. Defaults to 1.
440
291
 
441
- Returns
442
- -------
443
- NDArray
444
- Wege mask.
445
- """
446
- from ..backends import NumpyFFTWBackend
292
+ Returns
293
+ -------
294
+ NDArray
295
+ Wege mask.
296
+ """
297
+ from ..backends import NumpyFFTWBackend
298
+
299
+ angles = np.asarray(be.to_numpy_array(angles))
300
+
301
+ if weights is None:
302
+ weights = np.ones(angles.size)
303
+ weights = np.asarray(weights)
304
+
305
+ shape = tuple(int(x) for x in shape)
306
+ opening_axis, tilt_axis = int(opening_axis), int(tilt_axis)
307
+
308
+ weights = np.repeat(weights, angles.size // weights.size)
309
+ plane = np.zeros(
310
+ (shape[opening_axis], shape[tilt_axis] + (1 - shape[tilt_axis] % 2)),
311
+ dtype=np.float32,
312
+ )
313
+
314
+ aspect_ratio = plane.shape[0] / plane.shape[1]
315
+ angles = np.degrees(np.arctan(np.tan(np.radians(angles)) * aspect_ratio))
316
+
317
+ rec_filter = 1
318
+ if reconstruction_filter is not None:
319
+ rec_filter = create_reconstruction_filter(
320
+ plane.shape[::-1], filter_type=reconstruction_filter, tilt_angles=angles
321
+ ).T
322
+
323
+ subset = tuple(
324
+ slice(None) if i != 0 else slice(x // 2, x // 2 + 1)
325
+ for i, x in enumerate(plane.shape)
326
+ )
327
+ plane_rotated, wedge_volume = np.zeros_like(plane), np.zeros_like(plane)
328
+ for index in range(angles.shape[0]):
329
+ plane_rotated.fill(0)
330
+ plane[subset] = 1
331
+
332
+ angle_rad = np.radians(angles[index])
333
+ rotation_matrix = np.array(
334
+ [
335
+ [np.cos(angle_rad), -np.sin(angle_rad)],
336
+ [np.sin(angle_rad), np.cos(angle_rad)],
337
+ ]
338
+ )
339
+ # We want a push rotation but rigid transform assumes pull
340
+ NumpyFFTWBackend().rigid_transform(
341
+ arr=plane * rec_filter,
342
+ rotation_matrix=rotation_matrix.T,
343
+ out=plane_rotated,
344
+ use_geometric_center=True,
345
+ order=1,
346
+ )
347
+ wedge_volume += plane_rotated * weights[index]
447
348
 
448
- angles = np.asarray(be.to_numpy_array(angles))
349
+ subset = center_slice(wedge_volume.shape, (shape[opening_axis], shape[tilt_axis]))
350
+ wedge_volume = wedge_volume[subset]
449
351
 
450
- if weights is None:
451
- weights = np.ones(angles.size)
452
- weights = np.asarray(weights)
352
+ np.fmin(wedge_volume, np.max(weights), wedge_volume)
453
353
 
454
- shape = tuple(int(x) for x in shape)
455
- opening_axis, tilt_axis = int(opening_axis), int(tilt_axis)
354
+ if opening_axis > tilt_axis:
355
+ wedge_volume = np.moveaxis(wedge_volume, 1, 0)
456
356
 
457
- weights = np.repeat(weights, angles.size // weights.size)
458
- plane = np.zeros(
459
- (shape[opening_axis], shape[tilt_axis] + (1 - shape[tilt_axis] % 2)),
460
- dtype=np.float32,
461
- )
357
+ reshape_dimensions = tuple(
358
+ x if i in (opening_axis, tilt_axis) else 1 for i, x in enumerate(shape)
359
+ )
462
360
 
463
- aspect_ratio = plane.shape[0] / plane.shape[1]
464
- angles = np.degrees(np.arctan(np.tan(np.radians(angles)) * aspect_ratio))
361
+ wedge_volume = wedge_volume.reshape(reshape_dimensions)
362
+ tile_dimensions = np.divide(shape, reshape_dimensions).astype(int)
363
+ return np.tile(wedge_volume, tile_dimensions)
465
364
 
466
- rec_filter = 1
467
- if reconstruction_filter is not None:
468
- rec_filter = create_reconstruction_filter(
469
- plane.shape[::-1], filter_type=reconstruction_filter, tilt_angles=angles
470
- ).T
471
365
 
472
- subset = tuple(
473
- slice(None) if i != 0 else slice(x // 2, x // 2 + 1)
474
- for i, x in enumerate(plane.shape)
475
- )
476
- plane_rotated, wedge_volume = np.zeros_like(plane), np.zeros_like(plane)
477
- for index in range(angles.shape[0]):
478
- plane_rotated.fill(0)
479
- plane[subset] = 1
480
- rotation_matrix = euler_to_rotationmatrix((angles[index], 0))
481
- rotation_matrix = rotation_matrix[np.ix_((0, 1), (0, 1))]
482
-
483
- NumpyFFTWBackend().rigid_transform(
484
- arr=plane * rec_filter,
485
- rotation_matrix=rotation_matrix,
486
- out=plane_rotated,
487
- use_geometric_center=True,
488
- order=1,
489
- )
490
- wedge_volume += plane_rotated * weights[index]
366
+ def weight_uniform(angles: Tuple[float, ...], *args, **kwargs) -> NDArray:
367
+ """
368
+ Generate uniform weighted wedges.
369
+ """
370
+ return weight_angle(angles=np.zeros_like(angles), *args, **kwargs)
491
371
 
492
- subset = center_slice(
493
- wedge_volume.shape, (shape[opening_axis], shape[tilt_axis])
494
- )
495
- wedge_volume = wedge_volume[subset]
496
372
 
497
- np.fmin(wedge_volume, np.max(weights), wedge_volume)
373
+ def weight_angle(
374
+ shape: Tuple[int, ...],
375
+ angles: Tuple[float, ...],
376
+ opening_axis: int,
377
+ tilt_axis: int,
378
+ **kwargs,
379
+ ) -> NDArray:
380
+ """
381
+ Generate weighted wedges based on the cosine of the current angle.
382
+ """
383
+ tilt_shape = compute_tilt_shape(
384
+ shape=shape, opening_axis=opening_axis, reduce_dim=True
385
+ )
386
+ wedges = np.zeros((len(angles), *tilt_shape))
387
+ for index, angle in enumerate(angles):
388
+ wedges[index] = np.cos(np.radians(angle))
389
+ return wedges
390
+
391
+
392
+ def weight_relion(
393
+ shape: Tuple[int, ...],
394
+ angles: Tuple[float, ...],
395
+ weights: Tuple[float, ...],
396
+ opening_axis: int,
397
+ tilt_axis: int,
398
+ sampling_rate: float = 1.0,
399
+ **kwargs,
400
+ ) -> NDArray:
401
+ """
402
+ Generate weighted wedges based on the RELION 1.4 formalism, weighting each
403
+ angle using the cosine of the angle and a Gaussian lowpass filter computed
404
+ with respect to the exposure per angstrom.
498
405
 
499
- if opening_axis > tilt_axis:
500
- wedge_volume = np.moveaxis(wedge_volume, 1, 0)
406
+ Returns
407
+ -------
408
+ NDArray
409
+ Weighted wedges.
410
+ """
411
+ tilt_shape = compute_tilt_shape(
412
+ shape=shape, opening_axis=opening_axis, reduce_dim=True
413
+ )
414
+ wedges = np.zeros((len(angles), *tilt_shape))
415
+ for index, angle in enumerate(angles):
416
+ frequency_grid = frequency_grid_at_angle(
417
+ shape=shape,
418
+ opening_axis=opening_axis,
419
+ tilt_axis=tilt_axis,
420
+ angle=angle,
421
+ sampling_rate=sampling_rate,
422
+ fftshift=False,
423
+ )
424
+ sigma = np.sqrt(weights[index] * 4 / (8 * np.pi**2))
425
+ sigma = -2 * np.pi**2 * sigma**2
426
+ frequency_grid = np.square(frequency_grid, out=frequency_grid)
427
+ frequency_grid = np.multiply(sigma, frequency_grid, out=frequency_grid)
428
+ frequency_grid = np.exp(frequency_grid, out=frequency_grid)
429
+ wedges[index] = np.multiply(frequency_grid, np.cos(np.radians(angle)))
430
+
431
+ return wedges
432
+
433
+
434
+ def weight_grigorieff(
435
+ shape: Tuple[int, ...],
436
+ angles: Tuple[float, ...],
437
+ weights: Tuple[float, ...],
438
+ opening_axis: int,
439
+ tilt_axis: int,
440
+ amplitude: float = 0.245,
441
+ power: float = -1.665,
442
+ offset: float = 2.81,
443
+ sampling_rate: float = 1.0,
444
+ **kwargs,
445
+ ) -> NDArray:
446
+ """
447
+ Generate weighted wedges based on the formalism introduced in [1]_.
501
448
 
502
- reshape_dimensions = tuple(
503
- x if i in (opening_axis, tilt_axis) else 1 for i, x in enumerate(shape)
449
+ Returns
450
+ -------
451
+ NDArray
452
+ Weighted wedges.
453
+
454
+ References
455
+ ----------
456
+ .. [1] Timothy Grant, Nikolaus Grigorieff (2015), eLife 4:e06980.
457
+ """
458
+ tilt_shape = compute_tilt_shape(
459
+ shape=shape, opening_axis=opening_axis, reduce_dim=True
460
+ )
461
+
462
+ wedges = np.zeros((len(angles), *tilt_shape), dtype=be._float_dtype)
463
+ for index, angle in enumerate(angles):
464
+ frequency_grid = frequency_grid_at_angle(
465
+ shape=shape,
466
+ opening_axis=opening_axis,
467
+ tilt_axis=tilt_axis,
468
+ angle=angle,
469
+ sampling_rate=sampling_rate,
470
+ fftshift=False,
504
471
  )
505
472
 
506
- wedge_volume = wedge_volume.reshape(reshape_dimensions)
507
- tile_dimensions = np.divide(shape, reshape_dimensions).astype(int)
508
- wedge_volume = np.tile(wedge_volume, tile_dimensions)
473
+ with np.errstate(divide="ignore"):
474
+ np.power(frequency_grid, power, out=frequency_grid)
475
+ np.multiply(amplitude, frequency_grid, out=frequency_grid)
476
+ np.add(frequency_grid, offset, out=frequency_grid)
477
+ np.multiply(-2, frequency_grid, out=frequency_grid)
478
+ np.divide(weights[index], frequency_grid, out=frequency_grid)
479
+ wedges[index] = np.exp(frequency_grid)
509
480
 
510
- return wedge_volume
481
+ return wedges
511
482
 
512
483
 
513
484
  def _from_xml(filename: str, **kwargs) -> Dict: