pytme 0.1.8__cp311-cp311-macosx_14_0_arm64.whl → 0.2.0__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 (42) hide show
  1. pytme-0.2.0.data/scripts/match_template.py +1019 -0
  2. pytme-0.2.0.data/scripts/postprocess.py +570 -0
  3. {pytme-0.1.8.data → pytme-0.2.0.data}/scripts/preprocessor_gui.py +244 -60
  4. {pytme-0.1.8.dist-info → pytme-0.2.0.dist-info}/METADATA +3 -1
  5. pytme-0.2.0.dist-info/RECORD +72 -0
  6. {pytme-0.1.8.dist-info → pytme-0.2.0.dist-info}/WHEEL +1 -1
  7. scripts/extract_candidates.py +218 -0
  8. scripts/match_template.py +459 -218
  9. pytme-0.1.8.data/scripts/match_template.py → scripts/match_template_filters.py +459 -218
  10. scripts/postprocess.py +380 -435
  11. scripts/preprocessor_gui.py +244 -60
  12. scripts/refine_matches.py +218 -0
  13. tme/__init__.py +2 -1
  14. tme/__version__.py +1 -1
  15. tme/analyzer.py +533 -78
  16. tme/backends/cupy_backend.py +80 -15
  17. tme/backends/npfftw_backend.py +35 -6
  18. tme/backends/pytorch_backend.py +15 -7
  19. tme/density.py +173 -78
  20. tme/extensions.cpython-311-darwin.so +0 -0
  21. tme/matching_constrained.py +195 -0
  22. tme/matching_data.py +78 -32
  23. tme/matching_exhaustive.py +369 -221
  24. tme/matching_memory.py +1 -0
  25. tme/matching_optimization.py +753 -649
  26. tme/matching_utils.py +152 -8
  27. tme/orientations.py +561 -0
  28. tme/preprocessing/__init__.py +2 -0
  29. tme/preprocessing/_utils.py +176 -0
  30. tme/preprocessing/composable_filter.py +30 -0
  31. tme/preprocessing/compose.py +52 -0
  32. tme/preprocessing/frequency_filters.py +322 -0
  33. tme/preprocessing/tilt_series.py +967 -0
  34. tme/preprocessor.py +35 -25
  35. tme/structure.py +2 -37
  36. pytme-0.1.8.data/scripts/postprocess.py +0 -625
  37. pytme-0.1.8.dist-info/RECORD +0 -61
  38. {pytme-0.1.8.data → pytme-0.2.0.data}/scripts/estimate_ram_usage.py +0 -0
  39. {pytme-0.1.8.data → pytme-0.2.0.data}/scripts/preprocess.py +0 -0
  40. {pytme-0.1.8.dist-info → pytme-0.2.0.dist-info}/LICENSE +0 -0
  41. {pytme-0.1.8.dist-info → pytme-0.2.0.dist-info}/entry_points.txt +0 -0
  42. {pytme-0.1.8.dist-info → pytme-0.2.0.dist-info}/top_level.txt +0 -0
tme/analyzer.py CHANGED
@@ -1,4 +1,4 @@
1
- """ Implements classes to analyze score spaces from systematic fitting.
1
+ """ Implements classes to analyze outputs from exhaustive template matching.
2
2
 
3
3
  Copyright (c) 2023 European Molecular Biology Laboratory
4
4
 
@@ -15,20 +15,22 @@ from numpy.typing import NDArray
15
15
  from scipy.stats import entropy
16
16
  from sklearn.cluster import DBSCAN
17
17
  from skimage.feature import peak_local_max
18
-
19
- from .extensions import max_index_by_label, online_statistics
18
+ from skimage.registration._phase_cross_correlation import _upsampled_dft
19
+ from .extensions import max_index_by_label, online_statistics, find_candidate_indices
20
20
  from .matching_utils import (
21
21
  split_numpy_array_slices,
22
22
  array_to_memmap,
23
23
  generate_tempfile_name,
24
+ euler_to_rotationmatrix,
25
+ apply_convolution_mode,
24
26
  )
25
-
26
27
  from .backends import backend
27
28
 
28
29
 
29
- def filter_points_indices(coordinates: NDArray, min_distance: Tuple[int]):
30
- if min_distance <= 0:
31
- return backend.arange(coordinates.shape[0])
30
+ def filter_points_indices_bucket(
31
+ coordinates: NDArray, min_distance: Tuple[int]
32
+ ) -> NDArray:
33
+ coordinates = backend.subtract(coordinates, backend.min(coordinates, axis=0))
32
34
  bucket_indices = backend.astype(backend.divide(coordinates, min_distance), int)
33
35
  multiplier = backend.power(
34
36
  backend.max(bucket_indices, axis=0) + 1, backend.arange(bucket_indices.shape[1])
@@ -40,7 +42,24 @@ def filter_points_indices(coordinates: NDArray, min_distance: Tuple[int]):
40
42
  return unique_indices
41
43
 
42
44
 
43
- def filter_points(coordinates: NDArray, min_distance: Tuple[int]):
45
+ def filter_points_indices(
46
+ coordinates: NDArray, min_distance: float, bucket_cutoff: int = 1e4
47
+ ) -> NDArray:
48
+ if min_distance <= 0:
49
+ return backend.arange(coordinates.shape[0])
50
+
51
+ if isinstance(coordinates, np.ndarray):
52
+ return find_candidate_indices(coordinates, min_distance)
53
+ elif coordinates.shape[0] > bucket_cutoff:
54
+ return filter_points_indices_bucket(coordinates, min_distance)
55
+ distances = np.linalg.norm(coordinates[:, None] - coordinates, axis=-1)
56
+ distances = np.tril(distances)
57
+ keep = np.sum(distances > min_distance, axis=1)
58
+ indices = np.arange(coordinates.shape[0])
59
+ return indices[keep == indices]
60
+
61
+
62
+ def filter_points(coordinates: NDArray, min_distance: Tuple[int]) -> NDArray:
44
63
  unique_indices = filter_points_indices(coordinates, min_distance)
45
64
  coordinates = coordinates[unique_indices]
46
65
  return coordinates
@@ -95,6 +114,12 @@ class PeakCaller(ABC):
95
114
  self.min_boundary_distance = min_boundary_distance
96
115
  self.number_of_peaks = number_of_peaks
97
116
 
117
+ # Postprocesing arguments
118
+ self.fourier_shift = kwargs.get("fourier_shift", None)
119
+ self.convolution_mode = kwargs.get("convolution_mode", None)
120
+ self.targetshape = kwargs.get("targetshape", None)
121
+ self.templateshape = kwargs.get("templateshape", None)
122
+
98
123
  def __iter__(self):
99
124
  """
100
125
  Returns a generator to list objects containing translation,
@@ -104,7 +129,10 @@ class PeakCaller(ABC):
104
129
  yield from self.peak_list
105
130
 
106
131
  def __call__(
107
- self, score_space: NDArray, rotation_matrix: NDArray, **kwargs
132
+ self,
133
+ score_space: NDArray,
134
+ rotation_matrix: NDArray,
135
+ **kwargs,
108
136
  ) -> None:
109
137
  """
110
138
  Update the internal parameter store based on input array.
@@ -116,7 +144,7 @@ class PeakCaller(ABC):
116
144
  rotation_matrix : NDArray
117
145
  Rotation matrix used to obtain the score array.
118
146
  **kwargs
119
- Additional keyword arguments.
147
+ Optional keyword arguments passed to :py:meth:`PeakCaller.call_peak`.
120
148
  """
121
149
  peak_positions, peak_details = self.call_peaks(
122
150
  score_space=score_space, rotation_matrix=rotation_matrix, **kwargs
@@ -159,11 +187,12 @@ class PeakCaller(ABC):
159
187
  peak_positions.shape[0],
160
188
  axis=0,
161
189
  )
190
+ peak_scores = score_space[tuple(peak_positions.T)]
162
191
 
163
192
  self._update(
164
193
  peak_positions=peak_positions,
165
194
  peak_details=peak_details,
166
- peak_scores=score_space[tuple(peak_positions.T)],
195
+ peak_scores=peak_scores,
167
196
  rotations=rotations,
168
197
  **kwargs,
169
198
  )
@@ -190,10 +219,8 @@ class PeakCaller(ABC):
190
219
 
191
220
  Returns
192
221
  -------
193
- NDArray
194
- Array of peak coordiantes.
195
- NDArray
196
- Array of peak details.
222
+ Tuple[NDArray, NDArray]
223
+ Array of peak coordinates and peak details.
197
224
  """
198
225
 
199
226
  @classmethod
@@ -231,6 +258,79 @@ class PeakCaller(ABC):
231
258
  )
232
259
  return tuple(base)
233
260
 
261
+ @staticmethod
262
+ def oversample_peaks(
263
+ score_space: NDArray, peak_positions: NDArray, oversampling_factor: int = 8
264
+ ):
265
+ """
266
+ Refines peaks positions in the corresponding score space.
267
+
268
+ Parameters
269
+ ----------
270
+ score_space : NDArray
271
+ The d-dimensional array representing the score space.
272
+ peak_positions : NDArray
273
+ An array of shape (n, d) containing the peak coordinates
274
+ to be refined, where n is the number of peaks and d is the
275
+ dimensionality of the score space.
276
+ oversampling_factor : int, optional
277
+ The oversampling factor for Fourier transforms. Defaults to 8.
278
+
279
+ Returns
280
+ -------
281
+ NDArray
282
+ An array of shape (n, d) containing the refined subpixel
283
+ coordinates of the peaks.
284
+
285
+ Notes
286
+ -----
287
+ Floating point peak positions are determined by oversampling the
288
+ score_space around peak_positions. The accuracy
289
+ of refinement scales with 1 / oversampling_factor.
290
+
291
+ References
292
+ ----------
293
+ .. [1] https://scikit-image.org/docs/stable/api/skimage.registration.html
294
+ .. [2] Manuel Guizar-Sicairos, Samuel T. Thurman, and
295
+ James R. Fienup, “Efficient subpixel image registration
296
+ algorithms,” Optics Letters 33, 156-158 (2008).
297
+ DOI:10.1364/OL.33.000156
298
+
299
+ """
300
+ score_space = backend.to_numpy_array(score_space)
301
+ peak_positions = backend.to_numpy_array(peak_positions)
302
+
303
+ peak_positions = np.round(
304
+ np.divide(
305
+ np.multiply(peak_positions, oversampling_factor), oversampling_factor
306
+ )
307
+ )
308
+ upsampled_region_size = np.ceil(np.multiply(oversampling_factor, 1.5))
309
+ dftshift = np.round(np.divide(upsampled_region_size, 2.0))
310
+ sample_region_offset = np.subtract(
311
+ dftshift, np.multiply(peak_positions, oversampling_factor)
312
+ )
313
+
314
+ score_space_ft = np.fft.fftn(score_space).conj()
315
+ for index in range(sample_region_offset.shape[0]):
316
+ cross_correlation_upsampled = _upsampled_dft(
317
+ data=score_space_ft,
318
+ upsampled_region_size=upsampled_region_size,
319
+ upsample_factor=oversampling_factor,
320
+ axis_offsets=sample_region_offset[index],
321
+ ).conj()
322
+
323
+ maxima = np.unravel_index(
324
+ np.argmax(np.abs(cross_correlation_upsampled)),
325
+ cross_correlation_upsampled.shape,
326
+ )
327
+ maxima = np.divide(np.subtract(maxima, dftshift), oversampling_factor)
328
+ peak_positions[index] = np.add(peak_positions[index], maxima)
329
+
330
+ peak_positions = backend.to_backend_array(peak_positions)
331
+
332
+ return peak_positions
333
+
234
334
  def _update(
235
335
  self,
236
336
  peak_positions: NDArray,
@@ -287,6 +387,61 @@ class PeakCaller(ABC):
287
387
  self.peak_list[2] = peak_scores[final_order]
288
388
  self.peak_list[3] = peak_details[final_order]
289
389
 
390
+ def _postprocess(
391
+ self, fourier_shift, convolution_mode, targetshape, templateshape, **kwargs
392
+ ):
393
+ peak_positions = self.peak_list[0]
394
+ if not len(peak_positions):
395
+ return self
396
+
397
+ if targetshape is None or templateshape is None:
398
+ return self
399
+
400
+ # Remove padding to next fast fourier length
401
+ score_space_shape = backend.add(targetshape, templateshape) - 1
402
+
403
+ if fourier_shift is not None:
404
+ peak_positions = backend.add(peak_positions, fourier_shift)
405
+ backend.divide(peak_positions, score_space_shape).astype(int)
406
+
407
+ backend.subtract(
408
+ peak_positions,
409
+ backend.multiply(
410
+ backend.divide(peak_positions, score_space_shape).astype(int),
411
+ score_space_shape,
412
+ ),
413
+ out=peak_positions,
414
+ )
415
+
416
+ if convolution_mode is None:
417
+ return None
418
+
419
+ if convolution_mode == "full":
420
+ output_shape = score_space_shape
421
+ elif convolution_mode == "same":
422
+ output_shape = targetshape
423
+ elif convolution_mode == "valid":
424
+ output_shape = backend.add(
425
+ backend.subtract(targetshape, templateshape),
426
+ backend.mod(templateshape, 2),
427
+ )
428
+
429
+ output_shape = backend.to_backend_array(output_shape)
430
+ starts = backend.divide(backend.subtract(score_space_shape, output_shape), 2)
431
+ starts = backend.astype(starts, int)
432
+ stops = backend.add(starts, output_shape)
433
+
434
+ valid_peaks = (
435
+ backend.sum(
436
+ backend.multiply(peak_positions > starts, peak_positions <= stops),
437
+ axis=1,
438
+ )
439
+ == peak_positions.shape[1]
440
+ )
441
+ self.peak_list[0] = backend.subtract(peak_positions, starts)
442
+ self.peak_list = [x[valid_peaks] for x in self.peak_list]
443
+ return self
444
+
290
445
 
291
446
  class PeakCallerSort(PeakCaller):
292
447
  """
@@ -297,7 +452,7 @@ class PeakCallerSort(PeakCaller):
297
452
  """
298
453
 
299
454
  def call_peaks(
300
- self, score_space: NDArray, rotation_matrix: NDArray, **kwargs
455
+ self, score_space: NDArray, minimum_score: float = None, **kwargs
301
456
  ) -> Tuple[NDArray, NDArray]:
302
457
  """
303
458
  Call peaks in the score space.
@@ -307,20 +462,20 @@ class PeakCallerSort(PeakCaller):
307
462
  score_space : NDArray
308
463
  Data array of scores.
309
464
  minimum_score : float
310
- Minimum score value to consider.
311
- min_distance : float
312
- Minimum distance between maxima.
465
+ Minimum score value to consider. If provided, superseeds limit given
466
+ by :py:attr:`PeakCaller.number_of_peaks`.
313
467
 
314
468
  Returns
315
469
  -------
316
- NDArray
317
- Array of peak coordiantes.
318
- NDArray
319
- Array of peak details.
470
+ Tuple[NDArray, NDArray]
471
+ Array of peak coordinates and peak details.
320
472
  """
321
473
  flat_score_space = score_space.reshape(-1)
322
474
  k = min(self.number_of_peaks, backend.size(flat_score_space))
323
475
 
476
+ if minimum_score is not None:
477
+ k = backend.sum(score_space >= minimum_score)
478
+
324
479
  top_k_indices, *_ = backend.topk_indices(flat_score_space, k)
325
480
 
326
481
  coordinates = backend.unravel_index(top_k_indices, score_space.shape)
@@ -333,12 +488,12 @@ class PeakCallerSort(PeakCaller):
333
488
  class PeakCallerMaximumFilter(PeakCaller):
334
489
  """
335
490
  Find local maxima by applying a maximum filter and enforcing a distance
336
- constraint subseqquently. This is similar to the strategy implemented in
491
+ constraint subsequently. This is similar to the strategy implemented in
337
492
  skimage.feature.peak_local_max.
338
493
  """
339
494
 
340
495
  def call_peaks(
341
- self, score_space: NDArray, rotation_matrix: NDArray, **kwargs
496
+ self, score_space: NDArray, minimum_score: float = None, **kwargs
342
497
  ) -> Tuple[NDArray, NDArray]:
343
498
  """
344
499
  Call peaks in the score space.
@@ -348,25 +503,25 @@ class PeakCallerMaximumFilter(PeakCaller):
348
503
  score_space : NDArray
349
504
  Data array of scores.
350
505
  minimum_score : float
351
- Minimum score value to consider.
352
- min_distance : float
353
- Minimum distance between maxima.
506
+ Minimum score value to consider. If provided, superseeds limit given
507
+ by :py:attr:`PeakCaller.number_of_peaks`.
354
508
 
355
509
  Returns
356
510
  -------
357
- NDArray
358
- Array of peak coordiantes.
359
- NDArray
360
- Array of peak details.
511
+ Tuple[NDArray, NDArray]
512
+ Array of peak coordinates and peak details.
361
513
  """
362
514
  peaks = backend.max_filter_coordinates(score_space, self.min_distance)
363
515
 
516
+ scores = score_space[tuple(peaks.T)]
517
+
364
518
  input_candidates = min(
365
519
  self.number_of_peaks, peaks.shape[0] - 1, backend.size(score_space) - 1
366
520
  )
367
- top_indices = backend.topk_indices(
368
- score_space[tuple(peaks.T)], input_candidates
369
- )
521
+ if minimum_score is not None:
522
+ input_candidates = backend.sum(scores >= minimum_score)
523
+
524
+ top_indices = backend.topk_indices(scores, input_candidates)
370
525
  peaks = peaks[top_indices]
371
526
 
372
527
  return peaks, None
@@ -382,7 +537,7 @@ class PeakCallerFast(PeakCaller):
382
537
  """
383
538
 
384
539
  def call_peaks(
385
- self, score_space: NDArray, rotation_matrix: NDArray, **kwargs
540
+ self, score_space: NDArray, minimum_score: float = None, **kwargs
386
541
  ) -> Tuple[NDArray, NDArray]:
387
542
  """
388
543
  Call peaks in the score space.
@@ -392,16 +547,13 @@ class PeakCallerFast(PeakCaller):
392
547
  score_space : NDArray
393
548
  Data array of scores.
394
549
  minimum_score : float
395
- Minimum score value to consider.
396
- min_distance : float
397
- Minimum distance between maxima.
550
+ Minimum score value to consider. If provided, superseeds limit given
551
+ by :py:attr:`PeakCaller.number_of_peaks`.
398
552
 
399
553
  Returns
400
554
  -------
401
- NDArray
402
- Array of peak coordiantes.
403
- NDArray
404
- Array of peak details.
555
+ Tuple[NDArray, NDArray]
556
+ Array of peak coordinates and peak details.
405
557
  """
406
558
  splits = {
407
559
  axis: score_space.shape[axis] // self.min_distance
@@ -457,7 +609,14 @@ class PeakCallerRecursiveMasking(PeakCaller):
457
609
  """
458
610
 
459
611
  def call_peaks(
460
- self, score_space: NDArray, rotation_matrix: NDArray, **kwargs
612
+ self,
613
+ score_space: NDArray,
614
+ rotation_matrix: NDArray,
615
+ mask: NDArray = None,
616
+ minimum_score: float = None,
617
+ rotation_space: NDArray = None,
618
+ rotation_mapping: Dict = None,
619
+ **kwargs,
461
620
  ) -> Tuple[NDArray, NDArray]:
462
621
  """
463
622
  Call peaks in the score space.
@@ -466,36 +625,195 @@ class PeakCallerRecursiveMasking(PeakCaller):
466
625
  ----------
467
626
  score_space : NDArray
468
627
  Data array of scores.
628
+ rotation_matrix : NDArray
629
+ Rotation matrix.
630
+ mask : NDArray, optional
631
+ Mask array, by default None.
632
+ rotation_space : NDArray, optional
633
+ Rotation space array, by default None.
634
+ rotation_mapping : Dict optional
635
+ Dictionary mapping values in rotation_space to Euler angles.
636
+ By default None
469
637
  minimum_score : float
470
- Minimum score value to consider.
471
- min_distance : float
472
- Minimum distance between maxima.
638
+ Minimum score value to consider. If provided, superseeds limit given
639
+ by :py:attr:`PeakCaller.number_of_peaks`.
473
640
 
474
641
  Returns
475
642
  -------
476
- NDArray
477
- Array of peak coordiantes.
478
- NDArray
479
- Array of peak details.
643
+ Tuple[NDArray, NDArray]
644
+ Array of peak coordinates and peak details.
645
+
646
+ Notes
647
+ -----
648
+ By default, scores are masked using a box with edge length self.min_distance.
649
+ If mask is provided, elements around each peak will be multiplied by the mask
650
+ values. If rotation_space and rotation_mapping is provided, the respective
651
+ rotation will be applied to the mask, otherwise rotation_matrix is used.
480
652
  """
481
- score_box = tuple(self.min_distance for _ in range(score_space.ndim))
482
- coordinates = []
653
+ coordinates, masking_function = [], self._mask_scores_rotate
654
+
655
+ if mask is None:
656
+ masking_function = self._mask_scores_box
657
+ shape = tuple(self.min_distance for _ in range(score_space.ndim))
658
+ mask = backend.zeros(shape, dtype=backend._default_dtype)
659
+
660
+ rotated_template = backend.zeros(mask.shape, dtype=mask.dtype)
661
+
662
+ peak_limit = self.number_of_peaks
663
+ if minimum_score is not None:
664
+ peak_limit = backend.size(score_space)
665
+ else:
666
+ minimum_score = backend.min(score_space) - 1
667
+
483
668
  while True:
484
669
  backend.argmax(score_space)
485
- max_coord = backend.unravel_index(
670
+ peak = backend.unravel_index(
486
671
  indices=backend.argmax(score_space), shape=score_space.shape
487
672
  )
488
- coordinates.append(max_coord)
489
- start = backend.maximum(backend.subtract(max_coord, score_box), 0)
490
- stop = backend.minimum(backend.add(max_coord, score_box), score_space.shape)
491
- start, stop = backend.astype(start, int), backend.astype(stop, int)
492
- coords = tuple(slice(*pos) for pos in zip(start, stop))
493
- score_space[coords] = 0
494
- if len(coordinates) >= self.number_of_peaks:
673
+ if score_space[tuple(peak)] < minimum_score:
674
+ break
675
+
676
+ coordinates.append(peak)
677
+
678
+ current_rotation_matrix = self._get_rotation_matrix(
679
+ peak=peak,
680
+ rotation_space=rotation_space,
681
+ rotation_mapping=rotation_mapping,
682
+ rotation_matrix=rotation_matrix,
683
+ )
684
+
685
+ masking_function(
686
+ score_space=score_space,
687
+ rotation_matrix=current_rotation_matrix,
688
+ peak=peak,
689
+ mask=mask,
690
+ rotated_template=rotated_template,
691
+ )
692
+
693
+ if len(coordinates) >= peak_limit:
495
694
  break
695
+
496
696
  peaks = backend.to_backend_array(coordinates)
497
697
  return peaks, None
498
698
 
699
+ @staticmethod
700
+ def _get_rotation_matrix(
701
+ peak: NDArray,
702
+ rotation_space: NDArray,
703
+ rotation_mapping: NDArray,
704
+ rotation_matrix: NDArray,
705
+ ) -> NDArray:
706
+ """
707
+ Get rotation matrix based on peak and rotation data.
708
+
709
+ Parameters
710
+ ----------
711
+ peak : NDArray
712
+ Peak coordinates.
713
+ rotation_space : NDArray
714
+ Rotation space array.
715
+ rotation_mapping : Dict
716
+ Dictionary mapping values in rotation_space to Euler angles.
717
+ rotation_matrix : NDArray
718
+ Current rotation matrix.
719
+
720
+ Returns
721
+ -------
722
+ NDArray
723
+ Rotation matrix.
724
+ """
725
+ if rotation_space is None or rotation_mapping is None:
726
+ return rotation_matrix
727
+
728
+ rotation = rotation_mapping[rotation_space[tuple(peak)]]
729
+
730
+ rotation_matrix = backend.to_backend_array(
731
+ euler_to_rotationmatrix(backend.to_numpy_array(rotation))
732
+ )
733
+ return rotation_matrix
734
+
735
+ @staticmethod
736
+ def _mask_scores_box(
737
+ score_space: NDArray, peak: NDArray, mask: NDArray, **kwargs: Dict
738
+ ) -> None:
739
+ """
740
+ Mask scores in a box around a peak.
741
+
742
+ Parameters
743
+ ----------
744
+ score_space : NDArray
745
+ Data array of scores.
746
+ peak : NDArray
747
+ Peak coordinates.
748
+ mask : NDArray
749
+ Mask array.
750
+ """
751
+ start = backend.maximum(backend.subtract(peak, mask.shape), 0)
752
+ stop = backend.minimum(backend.add(peak, mask.shape), score_space.shape)
753
+ start, stop = backend.astype(start, int), backend.astype(stop, int)
754
+ coords = tuple(slice(*pos) for pos in zip(start, stop))
755
+ score_space[coords] = 0
756
+ return None
757
+
758
+ @staticmethod
759
+ def _mask_scores_rotate(
760
+ score_space: NDArray,
761
+ peak: NDArray,
762
+ mask: NDArray,
763
+ rotated_template: NDArray,
764
+ rotation_matrix: NDArray,
765
+ **kwargs: Dict,
766
+ ) -> None:
767
+ """
768
+ Mask score_space using mask rotation around a peak.
769
+
770
+ Parameters
771
+ ----------
772
+ score_space : NDArray
773
+ Data array of scores.
774
+ peak : NDArray
775
+ Peak coordinates.
776
+ mask : NDArray
777
+ Mask array.
778
+ rotated_template : NDArray
779
+ Empty array to write mask rotations to.
780
+ rotation_matrix : NDArray
781
+ Rotation matrix.
782
+ """
783
+ left_pad = backend.divide(mask.shape, 2).astype(int)
784
+ right_pad = backend.add(left_pad, backend.mod(mask.shape, 2).astype(int))
785
+
786
+ score_start = backend.subtract(peak, left_pad)
787
+ score_stop = backend.add(peak, right_pad)
788
+
789
+ template_start = backend.subtract(backend.maximum(score_start, 0), score_start)
790
+ template_stop = backend.subtract(
791
+ score_stop, backend.minimum(score_stop, score_space.shape)
792
+ )
793
+ template_stop = backend.subtract(mask.shape, template_stop)
794
+
795
+ score_start = backend.maximum(score_start, 0)
796
+ score_stop = backend.minimum(score_stop, score_space.shape)
797
+ score_start = backend.astype(score_start, int)
798
+ score_stop = backend.astype(score_stop, int)
799
+
800
+ template_start = backend.astype(template_start, int)
801
+ template_stop = backend.astype(template_stop, int)
802
+ coords_score = tuple(slice(*pos) for pos in zip(score_start, score_stop))
803
+ coords_template = tuple(
804
+ slice(*pos) for pos in zip(template_start, template_stop)
805
+ )
806
+
807
+ rotated_template.fill(0)
808
+ backend.rotate_array(
809
+ arr=mask, rotation_matrix=rotation_matrix, order=1, out=rotated_template
810
+ )
811
+
812
+ score_space[coords_score] = backend.multiply(
813
+ score_space[coords_score], (rotated_template[coords_template] <= 0.1)
814
+ )
815
+ return None
816
+
499
817
 
500
818
  class PeakCallerScipy(PeakCaller):
501
819
  """
@@ -503,7 +821,7 @@ class PeakCallerScipy(PeakCaller):
503
821
  """
504
822
 
505
823
  def call_peaks(
506
- self, score_space: NDArray, rotation_matrix: NDArray, **kwargs
824
+ self, score_space: NDArray, minimum_score: float = None, **kwargs
507
825
  ) -> Tuple[NDArray, NDArray]:
508
826
  """
509
827
  Call peaks in the score space.
@@ -513,21 +831,25 @@ class PeakCallerScipy(PeakCaller):
513
831
  score_space : NDArray
514
832
  Data array of scores.
515
833
  minimum_score : float
516
- Minimum score value to consider.
517
- min_distance : float
518
- Minimum distance between maxima.
834
+ Minimum score value to consider. If provided, superseeds limit given
835
+ by :py:attr:`PeakCaller.number_of_peaks`.
519
836
 
520
837
  Returns
521
838
  -------
522
- NDArray
523
- Array of peak coordiantes.
524
- NDArray
525
- Array of peak details.
839
+ Tuple[NDArray, NDArray]
840
+ Array of peak coordinates and peak details.
526
841
  """
842
+
843
+ score_space = backend.to_numpy_array(score_space)
844
+ num_peaks = self.number_of_peaks
845
+ if minimum_score is not None:
846
+ num_peaks = np.inf
847
+
527
848
  peaks = peak_local_max(
528
849
  score_space,
529
- num_peaks=self.number_of_peaks,
850
+ num_peaks=num_peaks,
530
851
  min_distance=self.min_distance,
852
+ threshold_abs=minimum_score,
531
853
  )
532
854
  return peaks, None
533
855
 
@@ -879,8 +1201,63 @@ class MaxScoreOverRotations:
879
1201
 
880
1202
  self.use_memmap = use_memmap
881
1203
  self.lock = Manager().Lock() if thread_safe else nullcontext()
1204
+ self.lock_is_nullcontext = isinstance(
1205
+ self.score_space, type(backend.zeros((1)))
1206
+ )
882
1207
  self.observed_rotations = Manager().dict() if thread_safe else {}
883
1208
 
1209
+ def _postprocess(
1210
+ self,
1211
+ fourier_shift,
1212
+ convolution_mode,
1213
+ targetshape,
1214
+ templateshape,
1215
+ shared_memory_handler=None,
1216
+ **kwargs,
1217
+ ):
1218
+ internal_scores = backend.sharedarr_to_arr(
1219
+ shape=self.score_space_shape,
1220
+ dtype=self.score_space_dtype,
1221
+ shm=self.score_space,
1222
+ )
1223
+ internal_rotations = backend.sharedarr_to_arr(
1224
+ shape=self.score_space_shape,
1225
+ dtype=self.rotation_space_dtype,
1226
+ shm=self.rotations,
1227
+ )
1228
+
1229
+ if fourier_shift is not None:
1230
+ axis = tuple(i for i in range(len(fourier_shift)))
1231
+ internal_scores = backend.roll(
1232
+ internal_scores, shift=fourier_shift, axis=axis
1233
+ )
1234
+ internal_rotations = backend.roll(
1235
+ internal_rotations, shift=fourier_shift, axis=axis
1236
+ )
1237
+
1238
+ if convolution_mode is not None:
1239
+ internal_scores = apply_convolution_mode(
1240
+ internal_scores,
1241
+ convolution_mode=convolution_mode,
1242
+ s1=targetshape,
1243
+ s2=templateshape,
1244
+ )
1245
+ internal_rotations = apply_convolution_mode(
1246
+ internal_rotations,
1247
+ convolution_mode=convolution_mode,
1248
+ s1=targetshape,
1249
+ s2=templateshape,
1250
+ )
1251
+
1252
+ self.score_space_shape = internal_scores.shape
1253
+ self.score_space = backend.arr_to_sharedarr(
1254
+ internal_scores, shared_memory_handler
1255
+ )
1256
+ self.rotations = backend.arr_to_sharedarr(
1257
+ internal_rotations, shared_memory_handler
1258
+ )
1259
+ return self
1260
+
884
1261
  def __iter__(self):
885
1262
  internal_scores = backend.sharedarr_to_arr(
886
1263
  shape=self.score_space_shape,
@@ -938,11 +1315,21 @@ class MaxScoreOverRotations:
938
1315
  **kwargs
939
1316
  Arbitrary keyword arguments.
940
1317
  """
1318
+ rotation = backend.tobytes(rotation_matrix)
1319
+ rotation_index = self.observed_rotations.setdefault(
1320
+ rotation, len(self.observed_rotations)
1321
+ )
1322
+
1323
+ if self.lock_is_nullcontext:
1324
+ backend.max_score_over_rotations(
1325
+ score_space=score_space,
1326
+ internal_scores=self.score_space,
1327
+ internal_rotations=self.rotations,
1328
+ rotation_index=rotation_index,
1329
+ )
1330
+ return None
1331
+
941
1332
  with self.lock:
942
- rotation = backend.tobytes(rotation_matrix)
943
- if rotation not in self.observed_rotations:
944
- self.observed_rotations[rotation] = len(self.observed_rotations)
945
- rotation_index = self.observed_rotations[rotation]
946
1333
  internal_scores = backend.sharedarr_to_arr(
947
1334
  shape=self.score_space_shape,
948
1335
  dtype=self.score_space_dtype,
@@ -953,9 +1340,14 @@ class MaxScoreOverRotations:
953
1340
  dtype=self.rotation_space_dtype,
954
1341
  shm=self.rotations,
955
1342
  )
956
- indices = score_space > internal_scores
957
- internal_scores[indices] = score_space[indices]
958
- internal_rotations[indices] = rotation_index
1343
+
1344
+ backend.max_score_over_rotations(
1345
+ score_space=score_space,
1346
+ internal_scores=internal_scores,
1347
+ internal_rotations=internal_rotations,
1348
+ rotation_index=rotation_index,
1349
+ )
1350
+ return None
959
1351
 
960
1352
  @classmethod
961
1353
  def merge(cls, param_stores=List[Tuple], **kwargs) -> Tuple[NDArray]:
@@ -1007,14 +1399,22 @@ class MaxScoreOverRotations:
1007
1399
  scores_out = np.memmap(
1008
1400
  scores_out_filename, mode="w+", shape=base_max, dtype=scores_out_dtype
1009
1401
  )
1402
+ scores_out.fill(kwargs.get("score_threshold", 0))
1403
+ scores_out.flush()
1010
1404
  rotations_out = np.memmap(
1011
1405
  rotations_out_filename,
1012
1406
  mode="w+",
1013
1407
  shape=base_max,
1014
1408
  dtype=rotations_out_dtype,
1015
1409
  )
1410
+ rotations_out.fill(-1)
1411
+ rotations_out.flush()
1016
1412
  else:
1017
- scores_out = np.zeros(base_max, dtype=scores_out_dtype)
1413
+ scores_out = np.full(
1414
+ base_max,
1415
+ fill_value=kwargs.get("score_threshold", 0),
1416
+ dtype=scores_out_dtype,
1417
+ )
1018
1418
  rotations_out = np.full(base_max, fill_value=-1, dtype=rotations_out_dtype)
1019
1419
 
1020
1420
  for i in range(len(param_stores)):
@@ -1079,6 +1479,61 @@ class MaxScoreOverRotations:
1079
1479
  )
1080
1480
 
1081
1481
 
1482
+ class _MaxScoreOverTranslations(MaxScoreOverRotations):
1483
+ """
1484
+ Obtain the maximum translation score over various rotations.
1485
+
1486
+ Attributes
1487
+ ----------
1488
+ score_space : NDArray
1489
+ The score space for the observed rotations.
1490
+ rotations : NDArray
1491
+ The rotation identifiers for each score.
1492
+ translation_offset : NDArray, optional
1493
+ The offset applied during translation.
1494
+ observed_rotations : int
1495
+ Count of observed rotations.
1496
+ use_memmap : bool, optional
1497
+ Whether to offload internal data arrays to disk
1498
+ thread_safe: bool, optional
1499
+ Whether access to internal data arrays should be thread safe
1500
+ """
1501
+
1502
+ def __call__(
1503
+ self, score_space: NDArray, rotation_matrix: NDArray, **kwargs
1504
+ ) -> None:
1505
+ """
1506
+ Update internal parameter store based on `score_space`.
1507
+
1508
+ Parameters
1509
+ ----------
1510
+ score_space : ndarray
1511
+ Numpy array containing the score space.
1512
+ rotation_matrix : ndarray
1513
+ Square matrix describing the current rotation.
1514
+ **kwargs
1515
+ Arbitrary keyword arguments.
1516
+ """
1517
+ from tme.matching_utils import centered_mask
1518
+
1519
+ with self.lock:
1520
+ rotation = backend.tobytes(rotation_matrix)
1521
+ if rotation not in self.observed_rotations:
1522
+ self.observed_rotations[rotation] = len(self.observed_rotations)
1523
+ score_space = centered_mask(score_space, kwargs["template_shape"])
1524
+ rotation_index = self.observed_rotations[rotation]
1525
+ internal_scores = backend.sharedarr_to_arr(
1526
+ shape=self.score_space_shape,
1527
+ dtype=self.score_space_dtype,
1528
+ shm=self.score_space,
1529
+ )
1530
+ max_score = score_space.max(axis=(1, 2, 3))
1531
+ mean_score = score_space.mean(axis=(1, 2, 3))
1532
+ std_score = score_space.std(axis=(1, 2, 3))
1533
+ z_score = (max_score - mean_score) / std_score
1534
+ internal_scores[rotation_index] = z_score
1535
+
1536
+
1082
1537
  class MemmapHandler:
1083
1538
  """
1084
1539
  Create numpy memmap objects to write score spaces to.