pytme 0.2.0b0__cp311-cp311-macosx_14_0_arm64.whl → 0.2.2__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 (52) hide show
  1. pytme-0.2.2.data/scripts/match_template.py +1187 -0
  2. {pytme-0.2.0b0.data → pytme-0.2.2.data}/scripts/postprocess.py +170 -71
  3. {pytme-0.2.0b0.data → pytme-0.2.2.data}/scripts/preprocessor_gui.py +179 -86
  4. pytme-0.2.2.dist-info/METADATA +91 -0
  5. pytme-0.2.2.dist-info/RECORD +74 -0
  6. {pytme-0.2.0b0.dist-info → pytme-0.2.2.dist-info}/WHEEL +1 -1
  7. scripts/extract_candidates.py +126 -87
  8. scripts/match_template.py +596 -209
  9. scripts/match_template_filters.py +571 -223
  10. scripts/postprocess.py +170 -71
  11. scripts/preprocessor_gui.py +179 -86
  12. scripts/refine_matches.py +567 -159
  13. tme/__init__.py +0 -1
  14. tme/__version__.py +1 -1
  15. tme/analyzer.py +627 -855
  16. tme/backends/__init__.py +41 -11
  17. tme/backends/_jax_utils.py +185 -0
  18. tme/backends/cupy_backend.py +120 -225
  19. tme/backends/jax_backend.py +282 -0
  20. tme/backends/matching_backend.py +464 -388
  21. tme/backends/mlx_backend.py +45 -68
  22. tme/backends/npfftw_backend.py +256 -514
  23. tme/backends/pytorch_backend.py +41 -154
  24. tme/density.py +312 -421
  25. tme/extensions.cpython-311-darwin.so +0 -0
  26. tme/matching_data.py +366 -303
  27. tme/matching_exhaustive.py +279 -1521
  28. tme/matching_optimization.py +234 -129
  29. tme/matching_scores.py +884 -0
  30. tme/matching_utils.py +281 -387
  31. tme/memory.py +377 -0
  32. tme/orientations.py +226 -66
  33. tme/parser.py +3 -4
  34. tme/preprocessing/__init__.py +2 -0
  35. tme/preprocessing/_utils.py +217 -0
  36. tme/preprocessing/composable_filter.py +31 -0
  37. tme/preprocessing/compose.py +55 -0
  38. tme/preprocessing/frequency_filters.py +388 -0
  39. tme/preprocessing/tilt_series.py +1011 -0
  40. tme/preprocessor.py +574 -530
  41. tme/structure.py +495 -189
  42. tme/types.py +5 -3
  43. pytme-0.2.0b0.data/scripts/match_template.py +0 -800
  44. pytme-0.2.0b0.dist-info/METADATA +0 -73
  45. pytme-0.2.0b0.dist-info/RECORD +0 -66
  46. tme/helpers.py +0 -881
  47. tme/matching_constrained.py +0 -195
  48. {pytme-0.2.0b0.data → pytme-0.2.2.data}/scripts/estimate_ram_usage.py +0 -0
  49. {pytme-0.2.0b0.data → pytme-0.2.2.data}/scripts/preprocess.py +0 -0
  50. {pytme-0.2.0b0.dist-info → pytme-0.2.2.dist-info}/LICENSE +0 -0
  51. {pytme-0.2.0b0.dist-info → pytme-0.2.2.dist-info}/entry_points.txt +0 -0
  52. {pytme-0.2.0b0.dist-info → pytme-0.2.2.dist-info}/top_level.txt +0 -0
tme/density.py CHANGED
@@ -18,70 +18,72 @@ import numpy as np
18
18
  import skimage.io as skio
19
19
 
20
20
  from scipy.ndimage import (
21
+ zoom,
21
22
  laplace,
22
- generic_gradient_magnitude,
23
- minimum_filter,
24
23
  sobel,
24
+ minimum_filter,
25
25
  binary_erosion,
26
- zoom,
26
+ generic_gradient_magnitude,
27
27
  )
28
28
  from scipy.spatial import ConvexHull
29
29
 
30
+ from .types import NDArray
31
+ from .backends import NumpyFFTWBackend
30
32
  from .structure import Structure
31
33
  from .matching_utils import (
32
- minimum_enclosing_box,
33
34
  array_to_memmap,
34
35
  memmap_to_array,
36
+ minimum_enclosing_box,
35
37
  )
36
- from .types import NDArray
37
- from .helpers import is_gzipped
38
- from .backends import NumpyFFTWBackend
39
38
 
40
39
 
41
40
  class Density:
42
41
  """
43
- Contains electron density data and implements operations on it.
42
+ Abstract representation of N-dimensional densities.
44
43
 
45
44
  Parameters
46
45
  ----------
47
- data : NDArray
48
- Electron density data.
49
- origin : NDArray, optional
50
- Origin of the coordinate system. Defaults to zero.
51
- sampling_rate : NDArray, optional
52
- Sampling rate along data axis. Defaults to one.
46
+ data : array_like
47
+ Array of densities.
48
+ origin : array_like, optional
49
+ Origin of the coordinate system, zero by default.
50
+ sampling_rate : array_like, optional
51
+ Sampling rate along data axis, one by default.
53
52
  metadata : dict, optional
54
53
  Dictionary with metadata information, empty by default.
55
54
 
56
55
  Raises
57
56
  ------
58
57
  ValueError
59
- The metadata parameter is not a dictionary.
58
+ If metadata is not a dictionary.
59
+
60
+ If sampling rate / origin is not defined for a single or all axes.
60
61
 
61
62
  Examples
62
63
  --------
63
- The following achieves the minimal definition of a :py:class:`Density` instance.
64
+ The following achieves the minimal definition of a :py:class:`Density` instance
64
65
 
65
66
  >>> import numpy as np
66
67
  >>> from tme import Density
67
68
  >>> data = np.random.rand(50,70,40)
68
- >>> Density(data = data)
69
+ >>> Density(data=data)
69
70
 
70
- Optional parameters are ``origin`` and ``sampling_rate`` that correspond
71
- to the coordinate system reference and the edge length per axis element,
72
- as well as the ``metadata`` dictionary. By default,
73
- :py:attr:`Density.origin` is set to zero and :py:attr:`Density.sampling_rate`
74
- to 1. If provided, origin or sampling_rate either need to be a single value:
71
+ Optional parameters ``origin`` correspond to the coordinate system reference,
72
+ ``sampling_rate`` to the spatial length per axis element, and ``metadata`` to
73
+ a dictionary with supplementary information. By default,
74
+ :py:attr:`Density.origin` is set to zero, :py:attr:`Density.sampling_rate`
75
+ to one, and :py:attr:`Density.metadata` is an empty dictionary. If provided,
76
+ ``origin`` and ``sampling_rate`` either need to be a single value
75
77
 
76
- >>> Density(data = data, origin = 0, sampling_rate = 1)
78
+ >>> Density(data=data, origin=0, sampling_rate=1)
77
79
 
78
- Be specified along each data axis:
80
+ be specified along each data axis
79
81
 
80
- >>> Density(data = data, origin = (0, 0, 0), sampling_rate = (1.5, 1.1, 1.2))
82
+ >>> Density(data=data, origin=(0, 0, 0), sampling_rate=(1.5, 1.1, 1.2))
81
83
 
82
- Or a combination of both:
84
+ or be a combination of both
83
85
 
84
- >>> Density(data = data, origin = 0, sampling_rate = (1.5, 1.1, 1.2))
86
+ >>> Density(data=data, origin=0, sampling_rate=(1.5, 1.1, 1.2))
85
87
  """
86
88
 
87
89
  def __init__(
@@ -111,7 +113,7 @@ class Density:
111
113
  self.metadata = metadata
112
114
 
113
115
  def __repr__(self):
114
- response = "Density object at {}\nOrigin: {}, sampling_rate: {}, Shape: {}"
116
+ response = "Density object at {}\nOrigin: {}, Sampling Rate: {}, Shape: {}"
115
117
  return response.format(
116
118
  hex(id(self)),
117
119
  tuple(np.round(self.origin, 3)),
@@ -124,22 +126,22 @@ class Density:
124
126
  cls, filename: str, subset: Tuple[slice] = None, use_memmap: bool = False
125
127
  ) -> "Density":
126
128
  """
127
- Reads in a file and converts it into :py:class:`Density` instance.
129
+ Reads a file into a :py:class:`Density` instance.
128
130
 
129
131
  Parameters
130
132
  ----------
131
133
  filename : str
132
134
  Path to a file in CCP4/MRC, EM, HDF5 or a format supported by
133
- skimage.io.imread. The file can be gzip compressed.
135
+ :obj:`skimage.io.imread`. The file can be gzip compressed.
134
136
  subset : tuple of slices, optional
135
137
  Slices representing the desired subset along each dimension.
136
138
  use_memmap : bool, optional
137
- Whether the Density objects data attribute should be memmory mapped.
139
+ Memory map the data contained in ``filename`` to save memory.
138
140
 
139
141
  Returns
140
142
  -------
141
- Density
142
- An instance of the :py:class:`Density` class.
143
+ :py:class:`Density`
144
+ Class instance representing the data in ``filename``.
143
145
 
144
146
  References
145
147
  ----------
@@ -149,7 +151,7 @@ class Density:
149
151
 
150
152
  Examples
151
153
  --------
152
- :py:meth:`Density.from_file` reads files in CCP4/MRC, EM, or a format supported
154
+ :py:meth:`Density.from_file` reads files in CCP4/MRC, EM, or a format supported
153
155
  by skimage.io.imread and converts them into a :py:class:`Density` instance. The
154
156
  following outlines how to read a file in the CCP4/MRC format [1]_:
155
157
 
@@ -163,9 +165,9 @@ class Density:
163
165
  >>> subset_slices = (slice(0, 50), slice(0, 50), slice(0, 50))
164
166
  >>> Density.from_file("/path/to/file.mrc", subset=subset_slices)
165
167
 
166
- For large density maps, memory mapping can be used to read the file directly
167
- from disk without loading it entirely into memory. This is particularly useful
168
- for large datasets or when working with limited memory resources:
168
+ Memory mapping can be used to read the file from disk without loading
169
+ it entirely into memory. This is particularly useful for large datasets
170
+ or when working with limited memory resources:
169
171
 
170
172
  >>> Density.from_file("/path/to/large_file.mrc", use_memmap=True)
171
173
 
@@ -178,18 +180,17 @@ class Density:
178
180
  >>> Density.from_file("/path/to/file.em.gz")
179
181
 
180
182
  If the file format is not CCP4/MRC or EM, :py:meth:`Density.from_file` attempts
181
- to use skimage.io.imread to read the file [3]_. This fallback does not extract
183
+ to use :obj:`skimage.io.imread` to read the file [3]_, which does not extract
182
184
  origin or sampling_rate information from the file:
183
185
 
184
186
  >>> Density.from_file("/path/to/other_format.tif")
185
187
 
186
188
  Notes
187
189
  -----
188
- If ``filename`` ends with ".em" or ".em.gz" the method will parse it as EM file,
189
- if it ends with "h5" or "h5.gz" the method will parse the file as HDF5.
190
- Otherwise the method defaults to the CCP4/MRC format and on failure, switches to
191
- :obj:`skimage.io.imread` regardless of the extension. Currently, the later does not
192
- extract origin or sampling_rate information from the file.
190
+ If ``filename`` ends ".em" or ".h5" it will be parsed as EM or HDF5 file.
191
+ Otherwise, the default reader is CCP4/MRC and on failure
192
+ :obj:`skimage.io.imread` is used regardless of extension. The later does
193
+ not extract origin or sampling_rate information from the file.
193
194
 
194
195
  See Also
195
196
  --------
@@ -267,9 +268,9 @@ class Density:
267
268
  # nx := column; ny := row; nz := section
268
269
  start = np.array(
269
270
  [
270
- int(mrc.header["nxstart"]),
271
- int(mrc.header["nystart"]),
272
- int(mrc.header["nzstart"]),
271
+ mrc.header["nzstart"],
272
+ mrc.header["nystart"],
273
+ mrc.header["nxstart"],
273
274
  ]
274
275
  )
275
276
 
@@ -291,17 +292,9 @@ class Density:
291
292
  sampling_rate = mrc.voxel_size.astype(
292
293
  [("x", "<f4"), ("y", "<f4"), ("z", "<f4")]
293
294
  ).view(("<f4", 3))
294
- sampling_rate = sampling_rate[::-1]
295
- sampling_rate = np.array(sampling_rate)
295
+ sampling_rate = np.array(sampling_rate)[::-1]
296
296
 
297
- if np.all(origin == start):
298
- pass
299
- elif np.all(origin == 0) and not np.all(start == 0):
300
- origin = np.multiply(start, sampling_rate)
301
- elif np.all(
302
- np.abs(origin.astype(int))
303
- != np.abs((start * sampling_rate).astype(int))
304
- ) and not np.all(start == 0):
297
+ if np.allclose(origin, 0) and not np.allclose(start, 0):
305
298
  origin = np.multiply(start, sampling_rate)
306
299
 
307
300
  extended_header = mrc.header.nsymbt
@@ -317,12 +310,12 @@ class Density:
317
310
  if use_memmap:
318
311
  warnings.warn(
319
312
  f"Cannot open gzipped file {filename} as memmap."
320
- f" Please gunzip {filename} to use memmap functionality."
313
+ f" Please run 'gunzip {filename}' to use memmap functionality."
321
314
  )
322
315
  use_memmap = False
323
316
 
324
317
  if subset is not None:
325
- subset_shape = [x.stop - x.start for x in subset]
318
+ subset_shape = tuple(x.stop - x.start for x in subset)
326
319
  if np.allclose(subset_shape, data_shape):
327
320
  return cls._load_mrc(
328
321
  filename=filename, subset=None, use_memmap=use_memmap
@@ -345,7 +338,6 @@ class Density:
345
338
  data = mrc.data
346
339
 
347
340
  if not np.all(crs_index == (0, 1, 2)):
348
- data.setflags(write=True)
349
341
  data = np.transpose(data, crs_index)
350
342
  start = np.take(start, crs_index)
351
343
 
@@ -650,7 +642,7 @@ class Density:
650
642
  sampling_rate: NDArray = np.ones(1),
651
643
  origin: Tuple[float] = None,
652
644
  weight_type: str = "atomic_weight",
653
- scattering_args: Dict = dict(),
645
+ weight_type_args: Dict = {},
654
646
  chain: str = None,
655
647
  filter_by_elements: Set = None,
656
648
  filter_by_residues: Set = None,
@@ -680,6 +672,8 @@ class Density:
680
672
  weight_type : str, optional
681
673
  Which weight should be given to individual atoms. For valid values
682
674
  see :py:meth:`tme.structure.Structure.to_volume`.
675
+ weight_type_args : dict, optional
676
+ Additional arguments for atom weight computation.
683
677
  chain : str, optional
684
678
  The chain that should be extracted from the structure. If multiple chains
685
679
  should be selected, they needto be a comma separated string,
@@ -688,8 +682,6 @@ class Density:
688
682
  Set of atomic elements to keep. Default is all atoms.
689
683
  filter_by_residues : set, optional
690
684
  Set of residues to keep. Default is all residues.
691
- scattering_args : dict, optional
692
- Additional arguments for scattering factor computation.
693
685
 
694
686
  Returns
695
687
  -------
@@ -746,14 +738,14 @@ class Density:
746
738
  >>> )
747
739
 
748
740
  :py:meth:`Density.from_structure` supports a variety of methods to convert
749
- atoms into densities. In additino to 'atomic_weight', 'atomic_number',
750
- 'van_der_waals_radius' its possible to use experimentally determined scattering
751
- factors from various sources:
741
+ atoms into densities. In addition to 'atomic_weight', 'atomic_number',
742
+ and 'van_der_waals_radius', its possible to use experimentally determined
743
+ scattering factors from various sources:
752
744
 
753
745
  >>> density = Density.from_structure(
754
746
  >>> filename_or_structure = path_to_structure,
755
747
  >>> weight_type = "scattering_factors",
756
- >>> scattering_args={"source": "dt1969"}
748
+ >>> weight_type_args={"source": "dt1969"}
757
749
  >>> )
758
750
 
759
751
  or a lowpass filtered representation introduced in [1]_:
@@ -761,7 +753,7 @@ class Density:
761
753
  >>> density = Density.from_structure(
762
754
  >>> filename_or_structure = path_to_structure,
763
755
  >>> weight_type = "lowpass_scattering_factors",
764
- >>> scattering_args={"source": "dt1969"}
756
+ >>> weight_type_args={"source": "dt1969"}
765
757
  >>> )
766
758
 
767
759
  See Also
@@ -783,27 +775,27 @@ class Density:
783
775
  origin=origin,
784
776
  chain=chain,
785
777
  weight_type=weight_type,
786
- scattering_args=scattering_args,
778
+ weight_type_args=weight_type_args,
787
779
  )
788
780
 
789
781
  return cls(
790
782
  data=volume,
791
783
  origin=origin,
792
784
  sampling_rate=sampling_rate,
793
- metadata=structure.details.copy(),
785
+ metadata=structure.metadata.copy(),
794
786
  )
795
787
 
796
788
  def to_file(self, filename: str, gzip: bool = False) -> None:
797
789
  """
798
- Writes current class instance to disk.
790
+ Writes class instance to disk.
799
791
 
800
792
  Parameters
801
793
  ----------
802
794
  filename : str
803
795
  Path to write to.
804
796
  gzip : bool, optional
805
- If True, the output will be gzip compressed and "gz" will be added
806
- to the filename if not already present. By default False.
797
+ Gzip compress the output and add corresponding suffix to filename
798
+ if not present. False by default.
807
799
 
808
800
  References
809
801
  ----------
@@ -819,7 +811,7 @@ class Density:
819
811
  >>> import numpy as np
820
812
  >>> from tme import Density
821
813
  >>> data = np.random.rand(50,50,50)
822
- >>> dens = Density(data = data, origin = (0, 0, 0), sampling_rate = (1, 1, 1))
814
+ >>> dens = Density(data=data, origin=(0, 0, 0), sampling_rate=(1, 1, 1))
823
815
  >>> dens.to_file("example.mrc")
824
816
 
825
817
  The output file can also be directly ``gzip`` compressed. The corresponding
@@ -834,14 +826,14 @@ class Density:
834
826
  In addition, a variety of image file formats are supported [3]_:
835
827
 
836
828
  >>> data = np.random.rand(50,50)
837
- >>> dens = Density(data = data, origin = (0, 0), sampling_rate = (1, 1))
829
+ >>> dens = Density(data=data, origin=(0, 0), sampling_rate=(1, 1))
838
830
  >>> dens.to_file("example.tiff")
839
831
 
840
832
  Notes
841
833
  -----
842
- If ``filename`` ends with "em" or "em.gz" will create an EM file, "h5" or
843
- "h5.gz" will create a HDF5 file. Otherwise, the method defaults to the CCP4/MRC
844
- format, and on failure, falls back to :obj:`skimage.io.imsave`.
834
+ If ``filename`` endswith ".em" or ".h5" a EM file or HDF5 file will be created.
835
+ The default output format is CCP4/MRC and on failure, :obj:`skimage.io.imsave`
836
+ is used.
845
837
 
846
838
  See Also
847
839
  --------
@@ -862,7 +854,7 @@ class Density:
862
854
 
863
855
  def _save_mrc(self, filename: str, gzip: bool = False) -> None:
864
856
  """
865
- Writes current class instance to disk as mrc file.
857
+ Writes class instance to disk as mrc file.
866
858
 
867
859
  Parameters
868
860
  ----------
@@ -878,7 +870,7 @@ class Density:
878
870
  compression = "gzip" if gzip else None
879
871
  with mrcfile.new(filename, overwrite=True, compression=compression) as mrc:
880
872
  mrc.set_data(self.data.astype("float32"))
881
- mrc.header.nzstart, mrc.header.nystart, mrc.header.nxstart = np.ceil(
873
+ mrc.header.nzstart, mrc.header.nystart, mrc.header.nxstart = np.rint(
882
874
  np.divide(self.origin, self.sampling_rate)
883
875
  )
884
876
  # mrcfile library expects origin to be in xyz format
@@ -982,7 +974,7 @@ class Density:
982
974
  self.metadata["std"] = self.metadata.get("std", 0)
983
975
  self.metadata["min"] = self.metadata.get("min", 0)
984
976
  self.metadata["max"] = self.metadata.get("max", 0)
985
- if type(self.data) != np.memmap:
977
+ if not isinstance(self.data, np.memmap):
986
978
  self.metadata["mean"] = self.data.mean()
987
979
  self.metadata["std"] = self.data.std()
988
980
  self.metadata["min"] = self.data.min()
@@ -994,11 +986,15 @@ class Density:
994
986
  @property
995
987
  def empty(self) -> "Density":
996
988
  """
997
- Returns a copy of the current class instance with all elements in
998
- :py:attr:`Density.data` set to zero. :py:attr:`Density.origin` and
999
- :py:attr:`Density.sampling_rate` will be copied, while
1000
- :py:attr:`Density.metadata` will be initialized to contain min, max,
1001
- mean and standard deviation of :py:attr:`Density.data`.
989
+ Returns a copy of the class instance with all elements in
990
+ :py:attr:`Density.data` set to zero. :py:attr:`Density.metadata` will be
991
+ initialized accordingly. :py:attr:`Density.origin` and
992
+ :py:attr:`Density.sampling_rate` are copied.
993
+
994
+ Returns
995
+ -------
996
+ :py:class:`Density`
997
+ Empty class instance.
1002
998
 
1003
999
  Examples
1004
1000
  --------
@@ -1018,7 +1014,12 @@ class Density:
1018
1014
 
1019
1015
  def copy(self) -> "Density":
1020
1016
  """
1021
- Returns a copy of the current :py:class:`Density` instance.
1017
+ Create a copy of the class instance.
1018
+
1019
+ Returns
1020
+ -------
1021
+ :py:class:`Density`
1022
+ A copy of the class instance.
1022
1023
 
1023
1024
  Examples
1024
1025
  --------
@@ -1037,8 +1038,7 @@ class Density:
1037
1038
 
1038
1039
  def to_memmap(self) -> None:
1039
1040
  """
1040
- Converts the current class instance's :py:attr:`Density.data` attribute to
1041
- a :obj:`numpy.memmap` instance.
1041
+ Converts :py:attr:`Density.data` to a :obj:`numpy.memmap`.
1042
1042
 
1043
1043
  Examples
1044
1044
  --------
@@ -1063,7 +1063,7 @@ class Density:
1063
1063
  --------
1064
1064
  :py:meth:`Density.to_numpy`
1065
1065
  """
1066
- if type(self.data) == np.memmap:
1066
+ if isinstance(self.data, np.memmap):
1067
1067
  return None
1068
1068
 
1069
1069
  filename = array_to_memmap(arr=self.data)
@@ -1074,8 +1074,7 @@ class Density:
1074
1074
 
1075
1075
  def to_numpy(self) -> None:
1076
1076
  """
1077
- Converts the current class instance's :py:attr:`Density.data` attribute to
1078
- an in-memory :obj:`numpy.ndarray`.
1077
+ Converts :py:attr:`Density.data` to an in-memory :obj:`numpy.ndarray`.
1079
1078
 
1080
1079
  Examples
1081
1080
  --------
@@ -1093,8 +1092,7 @@ class Density:
1093
1092
  @property
1094
1093
  def shape(self) -> Tuple[int]:
1095
1094
  """
1096
- Returns the dimensions of current instance's :py:attr:`Density.data`
1097
- attribute.
1095
+ Returns the dimensions of :py:attr:`Density.data`.
1098
1096
 
1099
1097
  Returns
1100
1098
  -------
@@ -1103,8 +1101,6 @@ class Density:
1103
1101
 
1104
1102
  Examples
1105
1103
  --------
1106
- The following outlines the usage of :py:attr:`Density.shape`:
1107
-
1108
1104
  >>> import numpy as np
1109
1105
  >>> from tme import Density
1110
1106
  >>> dens = Density(np.array([0, 1, 1, 1, 0]))
@@ -1116,13 +1112,12 @@ class Density:
1116
1112
  @property
1117
1113
  def data(self) -> NDArray:
1118
1114
  """
1119
- Returns the value of the current instance's :py:attr:`Density.data`
1120
- attribute.
1115
+ Returns the value of :py:attr:`Density.data`.
1121
1116
 
1122
1117
  Returns
1123
1118
  -------
1124
1119
  NDArray
1125
- Value of the current instance's :py:attr:`Density.data` attribute.
1120
+ Value of the instance's :py:attr:`Density.data` attribute.
1126
1121
 
1127
1122
  Examples
1128
1123
  --------
@@ -1140,20 +1135,20 @@ class Density:
1140
1135
  @data.setter
1141
1136
  def data(self, data: NDArray) -> None:
1142
1137
  """
1143
- Sets the value of the current instance's :py:attr:`Density.data` attribute.
1138
+ Sets the value of the instance's :py:attr:`Density.data` attribute.
1144
1139
  """
1145
1140
  self._data = data
1146
1141
 
1147
1142
  @property
1148
1143
  def origin(self) -> NDArray:
1149
1144
  """
1150
- Returns the value of the current instance's :py:attr:`Density.origin`
1145
+ Returns the value of the instance's :py:attr:`Density.origin`
1151
1146
  attribute.
1152
1147
 
1153
1148
  Returns
1154
1149
  -------
1155
1150
  NDArray
1156
- Value of the current instance's :py:attr:`Density.origin` attribute.
1151
+ Value of the instance's :py:attr:`Density.origin` attribute.
1157
1152
 
1158
1153
  Examples
1159
1154
  --------
@@ -1179,8 +1174,7 @@ class Density:
1179
1174
  @property
1180
1175
  def sampling_rate(self) -> NDArray:
1181
1176
  """
1182
- Returns the value of the current instance's :py:attr:`Density.sampling_rate`
1183
- attribute.
1177
+ Returns the value of the instance's :py:attr:`Density.sampling_rate` attribute.
1184
1178
 
1185
1179
  Returns
1186
1180
  -------
@@ -1201,7 +1195,7 @@ class Density:
1201
1195
  @property
1202
1196
  def metadata(self) -> Dict:
1203
1197
  """
1204
- Returns the current instance's :py:attr:`Density.metadata` dictionary attribute.
1198
+ Returns the instance's :py:attr:`Density.metadata` attribute.
1205
1199
 
1206
1200
  Returns
1207
1201
  -------
@@ -1243,7 +1237,7 @@ class Density:
1243
1237
  Pads the internal data array according to box.
1244
1238
 
1245
1239
  Negative slices indices will result in a left-hand padding, while
1246
- slice indices larger than the box_size property of the current class
1240
+ slice indices larger than the box_size property of the class
1247
1241
  instance will result in a right-hand padding.
1248
1242
 
1249
1243
  Parameters
@@ -1272,17 +1266,15 @@ class Density:
1272
1266
 
1273
1267
  def adjust_box(self, box: Tuple[slice], pad_kwargs: Dict = {}) -> None:
1274
1268
  """
1275
- Adjusts the internal data array and origin of the current class instance
1269
+ Adjusts :py:attr:`Density.data` and :py:attr:`Density.origin`
1276
1270
  according to the provided box.
1277
1271
 
1278
1272
  Parameters
1279
1273
  ----------
1280
1274
  box : tuple of slices
1281
- A tuple of slices describing how each axis of the volume array
1282
- should be sliced. See :py:meth:`Density.trim_box` on how to produce
1283
- such an object.
1275
+ Description of how each axis of :py:attr:`Density.data` should be sliced.
1284
1276
  pad_kwargs: dict, optional
1285
- Parameter dictionary passed to numpy pad.
1277
+ Parameter dictionary passed to :obj:`numpy.pad`.
1286
1278
 
1287
1279
  See Also
1288
1280
  --------
@@ -1462,8 +1454,8 @@ class Density:
1462
1454
  Raises
1463
1455
  ------
1464
1456
  ValueError
1465
- If the length of `new_shape` does not match the dimensionality of the
1466
- internal data array.
1457
+ If the length of ``new_shape`` does not match the dimensionality of
1458
+ :py:attr:`Density.data`.
1467
1459
 
1468
1460
  Examples
1469
1461
  --------
@@ -1477,13 +1469,6 @@ class Density:
1477
1469
  >>> dens.data
1478
1470
  array([0, 1, 1, 1, 0])
1479
1471
 
1480
- It's also possible to pass a user-defined ``padding_value``:
1481
-
1482
- >>> dens = Density(np.array([1,1,1]))
1483
- >>> dens.pad(new_shape = (5,), center = True, padding_value = -1)
1484
- >>> dens.data
1485
- array([-1, 1, 1, 1, -1])
1486
-
1487
1472
  If ``center`` is set to False, the padding values will be appended:
1488
1473
 
1489
1474
  >>> dens = Density(np.array([1,1,1]))
@@ -1491,6 +1476,12 @@ class Density:
1491
1476
  >>> dens.data
1492
1477
  array([1, 1, 1, 0, 0])
1493
1478
 
1479
+ It's also possible to pass a user-defined ``padding_value``:
1480
+
1481
+ >>> dens = Density(np.array([1,1,1]))
1482
+ >>> dens.pad(new_shape = (5,), center = True, padding_value = -1)
1483
+ >>> dens.data
1484
+ array([-1, 1, 1, 1, -1])
1494
1485
  """
1495
1486
  if len(new_shape) != self.data.ndim:
1496
1487
  raise ValueError(
@@ -1529,64 +1520,51 @@ class Density:
1529
1520
 
1530
1521
  Returns
1531
1522
  -------
1532
- Density
1533
- A copy of the class instance whose data center of mass is in the
1534
- center of the data array.
1523
+ :py:class:`Density`
1524
+ A centered copy of the class instance.
1535
1525
  NDArray
1536
- The coordinate translation.
1526
+ The offset between array center and center of mass.
1537
1527
 
1538
1528
  See Also
1539
1529
  --------
1540
1530
  :py:meth:`Density.trim_box`
1541
1531
  :py:meth:`Density.minimum_enclosing_box`
1542
1532
 
1543
-
1544
1533
  Examples
1545
1534
  --------
1546
1535
  :py:meth:`Density.centered` returns a tuple containing a centered version
1547
1536
  of the current :py:class:`Density` instance, as well as an array with
1548
- translations. The translation corresponds to the shift that between the
1549
- center of mass and the center of the internal :py:attr:`Density.data` attribute.
1537
+ translations. The translation corresponds to the shift between the original and
1538
+ current center of mass.
1550
1539
 
1551
1540
  >>> import numpy as np
1552
1541
  >>> from tme import Density
1553
- >>> dens = Density(np.ones((5,5)))
1542
+ >>> dens = Density(np.ones((5,5,5)))
1554
1543
  >>> centered_dens, translation = dens.centered(0)
1555
1544
  >>> translation
1556
- array([-4.4408921e-16, 4.4408921e-16])
1545
+ array([0., 0., 0.])
1557
1546
 
1558
1547
  :py:meth:`Density.centered` extended the :py:attr:`Density.data` attribute
1559
1548
  of the current :py:class:`Density` instance and modified
1560
1549
  :py:attr:`Density.origin` accordingly.
1561
1550
 
1562
1551
  >>> centered_dens
1563
- Origin: (-1.0, -1.0), sampling_rate: (1, 1), Shape: (7, 7)
1564
-
1565
- :py:meth:`Density.centered` achieves centering via zero-padding the
1566
- internal :py:attr:`Density.data` attribute:
1567
-
1568
- >>> centered_dens.data
1569
- array([[0., 0., 0., 0., 0., 0., 0.],
1570
- [0., 1., 1., 1., 1., 1., 0.],
1571
- [0., 1., 1., 1., 1., 1., 0.],
1572
- [0., 1., 1., 1., 1., 1., 0.],
1573
- [0., 1., 1., 1., 1., 1., 0.],
1574
- [0., 1., 1., 1., 1., 1., 0.],
1575
- [0., 0., 0., 0., 0., 0., 0.]])
1576
-
1577
- `centered_dens` is sufficiently large to represent all rotations that
1578
- could be applied to the :py:attr:`Density.data` attribute. Lets look
1579
- at a random rotation obtained from
1552
+ Origin: (-2.0, -2.0, -2.0), sampling_rate: (1, 1, 1), Shape: (9, 9, 9)
1553
+
1554
+ :py:meth:`Density.centered` achieves centering via zero-padding and
1555
+ rigid-transform of the internal :py:attr:`Density.data` attribute.
1556
+ `centered_dens` is sufficiently large to represent all rotations of the
1557
+ :py:attr:`Density.data` attribute, such as ones obtained from
1580
1558
  :py:meth:`tme.matching_utils.get_rotation_matrices`.
1581
1559
 
1582
1560
  >>> from tme.matching_utils import get_rotation_matrices
1583
- >>> rotation_matrix = get_rotation_matrices(dim = 2 ,angular_sampling = 10)[0]
1561
+ >>> rotation_matrix = get_rotation_matrices(dim = 3 ,angular_sampling = 10)[0]
1584
1562
  >>> rotated_centered_dens = centered_dens.rigid_transform(
1585
1563
  >>> rotation_matrix = rotation_matrix,
1586
1564
  >>> order = None
1587
1565
  >>> )
1588
1566
  >>> print(centered_dens.data.sum(), rotated_centered_dens.data.sum())
1589
- 25.000000000000007 25.000000000000007
1567
+ 125.0 125.0
1590
1568
 
1591
1569
  """
1592
1570
  ret = self.copy()
@@ -1595,10 +1573,11 @@ class Density:
1595
1573
  ret.adjust_box(box)
1596
1574
 
1597
1575
  new_shape = np.maximum(ret.shape, self.shape)
1576
+ new_shape = np.add(new_shape, 1 - np.mod(new_shape, 2))
1598
1577
  ret.pad(new_shape)
1599
1578
 
1600
1579
  center = self.center_of_mass(ret.data, cutoff)
1601
- shift = np.subtract(np.divide(ret.shape, 2), center)
1580
+ shift = np.subtract(np.divide(np.subtract(ret.shape, 1), 2), center)
1602
1581
 
1603
1582
  ret = ret.rigid_transform(
1604
1583
  translation=shift,
@@ -1606,117 +1585,9 @@ class Density:
1606
1585
  use_geometric_center=False,
1607
1586
  order=1,
1608
1587
  )
1609
- offset = np.subtract(center, self.center_of_mass(ret.data, cutoff))
1610
-
1611
- return ret, offset
1612
-
1613
- @classmethod
1614
- def rotate_array(
1615
- cls,
1616
- arr: NDArray,
1617
- rotation_matrix: NDArray,
1618
- arr_mask: NDArray = None,
1619
- translation: NDArray = None,
1620
- use_geometric_center: bool = False,
1621
- out: NDArray = None,
1622
- out_mask: NDArray = None,
1623
- order: int = 3,
1624
- ) -> None:
1625
- """
1626
- Rotates coordinates of arr according to rotation_matrix.
1627
-
1628
- If no output array is provided, this method will compute an array with
1629
- sufficient space to hold all elements. If both `arr` and `arr_mask`
1630
- are provided, `arr_mask` will be centered according to arr.
1631
-
1632
- Parameters
1633
- ----------
1634
- arr : NDArray
1635
- The input array to be rotated.
1636
- arr_mask : NDArray, optional
1637
- The mask of `arr` that will be equivalently rotated.
1638
- rotation_matrix : NDArray
1639
- The rotation matrix to apply [d x d].
1640
- translation : NDArray
1641
- The translation to apply [d].
1642
- use_geometric_center : bool, optional
1643
- Whether the rotation should be centered around the geometric
1644
- or mass center. Default is mass center.
1645
- out : NDArray, optional
1646
- The output array to write the rotation of `arr` to.
1647
- out_mask : NDArray, optional
1648
- The output array to write the rotation of `arr_mask` to.
1649
- order : int, optional
1650
- Spline interpolation order. Has to be in the range 0-5.
1651
- """
1652
-
1653
- return NumpyFFTWBackend().rotate_array(
1654
- arr=arr,
1655
- rotation_matrix=rotation_matrix,
1656
- arr_mask=arr_mask,
1657
- translation=translation,
1658
- use_geometric_center=use_geometric_center,
1659
- out=out,
1660
- out_mask=out_mask,
1661
- order=order,
1662
- )
1663
-
1664
- @staticmethod
1665
- def rotate_array_coordinates(
1666
- arr: NDArray,
1667
- coordinates: NDArray,
1668
- rotation_matrix: NDArray,
1669
- translation: NDArray = None,
1670
- out: NDArray = None,
1671
- use_geometric_center: bool = True,
1672
- arr_mask: NDArray = None,
1673
- mask_coordinates: NDArray = None,
1674
- out_mask: NDArray = None,
1675
- ) -> None:
1676
- """
1677
- Rotates coordinates of arr according to rotation_matrix.
1678
-
1679
- If no output array is provided, this method will compute an array with
1680
- sufficient space to hold all elements. If both `arr` and `arr_mask`
1681
- are provided, `arr_mask` will be centered according to arr.
1682
1588
 
1683
- No centering will be performed if the rotation matrix is the identity matrix.
1684
-
1685
- Parameters
1686
- ----------
1687
- arr : NDArray
1688
- The input array to be rotated.
1689
- coordinates : NDArray
1690
- The pointcloud [d x N] containing elements of `arr` that should be rotated.
1691
- See :py:meth:`Density.to_pointcloud` on how to obtain the coordinates.
1692
- rotation_matrix : NDArray
1693
- The rotation matrix to apply [d x d].
1694
- rotation_matrix : NDArray
1695
- The translation to apply [d].
1696
- out : NDArray, optional
1697
- The output array to write the rotation of `arr` to.
1698
- use_geometric_center : bool, optional
1699
- Whether the rotation should be centered around the geometric
1700
- or mass center.
1701
- arr_mask : NDArray, optional
1702
- The mask of `arr` that will be equivalently rotated.
1703
- mask_coordinates : NDArray, optional
1704
- Equivalent to `coordinates`, but containing elements of `arr_mask`
1705
- that should be rotated.
1706
- out_mask : NDArray, optional
1707
- The output array to write the rotation of `arr_mask` to.
1708
- """
1709
- return NumpyFFTWBackend().rotate_array_coordinates(
1710
- arr=arr,
1711
- coordinates=coordinates,
1712
- rotation_matrix=rotation_matrix,
1713
- translation=translation,
1714
- out=out,
1715
- use_geometric_center=use_geometric_center,
1716
- arr_mask=arr_mask,
1717
- mask_coordinates=mask_coordinates,
1718
- out_mask=out_mask,
1719
- )
1589
+ shift = np.subtract(center, self.center_of_mass(ret.data, cutoff))
1590
+ return ret, shift
1720
1591
 
1721
1592
  def rigid_transform(
1722
1593
  self,
@@ -1726,124 +1597,183 @@ class Density:
1726
1597
  use_geometric_center: bool = False,
1727
1598
  ) -> "Density":
1728
1599
  """
1729
- Performs a rigid transform of the current class instance.
1600
+ Performs a rigid transform of the class instance.
1730
1601
 
1731
1602
  Parameters
1732
1603
  ----------
1733
1604
  rotation_matrix : NDArray
1734
- Rotation matrix to apply to the `Density` instance.
1605
+ Rotation matrix to apply.
1735
1606
  translation : NDArray
1736
- Translation to apply to the `Density` instance.
1607
+ Translation to apply.
1737
1608
  order : int, optional
1738
- Order of spline interpolation.
1609
+ Interpolation order to use. Default is 3, has to be in range 0-5.
1739
1610
  use_geometric_center : bool, optional
1740
- Whether to use geometric or coordinate center. If False,
1741
- class instance should be centered using :py:meth:`Density.centered`.
1611
+ Use geometric or mass center as rotation center.
1742
1612
 
1743
1613
  Returns
1744
1614
  -------
1745
1615
  Density
1746
- The transformed instance of :py:class:`tme.density.Density`.
1616
+ The transformed instance of :py:class:`Density`.
1747
1617
 
1748
1618
  Examples
1749
1619
  --------
1620
+ Define the :py:class:`Density` instance
1621
+
1750
1622
  >>> import numpy as np
1751
- >>> rotation_matrix = np.eye(3)
1752
- >>> rotation_matrix[0] = -1
1753
- >>> density.rotate(rotation_matrix = rotation_matrix)
1623
+ >>> from tme import Density
1624
+ >>> dens = Density(np.arange(9).reshape(3,3).astype(np.float32))
1625
+ >>> dens, translation = dens.centered(0)
1626
+
1627
+ and apply the rotation, in this case a mirror around the z-axis
1628
+
1629
+ >>> rotation_matrix = np.eye(dens.data.ndim)
1630
+ >>> rotation_matrix[0, 0] = -1
1631
+ >>> dens_transform = dens.rigid_transform(rotation_matrix = rotation_matrix)
1632
+ >>> dens_transform.data
1633
+ array([[0. , 0. , 0. , 0. , 0. ],
1634
+ [0.5 , 3.0833333 , 3.5833333 , 3.3333333 , 0. ],
1635
+ [0.75 , 4.6666665 , 5.6666665 , 5.4166665 , 0. ],
1636
+ [0.25 , 1.6666666 , 2.6666667 , 2.9166667 , 0. ],
1637
+ [0. , 0.08333334, 0.5833333 , 0.8333333 , 0. ]],
1638
+ dtype=float32)
1754
1639
 
1755
1640
  Notes
1756
1641
  -----
1757
- :py:meth:`Density.rigid_transform` that the internal data array is
1758
- sufficiently sized to accomodate the transform.
1642
+ This function assumes the internal :py:attr:`Density.data` attribute is
1643
+ sufficiently sized to hold the transformation.
1759
1644
 
1760
1645
  See Also
1761
1646
  --------
1762
1647
  :py:meth:`Density.centered`, :py:meth:`Density.minimum_enclosing_box`
1763
1648
  """
1764
- transformed_map = self.empty
1765
-
1766
- self.rotate_array(
1649
+ ret = self.empty
1650
+ NumpyFFTWBackend().rigid_transform(
1767
1651
  arr=self.data,
1768
1652
  rotation_matrix=rotation_matrix,
1769
1653
  translation=translation,
1770
- order=order,
1771
1654
  use_geometric_center=use_geometric_center,
1772
- out=transformed_map.data,
1655
+ out=ret.data,
1656
+ order=order,
1773
1657
  )
1774
- eps = np.finfo(transformed_map.data.dtype).eps
1775
- transformed_map.data[transformed_map.data < eps] = 0
1776
- return transformed_map
1777
1658
 
1778
- def align_origins(self, other_map: "Density") -> "Density":
1659
+ eps = np.finfo(ret.data.dtype).eps
1660
+ ret.data[np.abs(ret.data) < eps] = 0
1661
+ return ret
1662
+
1663
+ def resample(
1664
+ self, new_sampling_rate: Tuple[float], method: str = "spline", order: int = 1
1665
+ ) -> "Density":
1779
1666
  """
1780
- Aligns the origin of another to the origin of the current class instance.
1667
+ Resamples :py:attr:`Density.data` to ``new_sampling_rate``.
1781
1668
 
1782
1669
  Parameters
1783
1670
  ----------
1784
- other_map : Density
1785
- An instance of :py:class:`Density` class to align with the current map.
1671
+ new_sampling_rate : tuple of floats or float
1672
+ Sampling rate to resample to for a single or all axes.
1673
+ method: str, optional
1674
+ Resampling method to use, defaults to `spline`. Availabe options are:
1675
+
1676
+ +---------+----------------------------------------------------------+
1677
+ | spline | Smooth spline interpolation via :obj:`scipy.ndimage.zoom`|
1678
+ +---------+----------------------------------------------------------+
1679
+ | fourier | Frequency preserving Fourier cropping |
1680
+ +---------+----------------------------------------------------------+
1681
+
1682
+ order : int, optional
1683
+ Order of spline used for interpolation, by default 1. Ignored when
1684
+ ``method`` is `fourier`.
1786
1685
 
1787
1686
  Raises
1788
1687
  ------
1789
1688
  ValueError
1790
- If the sampling_rate of both class instances does not match.
1689
+ If ``method`` is not supported.
1791
1690
 
1792
1691
  Returns
1793
1692
  -------
1794
- Density
1795
- A modified copy of `other_map` with aligned origin.
1796
- """
1797
- if not np.allclose(self.sampling_rate, other_map.sampling_rate):
1798
- raise ValueError("sampling_rate of both maps have to match.")
1693
+ :py:class:`Density`
1694
+ A resampled copy of the class instance.
1799
1695
 
1800
- origin_difference = np.divide(
1801
- np.subtract(self.origin, other_map.origin), self.sampling_rate
1802
- )
1803
- origin_difference = origin_difference.astype(int)
1696
+ Examples
1697
+ --------
1698
+ The following makes use of :py:meth:`tme.matching_utils.create_mask`
1699
+ to define a :py:class:`Density` instance containing a 2D circle with
1700
+ a sampling rate of 2
1804
1701
 
1805
- box_start = np.minimum(origin_difference, other_map.shape)
1806
- box_end = np.maximum(origin_difference, other_map.shape)
1702
+ >>> from tme import Density
1703
+ >>> from tme.matching_utils import create_mask
1704
+ >>> mask = create_mask(
1705
+ >>> mask_type="ellipse",
1706
+ >>> shape=(11,11),
1707
+ >>> center=(5,5),
1708
+ >>> radius=3
1709
+ >>> )
1710
+ >>> dens = Density(mask, sampling_rate=2)
1711
+ >>> dens
1712
+ Origin: (0.0, 0.0), sampling_rate: (2, 2), Shape: (11, 11)
1807
1713
 
1808
- new_box = tuple(slice(*pos) for pos in zip(box_start, box_end))
1714
+ Using :py:meth:`Density.resample` we can modulate the sampling rate
1715
+ using spline interpolation of desired order
1809
1716
 
1810
- ret = other_map.copy()
1811
- ret.adjust_box(new_box)
1812
- return ret
1717
+ >>> dens.resample(new_sampling_rate= 4, method="spline", order=3)
1718
+ Origin: (0.0, 0.0), sampling_rate: (4, 4), Shape: (6, 6)
1813
1719
 
1814
- def resample(self, new_sampling_rate: Tuple[float], order: int = 1) -> "Density":
1815
- """
1816
- Resamples the current class instance to ``new_sampling_rate`` using
1817
- spline interpolation of order ``order``.
1720
+ Or Fourier cropping which results in a less smooth output, but more faithfully
1721
+ captures the contained frequency information
1818
1722
 
1819
- Parameters
1820
- ----------
1821
- new_sampling_rate : tuple of floats or float
1822
- Sampling rate to resample to.
1823
- order : int, optional
1824
- Order of spline used for interpolation, by default 1.
1723
+ >>> dens.resample(new_sampling_rate=4, method="fourier")
1724
+ Origin: (0.0, 0.0), sampling_rate: (4, 4), Shape: (6, 6)
1725
+
1726
+ ``new_sampling_rate`` can also be specified per axis
1727
+
1728
+ >>> dens.resample(new_sampling_rate=(4,1), method="spline", order=3)
1729
+ Origin: (0.0, 0.0), sampling_rate: (4, 1), Shape: (6, 22)
1825
1730
 
1826
- Returns
1827
- -------
1828
- Density
1829
- A resampled instance of `Density` class.
1830
1731
  """
1831
- map_copy, new_sampling_rate = self.copy(), np.array(new_sampling_rate)
1732
+ _supported_methods = ("spline", "fourier")
1733
+ if method not in _supported_methods:
1734
+ raise ValueError(
1735
+ f"Expected method to be one of {_supported_methods}, got '{method}'."
1736
+ )
1737
+ new_sampling_rate = np.array(new_sampling_rate)
1832
1738
  new_sampling_rate = np.repeat(
1833
- new_sampling_rate, map_copy.data.ndim // new_sampling_rate.size
1739
+ new_sampling_rate, self.data.ndim // new_sampling_rate.size
1834
1740
  )
1835
- scale_factor = np.divide(map_copy.sampling_rate, new_sampling_rate)
1836
1741
 
1837
- map_copy.data = zoom(map_copy.data, scale_factor, order=order)
1838
- map_copy.sampling_rate = new_sampling_rate
1742
+ ret = self.copy()
1743
+ scale_factor = np.divide(ret.sampling_rate, new_sampling_rate)
1744
+ if method == "spline":
1745
+ ret.data = zoom(ret.data, scale_factor, order=order)
1746
+ elif method == "fourier":
1747
+ ret_shape = np.round(np.multiply(scale_factor, ret.shape)).astype(int)
1748
+
1749
+ axis = range(len(ret_shape))
1750
+ mask = np.zeros(self.shape, dtype=bool)
1751
+ mask[tuple(slice(0, x) for x in ret_shape)] = 1
1752
+ mask = np.roll(
1753
+ mask, shift=-np.floor(np.divide(ret_shape, 2)).astype(int), axis=axis
1754
+ )
1755
+ mask_ret = np.zeros(ret_shape, dtype=bool)
1756
+ mask_ret[tuple(slice(0, x) for x in self.shape)] = 1
1757
+ mask_ret = np.roll(
1758
+ mask_ret,
1759
+ shift=-np.floor(np.divide(self.shape, 2)).astype(int),
1760
+ axis=axis,
1761
+ )
1839
1762
 
1840
- return map_copy
1763
+ arr_ft = np.fft.fftn(self.data)
1764
+ arr_ft *= np.prod(ret_shape) / np.prod(self.shape)
1765
+ ret_ft = np.zeros(ret_shape, dtype=arr_ft.dtype)
1766
+ ret_ft[mask_ret] = arr_ft[mask]
1767
+ ret.data = np.real(np.fft.ifftn(ret_ft))
1768
+
1769
+ ret.sampling_rate = new_sampling_rate
1770
+ return ret
1841
1771
 
1842
1772
  def density_boundary(
1843
1773
  self, weight: float, fraction_surface: float = 0.1, volume_factor: float = 1.21
1844
1774
  ) -> Tuple[float]:
1845
1775
  """
1846
- Computes the density boundary of the current class instance. The density
1776
+ Computes the density boundary of the class instance. The density
1847
1777
  boundary in this setting is defined as minimal and maximal density value
1848
1778
  enclosing a certain ``weight``.
1849
1779
 
@@ -1899,31 +1829,31 @@ class Density:
1899
1829
  self, density_boundaries: Tuple[float], method: str = "ConvexHull"
1900
1830
  ) -> NDArray:
1901
1831
  """
1902
- Calculates the surface coordinates of the current class instance using
1832
+ Calculates the surface coordinates of the class instance using
1903
1833
  different boundary and surface detection methods. This method is relevant
1904
- for determining coordinates used in template matching,
1905
- see :py:class:`tme.matching_exhaustive.FitRefinement`.
1834
+ for determining coordinates used in non-exhaustive template matching,
1835
+ see :py:class:`tme.matching_optimization.optimize_match`.
1906
1836
 
1907
1837
  Parameters
1908
1838
  ----------
1909
1839
  density_boundaries : tuple
1910
- Tuple of two floats with lower and upper bounds of density values
1911
- to be considered on the surface (see :py:meth:`Density.density_boundary`).
1840
+ Lower and upper bound of density values to be considered
1841
+ (can be obtained from :py:meth:`Density.density_boundary`).
1912
1842
  method : str, optional
1913
- Surface coordinates are determined using this method:
1843
+ Method to use for surface coordinate computation
1914
1844
 
1915
1845
  +--------------+-----------------------------------------------------+
1916
- | 'ConvexHull' | Use the lower bound density convex hull vertices. |
1846
+ | ConvexHull | Use the lower bound density convex hull vertices. |
1917
1847
  +--------------+-----------------------------------------------------+
1918
- | 'Weight' | Use all coordinates within ``density_boundaries``. |
1848
+ | Weight | Use all coordinates within ``density_boundaries``. |
1919
1849
  +--------------+-----------------------------------------------------+
1920
- | 'Sobel' | Set densities below the lower bound density to zero |
1850
+ | Sobel | Set densities below the lower bound density to zero |
1921
1851
  | | apply a sobel filter and return density coordinates |
1922
1852
  | | larger than 0.5 times the maximum filter value. |
1923
1853
  +--------------+-----------------------------------------------------+
1924
- | 'Laplace' | Like 'Sobel' but with a laplace filter. |
1854
+ | Laplace | Like 'Sobel', but with a Laplace filter. |
1925
1855
  +--------------+-----------------------------------------------------+
1926
- | 'Minimum' | Like 'Sobel' and 'Laplace' but with a spherical |
1856
+ | Minimum | Like 'Sobel' and 'Laplace' but with a spherical |
1927
1857
  | | minimum filter on the lower density bound. |
1928
1858
  +--------------+-----------------------------------------------------+
1929
1859
 
@@ -1935,15 +1865,11 @@ class Density:
1935
1865
  Returns
1936
1866
  -------
1937
1867
  NDArray
1938
- An array of surface coordinates with shape (number_of_points, dimensions).
1868
+ An array of surface coordinates with shape (points, dimensions).
1939
1869
 
1940
1870
  References
1941
1871
  ----------
1942
- .. [1] Cragnolini T, Sahota H, Joseph AP, Sweeney A, Malhotra S,
1943
- Vasishtan D, Topf M (2021a) TEMPy2: A Python library with
1944
- improved 3D electron microscopy density-fitting and validation
1945
- workflows. Acta Crystallogr Sect D Struct Biol 77:41–47.
1946
- https://doi.org/10.1107/S2059798320014928
1872
+ .. [1] Cragnolini T, et al. (2021) Acta Crys Sect D Struct Biol
1947
1873
 
1948
1874
  See Also
1949
1875
  --------
@@ -1953,12 +1879,12 @@ class Density:
1953
1879
  :py:class:`tme.matching_optimization.Envelope`
1954
1880
  :py:class:`tme.matching_optimization.Chamfer`
1955
1881
  """
1956
- available_methods = ["ConvexHull", "Weight", "Sobel", "Laplace", "Minimum"]
1882
+ _available_methods = ["ConvexHull", "Weight", "Sobel", "Laplace", "Minimum"]
1957
1883
 
1958
- if method not in available_methods:
1884
+ if method not in _available_methods:
1959
1885
  raise ValueError(
1960
1886
  "Argument method has to be one of the following: %s"
1961
- % ", ".join(available_methods)
1887
+ % ", ".join(_available_methods)
1962
1888
  )
1963
1889
 
1964
1890
  lower_bound, upper_bound = density_boundaries
@@ -2006,7 +1932,7 @@ class Density:
2006
1932
  def normal_vectors(self, coordinates: NDArray) -> NDArray:
2007
1933
  """
2008
1934
  Calculates the normal vectors for the given coordinates on the densities
2009
- of the current class instance. If the normal vector to a given coordinate
1935
+ of the class instance. If the normal vector to a given coordinate
2010
1936
  can not be computed, the zero vector is returned instead. The output of this
2011
1937
  function can e.g. be used in
2012
1938
  :py:class:`tme.matching_optimization.NormalVectorScore`.
@@ -2056,12 +1982,7 @@ class Density:
2056
1982
  in_box = np.logical_and(
2057
1983
  coordinates < np.array(self.shape), coordinates >= 0
2058
1984
  ).min(axis=1)
2059
-
2060
- out_of_box = np.invert(in_box)
2061
- if out_of_box.sum() > 0:
2062
- print(coordinates[out_of_box, :])
2063
- raise ValueError("Coordinates outside of self.data detected.")
2064
-
1985
+ coordinates = coordinates[in_box, :]
2065
1986
  for index in range(coordinates.shape[0]):
2066
1987
  point = coordinates[index, :]
2067
1988
  start = np.maximum(point - 1, 0)
@@ -2081,22 +2002,16 @@ class Density:
2081
2002
 
2082
2003
  def core_mask(self) -> NDArray:
2083
2004
  """
2084
- Calculates the weighted core mask of the current class instance.
2085
-
2086
- Core mask is calculated by performing binary erosion on the internal
2087
- data array in an iterative fashion until no non-zero data elements remain.
2088
- In each iteration, all data elements larger than zero are incremented by one
2089
- in a mask with same shape as the internal data array. Therefore,
2090
- data elements in the output array with a value of n remained non-zero for
2091
- n rounds of binary erosion. The higher the value, the more likely a data element
2092
- is part of the core of the density map.
2005
+ Calculates a weighted core mask by performing iterative binary erosion on
2006
+ :py:attr:`Density.data`. In each iteration, all mask elements corresponding
2007
+ to a non-zero data elemnt are incremented by one. Therefore, a mask element
2008
+ with value N corresponds to a data value that remained non-zero for N iterations.
2009
+ Mask elements with high values are likely part of the core density [1]_.
2093
2010
 
2094
2011
  Returns
2095
2012
  -------
2096
2013
  NDArray
2097
- An array with same shape as internal data array. Values contained
2098
- indicate how many rounds of binary erosion were necessary to nullify
2099
- a given data element.
2014
+ Core-weighted mask with shape of :py:attr:`Density.data`.
2100
2015
 
2101
2016
  References
2102
2017
  ----------
@@ -2131,19 +2046,7 @@ class Density:
2131
2046
  NDArray
2132
2047
  Center of mass with shape (arr.ndim).
2133
2048
  """
2134
- cutoff = arr.min() - 1 if cutoff is None else cutoff
2135
- arr = np.where(arr > cutoff, arr, 0)
2136
- denominator = np.sum(arr)
2137
- grids = np.ogrid[tuple(slice(0, i) for i in arr.shape)]
2138
-
2139
- center_of_mass = np.array(
2140
- [
2141
- np.sum(np.multiply(arr, grids[dim].astype(float))) / denominator
2142
- for dim in range(arr.ndim)
2143
- ]
2144
- )
2145
-
2146
- return center_of_mass
2049
+ return NumpyFFTWBackend().center_of_mass(arr, cutoff)
2147
2050
 
2148
2051
  @classmethod
2149
2052
  def match_densities(
@@ -2153,6 +2056,7 @@ class Density:
2153
2056
  cutoff_target: float = 0,
2154
2057
  cutoff_template: float = 0,
2155
2058
  scoring_method: str = "NormalizedCrossCorrelation",
2059
+ **kwargs,
2156
2060
  ) -> Tuple["Density", NDArray, NDArray, NDArray]:
2157
2061
  """
2158
2062
  Aligns two :py:class:`Density` instances target and template and returns
@@ -2177,6 +2081,9 @@ class Density:
2177
2081
  The scoring method to use for alignment. See
2178
2082
  :py:class:`tme.matching_optimization.create_score_object` for available methods,
2179
2083
  by default "NormalizedCrossCorrelation".
2084
+ kwargs : dict, optional
2085
+ Optional keyword arguments passed to
2086
+ :py:meth:`tme.matching_optimization.optimize_match`.
2180
2087
 
2181
2088
  Returns
2182
2089
  -------
@@ -2188,11 +2095,20 @@ class Density:
2188
2095
  -----
2189
2096
  No densities below cutoff_template are present in the returned Density object.
2190
2097
  """
2098
+ from .matching_utils import normalize_template
2191
2099
  from .matching_optimization import optimize_match, create_score_object
2192
2100
 
2101
+ template_mask = template.empty
2102
+ template_mask.data.fill(1)
2103
+
2104
+ normalize_template(
2105
+ template=template.data,
2106
+ mask=template_mask.data,
2107
+ n_observations=template_mask.data.sum(),
2108
+ )
2109
+
2193
2110
  target_sampling_rate = np.array(target.sampling_rate)
2194
2111
  template_sampling_rate = np.array(template.sampling_rate)
2195
-
2196
2112
  target_sampling_rate = np.repeat(
2197
2113
  target_sampling_rate, target.data.ndim // target_sampling_rate.size
2198
2114
  )
@@ -2224,16 +2140,21 @@ class Density:
2224
2140
  ).astype(int)
2225
2141
  template_coordinates += mass_center_difference[:, None]
2226
2142
 
2143
+ coordinates_mask = template_mask.to_pointcloud()
2144
+ coordinates_mask = coordinates_mask * template_scaling[:, None]
2145
+ coordinates_mask += mass_center_difference[:, None]
2146
+
2227
2147
  score_object = create_score_object(
2228
2148
  score=scoring_method,
2229
2149
  target=target.data,
2230
2150
  template_coordinates=template_coordinates,
2151
+ template_mask_coordinates=coordinates_mask,
2231
2152
  template_weights=template_weights,
2232
2153
  sampling_rate=np.ones(template.data.ndim),
2233
2154
  )
2234
2155
 
2235
2156
  translation, rotation_matrix, score = optimize_match(
2236
- score_object=score_object, optimization_method="basinhopping"
2157
+ score_object=score_object, **kwargs
2237
2158
  )
2238
2159
 
2239
2160
  translation += mass_center_difference
@@ -2255,6 +2176,8 @@ class Density:
2255
2176
  template: "Structure",
2256
2177
  cutoff_target: float = 0,
2257
2178
  scoring_method: str = "NormalizedCrossCorrelation",
2179
+ optimization_method: str = "basinhopping",
2180
+ maxiter: int = 500,
2258
2181
  ) -> Tuple["Structure", NDArray, NDArray]:
2259
2182
  """
2260
2183
  Aligns a :py:class:`tme.structure.Structure` template to :py:class:`Density`
@@ -2279,6 +2202,12 @@ class Density:
2279
2202
  The scoring method to use for template matching. See
2280
2203
  :py:class:`tme.matching_optimization.create_score_object` for available methods,
2281
2204
  by default "NormalizedCrossCorrelation".
2205
+ optimization_method : str, optional
2206
+ Optimizer that is used.
2207
+ See :py:meth:`tme.matching_optimization.optimize_match`.
2208
+ maxiter : int, optional
2209
+ Maximum number of iterations for the optimizer.
2210
+ See :py:meth:`tme.matching_optimization.optimize_match`.
2282
2211
 
2283
2212
  Returns
2284
2213
  -------
@@ -2302,66 +2231,22 @@ class Density:
2302
2231
  cutoff_target=cutoff_target,
2303
2232
  cutoff_template=0,
2304
2233
  scoring_method=scoring_method,
2234
+ optimization_method=optimization_method,
2235
+ maxiter=maxiter,
2305
2236
  )
2306
2237
  out = template.copy()
2307
- final_translation = np.add(
2308
- -template_density.origin,
2309
- np.multiply(translation, template_density.sampling_rate),
2310
- )
2238
+ final_translation = np.subtract(ret.origin, template_density.origin)
2311
2239
 
2312
2240
  # Atom coordinates are in xyz
2313
2241
  final_translation = final_translation[::-1]
2314
2242
  rotation_matrix = rotation_matrix[::-1, ::-1]
2315
2243
 
2316
- out.rigid_transform(
2244
+ out = out.rigid_transform(
2317
2245
  translation=final_translation, rotation_matrix=rotation_matrix
2318
2246
  )
2319
2247
 
2320
2248
  return out, final_translation, rotation_matrix
2321
2249
 
2322
- @staticmethod
2323
- def align_coordinate_systems(target: "Density", template: "Density") -> "Density":
2324
- """
2325
- Aligns the coordinate system of `target` and `template`.
2326
-
2327
- Parameters
2328
- ----------
2329
- target : Density
2330
- The target density whose coordinate system should remain unchanged.
2331
- template : Density
2332
- The template density that will be aligned to match the target's
2333
- coordinate system.
2334
-
2335
- Raises
2336
- ------
2337
- ValueError
2338
- If the `sampling_rate` of `target` and `template` do not match.
2339
-
2340
- Returns
2341
- -------
2342
- Density
2343
- A copy of `template` aligned to the coordinate system of `target`.
2344
- The `box_size` and `origin` will match that of `target`.
2345
-
2346
- See Also
2347
- --------
2348
- :py:meth:`Density.match_densities` : To match aligned template to target.
2349
- """
2350
- if not np.allclose(target.sampling_rate, template.sampling_rate):
2351
- raise ValueError("sampling_rate of both maps have to match.")
2352
-
2353
- template = template.copy()
2354
- template.pad(target.shape, center=True)
2355
-
2356
- origin_difference = np.divide(
2357
- np.subtract(template.origin, target.origin), target.sampling_rate
2358
- )
2359
- template = template.rigid_transform(
2360
- rotation_matrix=np.eye(template.data.ndim), translation=origin_difference
2361
- )
2362
- template.origin = target.origin.copy()
2363
- return template
2364
-
2365
2250
  @staticmethod
2366
2251
  def fourier_shell_correlation(density1: "Density", density2: "Density") -> NDArray:
2367
2252
  """
@@ -2418,3 +2303,9 @@ class Density:
2418
2303
  qidx = np.where(qbins < qx.max())
2419
2304
 
2420
2305
  return np.vstack((qbins[qidx], FSC[qidx])).T
2306
+
2307
+
2308
+ def is_gzipped(filename: str) -> bool:
2309
+ """Check if a file is a gzip file by reading its magic number."""
2310
+ with open(filename, "rb") as f:
2311
+ return f.read(2) == b"\x1f\x8b"