pytme 0.1.7__cp311-cp311-macosx_14_0_arm64.whl → 0.1.8__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.
@@ -308,6 +308,49 @@ def cam_setup(**kwargs):
308
308
  return corr_setup(**kwargs)
309
309
 
310
310
 
311
+ def _normalize_under_mask(template: NDArray, mask: NDArray, mask_intensity) -> None:
312
+ """
313
+ Standardizes the values in in template by subtracting the mean and dividing by the
314
+ standard deviation based on the elements in mask. Subsequently, the template is
315
+ multiplied by the mask.
316
+
317
+ Parameters
318
+ ----------
319
+ template : NDArray
320
+ The data array to be normalized. This array is modified in-place.
321
+ mask : NDArray
322
+ A boolean array of the same shape as `template`. True values indicate the positions in `template`
323
+ to consider for normalization.
324
+ mask_intensity : float
325
+ Mask intensity used to compute expectations.
326
+
327
+ References
328
+ ----------
329
+ .. [1] T. Hrabe, Y. Chen, S. Pfeffer, L. Kuhn Cuellar, A.-V. Mangold,
330
+ and F. Förster, J. Struct. Biol. 178, 177 (2012).
331
+ .. [2] M. L. Chaillet, G. van der Schot, I. Gubins, S. Roet,
332
+ R. C. Veltkamp, and F. Förster, Int. J. Mol. Sci. 24,
333
+ 13375 (2023)
334
+
335
+ Returns
336
+ -------
337
+ None
338
+ This function modifies `template` in-place and does not return any value.
339
+ """
340
+ masked_mean = backend.sum(backend.multiply(template, mask))
341
+ masked_mean = backend.divide(masked_mean, mask_intensity)
342
+ masked_std = backend.sum(backend.multiply(backend.square(template), mask))
343
+ masked_std = backend.subtract(
344
+ masked_std / mask_intensity, backend.square(masked_mean)
345
+ )
346
+ masked_std = backend.sqrt(backend.maximum(masked_std, 0))
347
+
348
+ backend.subtract(template, masked_mean, out=template)
349
+ backend.divide(template, masked_std, out=template)
350
+ backend.multiply(template, mask, out=template)
351
+ return None
352
+
353
+
311
354
  def flc_setup(
312
355
  rfftn: Callable,
313
356
  irfftn: Callable,
@@ -359,7 +402,8 @@ def flc_setup(
359
402
  ft_target = backend.preallocate_array(fast_ft_shape, complex_dtype)
360
403
  ft_target2 = backend.preallocate_array(fast_ft_shape, complex_dtype)
361
404
  rfftn(target_pad, ft_target)
362
- rfftn(backend.square(target_pad), ft_target2)
405
+ backend.square(target_pad, out=target_pad)
406
+ rfftn(target_pad, ft_target2)
363
407
 
364
408
  # Convert arrays used in subsequent fitting to SharedMemory objects
365
409
  ft_target = backend.arr_to_sharedarr(
@@ -369,13 +413,9 @@ def flc_setup(
369
413
  arr=ft_target2, shared_memory_handler=shared_memory_handler
370
414
  )
371
415
 
372
- template_mask = template_mask > 0
373
- template_mean = backend.mean(template[template_mask])
374
- template_std = backend.std(template[template_mask])
375
- template_mask = backend.astype(template_mask, real_dtype)
376
-
377
- backend.divide(template - template_mean, template_std, out=template)
378
- backend.multiply(template, template_mask, out=template)
416
+ _normalize_under_mask(
417
+ template=template, mask=template_mask, mask_intensity=backend.sum(template_mask)
418
+ )
379
419
 
380
420
  template_buffer = backend.arr_to_sharedarr(
381
421
  arr=template, shared_memory_handler=shared_memory_handler
@@ -455,7 +495,6 @@ def flcSphericalMask_setup(
455
495
  :py:meth:`corr_scoring`
456
496
  """
457
497
  target_pad = backend.topleft_pad(target, fast_shape)
458
- template_mask_pad = backend.topleft_pad(template_mask, fast_shape)
459
498
 
460
499
  # Target and squared target window sums
461
500
  ft_target = backend.preallocate_array(fast_ft_shape, complex_dtype)
@@ -467,16 +506,17 @@ def flcSphericalMask_setup(
467
506
  numerator2 = backend.preallocate_array(1, real_dtype)
468
507
 
469
508
  eps = backend.eps(real_dtype)
470
- n_observations = backend.sum(template_mask_pad > np.exp(-2))
509
+ n_observations = backend.sum(template_mask)
510
+
511
+ template_mask_pad = backend.topleft_pad(template_mask, fast_shape)
471
512
  rfftn(template_mask_pad, ft_template_mask)
472
513
 
473
- # Variance part denominator
514
+ # Denominator E(X^2) - E(X)^2
474
515
  rfftn(backend.square(target_pad), ft_target)
475
516
  backend.multiply(ft_target, ft_template_mask, out=ft_temp)
476
517
  irfftn(ft_temp, temp2)
477
518
  backend.divide(temp2, n_observations, out=temp2)
478
519
 
479
- # Mean part denominator
480
520
  rfftn(target_pad, ft_target)
481
521
  backend.multiply(ft_target, ft_template_mask, out=ft_temp)
482
522
  irfftn(ft_temp, temp)
@@ -495,14 +535,10 @@ def flcSphericalMask_setup(
495
535
  backend.fill(temp2, 0)
496
536
  temp2[nonzero_indices] = 1 / temp[nonzero_indices]
497
537
 
498
- template_mask = template_mask > np.exp(-2)
499
- template_mean = backend.mean(template[template_mask])
500
- template_std = backend.std(template[template_mask])
501
-
502
- template = backend.divide(backend.subtract(template, template_mean), template_std)
503
- backend.multiply(template, template_mask, out=template)
538
+ _normalize_under_mask(
539
+ template=template, mask=template_mask, mask_intensity=backend.sum(template_mask)
540
+ )
504
541
 
505
- # Convert arrays used in subsequent fitting to SharedMemory objects
506
542
  template_buffer = backend.arr_to_sharedarr(
507
543
  arr=template, shared_memory_handler=shared_memory_handler
508
544
  )
@@ -773,6 +809,7 @@ def corr_scoring(
773
809
  fourier_shift = callback_class_args.get("fourier_shift", backend.zeros(arr.ndim))
774
810
  fourier_shift_scores = backend.sum(fourier_shift != 0) != 0
775
811
 
812
+ template_sum = backend.sum(template)
776
813
  for index in range(rotations.shape[0]):
777
814
  rotation = rotations[index]
778
815
  backend.fill(arr, 0)
@@ -783,6 +820,9 @@ def corr_scoring(
783
820
  use_geometric_center=False,
784
821
  order=interpolation_order,
785
822
  )
823
+ rotation_norm = template_sum / backend.sum(arr)
824
+ backend.multiply(arr, rotation_norm, out=arr)
825
+
786
826
  rfftn(arr, ft_temp)
787
827
  template_filter_func(ft_temp, template_filter, out=ft_temp)
788
828
 
@@ -905,6 +945,7 @@ def flc_scoring(
905
945
  fourier_shift = callback_class_args.get("fourier_shift", backend.zeros(arr.ndim))
906
946
  fourier_shift_scores = backend.sum(fourier_shift != 0) != 0
907
947
 
948
+ unpadded_slice = tuple(slice(0, stop) for stop in template.shape)
908
949
  for index in range(rotations.shape[0]):
909
950
  rotation = rotations[index]
910
951
  backend.fill(arr, 0)
@@ -918,8 +959,15 @@ def flc_scoring(
918
959
  use_geometric_center=False,
919
960
  order=interpolation_order,
920
961
  )
962
+ # Given the amount of FFTs, might aswell normalize properly
921
963
  n_observations = backend.sum(temp)
922
964
 
965
+ _normalize_under_mask(
966
+ template=arr[unpadded_slice],
967
+ mask=temp[unpadded_slice],
968
+ mask_intensity=n_observations,
969
+ )
970
+
923
971
  rfftn(temp, ft_temp)
924
972
 
925
973
  backend.multiply(ft_target, ft_temp, out=ft_denom)
tme/matching_utils.py CHANGED
@@ -763,10 +763,14 @@ def euler_to_rotationmatrix(angles: Tuple[float]) -> NDArray:
763
763
  NDArray
764
764
  The generated rotation matrix.
765
765
  """
766
- if len(angles) == 1:
766
+ n_angles = len(angles)
767
+ angle_convention = "zyx"[:n_angles]
768
+ if n_angles == 1:
767
769
  angles = (angles, 0, 0)
768
770
  rotation_matrix = (
769
- Rotation.from_euler("zyx", angles, degrees=True).as_matrix().astype(np.float32)
771
+ Rotation.from_euler(angle_convention, angles, degrees=True)
772
+ .as_matrix()
773
+ .astype(np.float32)
770
774
  )
771
775
  return rotation_matrix
772
776
 
@@ -1052,7 +1056,7 @@ def tube_mask(
1052
1056
  symmetry_axis : int
1053
1057
  The axis of symmetry for the tube.
1054
1058
  base_center : tuple
1055
- Center of the base circle of the tube.
1059
+ Center of the tube.
1056
1060
  inner_radius : float
1057
1061
  Inner radius of the tube.
1058
1062
  outer_radius : float
@@ -1068,8 +1072,9 @@ def tube_mask(
1068
1072
  Raises
1069
1073
  ------
1070
1074
  ValueError
1071
- If the inner radius is larger than the outer radius. Or height is larger
1072
- than the symmetry axis shape.
1075
+ If the inner radius is larger than the outer radius, height is larger
1076
+ than the symmetry axis shape, or if base_center and shape do not have the
1077
+ same length.
1073
1078
  """
1074
1079
  if inner_radius > outer_radius:
1075
1080
  raise ValueError("inner_radius should be smaller than outer_radius.")
@@ -1080,8 +1085,11 @@ def tube_mask(
1080
1085
  if symmetry_axis > len(shape):
1081
1086
  raise ValueError(f"symmetry_axis can be not larger than {len(shape)}.")
1082
1087
 
1088
+ if len(base_center) != len(shape):
1089
+ raise ValueError("shape and base_center need to have the same length.")
1090
+
1083
1091
  circle_shape = tuple(b for ix, b in enumerate(shape) if ix != symmetry_axis)
1084
- base_center = tuple(b for ix, b in enumerate(base_center) if ix != symmetry_axis)
1092
+ circle_center = tuple(b for ix, b in enumerate(base_center) if ix != symmetry_axis)
1085
1093
 
1086
1094
  inner_circle = np.zeros(circle_shape)
1087
1095
  outer_circle = np.zeros_like(inner_circle)
@@ -1090,34 +1098,39 @@ def tube_mask(
1090
1098
  mask_type="ellipse",
1091
1099
  shape=circle_shape,
1092
1100
  radius=inner_radius,
1093
- center=base_center,
1101
+ center=circle_center,
1094
1102
  )
1095
1103
  if outer_radius > 0:
1096
1104
  outer_circle = create_mask(
1097
1105
  mask_type="ellipse",
1098
1106
  shape=circle_shape,
1099
1107
  radius=outer_radius,
1100
- center=base_center,
1108
+ center=circle_center,
1101
1109
  )
1102
1110
  circle = outer_circle - inner_circle
1103
1111
  circle = np.expand_dims(circle, axis=symmetry_axis)
1104
1112
 
1105
- center = shape[symmetry_axis] // 2
1106
- start_idx = center - height // 2
1107
- stop_idx = center + height // 2 + height % 2
1113
+ center = base_center[symmetry_axis]
1114
+ start_idx = int(center - height // 2)
1115
+ stop_idx = int(center + height // 2 + height % 2)
1116
+
1117
+ start_idx, stop_idx = max(start_idx, 0), min(stop_idx, shape[symmetry_axis])
1108
1118
 
1109
1119
  slice_indices = tuple(
1110
1120
  slice(None) if i != symmetry_axis else slice(start_idx, stop_idx)
1111
1121
  for i in range(len(shape))
1112
1122
  )
1113
1123
  tube = np.zeros(shape)
1114
- tube[slice_indices] = np.repeat(circle, height, axis=symmetry_axis)
1124
+ tube[slice_indices] = circle
1115
1125
 
1116
1126
  return tube
1117
1127
 
1118
1128
 
1119
1129
  def scramble_phases(
1120
- arr: NDArray, noise_proportion: float = 0.5, seed: int = 42
1130
+ arr: NDArray,
1131
+ noise_proportion: float = 0.5,
1132
+ seed: int = 42,
1133
+ normalize_power: bool = True,
1121
1134
  ) -> NDArray:
1122
1135
  """
1123
1136
  Applies random phase scrambling to a given array.
@@ -1135,6 +1148,8 @@ def scramble_phases(
1135
1148
  The proportion of noise in the phase scrambling, by default 0.5.
1136
1149
  seed : int, optional
1137
1150
  The seed for the random phase scrambling, by default 42.
1151
+ normalize_power : bool, optional
1152
+ Whether the returned template should have the same sum of squares as arr.
1138
1153
 
1139
1154
  Returns
1140
1155
  -------
@@ -1158,6 +1173,17 @@ def scramble_phases(
1158
1173
  ph_noise = np.random.permutation(ph)
1159
1174
  ph_new = ph * (1 - noise_proportion) + ph_noise * noise_proportion
1160
1175
  ret = np.real(np.fft.ifftn(amp * np.exp(1j * ph_new)))
1176
+
1177
+ if normalize_power:
1178
+ np.divide(
1179
+ np.subtract(ret, ret.min()), np.subtract(ret.max(), ret.min()), out=ret
1180
+ )
1181
+ np.multiply(ret, np.subtract(arr.max(), arr.min()), out=ret)
1182
+ np.add(ret, arr.min(), out=ret)
1183
+
1184
+ scaling = np.divide(np.abs(arr).sum(), np.abs(ret).sum())
1185
+ np.multiply(ret, scaling, out=ret)
1186
+
1161
1187
  return ret
1162
1188
 
1163
1189
 
tme/preprocessor.py CHANGED
@@ -1055,8 +1055,8 @@ class Preprocessor:
1055
1055
  be equivalent to the following
1056
1056
 
1057
1057
  >>> wedge = Preprocessor().continuous_wedge_mask(
1058
- >>> shape = (50,50,50),
1059
- >>> start_tilt = 50,
1058
+ >>> shape=(50,50,50),
1059
+ >>> start_tilt=50,
1060
1060
  >>> stop_tilt=55,
1061
1061
  >>> tilt_axis=1,
1062
1062
  >>> omit_negative_frequencies=False,
@@ -1074,6 +1074,7 @@ class Preprocessor:
1074
1074
 
1075
1075
  See Also
1076
1076
  --------
1077
+ :py:meth:`Preprocessor.step_wedge_mask`
1077
1078
  :py:meth:`Preprocessor.continuous_wedge_mask`
1078
1079
  """
1079
1080
  opening_axes = np.asarray(opening_axes)
@@ -1124,6 +1125,112 @@ class Preprocessor:
1124
1125
 
1125
1126
  return wedge_volume
1126
1127
 
1128
+ def step_wedge_mask(
1129
+ self,
1130
+ start_tilt: float,
1131
+ stop_tilt: float,
1132
+ tilt_step: float,
1133
+ shape: Tuple[int],
1134
+ opening_axis: int = 0,
1135
+ tilt_axis: int = 2,
1136
+ sigma: float = 0,
1137
+ omit_negative_frequencies: bool = True,
1138
+ ) -> NDArray:
1139
+ """
1140
+ Create a wedge mask with the same shape as template by rotating a
1141
+ plane according to tilt angles. The DC component of the filter is at the origin.
1142
+
1143
+ Parameters
1144
+ ----------
1145
+ start_tilt : float
1146
+ Starting tilt angle in degrees, e.g. a stage tilt of 70 degrees
1147
+ would yield a start_tilt value of 70.
1148
+ stop_tilt : float
1149
+ Ending tilt angle in degrees, , e.g. a stage tilt of -70 degrees
1150
+ would yield a stop_tilt value of 70.
1151
+ tilt_step : float
1152
+ Angle between the different tilt planes.
1153
+ shape : Tuple of ints
1154
+ Shape of the output wedge array.
1155
+ tilt_axis : int, optional
1156
+ Axis that the plane is tilted over.
1157
+ - 0 for Z-axis
1158
+ - 1 for Y-axis
1159
+ - 2 for X-axis
1160
+ opening_axis : int, optional
1161
+ Axis running through the void defined by the wedge.
1162
+ - 0 for Z-axis
1163
+ - 1 for Y-axis
1164
+ - 2 for X-axis
1165
+ sigma : float, optional
1166
+ Standard deviation for Gaussian kernel used for smoothing the wedge.
1167
+ omit_negative_frequencies : bool, optional
1168
+ Whether the wedge mask should omit negative frequencies, i.e. be
1169
+ applicable to symmetric Fourier transforms (see :obj:`numpy.fft.fftn`)
1170
+
1171
+ Returns
1172
+ -------
1173
+ NDArray
1174
+ A numpy array containing the wedge mask.
1175
+
1176
+ Notes
1177
+ -----
1178
+ This function is equivalent to :py:meth:`Preprocessor.wedge_mask`, but much faster
1179
+ for large shapes because it only considers a single tilt angle rather than the rotation
1180
+ of an N-1 dimensional hyperplane in N dimensions.
1181
+
1182
+ See Also
1183
+ --------
1184
+ :py:meth:`Preprocessor.wedge_mask`
1185
+ :py:meth:`Preprocessor.continuous_wedge_mask`
1186
+ """
1187
+ tilt_angles = np.arange(-start_tilt, stop_tilt + tilt_step, tilt_step)
1188
+ plane = np.zeros((shape[opening_axis], shape[tilt_axis]), dtype=np.float32)
1189
+ subset = tuple(
1190
+ slice(None) if i != 0 else slice(x // 2, x // 2 + 1)
1191
+ for i, x in enumerate(plane.shape)
1192
+ )
1193
+ plane[subset] = 1
1194
+ plane_rotated, wedge_volume = np.zeros_like(plane), np.zeros_like(plane)
1195
+ for index in range(tilt_angles.shape[0]):
1196
+ plane_rotated.fill(0)
1197
+
1198
+ rotation_matrix = euler_to_rotationmatrix((tilt_angles[index], 0))
1199
+ rotation_matrix = rotation_matrix[np.ix_((0, 1), (0, 1))]
1200
+
1201
+ Density.rotate_array(
1202
+ arr=plane,
1203
+ rotation_matrix=rotation_matrix,
1204
+ out=plane_rotated,
1205
+ use_geometric_center=True,
1206
+ order=1,
1207
+ )
1208
+ wedge_volume += plane_rotated
1209
+
1210
+ wedge_volume = self.gaussian_filter(
1211
+ template=wedge_volume, sigma=sigma, fourier=False
1212
+ )
1213
+ wedge_volume = np.where(wedge_volume > np.exp(-2), 1, 0)
1214
+
1215
+ if opening_axis > tilt_axis:
1216
+ wedge_volume = np.moveaxis(wedge_volume, 1, 0)
1217
+
1218
+ reshape_dimensions = tuple(
1219
+ x if i in (opening_axis, tilt_axis) else 1 for i, x in enumerate(shape)
1220
+ )
1221
+
1222
+ wedge_volume = wedge_volume.reshape(reshape_dimensions)
1223
+ tile_dimensions = np.divide(shape, reshape_dimensions).astype(int)
1224
+ wedge_volume = np.tile(wedge_volume, tile_dimensions)
1225
+
1226
+ wedge_volume = np.fft.ifftshift(wedge_volume)
1227
+
1228
+ if omit_negative_frequencies:
1229
+ stop = 1 + (wedge_volume.shape[-1] // 2)
1230
+ wedge_volume = wedge_volume[..., :stop]
1231
+
1232
+ return wedge_volume
1233
+
1127
1234
  def continuous_wedge_mask(
1128
1235
  self,
1129
1236
  start_tilt: float,
@@ -1192,6 +1299,7 @@ class Preprocessor:
1192
1299
  See Also
1193
1300
  --------
1194
1301
  :py:meth:`Preprocessor.wedge_mask`
1302
+ :py:meth:`Preprocessor.step_wedge_mask`
1195
1303
  """
1196
1304
  shape_center = np.divide(shape, 2).astype(int)
1197
1305
 
@@ -1348,7 +1456,7 @@ class LinearWhiteningFilter:
1348
1456
  def filter(
1349
1457
  self, template: NDArray, n_bins: int = None
1350
1458
  ) -> Tuple[NDArray, NDArray, NDArray]:
1351
- max_bins = int(np.linalg.norm(template.shape) // 2 + 1)
1459
+ max_bins = np.max(template.shape) // 2 + 1
1352
1460
  n_bins = max_bins if n_bins is None else n_bins
1353
1461
  n_bins = int(min(n_bins, max_bins))
1354
1462
 
@@ -1371,7 +1479,7 @@ class LinearWhiteningFilter:
1371
1479
  np.multiply(fourier_transform, radial_averages[bins], out=fourier_transform)
1372
1480
 
1373
1481
  ret = np.fft.irfftn(
1374
- np.fft.ifftshift(fourier_transform, axes=fft_shift_axes)
1482
+ np.fft.ifftshift(fourier_transform, axes=fft_shift_axes), s=template.shape
1375
1483
  ).real
1376
1484
  return ret, bin_edges, radial_averages
1377
1485
 
@@ -1389,7 +1497,7 @@ class LinearWhiteningFilter:
1389
1497
  bins = np.digitize(frequency_grid, bins=bin_edges, right=True)
1390
1498
  np.multiply(fourier_transform, radial_averages[bins], out=fourier_transform)
1391
1499
  ret = np.fft.irfftn(
1392
- np.fft.ifftshift(fourier_transform, axes=fft_shift_axes)
1500
+ np.fft.ifftshift(fourier_transform, axes=fft_shift_axes), s=template.shape
1393
1501
  ).real
1394
1502
 
1395
1503
  return ret
File without changes
File without changes