pytme 0.1.9__cp311-cp311-macosx_14_0_arm64.whl → 0.2.0b0__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 (36) hide show
  1. {pytme-0.1.9.data → pytme-0.2.0b0.data}/scripts/match_template.py +148 -126
  2. pytme-0.2.0b0.data/scripts/postprocess.py +570 -0
  3. {pytme-0.1.9.data → pytme-0.2.0b0.data}/scripts/preprocessor_gui.py +244 -60
  4. {pytme-0.1.9.dist-info → pytme-0.2.0b0.dist-info}/METADATA +3 -1
  5. pytme-0.2.0b0.dist-info/RECORD +66 -0
  6. {pytme-0.1.9.dist-info → pytme-0.2.0b0.dist-info}/WHEEL +1 -1
  7. scripts/extract_candidates.py +218 -0
  8. scripts/match_template.py +148 -126
  9. scripts/match_template_filters.py +852 -0
  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 +545 -78
  16. tme/backends/cupy_backend.py +80 -15
  17. tme/backends/npfftw_backend.py +33 -2
  18. tme/backends/pytorch_backend.py +15 -7
  19. tme/density.py +156 -63
  20. tme/extensions.cpython-311-darwin.so +0 -0
  21. tme/matching_constrained.py +195 -0
  22. tme/matching_data.py +74 -33
  23. tme/matching_exhaustive.py +351 -208
  24. tme/matching_memory.py +1 -0
  25. tme/matching_optimization.py +728 -651
  26. tme/matching_utils.py +152 -8
  27. tme/orientations.py +561 -0
  28. tme/preprocessor.py +21 -18
  29. tme/structure.py +2 -37
  30. pytme-0.1.9.data/scripts/postprocess.py +0 -625
  31. pytme-0.1.9.dist-info/RECORD +0 -61
  32. {pytme-0.1.9.data → pytme-0.2.0b0.data}/scripts/estimate_ram_usage.py +0 -0
  33. {pytme-0.1.9.data → pytme-0.2.0b0.data}/scripts/preprocess.py +0 -0
  34. {pytme-0.1.9.dist-info → pytme-0.2.0b0.dist-info}/LICENSE +0 -0
  35. {pytme-0.1.9.dist-info → pytme-0.2.0b0.dist-info}/entry_points.txt +0 -0
  36. {pytme-0.1.9.dist-info → pytme-0.2.0b0.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,66 @@ 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(
431
+ backend.subtract(score_space_shape, output_shape),
432
+ 2
433
+ )
434
+ starts = backend.astype(starts, int)
435
+ stops = backend.add(starts, output_shape)
436
+
437
+ valid_peaks = (
438
+ backend.sum(
439
+ backend.multiply(
440
+ peak_positions > starts,
441
+ peak_positions <= stops
442
+ ),
443
+ axis=1,
444
+ )
445
+ == peak_positions.shape[1]
446
+ )
447
+ self.peak_list[0] = backend.subtract(peak_positions, starts)
448
+ self.peak_list = [x[valid_peaks] for x in self.peak_list]
449
+ return self
290
450
 
291
451
  class PeakCallerSort(PeakCaller):
292
452
  """
@@ -297,7 +457,7 @@ class PeakCallerSort(PeakCaller):
297
457
  """
298
458
 
299
459
  def call_peaks(
300
- self, score_space: NDArray, rotation_matrix: NDArray, **kwargs
460
+ self, score_space: NDArray, minimum_score: float = None, **kwargs
301
461
  ) -> Tuple[NDArray, NDArray]:
302
462
  """
303
463
  Call peaks in the score space.
@@ -307,20 +467,20 @@ class PeakCallerSort(PeakCaller):
307
467
  score_space : NDArray
308
468
  Data array of scores.
309
469
  minimum_score : float
310
- Minimum score value to consider.
311
- min_distance : float
312
- Minimum distance between maxima.
470
+ Minimum score value to consider. If provided, superseeds limit given
471
+ by :py:attr:`PeakCaller.number_of_peaks`.
313
472
 
314
473
  Returns
315
474
  -------
316
- NDArray
317
- Array of peak coordiantes.
318
- NDArray
319
- Array of peak details.
475
+ Tuple[NDArray, NDArray]
476
+ Array of peak coordinates and peak details.
320
477
  """
321
478
  flat_score_space = score_space.reshape(-1)
322
479
  k = min(self.number_of_peaks, backend.size(flat_score_space))
323
480
 
481
+ if minimum_score is not None:
482
+ k = backend.sum(score_space >= minimum_score)
483
+
324
484
  top_k_indices, *_ = backend.topk_indices(flat_score_space, k)
325
485
 
326
486
  coordinates = backend.unravel_index(top_k_indices, score_space.shape)
@@ -333,12 +493,12 @@ class PeakCallerSort(PeakCaller):
333
493
  class PeakCallerMaximumFilter(PeakCaller):
334
494
  """
335
495
  Find local maxima by applying a maximum filter and enforcing a distance
336
- constraint subseqquently. This is similar to the strategy implemented in
496
+ constraint subsequently. This is similar to the strategy implemented in
337
497
  skimage.feature.peak_local_max.
338
498
  """
339
499
 
340
500
  def call_peaks(
341
- self, score_space: NDArray, rotation_matrix: NDArray, **kwargs
501
+ self, score_space: NDArray, minimum_score: float = None, **kwargs
342
502
  ) -> Tuple[NDArray, NDArray]:
343
503
  """
344
504
  Call peaks in the score space.
@@ -348,25 +508,25 @@ class PeakCallerMaximumFilter(PeakCaller):
348
508
  score_space : NDArray
349
509
  Data array of scores.
350
510
  minimum_score : float
351
- Minimum score value to consider.
352
- min_distance : float
353
- Minimum distance between maxima.
511
+ Minimum score value to consider. If provided, superseeds limit given
512
+ by :py:attr:`PeakCaller.number_of_peaks`.
354
513
 
355
514
  Returns
356
515
  -------
357
- NDArray
358
- Array of peak coordiantes.
359
- NDArray
360
- Array of peak details.
516
+ Tuple[NDArray, NDArray]
517
+ Array of peak coordinates and peak details.
361
518
  """
362
519
  peaks = backend.max_filter_coordinates(score_space, self.min_distance)
363
520
 
521
+ scores = score_space[tuple(peaks.T)]
522
+
364
523
  input_candidates = min(
365
524
  self.number_of_peaks, peaks.shape[0] - 1, backend.size(score_space) - 1
366
525
  )
367
- top_indices = backend.topk_indices(
368
- score_space[tuple(peaks.T)], input_candidates
369
- )
526
+ if minimum_score is not None:
527
+ input_candidates = backend.sum(scores >= minimum_score)
528
+
529
+ top_indices = backend.topk_indices(scores, input_candidates)
370
530
  peaks = peaks[top_indices]
371
531
 
372
532
  return peaks, None
@@ -382,7 +542,7 @@ class PeakCallerFast(PeakCaller):
382
542
  """
383
543
 
384
544
  def call_peaks(
385
- self, score_space: NDArray, rotation_matrix: NDArray, **kwargs
545
+ self, score_space: NDArray, minimum_score: float = None, **kwargs
386
546
  ) -> Tuple[NDArray, NDArray]:
387
547
  """
388
548
  Call peaks in the score space.
@@ -392,16 +552,13 @@ class PeakCallerFast(PeakCaller):
392
552
  score_space : NDArray
393
553
  Data array of scores.
394
554
  minimum_score : float
395
- Minimum score value to consider.
396
- min_distance : float
397
- Minimum distance between maxima.
555
+ Minimum score value to consider. If provided, superseeds limit given
556
+ by :py:attr:`PeakCaller.number_of_peaks`.
398
557
 
399
558
  Returns
400
559
  -------
401
- NDArray
402
- Array of peak coordiantes.
403
- NDArray
404
- Array of peak details.
560
+ Tuple[NDArray, NDArray]
561
+ Array of peak coordinates and peak details.
405
562
  """
406
563
  splits = {
407
564
  axis: score_space.shape[axis] // self.min_distance
@@ -457,7 +614,14 @@ class PeakCallerRecursiveMasking(PeakCaller):
457
614
  """
458
615
 
459
616
  def call_peaks(
460
- self, score_space: NDArray, rotation_matrix: NDArray, **kwargs
617
+ self,
618
+ score_space: NDArray,
619
+ rotation_matrix: NDArray,
620
+ mask: NDArray = None,
621
+ minimum_score: float = None,
622
+ rotation_space: NDArray = None,
623
+ rotation_mapping: Dict = None,
624
+ **kwargs,
461
625
  ) -> Tuple[NDArray, NDArray]:
462
626
  """
463
627
  Call peaks in the score space.
@@ -466,36 +630,195 @@ class PeakCallerRecursiveMasking(PeakCaller):
466
630
  ----------
467
631
  score_space : NDArray
468
632
  Data array of scores.
633
+ rotation_matrix : NDArray
634
+ Rotation matrix.
635
+ mask : NDArray, optional
636
+ Mask array, by default None.
637
+ rotation_space : NDArray, optional
638
+ Rotation space array, by default None.
639
+ rotation_mapping : Dict optional
640
+ Dictionary mapping values in rotation_space to Euler angles.
641
+ By default None
469
642
  minimum_score : float
470
- Minimum score value to consider.
471
- min_distance : float
472
- Minimum distance between maxima.
643
+ Minimum score value to consider. If provided, superseeds limit given
644
+ by :py:attr:`PeakCaller.number_of_peaks`.
473
645
 
474
646
  Returns
475
647
  -------
476
- NDArray
477
- Array of peak coordiantes.
478
- NDArray
479
- Array of peak details.
648
+ Tuple[NDArray, NDArray]
649
+ Array of peak coordinates and peak details.
650
+
651
+ Notes
652
+ -----
653
+ By default, scores are masked using a box with edge length self.min_distance.
654
+ If mask is provided, elements around each peak will be multiplied by the mask
655
+ values. If rotation_space and rotation_mapping is provided, the respective
656
+ rotation will be applied to the mask, otherwise rotation_matrix is used.
480
657
  """
481
- score_box = tuple(self.min_distance for _ in range(score_space.ndim))
482
- coordinates = []
658
+ coordinates, masking_function = [], self._mask_scores_rotate
659
+
660
+ if mask is None:
661
+ masking_function = self._mask_scores_box
662
+ shape = tuple(self.min_distance for _ in range(score_space.ndim))
663
+ mask = backend.zeros(shape, dtype=backend._default_dtype)
664
+
665
+ rotated_template = backend.zeros(mask.shape, dtype=mask.dtype)
666
+
667
+ peak_limit = self.number_of_peaks
668
+ if minimum_score is not None:
669
+ peak_limit = backend.size(score_space)
670
+ else:
671
+ minimum_score = backend.min(score_space) - 1
672
+
483
673
  while True:
484
674
  backend.argmax(score_space)
485
- max_coord = backend.unravel_index(
675
+ peak = backend.unravel_index(
486
676
  indices=backend.argmax(score_space), shape=score_space.shape
487
677
  )
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:
678
+ if score_space[tuple(peak)] < minimum_score:
495
679
  break
680
+
681
+ coordinates.append(peak)
682
+
683
+ current_rotation_matrix = self._get_rotation_matrix(
684
+ peak=peak,
685
+ rotation_space=rotation_space,
686
+ rotation_mapping=rotation_mapping,
687
+ rotation_matrix=rotation_matrix,
688
+ )
689
+
690
+ masking_function(
691
+ score_space=score_space,
692
+ rotation_matrix=current_rotation_matrix,
693
+ peak=peak,
694
+ mask=mask,
695
+ rotated_template=rotated_template,
696
+ )
697
+
698
+ if len(coordinates) >= peak_limit:
699
+ break
700
+
496
701
  peaks = backend.to_backend_array(coordinates)
497
702
  return peaks, None
498
703
 
704
+ @staticmethod
705
+ def _get_rotation_matrix(
706
+ peak: NDArray,
707
+ rotation_space: NDArray,
708
+ rotation_mapping: NDArray,
709
+ rotation_matrix: NDArray,
710
+ ) -> NDArray:
711
+ """
712
+ Get rotation matrix based on peak and rotation data.
713
+
714
+ Parameters
715
+ ----------
716
+ peak : NDArray
717
+ Peak coordinates.
718
+ rotation_space : NDArray
719
+ Rotation space array.
720
+ rotation_mapping : Dict
721
+ Dictionary mapping values in rotation_space to Euler angles.
722
+ rotation_matrix : NDArray
723
+ Current rotation matrix.
724
+
725
+ Returns
726
+ -------
727
+ NDArray
728
+ Rotation matrix.
729
+ """
730
+ if rotation_space is None or rotation_mapping is None:
731
+ return rotation_matrix
732
+
733
+ rotation = rotation_mapping[rotation_space[tuple(peak)]]
734
+
735
+ rotation_matrix = backend.to_backend_array(
736
+ euler_to_rotationmatrix(backend.to_numpy_array(rotation))
737
+ )
738
+ return rotation_matrix
739
+
740
+ @staticmethod
741
+ def _mask_scores_box(
742
+ score_space: NDArray, peak: NDArray, mask: NDArray, **kwargs: Dict
743
+ ) -> None:
744
+ """
745
+ Mask scores in a box around a peak.
746
+
747
+ Parameters
748
+ ----------
749
+ score_space : NDArray
750
+ Data array of scores.
751
+ peak : NDArray
752
+ Peak coordinates.
753
+ mask : NDArray
754
+ Mask array.
755
+ """
756
+ start = backend.maximum(backend.subtract(peak, mask.shape), 0)
757
+ stop = backend.minimum(backend.add(peak, mask.shape), score_space.shape)
758
+ start, stop = backend.astype(start, int), backend.astype(stop, int)
759
+ coords = tuple(slice(*pos) for pos in zip(start, stop))
760
+ score_space[coords] = 0
761
+ return None
762
+
763
+ @staticmethod
764
+ def _mask_scores_rotate(
765
+ score_space: NDArray,
766
+ peak: NDArray,
767
+ mask: NDArray,
768
+ rotated_template: NDArray,
769
+ rotation_matrix: NDArray,
770
+ **kwargs: Dict,
771
+ ) -> None:
772
+ """
773
+ Mask score_space using mask rotation around a peak.
774
+
775
+ Parameters
776
+ ----------
777
+ score_space : NDArray
778
+ Data array of scores.
779
+ peak : NDArray
780
+ Peak coordinates.
781
+ mask : NDArray
782
+ Mask array.
783
+ rotated_template : NDArray
784
+ Empty array to write mask rotations to.
785
+ rotation_matrix : NDArray
786
+ Rotation matrix.
787
+ """
788
+ left_pad = backend.divide(mask.shape, 2).astype(int)
789
+ right_pad = backend.add(left_pad, backend.mod(mask.shape, 2).astype(int))
790
+
791
+ score_start = backend.subtract(peak, left_pad)
792
+ score_stop = backend.add(peak, right_pad)
793
+
794
+ template_start = backend.subtract(backend.maximum(score_start, 0), score_start)
795
+ template_stop = backend.subtract(
796
+ score_stop, backend.minimum(score_stop, score_space.shape)
797
+ )
798
+ template_stop = backend.subtract(mask.shape, template_stop)
799
+
800
+ score_start = backend.maximum(score_start, 0)
801
+ score_stop = backend.minimum(score_stop, score_space.shape)
802
+ score_start = backend.astype(score_start, int)
803
+ score_stop = backend.astype(score_stop, int)
804
+
805
+ template_start = backend.astype(template_start, int)
806
+ template_stop = backend.astype(template_stop, int)
807
+ coords_score = tuple(slice(*pos) for pos in zip(score_start, score_stop))
808
+ coords_template = tuple(
809
+ slice(*pos) for pos in zip(template_start, template_stop)
810
+ )
811
+
812
+ rotated_template.fill(0)
813
+ backend.rotate_array(
814
+ arr=mask, rotation_matrix=rotation_matrix, order=1, out=rotated_template
815
+ )
816
+
817
+ score_space[coords_score] = backend.multiply(
818
+ score_space[coords_score], (rotated_template[coords_template] <= 0.1)
819
+ )
820
+ return None
821
+
499
822
 
500
823
  class PeakCallerScipy(PeakCaller):
501
824
  """
@@ -503,7 +826,7 @@ class PeakCallerScipy(PeakCaller):
503
826
  """
504
827
 
505
828
  def call_peaks(
506
- self, score_space: NDArray, rotation_matrix: NDArray, **kwargs
829
+ self, score_space: NDArray, minimum_score: float = None, **kwargs
507
830
  ) -> Tuple[NDArray, NDArray]:
508
831
  """
509
832
  Call peaks in the score space.
@@ -513,21 +836,25 @@ class PeakCallerScipy(PeakCaller):
513
836
  score_space : NDArray
514
837
  Data array of scores.
515
838
  minimum_score : float
516
- Minimum score value to consider.
517
- min_distance : float
518
- Minimum distance between maxima.
839
+ Minimum score value to consider. If provided, superseeds limit given
840
+ by :py:attr:`PeakCaller.number_of_peaks`.
519
841
 
520
842
  Returns
521
843
  -------
522
- NDArray
523
- Array of peak coordiantes.
524
- NDArray
525
- Array of peak details.
844
+ Tuple[NDArray, NDArray]
845
+ Array of peak coordinates and peak details.
526
846
  """
847
+
848
+ score_space = backend.to_numpy_array(score_space)
849
+ num_peaks = self.number_of_peaks
850
+ if minimum_score is not None:
851
+ num_peaks = np.inf
852
+
527
853
  peaks = peak_local_max(
528
854
  score_space,
529
- num_peaks=self.number_of_peaks,
855
+ num_peaks=num_peaks,
530
856
  min_distance=self.min_distance,
857
+ threshold_abs=minimum_score,
531
858
  )
532
859
  return peaks, None
533
860
 
@@ -879,8 +1206,68 @@ class MaxScoreOverRotations:
879
1206
 
880
1207
  self.use_memmap = use_memmap
881
1208
  self.lock = Manager().Lock() if thread_safe else nullcontext()
1209
+ self.lock_is_nullcontext = isinstance(self.score_space, type(backend.zeros((1))))
882
1210
  self.observed_rotations = Manager().dict() if thread_safe else {}
883
1211
 
1212
+
1213
+ def _postprocess(self,
1214
+ fourier_shift,
1215
+ convolution_mode,
1216
+ targetshape,
1217
+ templateshape,
1218
+ shared_memory_handler=None,
1219
+ **kwargs
1220
+ ):
1221
+ internal_scores = backend.sharedarr_to_arr(
1222
+ shape=self.score_space_shape,
1223
+ dtype=self.score_space_dtype,
1224
+ shm=self.score_space,
1225
+ )
1226
+ internal_rotations = backend.sharedarr_to_arr(
1227
+ shape=self.score_space_shape,
1228
+ dtype=self.rotation_space_dtype,
1229
+ shm=self.rotations,
1230
+ )
1231
+
1232
+ if fourier_shift is not None:
1233
+ axis = tuple(i for i in range(len(fourier_shift)))
1234
+ internal_scores = backend.roll(
1235
+ internal_scores,
1236
+ shift=fourier_shift,
1237
+ axis=axis
1238
+ )
1239
+ internal_rotations = backend.roll(
1240
+ internal_rotations,
1241
+ shift=fourier_shift,
1242
+ axis=axis
1243
+ )
1244
+
1245
+ if convolution_mode is not None:
1246
+ internal_scores = apply_convolution_mode(
1247
+ internal_scores,
1248
+ convolution_mode=convolution_mode,
1249
+ s1=targetshape,
1250
+ s2=templateshape
1251
+ )
1252
+ internal_rotations = apply_convolution_mode(
1253
+ internal_rotations,
1254
+ convolution_mode=convolution_mode,
1255
+ s1=targetshape,
1256
+ s2=templateshape
1257
+ )
1258
+
1259
+ self.score_space_shape = internal_scores.shape
1260
+ self.score_space = backend.arr_to_sharedarr(
1261
+ internal_scores,
1262
+ shared_memory_handler
1263
+ )
1264
+ self.rotations = backend.arr_to_sharedarr(
1265
+ internal_rotations,
1266
+ shared_memory_handler
1267
+ )
1268
+ return self
1269
+
1270
+
884
1271
  def __iter__(self):
885
1272
  internal_scores = backend.sharedarr_to_arr(
886
1273
  shape=self.score_space_shape,
@@ -938,11 +1325,21 @@ class MaxScoreOverRotations:
938
1325
  **kwargs
939
1326
  Arbitrary keyword arguments.
940
1327
  """
1328
+ rotation = backend.tobytes(rotation_matrix)
1329
+ rotation_index = self.observed_rotations.setdefault(
1330
+ rotation, len(self.observed_rotations)
1331
+ )
1332
+
1333
+ if self.lock_is_nullcontext:
1334
+ backend.max_score_over_rotations(
1335
+ score_space=score_space,
1336
+ internal_scores=self.score_space,
1337
+ internal_rotations=self.rotations,
1338
+ rotation_index=rotation_index,
1339
+ )
1340
+ return None
1341
+
941
1342
  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
1343
  internal_scores = backend.sharedarr_to_arr(
947
1344
  shape=self.score_space_shape,
948
1345
  dtype=self.score_space_dtype,
@@ -953,9 +1350,14 @@ class MaxScoreOverRotations:
953
1350
  dtype=self.rotation_space_dtype,
954
1351
  shm=self.rotations,
955
1352
  )
956
- indices = score_space > internal_scores
957
- internal_scores[indices] = score_space[indices]
958
- internal_rotations[indices] = rotation_index
1353
+
1354
+ backend.max_score_over_rotations(
1355
+ score_space=score_space,
1356
+ internal_scores=internal_scores,
1357
+ internal_rotations=internal_rotations,
1358
+ rotation_index=rotation_index,
1359
+ )
1360
+ return None
959
1361
 
960
1362
  @classmethod
961
1363
  def merge(cls, param_stores=List[Tuple], **kwargs) -> Tuple[NDArray]:
@@ -1007,14 +1409,22 @@ class MaxScoreOverRotations:
1007
1409
  scores_out = np.memmap(
1008
1410
  scores_out_filename, mode="w+", shape=base_max, dtype=scores_out_dtype
1009
1411
  )
1412
+ scores_out.fill(kwargs.get("score_threshold", 0))
1413
+ scores_out.flush()
1010
1414
  rotations_out = np.memmap(
1011
1415
  rotations_out_filename,
1012
1416
  mode="w+",
1013
1417
  shape=base_max,
1014
1418
  dtype=rotations_out_dtype,
1015
1419
  )
1420
+ rotations_out.fill(-1)
1421
+ rotations_out.flush()
1016
1422
  else:
1017
- scores_out = np.zeros(base_max, dtype=scores_out_dtype)
1423
+ scores_out = np.full(
1424
+ base_max,
1425
+ fill_value=kwargs.get("score_threshold", 0),
1426
+ dtype=scores_out_dtype,
1427
+ )
1018
1428
  rotations_out = np.full(base_max, fill_value=-1, dtype=rotations_out_dtype)
1019
1429
 
1020
1430
  for i in range(len(param_stores)):
@@ -1079,6 +1489,61 @@ class MaxScoreOverRotations:
1079
1489
  )
1080
1490
 
1081
1491
 
1492
+ class _MaxScoreOverTranslations(MaxScoreOverRotations):
1493
+ """
1494
+ Obtain the maximum translation score over various rotations.
1495
+
1496
+ Attributes
1497
+ ----------
1498
+ score_space : NDArray
1499
+ The score space for the observed rotations.
1500
+ rotations : NDArray
1501
+ The rotation identifiers for each score.
1502
+ translation_offset : NDArray, optional
1503
+ The offset applied during translation.
1504
+ observed_rotations : int
1505
+ Count of observed rotations.
1506
+ use_memmap : bool, optional
1507
+ Whether to offload internal data arrays to disk
1508
+ thread_safe: bool, optional
1509
+ Whether access to internal data arrays should be thread safe
1510
+ """
1511
+
1512
+ def __call__(
1513
+ self, score_space: NDArray, rotation_matrix: NDArray, **kwargs
1514
+ ) -> None:
1515
+ """
1516
+ Update internal parameter store based on `score_space`.
1517
+
1518
+ Parameters
1519
+ ----------
1520
+ score_space : ndarray
1521
+ Numpy array containing the score space.
1522
+ rotation_matrix : ndarray
1523
+ Square matrix describing the current rotation.
1524
+ **kwargs
1525
+ Arbitrary keyword arguments.
1526
+ """
1527
+ from tme.matching_utils import centered_mask
1528
+
1529
+ with self.lock:
1530
+ rotation = backend.tobytes(rotation_matrix)
1531
+ if rotation not in self.observed_rotations:
1532
+ self.observed_rotations[rotation] = len(self.observed_rotations)
1533
+ score_space = centered_mask(score_space, kwargs["template_shape"])
1534
+ rotation_index = self.observed_rotations[rotation]
1535
+ internal_scores = backend.sharedarr_to_arr(
1536
+ shape=self.score_space_shape,
1537
+ dtype=self.score_space_dtype,
1538
+ shm=self.score_space,
1539
+ )
1540
+ max_score = score_space.max(axis=(1, 2, 3))
1541
+ mean_score = score_space.mean(axis=(1, 2, 3))
1542
+ std_score = score_space.std(axis=(1, 2, 3))
1543
+ z_score = (max_score - mean_score) / std_score
1544
+ internal_scores[rotation_index] = z_score
1545
+
1546
+
1082
1547
  class MemmapHandler:
1083
1548
  """
1084
1549
  Create numpy memmap objects to write score spaces to.
@@ -1182,3 +1647,5 @@ class MemmapHandler:
1182
1647
  """
1183
1648
  rotation_string = "_".join(rotation_matrix.ravel().astype(str))
1184
1649
  return self._path_translation[rotation_string]
1650
+
1651
+