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
@@ -0,0 +1,763 @@
1
+ """ Backend using numpy and pyFFTW for template matching.
2
+
3
+ Copyright (c) 2023 European Molecular Biology Laboratory
4
+
5
+ Author: Valentin Maurer <valentin.maurer@embl-hamburg.de>
6
+ """
7
+
8
+ from typing import Tuple, Dict, List
9
+ from multiprocessing import shared_memory
10
+ from multiprocessing.managers import SharedMemoryManager
11
+ from contextlib import contextmanager
12
+
13
+ import numpy as np
14
+ from psutil import virtual_memory
15
+ from numpy.typing import NDArray
16
+ from pyfftw import zeros_aligned, simd_alignment, FFTW, next_fast_len
17
+ from pyfftw.builders import rfftn as rfftn_builder, irfftn as irfftn_builder
18
+ from scipy.ndimage import maximum_filter, affine_transform
19
+
20
+ from .matching_backend import MatchingBackend
21
+ from ..matching_utils import rigid_transform
22
+
23
+
24
+ class NumpyFFTWBackend(MatchingBackend):
25
+ """
26
+ A numpy and pyfftw based backend for template matching.
27
+ """
28
+
29
+ def __init__(
30
+ self,
31
+ array_backend=np,
32
+ default_dtype=np.float32,
33
+ complex_dtype=np.complex64,
34
+ default_dtype_int=np.int32,
35
+ **kwargs,
36
+ ):
37
+ super().__init__(
38
+ array_backend=array_backend,
39
+ default_dtype=default_dtype,
40
+ complex_dtype=complex_dtype,
41
+ default_dtype_int=np.int32,
42
+ )
43
+ self.affine_transform = affine_transform
44
+
45
+ def to_backend_array(self, arr: NDArray) -> NDArray:
46
+ if isinstance(arr, self._array_backend.ndarray):
47
+ return arr
48
+ return self._array_backend.asarray(arr)
49
+
50
+ def to_numpy_array(self, arr: NDArray) -> NDArray:
51
+ return arr
52
+
53
+ def to_cpu_array(self, arr: NDArray) -> NDArray:
54
+ return arr
55
+
56
+ def free_cache(self):
57
+ pass
58
+
59
+ def add(self, x1, x2, *args, **kwargs) -> NDArray:
60
+ x1 = self.to_backend_array(x1)
61
+ x2 = self.to_backend_array(x2)
62
+ return self._array_backend.add(x1, x2, *args, **kwargs)
63
+
64
+ def subtract(self, x1, x2, *args, **kwargs) -> NDArray:
65
+ x1 = self.to_backend_array(x1)
66
+ x2 = self.to_backend_array(x2)
67
+ return self._array_backend.subtract(x1, x2, *args, **kwargs)
68
+
69
+ def multiply(self, x1, x2, *args, **kwargs) -> NDArray:
70
+ x1 = self.to_backend_array(x1)
71
+ x2 = self.to_backend_array(x2)
72
+ return self._array_backend.multiply(x1, x2, *args, **kwargs)
73
+
74
+ def divide(self, x1, x2, *args, **kwargs) -> NDArray:
75
+ x1 = self.to_backend_array(x1)
76
+ x2 = self.to_backend_array(x2)
77
+ return self._array_backend.divide(x1, x2, *args, **kwargs)
78
+
79
+ def mod(self, x1, x2, *args, **kwargs):
80
+ x1 = self.to_backend_array(x1)
81
+ x2 = self.to_backend_array(x2)
82
+ return self._array_backend.mod(x1, x2, *args, **kwargs)
83
+
84
+ def sum(self, *args, **kwargs) -> NDArray:
85
+ return self._array_backend.sum(*args, **kwargs)
86
+
87
+ def einsum(self, *args, **kwargs) -> NDArray:
88
+ return self._array_backend.einsum(*args, **kwargs)
89
+
90
+ def mean(self, *args, **kwargs) -> NDArray:
91
+ return self._array_backend.mean(*args, **kwargs)
92
+
93
+ def std(self, *args, **kwargs) -> NDArray:
94
+ return self._array_backend.std(*args, **kwargs)
95
+
96
+ def max(self, *args, **kwargs) -> NDArray:
97
+ return self._array_backend.max(*args, **kwargs)
98
+
99
+ def min(self, *args, **kwargs) -> NDArray:
100
+ return self._array_backend.min(*args, **kwargs)
101
+
102
+ def maximum(self, x1, x2, *args, **kwargs) -> NDArray:
103
+ x1 = self.to_backend_array(x1)
104
+ x2 = self.to_backend_array(x2)
105
+ return self._array_backend.maximum(x1, x2, *args, **kwargs)
106
+
107
+ def minimum(self, x1, x2, *args, **kwargs) -> NDArray:
108
+ x1 = self.to_backend_array(x1)
109
+ x2 = self.to_backend_array(x2)
110
+ return self._array_backend.minimum(x1, x2, *args, **kwargs)
111
+
112
+ def sqrt(self, *args, **kwargs) -> NDArray:
113
+ return self._array_backend.sqrt(*args, **kwargs)
114
+
115
+ def square(self, *args, **kwargs) -> NDArray:
116
+ return self._array_backend.square(*args, **kwargs)
117
+
118
+ def abs(self, *args, **kwargs) -> NDArray:
119
+ return self._array_backend.abs(*args, **kwargs)
120
+
121
+ def transpose(self, arr):
122
+ return arr.T
123
+
124
+ def power(self, *args, **kwargs):
125
+ return self._array_backend.power(*args, **kwargs)
126
+
127
+ def tobytes(self, arr):
128
+ return arr.tobytes()
129
+
130
+ def size(self, arr):
131
+ return arr.size
132
+
133
+ def fill(self, arr: NDArray, value: float) -> None:
134
+ arr.fill(value)
135
+
136
+ def zeros(self, shape, dtype=np.float64) -> NDArray:
137
+ return self._array_backend.zeros(shape=shape, dtype=dtype)
138
+
139
+ def full(self, shape, fill_value, dtype=None, **kwargs) -> NDArray:
140
+ return self._array_backend.full(
141
+ shape, dtype=dtype, fill_value=fill_value, **kwargs
142
+ )
143
+
144
+ def eps(self, dtype: type) -> NDArray:
145
+ """
146
+ Returns the eps defined as diffeerence between 1.0 and the next
147
+ representable floating point value larger than 1.0.
148
+
149
+ Parameters
150
+ ----------
151
+ dtype : type
152
+ Data type for which eps should be returned.
153
+
154
+ Returns
155
+ -------
156
+ Scalar
157
+ The eps for the given data type
158
+ """
159
+ return self._array_backend.finfo(dtype).eps
160
+
161
+ def datatype_bytes(self, dtype: type) -> NDArray:
162
+ """
163
+ Return the number of bytes occupied by a given datatype.
164
+
165
+ Parameters
166
+ ----------
167
+ dtype : type
168
+ Datatype for which the number of bytes is to be determined.
169
+
170
+ Returns
171
+ -------
172
+ int
173
+ Number of bytes occupied by the datatype.
174
+ """
175
+ temp = self._array_backend.zeros(1, dtype=dtype)
176
+ return temp.nbytes
177
+
178
+ def clip(self, *args, **kwargs) -> NDArray:
179
+ return self._array_backend.clip(*args, **kwargs)
180
+
181
+ def flip(self, a, axis, **kwargs):
182
+ return self._array_backend.flip(a, axis, **kwargs)
183
+
184
+ @staticmethod
185
+ def astype(arr, dtype):
186
+ return arr.astype(dtype)
187
+
188
+ def arange(self, *args, **kwargs):
189
+ return self._array_backend.arange(*args, **kwargs)
190
+
191
+ def stack(self, *args, **kwargs):
192
+ return self._array_backend.stack(*args, **kwargs)
193
+
194
+ def concatenate(self, *args, **kwargs):
195
+ return self._array_backend.concatenate(*args, **kwargs)
196
+
197
+ def repeat(self, *args, **kwargs):
198
+ return self._array_backend.repeat(*args, **kwargs)
199
+
200
+ def topk_indices(self, arr: NDArray, k: int):
201
+ temp = arr.reshape(-1)
202
+ indices = self._array_backend.argpartition(temp, -k)[-k:][:k]
203
+ sorted_indices = indices[self._array_backend.argsort(temp[indices])][::-1]
204
+ sorted_indices = self.unravel_index(indices=sorted_indices, shape=arr.shape)
205
+ return sorted_indices
206
+
207
+ def indices(self, *args, **kwargs) -> NDArray:
208
+ return self._array_backend.indices(*args, **kwargs)
209
+
210
+ def roll(self, a, shift, axis, **kwargs):
211
+ return self._array_backend.roll(
212
+ a,
213
+ shift=shift,
214
+ axis=axis,
215
+ **kwargs,
216
+ )
217
+
218
+ def unique(self, *args, **kwargs):
219
+ return self._array_backend.unique(*args, **kwargs)
220
+
221
+ def argsort(self, *args, **kwargs):
222
+ return self._array_backend.argsort(*args, **kwargs)
223
+
224
+ def unravel_index(self, indices, shape):
225
+ return self._array_backend.unravel_index(indices=indices, shape=shape)
226
+
227
+ def tril_indices(self, *args, **kwargs):
228
+ return self._array_backend.tril_indices(*args, **kwargs)
229
+
230
+ def max_filter_coordinates(self, score_space, min_distance: Tuple[int]):
231
+ score_box = tuple(min_distance for _ in range(score_space.ndim))
232
+ max_filter = maximum_filter(score_space, size=score_box, mode="constant")
233
+ max_filter = max_filter == score_space
234
+
235
+ peaks = np.array(np.nonzero(max_filter)).T
236
+ return peaks
237
+
238
+ @staticmethod
239
+ def preallocate_array(shape: Tuple[int], dtype: type) -> NDArray:
240
+ """
241
+ Returns a byte-aligned array of zeros with specified shape and dtype.
242
+
243
+ Parameters
244
+ ----------
245
+ shape : Tuple[int]
246
+ Desired shape for the array.
247
+ dtype : type
248
+ Desired data type for the array.
249
+
250
+ Returns
251
+ -------
252
+ NDArray
253
+ Byte-aligned array of zeros with specified shape and dtype.
254
+ """
255
+ arr = zeros_aligned(shape, dtype=dtype, n=simd_alignment)
256
+ return arr
257
+
258
+ def sharedarr_to_arr(
259
+ self, shape: Tuple[int], dtype: str, shm: shared_memory.SharedMemory
260
+ ) -> NDArray:
261
+ """
262
+ Returns an array of given shape and dtype from shared memory location.
263
+
264
+ Parameters
265
+ ----------
266
+ shape : tuple
267
+ Tuple of integers specifying the shape of the array.
268
+ dtype : str
269
+ String specifying the dtype of the array.
270
+ shm : shared_memory.SharedMemory
271
+ Shared memory object where the array is stored.
272
+
273
+ Returns
274
+ -------
275
+ NDArray
276
+ Array of the specified shape and dtype from the shared memory location.
277
+ """
278
+ return self.ndarray(shape, dtype, shm.buf)
279
+
280
+ def arr_to_sharedarr(
281
+ self, arr: NDArray, shared_memory_handler: type = None
282
+ ) -> shared_memory.SharedMemory:
283
+ """
284
+ Converts a numpy array to an object shared in memory.
285
+
286
+ Parameters
287
+ ----------
288
+ arr : NDArray
289
+ Numpy array to convert.
290
+ shared_memory_handler : type, optional
291
+ The type of shared memory handler. Default is None.
292
+
293
+ Returns
294
+ -------
295
+ shared_memory.SharedMemory
296
+ The shared memory object containing the numpy array.
297
+ """
298
+ if type(shared_memory_handler) == SharedMemoryManager:
299
+ shm = shared_memory_handler.SharedMemory(size=arr.nbytes)
300
+ else:
301
+ shm = shared_memory.SharedMemory(create=True, size=arr.nbytes)
302
+ np_array = self.ndarray(arr.shape, dtype=arr.dtype, buffer=shm.buf)
303
+ np_array[:] = arr[:].copy()
304
+ return shm
305
+
306
+ def topleft_pad(self, arr: NDArray, shape: Tuple[int], padval: int = 0) -> NDArray:
307
+ """
308
+ Returns an array that has been padded to a specified shape with a padding
309
+ value at the top-left corner.
310
+
311
+ Parameters
312
+ ----------
313
+ arr : NDArray
314
+ Input array to be padded.
315
+ shape : Tuple[int]
316
+ Desired shape for the output array.
317
+ padval : int, optional
318
+ Value to use for padding, default is 0.
319
+
320
+ Returns
321
+ -------
322
+ NDArray
323
+ Array that has been padded to the specified shape.
324
+ """
325
+ b = self.preallocate_array(shape, arr.dtype)
326
+ self.add(b, padval, out=b)
327
+ aind = [slice(None, None)] * arr.ndim
328
+ bind = [slice(None, None)] * arr.ndim
329
+ for i in range(arr.ndim):
330
+ if arr.shape[i] > shape[i]:
331
+ aind[i] = slice(0, shape[i])
332
+ elif arr.shape[i] < shape[i]:
333
+ bind[i] = slice(0, arr.shape[i])
334
+ b[tuple(bind)] = arr[tuple(aind)]
335
+ return b
336
+
337
+ def build_fft(
338
+ self,
339
+ fast_shape: Tuple[int],
340
+ fast_ft_shape: Tuple[int],
341
+ real_dtype: type,
342
+ complex_dtype: type,
343
+ fftargs: Dict = {},
344
+ temp_real: NDArray = None,
345
+ temp_fft: NDArray = None,
346
+ ) -> Tuple[FFTW, FFTW]:
347
+ """
348
+ Build pyFFTW builder functions.
349
+
350
+ Parameters
351
+ ----------
352
+ fast_shape : tuple
353
+ Tuple of integers corresponding to fast convolution shape
354
+ (see `compute_convolution_shapes`).
355
+ fast_ft_shape : tuple
356
+ Tuple of integers corresponding to the shape of the fourier
357
+ transform array (see `compute_convolution_shapes`).
358
+ real_dtype : dtype
359
+ Numpy dtype of the inverse fourier transform.
360
+ complex_dtype : dtype
361
+ Numpy dtype of the fourier transform.
362
+ fftargs : dict, optional
363
+ Dictionary passed to pyFFTW builders.
364
+ temp_real : NDArray, optional
365
+ Temporary real numpy array, by default None.
366
+ temp_fft : NDArray, optional
367
+ Temporary fft numpy array, by default None.
368
+
369
+ Returns
370
+ -------
371
+ tuple
372
+ Tuple containing callable pyFFTW objects for forward and inverse
373
+ fourier transform.
374
+ """
375
+ if temp_real is None:
376
+ temp_real = self.preallocate_array(fast_shape, real_dtype)
377
+ if temp_fft is None:
378
+ temp_fft = self.preallocate_array(fast_ft_shape, complex_dtype)
379
+
380
+ default_values = {
381
+ "planner_effort": "FFTW_MEASURE",
382
+ "auto_align_input": False,
383
+ "auto_contiguous": False,
384
+ "avoid_copy": True,
385
+ "overwrite_input": True,
386
+ "threads": 1,
387
+ }
388
+ for key in default_values:
389
+ if key in fftargs:
390
+ continue
391
+ fftargs[key] = default_values[key]
392
+
393
+ rfftn = rfftn_builder(temp_real, s=fast_shape, **fftargs)
394
+
395
+ overwrite_input = None
396
+ if "overwrite_input" in fftargs:
397
+ overwrite_input = fftargs.pop("overwrite_input")
398
+ irfftn = irfftn_builder(temp_fft, s=fast_shape, **fftargs)
399
+
400
+ if overwrite_input is not None:
401
+ fftargs["overwrite_input"] = overwrite_input
402
+ return rfftn, irfftn
403
+
404
+ def extract_center(self, arr: NDArray, newshape: Tuple[int]) -> NDArray:
405
+ """
406
+ Extract the centered portion of an array based on a new shape.
407
+
408
+ Parameters
409
+ ----------
410
+ arr : NDArray
411
+ Input array.
412
+ newshape : tuple
413
+ Desired shape for the central portion.
414
+
415
+ Returns
416
+ -------
417
+ NDArray
418
+ Central portion of the array with shape `newshape`.
419
+
420
+ References
421
+ ----------
422
+ .. [1] https://github.com/scipy/scipy/blob/v1.11.2/scipy/signal/_signaltools.py
423
+ """
424
+ new_shape = self.to_backend_array(newshape)
425
+ current_shape = self.to_backend_array(arr.shape)
426
+ starts = self.subtract(current_shape, new_shape)
427
+ starts = self.astype(self.divide(starts, 2), int)
428
+ stops = self.add(starts, newshape)
429
+ box = tuple(slice(start, stop) for start, stop in zip(starts, stops))
430
+ return arr[box]
431
+
432
+ def compute_convolution_shapes(
433
+ self, arr1_shape: Tuple[int], arr2_shape: Tuple[int]
434
+ ) -> Tuple[List[int], List[int], List[int]]:
435
+ """
436
+ Computes regular, optimized and fourier convolution shape.
437
+
438
+ Parameters
439
+ ----------
440
+ arr1_shape : tuple
441
+ Tuple of integers corresponding to array1 shape.
442
+ arr2_shape : tuple
443
+ Tuple of integers corresponding to array2 shape.
444
+
445
+ Returns
446
+ -------
447
+ tuple
448
+ Tuple with regular convolution shape, convolution shape optimized for faster
449
+ fourier transform, shape of the forward fourier transform
450
+ (see :py:meth:`build_fft`).
451
+ """
452
+ convolution_shape = [
453
+ int(x) + int(y) - 1 for x, y in zip(arr1_shape, arr2_shape)
454
+ ]
455
+ fast_shape = [next_fast_len(x) for x in convolution_shape]
456
+ fast_ft_shape = list(fast_shape[:-1]) + [fast_shape[-1] // 2 + 1]
457
+
458
+ return convolution_shape, fast_shape, fast_ft_shape
459
+
460
+ def rotate_array(
461
+ self,
462
+ arr: NDArray,
463
+ rotation_matrix: NDArray,
464
+ arr_mask: NDArray = None,
465
+ translation: NDArray = None,
466
+ use_geometric_center: bool = False,
467
+ out: NDArray = None,
468
+ out_mask: NDArray = None,
469
+ order: int = 3,
470
+ ) -> None:
471
+ """
472
+ Rotates coordinates of arr according to rotation_matrix.
473
+
474
+ If no output array is provided, this method will compute an array with
475
+ sufficient space to hold all elements. If both `arr` and `arr_mask`
476
+ are provided, `arr_mask` will be centered according to arr.
477
+
478
+ Parameters
479
+ ----------
480
+ arr : NDArray
481
+ The input array to be rotated.
482
+ arr_mask : NDArray, optional
483
+ The mask of `arr` that will be equivalently rotated.
484
+ rotation_matrix : NDArray
485
+ The rotation matrix to apply [d x d].
486
+ translation : NDArray
487
+ The translation to apply [d].
488
+ use_geometric_center : bool, optional
489
+ Whether the rotation should be centered around the geometric
490
+ or mass center. Default is mass center.
491
+ out : NDArray, optional
492
+ The output array to write the rotation of `arr` to.
493
+ out_mask : NDArray, optional
494
+ The output array to write the rotation of `arr_mask` to.
495
+ order : int, optional
496
+ Spline interpolation order. Has to be in the range 0-5.
497
+ """
498
+
499
+ if order is None:
500
+ mask_coordinates = None
501
+ if arr_mask is not None:
502
+ mask_coordinates = np.array(np.where(arr_mask > 0))
503
+ return self.rotate_array_coordinates(
504
+ arr=arr,
505
+ arr_mask=arr_mask,
506
+ coordinates=np.array(np.where(arr > 0)),
507
+ mask_coordinates=mask_coordinates,
508
+ out=out,
509
+ out_mask=out_mask,
510
+ rotation_matrix=rotation_matrix,
511
+ translation=translation,
512
+ use_geometric_center=use_geometric_center,
513
+ )
514
+
515
+ rotate_mask = arr_mask is not None
516
+ return_type = (out is None) + 2 * rotate_mask * (out_mask is None)
517
+ translation = np.zeros(arr.ndim) if translation is None else translation
518
+
519
+ center = np.divide(arr.shape, 2)
520
+ if not use_geometric_center:
521
+ center = self.center_of_mass(arr, cutoff=0)
522
+
523
+ rotation_matrix_inverted = np.linalg.inv(rotation_matrix)
524
+ transformed_center = rotation_matrix_inverted @ center.reshape(-1, 1)
525
+ transformed_center = transformed_center.reshape(-1)
526
+ base_offset = np.subtract(center, transformed_center)
527
+ offset = np.subtract(base_offset, translation)
528
+
529
+ out = np.zeros_like(arr) if out is None else out
530
+ out_slice = tuple(slice(0, stop) for stop in arr.shape)
531
+
532
+ # Applying the prefilter can cause artifacts in the mask
533
+ affine_transform(
534
+ input=arr,
535
+ matrix=rotation_matrix_inverted,
536
+ offset=offset,
537
+ mode="constant",
538
+ output=out[out_slice],
539
+ order=order,
540
+ prefilter=True,
541
+ )
542
+
543
+ if rotate_mask:
544
+ out_mask = np.zeros_like(arr_mask) if out_mask is None else out_mask
545
+ out_mask_slice = tuple(slice(0, stop) for stop in arr_mask.shape)
546
+ affine_transform(
547
+ input=arr_mask,
548
+ matrix=rotation_matrix_inverted,
549
+ offset=offset,
550
+ mode="constant",
551
+ output=out_mask[out_mask_slice],
552
+ order=order,
553
+ prefilter=False,
554
+ )
555
+
556
+ match return_type:
557
+ case 0:
558
+ return None
559
+ case 1:
560
+ return out
561
+ case 2:
562
+ return out_mask
563
+ case 3:
564
+ return out, out_mask
565
+
566
+ @staticmethod
567
+ def rotate_array_coordinates(
568
+ arr: NDArray,
569
+ coordinates: NDArray,
570
+ rotation_matrix: NDArray,
571
+ translation: NDArray = None,
572
+ out: NDArray = None,
573
+ use_geometric_center: bool = True,
574
+ arr_mask: NDArray = None,
575
+ mask_coordinates: NDArray = None,
576
+ out_mask: NDArray = None,
577
+ ) -> None:
578
+ """
579
+ Rotates coordinates of arr according to rotation_matrix.
580
+
581
+ If no output array is provided, this method will compute an array with
582
+ sufficient space to hold all elements. If both `arr` and `arr_mask`
583
+ are provided, `arr_mask` will be centered according to arr.
584
+
585
+ No centering will be performed if the rotation matrix is the identity matrix.
586
+
587
+ Parameters
588
+ ----------
589
+ arr : NDArray
590
+ The input array to be rotated.
591
+ coordinates : NDArray
592
+ The pointcloud [d x N] containing elements of `arr` that should be rotated.
593
+ See :py:meth:`Density.to_pointcloud` on how to obtain the coordinates.
594
+ rotation_matrix : NDArray
595
+ The rotation matrix to apply [d x d].
596
+ rotation_matrix : NDArray
597
+ The translation to apply [d].
598
+ out : NDArray, optional
599
+ The output array to write the rotation of `arr` to.
600
+ use_geometric_center : bool, optional
601
+ Whether the rotation should be centered around the geometric
602
+ or mass center.
603
+ arr_mask : NDArray, optional
604
+ The mask of `arr` that will be equivalently rotated.
605
+ mask_coordinates : NDArray, optional
606
+ Equivalent to `coordinates`, but containing elements of `arr_mask`
607
+ that should be rotated.
608
+ out_mask : NDArray, optional
609
+ The output array to write the rotation of `arr_mask` to.
610
+ """
611
+ rotate_mask = arr_mask is not None and mask_coordinates is not None
612
+ return_type = (out is None) + 2 * rotate_mask * (out_mask is None)
613
+
614
+ # Otherwise array might be slightly shifted by centering
615
+ if np.allclose(
616
+ rotation_matrix,
617
+ np.eye(rotation_matrix.shape[0], dtype=rotation_matrix.dtype),
618
+ ):
619
+ center_rotation = False
620
+
621
+ coordinates_rotated = np.empty(coordinates.shape, dtype=rotation_matrix.dtype)
622
+ mask_rotated = (
623
+ np.empty(mask_coordinates.shape, dtype=rotation_matrix.dtype)
624
+ if rotate_mask
625
+ else None
626
+ )
627
+
628
+ center = np.array(arr.shape) // 2 if use_geometric_center else None
629
+ if translation is None:
630
+ translation = np.zeros(coordinates_rotated.shape[0])
631
+
632
+ rigid_transform(
633
+ coordinates=coordinates,
634
+ coordinates_mask=mask_coordinates,
635
+ out=coordinates_rotated,
636
+ out_mask=mask_rotated,
637
+ rotation_matrix=rotation_matrix,
638
+ translation=translation,
639
+ use_geometric_center=use_geometric_center,
640
+ center=center,
641
+ )
642
+
643
+ coordinates_rotated = coordinates_rotated.astype(int)
644
+ offset = coordinates_rotated.min(axis=1)
645
+ np.multiply(offset, offset < 0, out=offset)
646
+ coordinates_rotated -= offset[:, None]
647
+
648
+ out_offset = np.zeros(
649
+ coordinates_rotated.shape[0], dtype=coordinates_rotated.dtype
650
+ )
651
+ if out is None:
652
+ out_offset = coordinates_rotated.min(axis=1)
653
+ coordinates_rotated -= out_offset[:, None]
654
+ out = np.zeros(coordinates_rotated.max(axis=1) + 1, dtype=arr.dtype)
655
+
656
+ if rotate_mask:
657
+ mask_rotated = mask_rotated.astype(int)
658
+ if out_mask is None:
659
+ mask_rotated -= out_offset[:, None]
660
+ out_mask = np.zeros(
661
+ coordinates_rotated.max(axis=1) + 1, dtype=arr.dtype
662
+ )
663
+
664
+ in_box = np.logical_and(
665
+ mask_rotated < np.array(out_mask.shape)[:, None],
666
+ mask_rotated >= 0,
667
+ ).min(axis=0)
668
+ out_of_box = np.invert(in_box).sum()
669
+ if out_of_box != 0:
670
+ print(
671
+ f"{out_of_box} elements out of bounds. Perhaps increase"
672
+ " *arr_mask* size."
673
+ )
674
+
675
+ mask_coordinates = tuple(mask_coordinates[:, in_box])
676
+ mask_rotated = tuple(mask_rotated[:, in_box])
677
+ np.add.at(out_mask, mask_rotated, arr_mask[mask_coordinates])
678
+
679
+ # Negative coordinates would be (mis)interpreted as reverse index
680
+ in_box = np.logical_and(
681
+ coordinates_rotated < np.array(out.shape)[:, None], coordinates_rotated >= 0
682
+ ).min(axis=0)
683
+ out_of_box = np.invert(in_box).sum()
684
+ if out_of_box != 0:
685
+ print(f"{out_of_box} elements out of bounds. Perhaps increase *out* size.")
686
+
687
+ coordinates = coordinates[:, in_box]
688
+ coordinates_rotated = coordinates_rotated[:, in_box]
689
+
690
+ coordinates = tuple(coordinates)
691
+ coordinates_rotated = tuple(coordinates_rotated)
692
+ np.add.at(out, coordinates_rotated, arr[coordinates])
693
+
694
+ match return_type:
695
+ case 0:
696
+ return None
697
+ case 1:
698
+ return out
699
+ case 2:
700
+ return out_mask
701
+ case 3:
702
+ return out, out_mask
703
+
704
+ def center_of_mass(self, arr: NDArray, cutoff: float = None) -> NDArray:
705
+ """
706
+ Computes the center of mass of a numpy ndarray instance using all available
707
+ elements. For template matching it typically makes sense to only input
708
+ positive densities.
709
+
710
+ Parameters
711
+ ----------
712
+ arr : NDArray
713
+ Array to compute the center of mass of.
714
+ cutoff : float, optional
715
+ Densities less than or equal to cutoff are nullified for center
716
+ of mass computation. By default considers all values.
717
+
718
+ Returns
719
+ -------
720
+ NDArray
721
+ Center of mass with shape (arr.ndim).
722
+ """
723
+ cutoff = arr.min() - 1 if cutoff is None else cutoff
724
+ arr = self._array_backend.where(arr > cutoff, arr, 0)
725
+ denominator = self.sum(arr)
726
+ grids = self._array_backend.ogrid[tuple(slice(0, i) for i in arr.shape)]
727
+ grids = [grid.astype(self._default_dtype) for grid in grids]
728
+
729
+ center_of_mass = self.array(
730
+ [
731
+ self.sum(self.multiply(arr, grids[dim])) / denominator
732
+ for dim in range(arr.ndim)
733
+ ]
734
+ )
735
+
736
+ return center_of_mass
737
+
738
+ def get_available_memory(self) -> int:
739
+ return virtual_memory().available
740
+
741
+ @contextmanager
742
+ def set_device(self, device_index: int):
743
+ yield None
744
+
745
+ def device_count(self) -> int:
746
+ return 1
747
+
748
+ @staticmethod
749
+ def reverse(arr: NDArray) -> NDArray:
750
+ """
751
+ Reverse the order of elements in an array along all its axes.
752
+
753
+ Parameters
754
+ ----------
755
+ arr : NDArray
756
+ Input array.
757
+
758
+ Returns
759
+ -------
760
+ NDArray
761
+ Reversed array.
762
+ """
763
+ return arr[(slice(None, None, -1),) * arr.ndim]