Repository: ComputationalCryoEM/ASPIRE-Python
Pull request #1273: I symmetry and tests for symmetry_groups
Author: login: itamero
Labels: name: enhancement
description: New feature or request
Created: 2025-05-09T09:42:07Z | Merged: 2025-05-19T18:02:55Z

Added "test_group_order" for checking the number of elements in each group as well as incorporating the new I symmetry class in the existing tests.


Diff:
diff --git a/src/aspire/volume/__init__.py b/src/aspire/volume/__init__.py
index 595a07170a..ab30fa45fd 100644
--- a/src/aspire/volume/__init__.py
+++ b/src/aspire/volume/__init__.py
@@ -2,6 +2,7 @@
     CnSymmetryGroup,
     DnSymmetryGroup,
     IdentitySymmetryGroup,
+    ISymmetryGroup,
     OSymmetryGroup,
     SymmetryGroup,
     TSymmetryGroup,
diff --git a/src/aspire/volume/symmetry_groups.py b/src/aspire/volume/symmetry_groups.py
index e239144150..6bbfc5439b 100644
--- a/src/aspire/volume/symmetry_groups.py
+++ b/src/aspire/volume/symmetry_groups.py
@@ -2,6 +2,7 @@
 from abc import ABC, abstractmethod, abstractproperty
 
 import numpy as np
+from scipy.spatial.transform import Rotation as R
 
 from aspire.utils import Rotation
 
@@ -79,6 +80,7 @@ def parse(symmetry):
             "D": DnSymmetryGroup,
             "T": TSymmetryGroup,
             "O": OSymmetryGroup,
+            "I": ISymmetryGroup,
         }
         if symmetry_type not in map_to_sym_group.keys():
             raise ValueError(
@@ -303,3 +305,37 @@ def generate_rotations(self):
 
         # Return rotations.
         return Rotation.from_rotvec(rot_vecs, dtype=self.dtype)
+
+
+class ISymmetryGroup(SymmetryGroup):
+    """
+    Icosahedral symmetry group.
+    """
+
+    def __init__(self):
+        """
+        `ISymmetryGroup` instance that serves up a `Rotation` object
+        containing rotation matrices of the symmetry group (including the
+        Identity) accessed via the `matrices` attribute. Note, this is the
+        chiral icosahedral symmetry group which does not contain reflections.
+        """
+        super().__init__()
+
+        self._symmetry_group = self.generate_rotations()
+
+    @property
+    def to_string(self):
+        return "I"
+
+    def generate_rotations(self):
+        """
+        Icosahedral rotation group I (60 elements):
+        - 1 identity rotation (order 1)
+        - 24 rotations of order 5: ±72°, ±144° around 6 face-center axes
+        - 20 rotations of order 3: ±120° around 10 vertex axes
+        - 15 rotations of order 2: 180° around 15 edge-midpoint axes
+
+        :return: Rotation object containing the icosahedral symmetry group and the identity.
+        """
+        scipy_rotations = R.create_group("I").as_matrix()
+        return Rotation(scipy_rotations)
diff --git a/tests/test_symmetry_groups.py b/tests/test_symmetry_groups.py
index 2be56cbf31..441a7ee2ec 100644
--- a/tests/test_symmetry_groups.py
+++ b/tests/test_symmetry_groups.py
@@ -9,6 +9,7 @@
     CnSymmetryGroup,
     DnSymmetryGroup,
     IdentitySymmetryGroup,
+    ISymmetryGroup,
     OSymmetryGroup,
     SymmetryGroup,
     TSymmetryGroup,
@@ -23,6 +24,7 @@
 GROUPS_WITHOUT_ORDER = [
     (TSymmetryGroup,),
     (OSymmetryGroup,),
+    (ISymmetryGroup,),
 ]
 ORDERS = [2, 3, 4, 5]
 PARAMS_ORDER = list(itertools.product(GROUPS_WITH_ORDER, ORDERS))
@@ -96,3 +98,21 @@ def test_parser_error():
         ValueError, match=f"Symmetry type {junk_symmetry[0]} not supported.*"
     ):
         _ = SymmetryGroup.parse(junk_symmetry)
+
+
+def test_group_order(group_fixture):
+    """Check the number of elements (order) in each symmetry group."""
+    if type(group_fixture) in GROUPS_WITH_ORDER:
+        expected_orders = {
+            CnSymmetryGroup: 1 * group_fixture.order,
+            DnSymmetryGroup: 2 * group_fixture.order,
+        }
+    else:
+        expected_orders = {
+            IdentitySymmetryGroup: 1,
+            TSymmetryGroup: 12,
+            OSymmetryGroup: 24,
+            ISymmetryGroup: 60,
+        }
+    expected_order = expected_orders[type(group_fixture)]
+    assert len(group_fixture.matrices) == expected_order


Reviews:
user: login: j-c-c
body: Looks great. Just one change.
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-05-09T14:19:20Z
user: login: itamero
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-11T07:55:38Z
user: login: j-c-c
body: Great. Thanks for making those changes!
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-12T15:25:23Z
user: login: janden
body: LGTM
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-19T16:49:24Z

Review comments:
diff_hunk: @@ -5,10 +5,11 @@
 import pytest
 
 from aspire.utils import Rotation
-from aspire.volume import (
+from aspire.volume.symmetry_groups import (
path: tests/test_symmetry_groups.py
user: login: j-c-c
body: Instead of importing from the `aspire.volume.symmetry_groups` module, we can make `ISymmetryGroup` accessible from `aspire.volume` by adding it to the imports in `src/aspire/volume/__init__.py`.
created_at: 2025-05-09T14:17:24Z
author_association: COLLABORATOR
diff_hunk: @@ -96,3 +98,21 @@ def test_parser_error():
         ValueError, match=f"Symmetry type {junk_symmetry[0]} not supported.*"
     ):
         _ = SymmetryGroup.parse(junk_symmetry)
+
+
+def test_group_order(group_fixture):
path: tests/test_symmetry_groups.py
user: login: j-c-c
body: Nice! Thanks for adding this.
created_at: 2025-05-09T14:19:04Z
author_association: COLLABORATOR
diff_hunk: @@ -5,10 +5,11 @@
 import pytest
 
 from aspire.utils import Rotation
-from aspire.volume import (
+from aspire.volume.symmetry_groups import (
path: tests/test_symmetry_groups.py
user: login: itamero
body: Fixed.
created_at: 2025-05-11T07:55:38Z
author_association: NONE

PR comments:
user: login: itamero
created_at: 2025-05-09T10:09:02Z
body: Hey @j-c-c thank you for the helpful and detailed instructions! 

I closed the old PR and created this one which I think should be okay, but I am somewhat of a git noob so apologies if this is still problematic and I'll try to fix any issues you let me know of :)

I ran `isort` and `black` which hopefully matched the formatting and I also incorporated the new I group class into the tests and added another test which verifies the order of each group.

I would like to make two unrelated suggestions and hear your thoughts about them:

1. Sometimes when using the `Image.show()`  method it can be confusing to know what each plot is referring to, especially when showing multiple plots. I suggest adding a simple `title=None` and `plt.suptitle(title)` for added clarification.
2. Due to symmetry ambiguity, when interested in comparing estimated rotations to ground truth rotations we actually want the minimal difference up to multiplication by some rotation group element. Would be nice if some of the `Image` methods, like `mean_angular_distance` or `mse` could have an option to compare the metric across the symmetry group.

Thanks again for your help,
Itamar
author_association: NONE
user: login: j-c-c
created_at: 2025-05-09T14:12:48Z
body: Hey @itamero, this looks great. Just two more things and this should be good to go.

1. We've had some PRs merge into develop since you've created this branch, so your branch is currently "out-of-date". This means you'll need to rebase your branch to get it ready for merging.
2. Also there is a change or two that I'll suggest in review comments that will need to be added to the PR.

No worries about being a git noob. Totally understand. Performing the above will look something like:
```
# Ensure you have the upstream remote (this might be done already, but won't hurt either way)
git remote add upstream git@github.com:ComputationalCryoEM/ASPIRE-Python.git

# Fetch the latest changes from develop
git fetch upstream develop

# Rebase your feature branch onto develop
git checkout I_sym 
git rebase upstream/develop

# Then make the changes I'm requesting in the review comments by
# editing/saving the files. Run isort and black afterwards to make sure
# formatting is good.
isort . && black .

# Commit the changes
git commit -am "commit message..."

# Then push your updated branch (will need to force because of the rebase)
git push -f origin I_sym
```

Forgive me if I'm telling you how to do anything you're already familiar with. Let me know if you have trouble with any this. I'll follow up about the other issues you mentioned.

Best,
Josh
author_association: COLLABORATOR
user: login: j-c-c
created_at: 2025-05-09T14:57:19Z
body: Hey Itamar,

In regards to your other suggestions:

1. I believe we might have discussed adding some options, like titles, to the `Image.show()` method previously, so I'll bring it up at the next dev meeting.
2. I agree that it would be useful to handle symmetry in some of our diagnostic tools. We currently do this in the unit tests for the Cn and D2 common-lines methods. You can see the method `g_sync_d2` [here](https://github.com/ComputationalCryoEM/ASPIRE-Python/blob/d0f3d097fc51300652af186124f2576f76a46315/tests/test_orient_d2.py#L390), which synchronizes the set of ground truth rotations to the estimated rotations by applying the best symmetry group element to each rotation to give the best global alignment. Maybe a good start would be generalizing this method to any symmetry group.

I'll discuss both of these points with the dev team and get back to you.

Best,
Josh
author_association: COLLABORATOR
user: login: codecov[bot]
created_at: 2025-05-09T15:08:35Z
body: ## [Codecov](https://app.codecov.io/gh/ComputationalCryoEM/ASPIRE-Python/pull/1273?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ComputationalCryoEM) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 90.62%. Comparing base [(`0c1dab9`)](https://app.codecov.io/gh/ComputationalCryoEM/ASPIRE-Python/commit/0c1dab9cf5ba39e31eef1a20ea2b4805547550b7?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ComputationalCryoEM) to head [(`7d6b559`)](https://app.codecov.io/gh/ComputationalCryoEM/ASPIRE-Python/commit/7d6b559d3fa4a38ff2e37e467246954a668db66e?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ComputationalCryoEM).
> Report is 2 commits behind head on develop.

<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff            @@
##           develop    #1273   +/-   ##
========================================
  Coverage    90.61%   90.62%           
========================================
  Files          132      132           
  Lines        14153    14164   +11     
========================================
+ Hits         12825    12836   +11     
  Misses        1328     1328           
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/ComputationalCryoEM/ASPIRE-Python/pull/1273?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ComputationalCryoEM).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ComputationalCryoEM).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>
author_association: NONE
user: login: itamero
created_at: 2025-05-11T07:57:27Z
body: Made the requested changes, thank you for listing the git commands that was very helpful!
author_association: NONE
user: login: j-c-c
created_at: 2025-05-14T17:16:17Z
body: @janden This is ready for you to take a look. I will follow up on this PR and consider using scipy's `create_group` for our other symmetry groups. Created issue #1275.
author_association: COLLABORATOR