From 2ce4d5876068526a21d0d7e5c7058b070d4f5a72 Mon Sep 17 00:00:00 2001 From: Marten <58044494+McHaillet@users.noreply.github.com> Date: Thu, 2 Nov 2023 09:01:35 +0100 Subject: [PATCH 01/15] apply rotation symmetry in extraction threshold estimation --- src/pytom_tm/extract.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pytom_tm/extract.py b/src/pytom_tm/extract.py index 3437efa6..ec721f69 100644 --- a/src/pytom_tm/extract.py +++ b/src/pytom_tm/extract.py @@ -61,7 +61,7 @@ def extract_particles( search_space = ( # wherever the score volume has not been explicitly set to -1 is the size of the search region (score_volume > -1).sum() * - job.n_rotations + int(np.ceil(job.n_rotations / job.rotational_symmetry)) ) cut_off = erfcinv((2 * n_false_positives) / search_space) * np.sqrt(2) * sigma logging.info(f'cut off for particle extraction: {cut_off}') From 5b1247608e9331e44146efd6e9e37672edbc716b Mon Sep 17 00:00:00 2001 From: Marten <58044494+McHaillet@users.noreply.github.com> Date: Thu, 2 Nov 2023 09:02:21 +0100 Subject: [PATCH 02/15] update to 0.3.1 for bug fix --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 94e15cd4..93f5b288 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ name='pytom-template-matching-gpu', packages=['pytom_tm', 'pytom_tm.angle_lists'], package_dir={'': 'src'}, - version='0.3.0', # for versioning definition see https://semver.org/ + version='0.3.1', # for versioning definition see https://semver.org/ description='GPU template matching from PyTOM as a lightweight pip package', long_description=long_description, long_description_content_type='text/markdown', From 502abb03a9bcf5f55570a3039332b030ed0cb416 Mon Sep 17 00:00:00 2001 From: Marten <58044494+McHaillet@users.noreply.github.com> Date: Thu, 2 Nov 2023 10:11:47 +0100 Subject: [PATCH 03/15] angle sorting is crucial for symmetry --- src/pytom_tm/angles.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pytom_tm/angles.py b/src/pytom_tm/angles.py index d5d9be4f..5a52e376 100644 --- a/src/pytom_tm/angles.py +++ b/src/pytom_tm/angles.py @@ -26,6 +26,7 @@ def load_angle_list(file_name: pathlib.Path) -> list[tuple[float, float, float]] with open(str(file_name)) as fstream: lines = fstream.readlines() angle_list = [tuple(map(float, x.strip().split(' '))) for x in lines] + angle_list.sort(key=lambda x: x[0]) # angle list needs to be sorted otherwise symmetry reduction cannot be used! if not all([len(a) == 3 for a in angle_list]): raise ValueError('Invalid angle file provided, each line should have 3 ZXZ Euler angles!') else: From 402636fe5208f7b8a83b94f417c742ba5cb760f2 Mon Sep 17 00:00:00 2001 From: McHaillet Date: Thu, 2 Nov 2023 11:54:40 +0100 Subject: [PATCH 04/15] sorting key is not neccesary + sort after checking if list is valid --- src/pytom_tm/angles.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pytom_tm/angles.py b/src/pytom_tm/angles.py index 5a52e376..784e61e2 100644 --- a/src/pytom_tm/angles.py +++ b/src/pytom_tm/angles.py @@ -26,10 +26,10 @@ def load_angle_list(file_name: pathlib.Path) -> list[tuple[float, float, float]] with open(str(file_name)) as fstream: lines = fstream.readlines() angle_list = [tuple(map(float, x.strip().split(' '))) for x in lines] - angle_list.sort(key=lambda x: x[0]) # angle list needs to be sorted otherwise symmetry reduction cannot be used! if not all([len(a) == 3 for a in angle_list]): raise ValueError('Invalid angle file provided, each line should have 3 ZXZ Euler angles!') else: + angle_list.sort() # angle list needs to be sorted otherwise symmetry reduction cannot be used! return angle_list From e5dc0b755b9501d6e5516e5b7f0d94b514460807 Mon Sep 17 00:00:00 2001 From: McHaillet Date: Thu, 2 Nov 2023 12:17:44 +0100 Subject: [PATCH 05/15] fix unittests after new sorting --- tests/test_template_matching.py | 2 +- tests/test_tmjob.py | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/test_template_matching.py b/tests/test_template_matching.py index 62befd5c..802822ff 100644 --- a/tests/test_template_matching.py +++ b/tests/test_template_matching.py @@ -40,7 +40,7 @@ def test_search(self): self.assertEqual(angle_id, angle_volume[ind]) self.assertSequenceEqual(loc, ind) self.assertEqual(stats['search_space'], 256000000, msg='Search space should exactly equal this value') - self.assertAlmostEqual(stats['std'], 0.005175, places=6, + self.assertAlmostEqual(stats['std'], 0.005175, places=5, msg='Standard deviation of the search should be almost equal') diff --git a/tests/test_tmjob.py b/tests/test_tmjob.py index 3f0a9a19..64ddcf25 100644 --- a/tests/test_tmjob.py +++ b/tests/test_tmjob.py @@ -172,7 +172,9 @@ def test_tm_job_split_volume(self): score, angle = self.job.merge_sub_jobs() ind = np.unravel_index(score.argmax(), score.shape) - self.assertTrue(score.max() > 0.934, msg='lcc max value lower than expected') + print(score.max()) + + self.assertTrue(score.max() > 0.931, msg='lcc max value lower than expected') self.assertEqual(ANGLE_ID, angle[ind]) self.assertSequenceEqual(LOCATION, ind) @@ -204,7 +206,7 @@ def test_tm_job_split_angles(self): score, angle = self.job.merge_sub_jobs() ind = np.unravel_index(score.argmax(), score.shape) - self.assertTrue(score.max() > 0.934, msg='lcc max value lower than expected') + self.assertTrue(score.max() > 0.931, msg='lcc max value lower than expected') self.assertEqual(ANGLE_ID, angle[ind]) self.assertSequenceEqual(LOCATION, ind) @@ -217,7 +219,7 @@ def test_parallel_manager(self): score, angle = run_job_parallel(self.job, volume_splits=(1, 3, 1), gpu_ids=[0]) ind = np.unravel_index(score.argmax(), score.shape) - self.assertTrue(score.max() > 0.934, msg='lcc max value lower than expected') + self.assertTrue(score.max() > 0.931, msg='lcc max value lower than expected') self.assertEqual(ANGLE_ID, angle[ind]) self.assertSequenceEqual(LOCATION, ind) From 7c4aba5cf16316790cc376c9ae55da538eee9b95 Mon Sep 17 00:00:00 2001 From: McHaillet Date: Thu, 2 Nov 2023 12:18:26 +0100 Subject: [PATCH 06/15] remove print line --- tests/test_tmjob.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/test_tmjob.py b/tests/test_tmjob.py index 64ddcf25..5ea84732 100644 --- a/tests/test_tmjob.py +++ b/tests/test_tmjob.py @@ -172,8 +172,6 @@ def test_tm_job_split_volume(self): score, angle = self.job.merge_sub_jobs() ind = np.unravel_index(score.argmax(), score.shape) - print(score.max()) - self.assertTrue(score.max() > 0.931, msg='lcc max value lower than expected') self.assertEqual(ANGLE_ID, angle[ind]) self.assertSequenceEqual(LOCATION, ind) From be667a653868e6929cfe89a92515d87ece16962c Mon Sep 17 00:00:00 2001 From: McHaillet Date: Thu, 2 Nov 2023 12:23:49 +0100 Subject: [PATCH 07/15] added note to README about rotational symmetry update --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 8a58cf01..c8985163 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,9 @@ PyTOM's GPU template matching as a single pip plugin that can only be run from t The full PyTOM repository can be found at: https://github.com/SBC-Utrecht/PyTom +**NOTE:** Extracting particles from score/angle maps in versions >= 0.3.1 is incompatible with +results calculated in versions < 0.3.1 due to the rotational symmetry update. + ## Requires ``` From 74fde2e506d0c2d3c70229e7dd63eee7e5b15c6d Mon Sep 17 00:00:00 2001 From: McHaillet Date: Thu, 2 Nov 2023 13:26:43 +0100 Subject: [PATCH 08/15] rotational symmetry only possible around z-axis --- src/bin/pytom_match_template.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/bin/pytom_match_template.py b/src/bin/pytom_match_template.py index 26dcb7fe..e7a9bb94 100644 --- a/src/bin/pytom_match_template.py +++ b/src/bin/pytom_match_template.py @@ -41,9 +41,10 @@ def main(): 'Alternatively, a .txt file can be provided with three Euler angles (in radians) per ' 'line that define the angular search. Angle format is ZXZ anti-clockwise (see: ' 'https://www.ccpem.ac.uk/user_help/rotation_conventions.php).') - parser.add_argument('--rotational-symmetry', type=int, required=False, action=LargerThanZero, default=1, - help='Integer value indicating the rotational symmetry of the template. The length of the ' - 'rotation search will be shortened through division by this value.') + parser.add_argument('--z-axis-rotational-symmetry', type=int, required=False, action=LargerThanZero, default=1, + help='Integer value indicating the rotational symmetry of the template around the z-axis. The ' + 'length of the rotation search will be shortened through division by this value. Only ' + 'works for template symmetry around the z-axis.') parser.add_argument('-s', '--volume-split', nargs=3, type=int, required=False, default=[1, 1, 1], help='Split the volume into smaller parts for the search, can be relevant if the volume does ' 'not fit into GPU memory. Format is x y z, e.g. --volume-split 1 2 1') From 57323e41ed96b37417a9ad4259a18fb7690ba4ec Mon Sep 17 00:00:00 2001 From: Marten <58044494+McHaillet@users.noreply.github.com> Date: Thu, 2 Nov 2023 16:38:58 +0100 Subject: [PATCH 09/15] Update pytom_match_template.py --- src/bin/pytom_match_template.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bin/pytom_match_template.py b/src/bin/pytom_match_template.py index e7a9bb94..e240f9c7 100644 --- a/src/bin/pytom_match_template.py +++ b/src/bin/pytom_match_template.py @@ -126,7 +126,7 @@ def main(): dose_accumulation=args.dose_accumulation, ctf_data=ctf_params, whiten_spectrum=args.spectral_whitening, - rotational_symmetry=args.rotational_symmetry, + rotational_symmetry=args.z_axis_rotational_symmetry, ) score_volume, angle_volume = run_job_parallel(job, tuple(args.volume_split), args.gpu_ids) From c3711a17e75d4f8e72e780e3fdbea2dab012aaf5 Mon Sep 17 00:00:00 2001 From: McHaillet Date: Fri, 3 Nov 2023 11:35:54 +0100 Subject: [PATCH 10/15] remove readme warning --- README.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/README.md b/README.md index c8985163..8a58cf01 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,6 @@ PyTOM's GPU template matching as a single pip plugin that can only be run from t The full PyTOM repository can be found at: https://github.com/SBC-Utrecht/PyTom -**NOTE:** Extracting particles from score/angle maps in versions >= 0.3.1 is incompatible with -results calculated in versions < 0.3.1 due to the rotational symmetry update. - ## Requires ``` From 8f70260739a20444e822b6514a916c77c88ae15d Mon Sep 17 00:00:00 2001 From: McHaillet Date: Fri, 3 Nov 2023 11:53:23 +0100 Subject: [PATCH 11/15] added version to the job and made version loading more dynamic --- setup.py | 8 ++++++-- src/pytom_tm/__init__.py | 2 ++ src/pytom_tm/_version.py | 1 + src/pytom_tm/tmjob.py | 9 ++++++++- 4 files changed, 17 insertions(+), 3 deletions(-) create mode 100644 src/pytom_tm/_version.py diff --git a/setup.py b/setup.py index 93f5b288..1a4b788d 100644 --- a/setup.py +++ b/setup.py @@ -1,5 +1,8 @@ import setuptools +# get the version number +exec(open('src/pytom_tm/_version.py').read()) + # readme fetch with open('README.md', 'r') as f: long_description = f.read() @@ -8,7 +11,7 @@ name='pytom-template-matching-gpu', packages=['pytom_tm', 'pytom_tm.angle_lists'], package_dir={'': 'src'}, - version='0.3.1', # for versioning definition see https://semver.org/ + version=__version__, # for versioning definition see https://semver.org/ description='GPU template matching from PyTOM as a lightweight pip package', long_description=long_description, long_description_content_type='text/markdown', @@ -24,7 +27,8 @@ 'tqdm', 'mrcfile', 'starfile', - 'importlib_resources' + 'importlib_resources', + 'packaging', ], extras_require={ 'plotting': ['matplotlib', 'seaborn'] diff --git a/src/pytom_tm/__init__.py b/src/pytom_tm/__init__.py index 2b5a53b7..7aee39ef 100644 --- a/src/pytom_tm/__init__.py +++ b/src/pytom_tm/__init__.py @@ -1,3 +1,5 @@ +from ._version import __version__ + try: import cupy except (ModuleNotFoundError, ImportError): diff --git a/src/pytom_tm/_version.py b/src/pytom_tm/_version.py new file mode 100644 index 00000000..e1424ed0 --- /dev/null +++ b/src/pytom_tm/_version.py @@ -0,0 +1 @@ +__version__ = '0.3.1' diff --git a/src/pytom_tm/tmjob.py b/src/pytom_tm/tmjob.py index 6cd56ca9..f682535a 100644 --- a/src/pytom_tm/tmjob.py +++ b/src/pytom_tm/tmjob.py @@ -1,4 +1,5 @@ from __future__ import annotations +from pytom_tm import __version__ import pathlib import copy import numpy as np @@ -39,6 +40,8 @@ def load_json_to_tmjob(file_name: pathlib.Path) -> TMJob: ctf_data=data.get('ctf_data', None), whiten_spectrum=data.get('whiten_spectrum', False), rotational_symmetry=data.get('rotational_symmetry', 1), + # if version number is not in the .json, it must be 0.3.0 or older + pytom_tm_version_number=data.get('pytom_tm_version_number', '0.3.0'), ) job.rotation_file = pathlib.Path(data['rotation_file']) job.whole_start = data['whole_start'] @@ -79,7 +82,8 @@ def __init__( dose_accumulation: Optional[list[float, ...]] = None, ctf_data: Optional[list[dict, ...]] = None, whiten_spectrum: bool = False, - rotational_symmetry: int = 1 + rotational_symmetry: int = 1, + pytom_tm_version_number: str = __version__ ): self.mask = mask self.mask_is_spherical = mask_is_spherical @@ -188,6 +192,9 @@ def __init__( self.log_level = log_level + # version number of the job + self.pytom_tm_version_number = pytom_tm_version_number + def copy(self) -> TMJob: return copy.deepcopy(self) From 636fa60392475dd3f8850ff0c97f8d1a06351da4 Mon Sep 17 00:00:00 2001 From: McHaillet Date: Fri, 3 Nov 2023 13:25:46 +0100 Subject: [PATCH 12/15] add sorting option to angles based on version number; whitening filter is now only calcualted if not detected in the output dir, otherwise neesds to be recalculated for every job init --- src/pytom_tm/angles.py | 5 +++-- src/pytom_tm/extract.py | 5 ++++- src/pytom_tm/tmjob.py | 16 ++++++++++------ 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/src/pytom_tm/angles.py b/src/pytom_tm/angles.py index 784e61e2..086a0f5f 100644 --- a/src/pytom_tm/angles.py +++ b/src/pytom_tm/angles.py @@ -22,14 +22,15 @@ v[0] = ANGLE_LIST_DIR.joinpath(v[0]) -def load_angle_list(file_name: pathlib.Path) -> list[tuple[float, float, float]]: +def load_angle_list(file_name: pathlib.Path, sort_angles: bool = True) -> list[tuple[float, float, float]]: with open(str(file_name)) as fstream: lines = fstream.readlines() angle_list = [tuple(map(float, x.strip().split(' '))) for x in lines] if not all([len(a) == 3 for a in angle_list]): raise ValueError('Invalid angle file provided, each line should have 3 ZXZ Euler angles!') else: - angle_list.sort() # angle list needs to be sorted otherwise symmetry reduction cannot be used! + if sort_angles: + angle_list.sort() # angle list needs to be sorted otherwise symmetry reduction cannot be used! return angle_list diff --git a/src/pytom_tm/extract.py b/src/pytom_tm/extract.py index ec721f69..f52cba0c 100644 --- a/src/pytom_tm/extract.py +++ b/src/pytom_tm/extract.py @@ -36,7 +36,10 @@ def extract_particles( score_volume = read_mrc(job.output_dir.joinpath(f'{job.tomo_id}_scores.mrc')) angle_volume = read_mrc(job.output_dir.joinpath(f'{job.tomo_id}_angles.mrc')) - angle_list = load_angle_list(job.rotation_file) + angle_list = load_angle_list( + job.rotation_file, + sort_angles=version.parse(job.pytom_tm_version_number) > version.parse('0.3.0') + ) # mask edges of score volume score_volume[0: particle_radius_px, :, :] = -1 diff --git a/src/pytom_tm/tmjob.py b/src/pytom_tm/tmjob.py index f682535a..92fa68b4 100644 --- a/src/pytom_tm/tmjob.py +++ b/src/pytom_tm/tmjob.py @@ -1,5 +1,6 @@ from __future__ import annotations from pytom_tm import __version__ +from packaging import version import pathlib import copy import numpy as np @@ -175,12 +176,12 @@ def __init__( self.dose_accumulation = dose_accumulation self.ctf_data = ctf_data self.whiten_spectrum = whiten_spectrum - - if self.whiten_spectrum: + self.whitening_filter = self.output_dir.joinpath(f'{self.tomo_id}_whitening_filter.npy') + if self.whiten_spectrum and not self.whitening_filter.exists(): logging.info('Estimating whitening filter...') weights = 1 / np.sqrt(power_spectrum_profile(read_mrc(self.tomogram))) weights /= weights.max() # scale to 1 - np.save(self.output_dir.joinpath('whitening_filter.npy'), weights) + np.save(self.whitening_filter, weights) # Job details self.job_key = job_key @@ -402,7 +403,7 @@ def start_job( self.low_pass, self.high_pass ) * (profile_to_weighting( - np.load(self.output_dir.joinpath('whitening_filter.npy')), + np.load(self.whitening_filter), search_volume.shape ) if self.whiten_spectrum else 1)).astype(np.float32) @@ -422,7 +423,7 @@ def start_job( accumulated_dose_per_tilt=self.dose_accumulation, ctf_params_per_tilt=self.ctf_data ) * (profile_to_weighting( - np.load(self.output_dir.joinpath('whitening_filter.npy')), + np.load(self.whitening_filter), self.template_shape ) if self.whiten_spectrum else 1)).astype(np.float32) @@ -454,7 +455,10 @@ def start_job( int(np.ceil(self.n_rotations / self.rotational_symmetry)), self.steps_slice )) - angle_list = load_angle_list(self.rotation_file)[slice( + angle_list = load_angle_list( + self.rotation_file, + sort_angles=version.parse(self.pytom_tm_version_number) > version.parse('0.3.0') + )[slice( self.start_slice, int(np.ceil(self.n_rotations / self.rotational_symmetry)), self.steps_slice From 405b2afef70253b793f8f300b8fd55ae65cf1a4d Mon Sep 17 00:00:00 2001 From: McHaillet Date: Fri, 3 Nov 2023 14:06:55 +0100 Subject: [PATCH 13/15] extract.py also needs to import the version module from packaging --- src/pytom_tm/extract.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pytom_tm/extract.py b/src/pytom_tm/extract.py index f52cba0c..c85c6e3b 100644 --- a/src/pytom_tm/extract.py +++ b/src/pytom_tm/extract.py @@ -1,3 +1,4 @@ +from packaging import version import pandas as pd import numpy as np import numpy.typing as npt From de3eb9d8f5bd3cd24c4372239333adf852e21f87 Mon Sep 17 00:00:00 2001 From: McHaillet Date: Fri, 3 Nov 2023 14:38:03 +0100 Subject: [PATCH 14/15] switched to importlib for detecting version --- setup.py | 5 +---- src/pytom_tm/__init__.py | 3 ++- src/pytom_tm/_version.py | 1 - src/pytom_tm/tmjob.py | 4 ++-- 4 files changed, 5 insertions(+), 8 deletions(-) delete mode 100644 src/pytom_tm/_version.py diff --git a/setup.py b/setup.py index 1a4b788d..142100b2 100644 --- a/setup.py +++ b/setup.py @@ -1,8 +1,5 @@ import setuptools -# get the version number -exec(open('src/pytom_tm/_version.py').read()) - # readme fetch with open('README.md', 'r') as f: long_description = f.read() @@ -11,7 +8,7 @@ name='pytom-template-matching-gpu', packages=['pytom_tm', 'pytom_tm.angle_lists'], package_dir={'': 'src'}, - version=__version__, # for versioning definition see https://semver.org/ + version='0.3.1', # for versioning definition see https://semver.org/ description='GPU template matching from PyTOM as a lightweight pip package', long_description=long_description, long_description_content_type='text/markdown', diff --git a/src/pytom_tm/__init__.py b/src/pytom_tm/__init__.py index 7aee39ef..22d3ec9e 100644 --- a/src/pytom_tm/__init__.py +++ b/src/pytom_tm/__init__.py @@ -1,4 +1,5 @@ -from ._version import __version__ +from importlib import metadata +__version__ = metadata.version('pytom-template-matching-gpu') try: import cupy diff --git a/src/pytom_tm/_version.py b/src/pytom_tm/_version.py deleted file mode 100644 index e1424ed0..00000000 --- a/src/pytom_tm/_version.py +++ /dev/null @@ -1 +0,0 @@ -__version__ = '0.3.1' diff --git a/src/pytom_tm/tmjob.py b/src/pytom_tm/tmjob.py index 92fa68b4..95ef1d03 100644 --- a/src/pytom_tm/tmjob.py +++ b/src/pytom_tm/tmjob.py @@ -1,5 +1,5 @@ from __future__ import annotations -from pytom_tm import __version__ +from importlib import metadata from packaging import version import pathlib import copy @@ -84,7 +84,7 @@ def __init__( ctf_data: Optional[list[dict, ...]] = None, whiten_spectrum: bool = False, rotational_symmetry: int = 1, - pytom_tm_version_number: str = __version__ + pytom_tm_version_number: str = metadata.version('pytom-template-matching-gpu') ): self.mask = mask self.mask_is_spherical = mask_is_spherical From 99ff7a40396ac2f66a9b4dcd99e83a08378d4a36 Mon Sep 17 00:00:00 2001 From: Marten <58044494+McHaillet@users.noreply.github.com> Date: Fri, 3 Nov 2023 14:39:59 +0100 Subject: [PATCH 15/15] Update src/pytom_tm/angles.py Co-authored-by: Sander Roet --- src/pytom_tm/angles.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/pytom_tm/angles.py b/src/pytom_tm/angles.py index 086a0f5f..ca9e15bb 100644 --- a/src/pytom_tm/angles.py +++ b/src/pytom_tm/angles.py @@ -28,10 +28,9 @@ def load_angle_list(file_name: pathlib.Path, sort_angles: bool = True) -> list[t angle_list = [tuple(map(float, x.strip().split(' '))) for x in lines] if not all([len(a) == 3 for a in angle_list]): raise ValueError('Invalid angle file provided, each line should have 3 ZXZ Euler angles!') - else: - if sort_angles: - angle_list.sort() # angle list needs to be sorted otherwise symmetry reduction cannot be used! - return angle_list + if sort_angles: + angle_list.sort() # angle list needs to be sorted otherwise symmetry reduction cannot be used! + return angle_list def convert_euler(