pytme 0.1.8__cp311-cp311-macosx_14_0_arm64.whl → 0.2.0__cp311-cp311-macosx_14_0_arm64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. pytme-0.2.0.data/scripts/match_template.py +1019 -0
  2. pytme-0.2.0.data/scripts/postprocess.py +570 -0
  3. {pytme-0.1.8.data → pytme-0.2.0.data}/scripts/preprocessor_gui.py +244 -60
  4. {pytme-0.1.8.dist-info → pytme-0.2.0.dist-info}/METADATA +3 -1
  5. pytme-0.2.0.dist-info/RECORD +72 -0
  6. {pytme-0.1.8.dist-info → pytme-0.2.0.dist-info}/WHEEL +1 -1
  7. scripts/extract_candidates.py +218 -0
  8. scripts/match_template.py +459 -218
  9. pytme-0.1.8.data/scripts/match_template.py → scripts/match_template_filters.py +459 -218
  10. scripts/postprocess.py +380 -435
  11. scripts/preprocessor_gui.py +244 -60
  12. scripts/refine_matches.py +218 -0
  13. tme/__init__.py +2 -1
  14. tme/__version__.py +1 -1
  15. tme/analyzer.py +533 -78
  16. tme/backends/cupy_backend.py +80 -15
  17. tme/backends/npfftw_backend.py +35 -6
  18. tme/backends/pytorch_backend.py +15 -7
  19. tme/density.py +173 -78
  20. tme/extensions.cpython-311-darwin.so +0 -0
  21. tme/matching_constrained.py +195 -0
  22. tme/matching_data.py +78 -32
  23. tme/matching_exhaustive.py +369 -221
  24. tme/matching_memory.py +1 -0
  25. tme/matching_optimization.py +753 -649
  26. tme/matching_utils.py +152 -8
  27. tme/orientations.py +561 -0
  28. tme/preprocessing/__init__.py +2 -0
  29. tme/preprocessing/_utils.py +176 -0
  30. tme/preprocessing/composable_filter.py +30 -0
  31. tme/preprocessing/compose.py +52 -0
  32. tme/preprocessing/frequency_filters.py +322 -0
  33. tme/preprocessing/tilt_series.py +967 -0
  34. tme/preprocessor.py +35 -25
  35. tme/structure.py +2 -37
  36. pytme-0.1.8.data/scripts/postprocess.py +0 -625
  37. pytme-0.1.8.dist-info/RECORD +0 -61
  38. {pytme-0.1.8.data → pytme-0.2.0.data}/scripts/estimate_ram_usage.py +0 -0
  39. {pytme-0.1.8.data → pytme-0.2.0.data}/scripts/preprocess.py +0 -0
  40. {pytme-0.1.8.dist-info → pytme-0.2.0.dist-info}/LICENSE +0 -0
  41. {pytme-0.1.8.dist-info → pytme-0.2.0.dist-info}/entry_points.txt +0 -0
  42. {pytme-0.1.8.dist-info → pytme-0.2.0.dist-info}/top_level.txt +0 -0
tme/orientations.py ADDED
@@ -0,0 +1,561 @@
1
+ #!python3
2
+ """ Handle template matching peaks and convert between formats.
3
+
4
+ Copyright (c) 2024 European Molecular Biology Laboratory
5
+
6
+ Author: Valentin Maurer <valentin.maurer@embl-hamburg.de>
7
+ """
8
+ import re
9
+ from collections import deque
10
+ from dataclasses import dataclass
11
+ from typing import List, Tuple, Dict
12
+
13
+ import numpy as np
14
+ from scipy.spatial.transform import Rotation
15
+
16
+
17
+ @dataclass
18
+ class Orientations:
19
+ """
20
+ Handle template matching peaks and convert between formats.
21
+ """
22
+
23
+ #: Return a numpy array with translations of each orientation (n x d).
24
+ translations: np.ndarray
25
+
26
+ #: Return a numpy array with euler angles of each orientation in zxy format (n x d).
27
+ rotations: np.ndarray
28
+
29
+ #: Return a numpy array with the score of each orientation (n, ).
30
+ scores: np.ndarray
31
+
32
+ #: Return a numpy array with additional orientation details (n, ).
33
+ details: np.ndarray
34
+
35
+ def __iter__(self) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
36
+ """
37
+ Iterate over the current class instance. Each iteration returns a orientation
38
+ defined by its translation, rotation, score and additional detail.
39
+
40
+ Yields
41
+ ------
42
+ Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]
43
+ A tuple of arrays defining the given orientation.
44
+ """
45
+ yield from zip(self.translations, self.rotations, self.scores, self.details)
46
+
47
+ def __getitem__(self, indices: List[int]) -> "Orientations":
48
+ """
49
+ Retrieve a subset of orientations based on the provided indices.
50
+
51
+ Parameters
52
+ ----------
53
+ indices : List[int]
54
+ A list of indices specifying the orientations to be retrieved.
55
+
56
+ Returns
57
+ -------
58
+ :py:class:`Orientations`
59
+ A new :py:class:`Orientations`instance containing only the selected orientations.
60
+ """
61
+ indices = np.asarray(indices)
62
+ attributes = (
63
+ "translations",
64
+ "rotations",
65
+ "scores",
66
+ "details",
67
+ )
68
+ kwargs = {attr: getattr(self, attr)[indices] for attr in attributes}
69
+ return self.__class__(**kwargs)
70
+
71
+ def to_file(self, filename: str, file_format: type = None, **kwargs) -> None:
72
+ """
73
+ Save the current class instance to a file in the specified format.
74
+
75
+ Parameters
76
+ ----------
77
+ filename : str
78
+ The name of the file where the orientations will be saved.
79
+ file_format : type, optional
80
+ The format in which to save the orientations. Supported formats are 'text' and 'relion'.
81
+ **kwargs : dict
82
+ Additional keyword arguments specific to the file format.
83
+
84
+ Raises
85
+ ------
86
+ ValueError
87
+ If an unsupported file format is specified.
88
+ """
89
+ mapping = {
90
+ "text": self._to_text,
91
+ "relion": self._to_relion_star,
92
+ "dynamo": self._to_dynamo_tbl,
93
+ }
94
+ if file_format is None:
95
+ file_format = "text"
96
+ if filename.lower().endswith(".star"):
97
+ file_format = "relion"
98
+ elif filename.lower().endswith(".tbl"):
99
+ file_format = "dynamo"
100
+
101
+ func = mapping.get(file_format, None)
102
+ if func is None:
103
+ raise ValueError(
104
+ f"{file_format} not implemented. Supported are {','.join(mapping.keys())}."
105
+ )
106
+
107
+ return func(filename=filename, **kwargs)
108
+
109
+ def _to_text(self, filename: str) -> None:
110
+ """
111
+ Save orientations in a text file format.
112
+
113
+ Parameters
114
+ ----------
115
+ filename : str
116
+ The name of the file to save the orientations.
117
+
118
+ Notes
119
+ -----
120
+ The file is saved with a header specifying each column: z, y, x, euler_z,
121
+ euler_y, euler_x, score, detail. Each row in the file corresponds to an orientation.
122
+ """
123
+ header = "\t".join(
124
+ ["z", "y", "x", "euler_z", "euler_y", "euler_x", "score", "detail"]
125
+ )
126
+ with open(filename, mode="w", encoding="utf-8") as ofile:
127
+ _ = ofile.write(f"{header}\n")
128
+ for translation, angles, score, detail in self:
129
+ translation_string = "\t".join([str(x) for x in translation])
130
+ angle_string = "\t".join([str(x) for x in angles])
131
+ _ = ofile.write(
132
+ f"{translation_string}\t{angle_string}\t{score}\t{detail}\n"
133
+ )
134
+ return None
135
+
136
+ def _to_dynamo_tbl(
137
+ self,
138
+ filename: str,
139
+ name_prefix: str = None,
140
+ sampling_rate: float = 1.0,
141
+ subtomogram_size: int = 0,
142
+ ) -> None:
143
+ """
144
+ Save orientations in Dynamo's tbl file format.
145
+
146
+ Parameters
147
+ ----------
148
+ filename : str
149
+ The name of the file to save the orientations.
150
+ sampling_rate : float, optional
151
+ Subtomogram sampling rate in angstrom per voxel
152
+
153
+ Notes
154
+ -----
155
+ The file is saved with a standard header used in Dynamo tbl files
156
+ outlined in [1]_. Each row corresponds to a particular partice.
157
+
158
+ References
159
+ ----------
160
+ .. [1] https://wiki.dynamo.biozentrum.unibas.ch/w/index.php/Table
161
+
162
+ The file is saved with a standard header used in Dynamo STAR files.
163
+ Each row in the file corresponds to an orientation.
164
+ """
165
+ with open(filename, mode="w", encoding="utf-8") as ofile:
166
+ for index, (translation, rotation, score, detail) in enumerate(self):
167
+ rotation = Rotation.from_euler("zyx", rotation, degrees=True)
168
+ rotation = rotation.as_euler(seq="xyx", degrees=True)
169
+ out = [
170
+ index,
171
+ 1,
172
+ 0,
173
+ 0,
174
+ 0,
175
+ 0,
176
+ *rotation,
177
+ self.scores[index],
178
+ self.scores[index],
179
+ 0,
180
+ 0,
181
+ # Wedge parameters
182
+ -90,
183
+ 90,
184
+ -60,
185
+ 60,
186
+ 0,
187
+ 0,
188
+ 0,
189
+ 0,
190
+ 0,
191
+ 0,
192
+ # Coordinate in original volume
193
+ *translation[::-1],
194
+ 0,
195
+ 0,
196
+ 0,
197
+ 0,
198
+ 0,
199
+ 0,
200
+ 0,
201
+ 0,
202
+ sampling_rate,
203
+ 3,
204
+ 0,
205
+ 0,
206
+ ]
207
+ _ = ofile.write(" ".join([str(x) for x in out]) + "\n")
208
+
209
+ return None
210
+
211
+ def _to_relion_star(
212
+ self,
213
+ filename: str,
214
+ name_prefix: str = None,
215
+ ctf_image: str = None,
216
+ sampling_rate: float = 1.0,
217
+ subtomogram_size: int = 0,
218
+ ) -> None:
219
+ """
220
+ Save orientations in RELION's STAR file format.
221
+
222
+ Parameters
223
+ ----------
224
+ filename : str
225
+ The name of the file to save the orientations.
226
+ name_prefix : str, optional
227
+ A prefix to add to the image names in the STAR file.
228
+ ctf_image : str, optional
229
+ Path to CTF or wedge mask RELION.
230
+ sampling_rate : float, optional
231
+ Subtomogram sampling rate in angstrom per voxel
232
+ subtomogram_size : int, optional
233
+ Size of the square shaped subtomogram.
234
+
235
+ Notes
236
+ -----
237
+ The file is saved with a standard header used in RELION STAR files.
238
+ Each row in the file corresponds to an orientation.
239
+ """
240
+ optics_header = [
241
+ "# version 30001",
242
+ "data_optics",
243
+ "",
244
+ "loop_",
245
+ "_rlnOpticsGroup",
246
+ "_rlnOpticsGroupName",
247
+ "_rlnSphericalAberration",
248
+ "_rlnVoltage",
249
+ "_rlnImageSize",
250
+ "_rlnImageDimensionality",
251
+ "_rlnImagePixelSize",
252
+ ]
253
+ optics_data = [
254
+ "1",
255
+ "opticsGroup1",
256
+ "2.700000",
257
+ "300.000000",
258
+ str(int(subtomogram_size)),
259
+ "3",
260
+ str(float(sampling_rate)),
261
+ ]
262
+ optics_header = "\n".join(optics_header)
263
+ optics_data = "\t".join(optics_data)
264
+
265
+ header = [
266
+ "data_particles",
267
+ "",
268
+ "loop_",
269
+ "_rlnCoordinateX",
270
+ "_rlnCoordinateY",
271
+ "_rlnCoordinateZ",
272
+ "_rlnImageName",
273
+ "_rlnAngleRot",
274
+ "_rlnAngleTilt",
275
+ "_rlnAnglePsi",
276
+ "_rlnOpticsGroup",
277
+ ]
278
+ if ctf_image is not None:
279
+ header.append("_rlnCtfImage")
280
+
281
+ ctf_image = "" if ctf_image is None else f"\t{ctf_image}"
282
+
283
+ header = "\n".join(header)
284
+ name_prefix = "" if name_prefix is None else name_prefix
285
+
286
+ with open(filename, mode="w", encoding="utf-8") as ofile:
287
+ _ = ofile.write(f"{optics_header}\n")
288
+ _ = ofile.write(f"{optics_data}\n")
289
+
290
+ _ = ofile.write("\n# version 30001\n")
291
+ _ = ofile.write(f"{header}\n")
292
+
293
+ # pyTME uses a zyx data layout
294
+ for index, (translation, rotation, score, detail) in enumerate(self):
295
+ rotation = Rotation.from_euler("zyx", rotation, degrees=True)
296
+ rotation = rotation.as_euler(seq="xyx", degrees=True)
297
+
298
+ translation_string = "\t".join([str(x) for x in translation][::-1])
299
+ angle_string = "\t".join([str(x) for x in rotation])
300
+ name = f"{name_prefix}_{index}.mrc"
301
+ _ = ofile.write(
302
+ f"{translation_string}\t{name}\t{angle_string}\t1{ctf_image}\n"
303
+ )
304
+
305
+ return None
306
+
307
+ @classmethod
308
+ def from_file(
309
+ cls, filename: str, file_format: type = None, **kwargs
310
+ ) -> "Orientations":
311
+ """
312
+ Create an instance of :py:class:`Orientations` from a file.
313
+
314
+ Parameters
315
+ ----------
316
+ filename : str
317
+ The name of the file from which to read the orientations.
318
+ file_format : type, optional
319
+ The format of the file. Currently, only 'text' format is supported.
320
+ **kwargs : dict
321
+ Additional keyword arguments specific to the file format.
322
+
323
+ Returns
324
+ -------
325
+ :py:class:`Orientations`
326
+ An instance of :py:class:`Orientations` populated with data from the file.
327
+
328
+ Raises
329
+ ------
330
+ ValueError
331
+ If an unsupported file format is specified.
332
+ """
333
+ mapping = {"text": cls._from_text, "relion": cls._from_relion_star}
334
+ if file_format is None:
335
+ file_format = "text"
336
+ if filename.lower().endswith(".star"):
337
+ file_format = "relion"
338
+
339
+ func = mapping.get(file_format, None)
340
+ if func is None:
341
+ raise ValueError(
342
+ f"{file_format} not implemented. Supported are {','.join(mapping.keys())}."
343
+ )
344
+
345
+ translations, rotations, scores, details, *_ = func(filename=filename, **kwargs)
346
+ return cls(
347
+ translations=translations,
348
+ rotations=rotations,
349
+ scores=scores,
350
+ details=details,
351
+ )
352
+
353
+ @staticmethod
354
+ def _from_text(
355
+ filename: str,
356
+ ) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
357
+ """
358
+ Read orientations from a text file.
359
+
360
+ Parameters
361
+ ----------
362
+ filename : str
363
+ The name of the file from which to read the orientations.
364
+
365
+ Returns
366
+ -------
367
+ Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]
368
+ A tuple containing numpy arrays for translations, rotations, scores,
369
+ and details.
370
+
371
+ Notes
372
+ -----
373
+ The text file is expected to have a header and data in columns corresponding to
374
+ z, y, x, euler_z, euler_y, euler_x, score, detail.
375
+ """
376
+ with open(filename, mode="r", encoding="utf-8") as infile:
377
+ data = [x.strip().split("\t") for x in infile.read().split("\n")]
378
+ _ = data.pop(0)
379
+
380
+ translation, rotation, score, detail = [], [], [], []
381
+ for candidate in data:
382
+ if len(candidate) <= 1:
383
+ continue
384
+ if len(candidate) != 8:
385
+ candidate.append(-1)
386
+
387
+ candidate = [float(x) for x in candidate]
388
+ translation.append((candidate[0], candidate[1], candidate[2]))
389
+ rotation.append((candidate[3], candidate[4], candidate[5]))
390
+ score.append(candidate[6])
391
+ detail.append(candidate[7])
392
+
393
+ translation = np.vstack(translation).astype(int)
394
+ rotation = np.vstack(rotation).astype(float)
395
+ score = np.array(score).astype(float)
396
+ detail = np.array(detail).astype(float)
397
+
398
+ return translation, rotation, score, detail
399
+
400
+ @staticmethod
401
+ def _parse_star(filename: str, delimiter: str = None) -> Dict:
402
+ pattern = re.compile(r"\s*#.*")
403
+ with open(filename, mode="r", encoding="utf-8") as infile:
404
+ data = infile.read()
405
+
406
+ data = deque(filter(lambda line: line and line[0] != "#", data.split("\n")))
407
+
408
+ ret, category, block = {}, None, []
409
+ while data:
410
+ line = data.popleft()
411
+
412
+ if line.startswith("data") and not line.startswith("_"):
413
+ if category != line and category is not None:
414
+ headers = list(ret[category].keys())
415
+ headers = [pattern.sub("", x) for x in headers]
416
+ ret[category] = {
417
+ header: list(column)
418
+ for header, column in zip(headers, zip(*block))
419
+ }
420
+ block.clear()
421
+ category = line
422
+ if category not in ret:
423
+ ret[category] = {}
424
+ continue
425
+
426
+ if line.startswith("_"):
427
+ ret[category][line] = []
428
+ continue
429
+
430
+ if line.startswith("loop"):
431
+ continue
432
+
433
+ line_split = line.split(delimiter)
434
+ if len(line_split):
435
+ block.append(line_split)
436
+
437
+ headers = list(ret[category].keys())
438
+ headers = [pattern.sub("", x) for x in headers]
439
+ ret[category] = {
440
+ header: list(column) for header, column in zip(headers, zip(*block))
441
+ }
442
+ return ret
443
+
444
+ @classmethod
445
+ def _from_relion_star(
446
+ cls, filename: str, delimiter: str = None
447
+ ) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
448
+ ret = cls._parse_star(filename=filename, delimiter=delimiter)
449
+ ret = ret["data_particles"]
450
+
451
+ translation = (
452
+ np.vstack(
453
+ (ret["_rlnCoordinateZ"], ret["_rlnCoordinateY"], ret["_rlnCoordinateX"])
454
+ )
455
+ .astype(np.float32)
456
+ .astype(int)
457
+ .T
458
+ )
459
+
460
+ rotation = (
461
+ np.vstack((ret["_rlnAngleRot"], ret["_rlnAngleTilt"], ret["_rlnAnglePsi"]))
462
+ .astype(np.float32)
463
+ .T
464
+ )
465
+
466
+ rotation = Rotation.from_euler("xyx", rotation, degrees=True)
467
+ rotation = rotation.as_euler(seq="zyx", degrees=True)
468
+ score = np.ones(translation.shape[0])
469
+ detail = np.ones(translation.shape[0]) * 1
470
+
471
+ return translation, rotation, score, detail
472
+
473
+ def get_extraction_slices(
474
+ self,
475
+ target_shape: Tuple[int],
476
+ extraction_shape: Tuple[int],
477
+ drop_out_of_box: bool = False,
478
+ return_orientations: bool = False,
479
+ ) -> "Orientations":
480
+ """
481
+ Calculate slices for extracting regions of interest within a larger array.
482
+
483
+ Parameters
484
+ ----------
485
+ target_shape : Tuple[int]
486
+ The shape of the target array within which regions are to be extracted.
487
+ extraction_shape : Tuple[int]
488
+ The shape of the regions to be extracted.
489
+ drop_out_of_box : bool, optional
490
+ If True, drop regions that extend beyond the target array boundary, by default False.
491
+ return_orientations : bool, optional
492
+ If True, return orientations along with slices, by default False.
493
+
494
+ Returns
495
+ -------
496
+ Union[Tuple[List[slice]], Tuple["Orientations", List[slice], List[slice]]]
497
+ If return_orientations is False, returns a tuple containing slices for candidate
498
+ regions and observation regions.
499
+ If return_orientations is True, returns a tuple containing orientations along
500
+ with slices for candidate regions and observation regions.
501
+
502
+ Raises
503
+ ------
504
+ SystemExit
505
+ If no peak remains after filtering, indicating an error.
506
+ """
507
+ left_pad = np.divide(extraction_shape, 2).astype(int)
508
+ right_pad = np.add(left_pad, np.mod(extraction_shape, 2)).astype(int)
509
+
510
+ obs_start = np.subtract(self.translations, left_pad)
511
+ obs_stop = np.add(self.translations, right_pad)
512
+
513
+ cand_start = np.subtract(np.maximum(obs_start, 0), obs_start)
514
+ cand_stop = np.subtract(obs_stop, np.minimum(obs_stop, target_shape))
515
+ cand_stop = np.subtract(extraction_shape, cand_stop)
516
+ obs_start = np.maximum(obs_start, 0)
517
+ obs_stop = np.minimum(obs_stop, target_shape)
518
+
519
+ subset = self
520
+ if drop_out_of_box:
521
+ stops = np.subtract(cand_stop, extraction_shape)
522
+ keep_peaks = (
523
+ np.sum(
524
+ np.multiply(cand_start == 0, stops == 0),
525
+ axis=1,
526
+ )
527
+ == self.translations.shape[1]
528
+ )
529
+ n_remaining = keep_peaks.sum()
530
+ if n_remaining == 0:
531
+ print(
532
+ "No peak remaining after filtering. Started with"
533
+ f" {self.translations.shape[0]} filtered to {n_remaining}."
534
+ " Consider reducing min_distance, increase num_peaks or use"
535
+ " a different peak caller."
536
+ )
537
+ exit(-1)
538
+
539
+ cand_start = cand_start[keep_peaks,]
540
+ cand_stop = cand_stop[keep_peaks,]
541
+ obs_start = obs_start[keep_peaks,]
542
+ obs_stop = obs_stop[keep_peaks,]
543
+ subset = self[keep_peaks]
544
+
545
+ cand_start, cand_stop = cand_start.astype(int), cand_stop.astype(int)
546
+ obs_start, obs_stop = obs_start.astype(int), obs_stop.astype(int)
547
+
548
+ candidate_slices = [
549
+ tuple(slice(s, e) for s, e in zip(start_row, stop_row))
550
+ for start_row, stop_row in zip(cand_start, cand_stop)
551
+ ]
552
+
553
+ observation_slices = [
554
+ tuple(slice(s, e) for s, e in zip(start_row, stop_row))
555
+ for start_row, stop_row in zip(obs_start, obs_stop)
556
+ ]
557
+
558
+ if return_orientations:
559
+ return subset, candidate_slices, observation_slices
560
+
561
+ return candidate_slices, observation_slices
@@ -0,0 +1,2 @@
1
+ from .compose import Compose
2
+ from .frequency_filters import BandPassFilter, LinearWhiteningFilter