Repository: ComputationalCryoEM/ASPIRE-Python

Issue:
Cleanup `SymmetryGroup` warnings
I frequently see repeated warnings like below.  They should either be logged once, or converted to warnings limited to once per meaningful encounter.

```
2025-02-19 13:50:42,986 WARNING [aspire.volume.symmetry_groups] Recasting SymmetryGroup with dty[10/216]
64.                                                                                                     
2025-02-19 13:50:43,011 WARNING [aspire.volume.symmetry_groups] Recasting SymmetryGroup with dtype float32.  
2025-02-19 13:50:43,340 WARNING [aspire.volume.symmetry_groups] Recasting SymmetryGroup with dtype float
64.                                                                                                     
2025-02-19 13:50:44,696 WARNING [aspire.volume.symmetry_groups] Recasting SymmetryGroup with dtype float
64.                                                                                                     
2025-02-19 13:50:44,721 WARNING [aspire.volume.symmetry_groups] Recasting SymmetryGroup with dtype float
32.                                                                                                     2025-02-19 13:50:45,050 WARNING [aspire.volume.symmetry_groups] Recasting SymmetryGroup with dtype float
64.             
2025-02-19 13:50:46,416 WARNING [aspire.volume.symmetry_groups] Recasting SymmetryGroup with dtype float
64.
2025-02-19 13:50:46,440 WARNING [aspire.volume.symmetry_groups] Recasting SymmetryGroup with dtype float
32.
2025-02-19 13:50:46,768 WARNING [aspire.volume.symmetry_groups] Recasting SymmetryGroup with dtype float
64.
2025-02-19 13:50:48,133 WARNING [aspire.volume.symmetry_groups] Recasting SymmetryGroup with dtype float
64.
2025-02-19 13:50:48,158 WARNING [aspire.volume.symmetry_groups] Recasting SymmetryGroup with dtype float
32.
2025-02-19 13:50:48,450 WARNING [aspire.volume.symmetry_groups] Recasting SymmetryGroup with dtype float
64.
2025-02-19 13:50:49,679 WARNING [aspire.volume.symmetry_groups] Recasting SymmetryGroup with dtype float
64.
2025-02-19 13:50:49,704 WARNING [aspire.volume.symmetry_groups] Recasting SymmetryGroup with dtype float
32.
```

Hints from issue comments:
@j-c-c: Small update:

It turns out most of these warnings are related to dtype bugs, either in the code or in test fixtures. For example, uses of `np.ones` and `np.sqrt` that are not properly dtyped. I can alter the way the warnings are logged, but I'm working on removing the bugs first.@garrettwrong: My report was not generated from our tests, but fixing those would be good too if they are generating the same.

I took a peek around, didn't seem like that was the issue I was seeing, but I guess I'll have to wait and see 😇 .@j-c-c: Closing. Resolved by #1238 

Solution patch:
diff --git a/src/aspire/abinitio/commonline_d2.py b/src/aspire/abinitio/commonline_d2.py
index a8e951c6..5d3d2c6b 100644
--- a/src/aspire/abinitio/commonline_d2.py
+++ b/src/aspire/abinitio/commonline_d2.py
@@ -81,7 +81,9 @@ class CLSymmetryD2(CLOrient3D):
         # D2 symmetry group.
         # Rearrange in order Identity, about_x, about_y, about_z.
         # This ordering is necessary for reproducing MATLAB code results.
-        self.gs = DnSymmetryGroup(order=2, dtype=self.dtype).matrices[[0, 3, 2, 1]]
+        self.gs = DnSymmetryGroup(order=2).matrices.astype(self.dtype, copy=False)[
+            [0, 3, 2, 1]
+        ]
 
     def estimate_rotations(self):
         """
diff --git a/src/aspire/image/image.py b/src/aspire/image/image.py
index e03de5b2..92f43caa 100644
--- a/src/aspire/image/image.py
+++ b/src/aspire/image/image.py
@@ -732,7 +732,9 @@ class Image:
         ), "Number of rotation matrices must match the number of images"
 
         # Get symmetry rotations from SymmetryGroup.
-        symmetry_rots = SymmetryGroup.parse(symmetry_group, dtype=self.dtype).matrices
+        symmetry_rots = SymmetryGroup.parse(symmetry_group).matrices.astype(
+            self.dtype, copy=False
+        )
         if len(symmetry_rots) > 1:
             logger.info(f"Boosting with {len(symmetry_rots)} rotational symmetries.")
 
diff --git a/src/aspire/reconstruction/mean.py b/src/aspire/reconstruction/mean.py
index e3888ee2..16776930 100644
--- a/src/aspire/reconstruction/mean.py
+++ b/src/aspire/reconstruction/mean.py
@@ -53,9 +53,9 @@ class WeightedVolumesEstimator(Estimator):
         :param weights: Matrix of weights, n x r.
         """
 
-        self.weights = weights
-        self.r = self.weights.shape[1]
         super().__init__(*args, **kwargs)
+        self.weights = weights.astype(self.src.dtype, copy=False)
+        self.r = self.weights.shape[1]
         assert self.src.n == self.weights.shape[0]
 
     def __getattr__(self, name):
@@ -96,7 +96,7 @@ class WeightedVolumesEstimator(Estimator):
         # Handle symmetry boosting.
         sym_rots = np.eye(3, dtype=self.dtype)[None]
         if self.boost:
-            sym_rots = self.src.symmetry_group.matrices
+            sym_rots = self.src.symmetry_group.matrices.astype(self.dtype, copy=False)
 
         for i in range(0, self.src.n, self.batch_size):
             _range = np.arange(i, min(self.src.n, i + self.batch_size), dtype=int)
diff --git a/src/aspire/source/image.py b/src/aspire/source/image.py
index 96dbaea3..c6fe79aa 100644
--- a/src/aspire/source/image.py
+++ b/src/aspire/source/image.py
@@ -233,7 +233,7 @@ class ImageSource(ABC):
                 f"This source is no longer mutable. Try new_source = source.update(symmetry_group='{value}')."
             )
 
-        self._symmetry_group = SymmetryGroup.parse(value, dtype=self.dtype)
+        self._symmetry_group = SymmetryGroup.parse(value)
         self.set_metadata(["_rlnSymmetryGroup"], str(self.symmetry_group))
 
     def _populate_symmetry_group(self, symmetry_group):
@@ -249,10 +249,9 @@ class ImageSource(ABC):
             else:
                 symmetry_group = SymmetryGroup.parse(
                     symmetry=self.get_metadata(["_rlnSymmetryGroup"])[0],
-                    dtype=self.dtype,
                 )
 
-        self.symmetry_group = symmetry_group or IdentitySymmetryGroup(dtype=self.dtype)
+        self.symmetry_group = symmetry_group or IdentitySymmetryGroup()
 
     def __getitem__(self, indices):
         """
@@ -419,7 +418,10 @@ class ImageSource(ABC):
     @property
     def offsets(self):
         return np.atleast_2d(
-            self.get_metadata(["_rlnOriginX", "_rlnOriginY"], default_value=0.0)
+            self.get_metadata(
+                ["_rlnOriginX", "_rlnOriginY"],
+                default_value=np.array(0.0, dtype=self.dtype),
+            )
         )
 
     @offsets.setter
@@ -430,7 +432,11 @@ class ImageSource(ABC):
 
     @property
     def amplitudes(self):
-        return np.atleast_1d(self.get_metadata("_rlnAmplitude", default_value=1.0))
+        return np.atleast_1d(
+            self.get_metadata(
+                "_rlnAmplitude", default_value=np.array(1.0, dtype=self.dtype)
+            )
+        )
 
     @amplitudes.setter
     def amplitudes(self, values):
diff --git a/src/aspire/source/simulation.py b/src/aspire/source/simulation.py
index de5f0957..a2b9de71 100644
--- a/src/aspire/source/simulation.py
+++ b/src/aspire/source/simulation.py
@@ -413,7 +413,7 @@ class Simulation(ImageSource):
         C = self.C
         vols_c = self.vols - self.mean_true()
 
-        p = np.ones(C) / C
+        p = np.full(C, 1 / C, dtype=self.dtype)
         Q, R = qr(vols_c.to_vec().T, mode="economic")
 
         # Rank is at most C-1, so remove last vector
diff --git a/src/aspire/utils/rotation.py b/src/aspire/utils/rotation.py
index 07a31df9..2a2e0f98 100644
--- a/src/aspire/utils/rotation.py
+++ b/src/aspire/utils/rotation.py
@@ -274,7 +274,11 @@ class Rotation:
 
         :return: Rotation object
         """
-        dtype = dtype or getattr(angles, "dtype", np.float32)
+        dtype = dtype or (
+            np.float64
+            if isinstance(angles, float)
+            else getattr(angles, "dtype", np.float32)
+        )
         axes = ["x", "y", "z"]
         if axis.lower() not in axes:
             raise ValueError("`axis` must be 'x', 'y', or 'z'.")
diff --git a/src/aspire/volume/symmetry_groups.py b/src/aspire/volume/symmetry_groups.py
index 1384b951..e2391441 100644
--- a/src/aspire/volume/symmetry_groups.py
+++ b/src/aspire/volume/symmetry_groups.py
@@ -13,11 +13,11 @@ class SymmetryGroup(ABC):
     Base class for symmetry groups.
     """
 
-    def __init__(self, dtype):
+    def __init__(self):
         """
-        :param dtype: Numpy dtype to be used for rotation matrices.
+        Abstract class for symmetry groups.
         """
-        self.dtype = np.dtype(dtype)
+        self.dtype = np.float64
         self.rotations = self.generate_rotations()
 
     @abstractmethod
@@ -46,26 +46,19 @@ class SymmetryGroup(ABC):
         return f"{self.to_string}"
 
     @staticmethod
-    def parse(symmetry, dtype):
+    def parse(symmetry):
         """
-        Takes a SymmetryGroup instance or a string, ie. 'C1', 'C7', 'D3', 'T', 'O', and returns a concrete
-        SymmetryGroup object with the specified dtype.
+        Takes a SymmetryGroup instance or a string, ie. 'C1', 'C7', 'D3', 'T', 'O',
+        and returns a concrete SymmetryGroup object.
 
         :param symmetry: A string (or SymmetryGroup instance) indicating the symmetry of a molecule.
-        :param dtype: dtype for rotation matrices.
         :return: Concrete SymmetryGroup object.
         """
 
         if symmetry is None:
-            return IdentitySymmetryGroup(dtype=dtype)
+            return IdentitySymmetryGroup()
 
         if isinstance(symmetry, SymmetryGroup):
-            if symmetry.dtype != dtype:
-                logger.warning(f"Recasting SymmetryGroup with dtype {dtype}.")
-                group_kwargs = dict(dtype=dtype)
-                if getattr(symmetry, "order", False) and symmetry.order > 1:
-                    group_kwargs["order"] = symmetry.order
-                symmetry = symmetry.__class__(**group_kwargs)
             return symmetry
 
         if not isinstance(symmetry, str):
@@ -73,9 +66,10 @@ class SymmetryGroup(ABC):
                 f"`symmetry` must be a string or `SymmetryGroup` instance. Found {type(symmetry)}"
             )
 
+        # Parse symmetry provided as a string.
         symmetry = symmetry.upper()
         if symmetry == "C1":
-            return IdentitySymmetryGroup(dtype=dtype)
+            return IdentitySymmetryGroup()
 
         symmetry_type = symmetry[0]
         symmetric_order = symmetry[1:]
@@ -92,7 +86,7 @@ class SymmetryGroup(ABC):
             )
 
         symmetry_group = map_to_sym_group[symmetry_type]
-        group_kwargs = dict(dtype=dtype)
+        group_kwargs = dict()
         if symmetric_order:
             group_kwargs["order"] = int(symmetric_order)
 
@@ -104,18 +98,17 @@ class CnSymmetryGroup(SymmetryGroup):
     Cyclic symmetry group.
     """
 
-    def __init__(self, order, dtype):
+    def __init__(self, order):
         """
         `CnSymmetryGroup` instance that serves up a `Rotation` object
         containing rotation matrices of the symmetry group (including
         the identity) accessed via the `matrices` attribute.
 
         :param order: The cyclic order for the symmetry group (int).
-        :param dtype: Numpy dtype to be used for rotation matrices.
         """
 
         self.order = int(order)
-        super().__init__(dtype=dtype)
+        super().__init__()
 
     @property
     def to_string(self):
@@ -131,7 +124,7 @@ class CnSymmetryGroup(SymmetryGroup):
 
         :return: Rotation object containing the Cn symmetry group and the identity.
         """
-        angles = 2 * np.pi * np.arange(self.order) / self.order
+        angles = np.linspace(0, 2 * np.pi, self.order, endpoint=False)
         return Rotation.about_axis("z", angles, dtype=self.dtype)
 
 
@@ -140,15 +133,13 @@ class IdentitySymmetryGroup(CnSymmetryGroup):
     The identity symmetry group.
     """
 
-    def __init__(self, dtype):
+    def __init__(self):
         """
         `IdentitySymmetryGroup` instance that serves up a `Rotation` object
         containing the identity matrix.
-
-        :param dtype: Numpy dtype to be used for rotation matrices.
         """
 
-        super().__init__(order=1, dtype=dtype)
+        super().__init__(order=1)
 
 
 class DnSymmetryGroup(SymmetryGroup):
@@ -156,7 +147,7 @@ class DnSymmetryGroup(SymmetryGroup):
     Dihedral symmetry group.
     """
 
-    def __init__(self, order, dtype):
+    def __init__(self, order):
         """
         `DnSymmetryGroup` instance that serves up a `Rotation` object
         containing rotation matrices of the symmetry group (including
@@ -164,11 +155,10 @@ class DnSymmetryGroup(SymmetryGroup):
         is the chiral dihedral symmetry group which does contain reflections.
 
         :param order: The cyclic order for the symmetry group (int).
-        :param dtype: Numpy dtype to be used for rotation matrices.
         """
 
         self.order = int(order)
-        super().__init__(dtype=dtype)
+        super().__init__()
 
     @property
     def to_string(self):
@@ -182,15 +172,16 @@ class DnSymmetryGroup(SymmetryGroup):
 
         :return: Rotation object containing the Dn symmetry group and the identity.
         """
+
         # Rotations to induce cyclic symmetry
-        angles = 2 * np.pi * np.arange(self.order, dtype=self.dtype) / self.order
-        rot_z = Rotation.about_axis("z", angles).matrices
+        angles = np.linspace(0, 2 * np.pi, self.order, endpoint=False)
+        rot_z = Rotation.about_axis("z", angles, dtype=self.dtype).matrices
 
         # Perpendicular rotation to induce dihedral symmetry
         rot_perp = Rotation.about_axis("y", np.pi, dtype=self.dtype).matrices
 
         # Full set of rotations.
-        rots = np.concatenate((rot_z, rot_z @ rot_perp[0]), dtype=self.dtype)
+        rots = np.concatenate((rot_z, rot_z @ rot_perp[0]))
 
         return Rotation(rots)
 
@@ -200,17 +191,15 @@ class TSymmetryGroup(SymmetryGroup):
     Tetrahedral symmetry group.
     """
 
-    def __init__(self, dtype):
+    def __init__(self):
         """
         `TSymmetryGroup` 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 tetrahedral symmetry group which does not contain reflections.
-
-        :param dtype: Numpy dtype to be used for rotation matrices.
         """
 
-        super().__init__(dtype=dtype)
+        super().__init__()
 
     @property
     def to_string(self):
@@ -228,23 +217,19 @@ class TSymmetryGroup(SymmetryGroup):
         """
         # C3 rotation vectors, ie. angle * axis.
         axes_C3 = np.array(
-            [[1, 1, 1], [-1, -1, 1], [1, -1, -1], [-1, 1, -1]], dtype=self.dtype
+            [[1.0, 1.0, 1.0], [-1.0, -1.0, 1.0], [1.0, -1.0, -1.0], [-1.0, 1.0, -1.0]],
         )
         axes_C3 /= np.linalg.norm(axes_C3, axis=-1)[..., np.newaxis]
-        angles_C3 = np.array([2 * np.pi / 3, 4 * np.pi / 3], dtype=self.dtype)
-        rot_vecs_C3 = np.concatenate(
-            [angle * axes_C3 for angle in angles_C3], dtype=self.dtype
-        )
+        angles_C3 = np.array([2 * np.pi / 3, 4 * np.pi / 3])
+        rot_vecs_C3 = np.concatenate([angle * axes_C3 for angle in angles_C3])
 
         # C2 rotation vectors.
-        axes_C2 = np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]], dtype=self.dtype)
+        axes_C2 = np.array([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]])
         rot_vecs_C2 = np.pi * axes_C2
 
         # The full set of rotation vectors inducing tetrahedral symmetry.
-        rot_vec_I = np.zeros((1, 3), dtype=self.dtype)
-        rot_vecs = np.concatenate(
-            (rot_vec_I, rot_vecs_C3, rot_vecs_C2), dtype=self.dtype
-        )
+        rot_vec_I = np.zeros((1, 3))
+        rot_vecs = np.concatenate((rot_vec_I, rot_vecs_C3, rot_vecs_C2))
 
         # Return rotations.
         return Rotation.from_rotvec(rot_vecs, dtype=self.dtype)
@@ -255,16 +240,14 @@ class OSymmetryGroup(SymmetryGroup):
     Octahedral symmetry group.
     """
 
-    def __init__(self, dtype):
+    def __init__(self):
         """
         `OSymmetryGroup` 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 octahedral symmetry group which does not contain reflections.
-
-        :param dtype: Numpy dtype to be used for rotation matrices.
         """
-        super().__init__(dtype=dtype)
+        super().__init__()
 
         self._symmetry_group = self.generate_rotations()
 
@@ -284,35 +267,39 @@ class OSymmetryGroup(SymmetryGroup):
         """
 
         # C4 rotation vectors, ie angle * axis
-        axes_C4 = np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]], dtype=self.dtype)
-        angles_C4 = np.array([np.pi / 2, np.pi, 3 * np.pi / 2], dtype=self.dtype)
+        axes_C4 = np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]])
+        angles_C4 = np.array([np.pi / 2, np.pi, 3 * np.pi / 2])
         rot_vecs_C4 = np.array(
-            [angle * axes_C4 for angle in angles_C4], dtype=self.dtype
+            [angle * axes_C4 for angle in angles_C4],
         ).reshape((9, 3))
 
         # C3 rotation vectors
         axes_C3 = np.array(
-            [[1, 1, 1], [-1, 1, 1], [1, -1, 1], [1, 1, -1]], dtype=self.dtype
+            [[1.0, 1.0, 1.0], [-1.0, 1.0, 1.0], [1.0, -1.0, 1.0], [1.0, 1.0, -1.0]]
         )
         axes_C3 /= np.linalg.norm(axes_C3, axis=-1)[..., np.newaxis]
-        angles_C3 = np.array([2 * np.pi / 3, 4 * np.pi / 3], dtype=self.dtype)
+        angles_C3 = np.array([2 * np.pi / 3, 4 * np.pi / 3])
         rot_vecs_C3 = np.array(
-            [angle * axes_C3 for angle in angles_C3], dtype=self.dtype
+            [angle * axes_C3 for angle in angles_C3],
         ).reshape((8, 3))
 
         # C2 rotation vectors
         axes_C2 = np.array(
-            [[1, 1, 0], [-1, 1, 0], [1, 0, 1], [-1, 0, 1], [0, 1, 1], [0, -1, 1]],
-            dtype=self.dtype,
+            [
+                [1.0, 1.0, 0.0],
+                [-1.0, 1.0, 0.0],
+                [1.0, 0.0, 1.0],
+                [-1.0, 0.0, 1.0],
+                [0.0, 1.0, 1.0],
+                [0.0, -1.0, 1.0],
+            ],
         )
         axes_C2 /= np.linalg.norm(axes_C2, axis=-1)[..., np.newaxis]
         rot_vecs_C2 = np.pi * axes_C2
 
         # The full set of rotation vectors inducing octahedral symmetry.
-        rot_vec_I = np.zeros((1, 3), dtype=self.dtype)
-        rot_vecs = np.concatenate(
-            (rot_vec_I, rot_vecs_C4, rot_vecs_C3, rot_vecs_C2), dtype=self.dtype
-        )
+        rot_vec_I = np.zeros((1, 3))
+        rot_vecs = np.concatenate((rot_vec_I, rot_vecs_C4, rot_vecs_C3, rot_vecs_C2))
 
         # Return rotations.
         return Rotation.from_rotvec(rot_vecs, dtype=self.dtype)
diff --git a/src/aspire/volume/volume.py b/src/aspire/volume/volume.py
index 04ae8429..013642bb 100644
--- a/src/aspire/volume/volume.py
+++ b/src/aspire/volume/volume.py
@@ -185,7 +185,7 @@ class Volume:
 
         :param value: A `SymmetryGroup` instance or string indicating symmetry, ie. "C5", "D7", "T", etc.
         """
-        self._symmetry_group = SymmetryGroup.parse(value, dtype=self.dtype)
+        self._symmetry_group = SymmetryGroup.parse(value)
 
     def _symmetry_group_warning(self, stacklevel):
         """
@@ -228,7 +228,7 @@ class Volume:
 
         if any([axes_altering_transformation, incompat_syms, arbitrary_array]):
             self._symmetry_group_warning(stacklevel=stacklevel)
-            result_symmetry = IdentitySymmetryGroup(dtype=self.dtype)
+            result_symmetry = IdentitySymmetryGroup()
 
         return result_symmetry
 
diff --git a/src/aspire/volume/volume_synthesis.py b/src/aspire/volume/volume_synthesis.py
index 43f794bf..d0d9225f 100644
--- a/src/aspire/volume/volume_synthesis.py
+++ b/src/aspire/volume/volume_synthesis.py
@@ -126,7 +126,7 @@ class GaussianBlobsVolume(SyntheticVolumeBase):
         """
         Called to add symmetry to Volumes by generating for each Gaussian blob duplicates in symmetric positions.
         """
-        rots = self.symmetry_group.matrices
+        rots = self.symmetry_group.matrices.astype(self.dtype, copy=False)
 
         Q_rot = np.zeros(shape=(self.n_blobs, 3, 3)).astype(self.dtype)
         D_sym = np.zeros(shape=(self.n_blobs, 3, 3)).astype(self.dtype)
@@ -207,7 +207,7 @@ class CnSymmetricVolume(GaussianBlobsVolume):
             )
 
     def _set_symmetry_group(self):
-        self._symmetry_group = CnSymmetryGroup(order=self.order, dtype=self.dtype)
+        self._symmetry_group = CnSymmetryGroup(order=self.order)
 
     @property
     def n_blobs(self):
@@ -220,7 +220,7 @@ class DnSymmetricVolume(CnSymmetricVolume):
     """
 
     def _set_symmetry_group(self):
-        self._symmetry_group = DnSymmetryGroup(order=self.order, dtype=self.dtype)
+        self._symmetry_group = DnSymmetryGroup(order=self.order)
 
     @property
     def n_blobs(self):
@@ -233,7 +233,7 @@ class TSymmetricVolume(GaussianBlobsVolume):
     """
 
     def _set_symmetry_group(self):
-        self._symmetry_group = TSymmetryGroup(dtype=self.dtype)
+        self._symmetry_group = TSymmetryGroup()
 
     @property
     def n_blobs(self):
@@ -246,7 +246,7 @@ class OSymmetricVolume(GaussianBlobsVolume):
     """
 
     def _set_symmetry_group(self):
-        self._symmetry_group = OSymmetryGroup(dtype=self.dtype)
+        self._symmetry_group = OSymmetryGroup()
 
     @property
     def n_blobs(self):
@@ -270,7 +270,7 @@ class AsymmetricVolume(CnSymmetricVolume):
             )
 
     def _set_symmetry_group(self):
-        self._symmetry_group = IdentitySymmetryGroup(dtype=self.dtype)
+        self._symmetry_group = IdentitySymmetryGroup()
 
     def _symmetrize_gaussians(self, Q, D, mu):
         return Q, D, mu


Test patch:
diff --git a/tests/test_array_image_source.py b/tests/test_array_image_source.py
index a43db1ee..ffab418c 100644
--- a/tests/test_array_image_source.py
+++ b/tests/test_array_image_source.py
@@ -5,6 +5,7 @@ import tempfile
 from unittest import TestCase
 
 import numpy as np
+import pytest
 from pytest import raises
 
 from aspire.basis import FBBasis3D
@@ -242,3 +243,21 @@ class ImageTestCase(TestCase):
             # Test the content matched when loaded
             src2 = RelionSource(star_path)
             np.testing.assert_allclose(src.images[:], src2.images[:])
+
+
+@pytest.mark.parametrize("dtype", [np.float32, np.float64])
+def test_dtype_passthrough(dtype):
+    """
+    Test dtypes are passed appropriately to metadata.
+    """
+    n_ims = 10
+    res = 32
+    ims = np.ones((n_ims, res, res), dtype=dtype)
+
+    src = ArrayImageSource(ims)
+
+    # Check dtypes
+    np.testing.assert_equal(src.dtype, dtype)
+    np.testing.assert_equal(src.images[:].dtype, dtype)
+    np.testing.assert_equal(src.amplitudes.dtype, dtype)
+    np.testing.assert_equal(src.offsets.dtype, dtype)
diff --git a/tests/test_bot_align.py b/tests/test_bot_align.py
index dff3e573..a6758828 100644
--- a/tests/test_bot_align.py
+++ b/tests/test_bot_align.py
@@ -74,10 +74,10 @@ def vol_data_fixture(snr, dtype):
     L = v.resolution
     shape = (L, L, L)
     ns_std = np.sqrt(norm(v) ** 2 / (L**3 * snr)).astype(v.dtype)
-    reference_vol = v + normal(0, ns_std, shape)
+    reference_vol = v + normal(0, ns_std, shape).astype(dtype, copy=False)
     r = Rotation.generate_random_rotations(1, dtype=v.dtype, seed=1234)
     R_true = r.matrices[0]
-    test_vol = v.rotate(r) + normal(0, ns_std, shape)
+    test_vol = v.rotate(r) + normal(0, ns_std, shape).astype(dtype, copy=False)
 
     return reference_vol, test_vol, R_true
 
diff --git a/tests/test_image.py b/tests/test_image.py
index 4b76d968..4e8513a2 100644
--- a/tests/test_image.py
+++ b/tests/test_image.py
@@ -338,7 +338,7 @@ def test_backproject_symmetry_group(dtype):
     assert isinstance(vol.symmetry_group, CnSymmetryGroup)
 
     # Symmetry from instance.
-    vol = im.backproject(rots, symmetry_group=CnSymmetryGroup(order=3, dtype=dtype))
+    vol = im.backproject(rots, symmetry_group=CnSymmetryGroup(order=3))
     assert isinstance(vol.symmetry_group, CnSymmetryGroup)
 
 
diff --git a/tests/test_mean_estimator_boosting.py b/tests/test_mean_estimator_boosting.py
index 6eac1591..568cad1e 100644
--- a/tests/test_mean_estimator_boosting.py
+++ b/tests/test_mean_estimator_boosting.py
@@ -148,7 +148,7 @@ def test_boost_flag(source, estimated_volume):
     """Manually boost a source and reconstruct without boosting."""
     ims = source.projections[:]
     rots = source.rotations
-    sym_rots = source.symmetry_group.matrices
+    sym_rots = source.symmetry_group.matrices.astype(dtype=source.dtype)
     sym_order = len(sym_rots)
 
     # Manually boosted images and rotations.
diff --git a/tests/test_orient_d2.py b/tests/test_orient_d2.py
index ca972bcf..968722e9 100644
--- a/tests/test_orient_d2.py
+++ b/tests/test_orient_d2.py
@@ -405,7 +405,7 @@ def g_sync_d2(rots, rots_gt):
     n_img = len(rots)
     dtype = rots.dtype
 
-    rots_symm = DnSymmetryGroup(2, dtype).matrices
+    rots_symm = DnSymmetryGroup(2).matrices.astype(dtype, copy=False)
     order = len(rots_symm)
 
     A_g = np.zeros((n_img, n_img), dtype=complex)
diff --git a/tests/test_rotation.py b/tests/test_rotation.py
index ee87b11d..5ecf9284 100644
--- a/tests/test_rotation.py
+++ b/tests/test_rotation.py
@@ -143,6 +143,18 @@ def test_dtype(dtype, rot_obj):
     assert dtype == rot_obj.dtype
 
 
+def test_about_axis_dtype(dtype):
+    angles = np.random.uniform(0, 2 * np.pi, 10).astype(dtype, copy=False)
+    rots = Rotation.about_axis("x", angles).matrices
+    np.testing.assert_equal(rots.dtype, dtype)
+
+
+def test_about_axis_float_dtype():
+    angle = np.pi
+    rot = Rotation.about_axis("y", angle).matrices
+    np.testing.assert_equal(rot.dtype, np.float64)
+
+
 def test_from_rotvec(rot_obj):
     # Build random rotation vectors.
     axis = np.array([1, 0, 0], dtype=rot_obj.dtype)
diff --git a/tests/test_simulation.py b/tests/test_simulation.py
index 944e2e7c..3909aa3e 100644
--- a/tests/test_simulation.py
+++ b/tests/test_simulation.py
@@ -494,7 +494,9 @@ class SimTestCase(TestCase):
         np.testing.assert_allclose(result, covar[:, :, 4, 4, 4, 4], atol=1e-4)
 
     def testSimulationEvalMean(self):
-        mean_est = Volume(np.load(os.path.join(DATA_DIR, "mean_8_8_8.npy")))
+        mean_est = Volume(
+            np.load(os.path.join(DATA_DIR, "mean_8_8_8.npy")), dtype=self.dtype
+        )
         result = self.sim.eval_mean(mean_est)
 
         np.testing.assert_allclose(result["err"], 2.664116055950763, atol=1e-4)
@@ -510,14 +512,17 @@ class SimTestCase(TestCase):
         np.testing.assert_allclose(result["corr"], 0.8405347287741631, atol=1e-4)
 
     def testSimulationEvalCoords(self):
-        mean_est = Volume(np.load(os.path.join(DATA_DIR, "mean_8_8_8.npy")))
+        mean_est = Volume(
+            np.load(os.path.join(DATA_DIR, "mean_8_8_8.npy")), dtype=self.dtype
+        )
         eigs_est = Volume(
-            np.load(os.path.join(DATA_DIR, "eigs_est_8_8_8_1.npy"))[..., 0]
+            np.load(os.path.join(DATA_DIR, "eigs_est_8_8_8_1.npy"))[..., 0],
+            dtype=self.dtype,
         )
 
         clustered_coords_est = np.load(
             os.path.join(DATA_DIR, "clustered_coords_est.npy")
-        )
+        ).astype(dtype=self.dtype)
 
         result = self.sim.eval_coords(mean_est, eigs_est, clustered_coords_est)
 
@@ -551,6 +556,8 @@ class SimTestCase(TestCase):
                 0.11048937,
                 0.11048937,
             ],
+            rtol=1e-05,
+            atol=1e-08,
         )
 
         np.testing.assert_allclose(
diff --git a/tests/test_symmetry_groups.py b/tests/test_symmetry_groups.py
index 236f1c60..2be56cbf 100644
--- a/tests/test_symmetry_groups.py
+++ b/tests/test_symmetry_groups.py
@@ -21,36 +21,30 @@ GROUPS_WITH_ORDER = [
     DnSymmetryGroup,
 ]
 GROUPS_WITHOUT_ORDER = [
-    TSymmetryGroup,
-    OSymmetryGroup,
+    (TSymmetryGroup,),
+    (OSymmetryGroup,),
 ]
 ORDERS = [2, 3, 4, 5]
-DTYPES = [np.float32, np.float64]
-PARAMS_ORDER = list(itertools.product(GROUPS_WITH_ORDER, DTYPES, ORDERS))
-PARAMS = list(itertools.product(GROUPS_WITHOUT_ORDER, DTYPES))
+PARAMS_ORDER = list(itertools.product(GROUPS_WITH_ORDER, ORDERS))
 
 
 def group_fixture_id(params):
     group_class = params[0]
-    dtype = params[1]
-    if len(params) > 2:
-        order = params[2]
-        return f"{group_class.__name__}, order={order}, dtype={dtype}"
+    if len(params) > 1:
+        order = params[1]
+        return f"{group_class.__name__}, order={order}"
     else:
-        return f"{group_class.__name__}, dtype={dtype}"
+        return f"{group_class.__name__}"
 
 
 # Create SymmetryGroup fixture for the set of parameters.
-@pytest.fixture(params=PARAMS + PARAMS_ORDER, ids=group_fixture_id)
+@pytest.fixture(params=GROUPS_WITHOUT_ORDER + PARAMS_ORDER, ids=group_fixture_id)
 def group_fixture(request):
     params = request.param
     group_class = params[0]
-    dtype = params[1]
-    group_kwargs = dict(
-        dtype=dtype,
-    )
-    if len(params) > 2:
-        group_kwargs["order"] = params[2]
+    group_kwargs = dict()
+    if len(params) > 1:
+        group_kwargs["order"] = params[1]
 
     return group_class(**group_kwargs)
 
@@ -68,7 +62,7 @@ def test_group_str(group_fixture):
 
 
 def test_group_equivalence(group_fixture):
-    C2_symmetry_group = CnSymmetryGroup(order=2, dtype=group_fixture.dtype)
+    C2_symmetry_group = CnSymmetryGroup(order=2)
     if str(group_fixture) == "C2":
         assert C2_symmetry_group == group_fixture
     else:
@@ -80,28 +74,20 @@ def test_group_rotations(group_fixture):
     assert isinstance(rotations, Rotation)
 
 
+def test_dtype(group_fixture):
+    """Test SymmetryGroup matrices are always doubles."""
+    np.testing.assert_equal(group_fixture.matrices.dtype, np.float64)
+
+
 def test_parser_identity():
-    result = SymmetryGroup.parse("C1", dtype=np.float32)
+    result = SymmetryGroup.parse("C1")
     assert isinstance(result, IdentitySymmetryGroup)
 
 
 def test_parser_with_group(group_fixture):
     """Test SymmetryGroup instance are parsed correctly."""
-    result = SymmetryGroup.parse(group_fixture, group_fixture.dtype)
+    result = SymmetryGroup.parse(group_fixture)
     assert result == group_fixture
-    assert result.dtype == group_fixture.dtype
-
-
-def test_parser_dtype_casting(group_fixture, caplog):
-    """Test that dtype gets re-cast and warns."""
-    dtype = np.float32
-    if group_fixture.dtype == np.float32:
-        dtype = np.float64
-
-    caplog.clear()
-    msg = f"Recasting SymmetryGroup with dtype {dtype}."
-    _ = SymmetryGroup.parse(group_fixture, dtype)
-    assert msg in caplog.text
 
 
 def test_parser_error():
@@ -109,4 +95,4 @@ def test_parser_error():
     with pytest.raises(
         ValueError, match=f"Symmetry type {junk_symmetry[0]} not supported.*"
     ):
-        _ = SymmetryGroup.parse(junk_symmetry, dtype=np.float32)
+        _ = SymmetryGroup.parse(junk_symmetry)
diff --git a/tests/test_synthetic_volume.py b/tests/test_synthetic_volume.py
index fec75917..019b99b4 100644
--- a/tests/test_synthetic_volume.py
+++ b/tests/test_synthetic_volume.py
@@ -114,6 +114,12 @@ def test_volume_generate(vol_fixture):
         np.testing.assert_approx_equal(v.pixel_size, PXSZ)
 
 
+def test_dtype_passthrough(vol_fixture, dtype_fixture):
+    """Test Volume are generated with correct dtype."""
+    vol = vol_fixture.generate()
+    np.testing.assert_equal(vol._data.dtype, dtype_fixture)
+
+
 def test_simulation_init(vol_fixture):
     """Test that a Simulation initializes provided a synthetic Volume."""
     _ = Simulation(L=vol_fixture.L, vols=vol_fixture.generate())
@@ -135,12 +141,12 @@ def test_compact_support(vol_fixture):
 
 # Supress expected warnings due to rotation of symmetric volume.
 @pytest.mark.filterwarnings("ignore:`symmetry_group` attribute is being set to `C1`")
-def test_volume_symmetry(vol_fixture):
+def test_volume_symmetry(vol_fixture, dtype_fixture):
     """Test that volumes have intended symmetry."""
     vol = vol_fixture.generate()
 
     # Rotations in symmetry group, excluding the Identity.
-    rots = vol_fixture.symmetry_group.matrices[1:]
+    rots = vol_fixture.symmetry_group.matrices[1:].astype(dtype_fixture)
 
     for rot in rots:
         # Rotate volume by an element of the symmetric group.
diff --git a/tests/test_volume.py b/tests/test_volume.py
index d2208607..42ed4675 100644
--- a/tests/test_volume.py
+++ b/tests/test_volume.py
@@ -821,7 +821,7 @@ def test_project_broadcast(dtype):
 # SYM_GROUP_PARAMS consists of (initializing method, string representation).
 # Testing just the basic cases of setting the symmetry group from
 # a SymmetryGroup instance, a string, and the default.
-SYM_GROUP_PARAMS = [(TSymmetryGroup(np.float32), "T"), ("D2", "D2"), (None, "C1")]
+SYM_GROUP_PARAMS = [(TSymmetryGroup(), "T"), ("D2", "D2"), (None, "C1")]
 
 
 @pytest.mark.parametrize("sym_group, sym_string", SYM_GROUP_PARAMS)
diff --git a/tests/test_weighted_mean_estimator.py b/tests/test_weighted_mean_estimator.py
index eabcd057..3f58fcdd 100644
--- a/tests/test_weighted_mean_estimator.py
+++ b/tests/test_weighted_mean_estimator.py
@@ -43,6 +43,11 @@ def dtype(request):
     return request.param
 
 
+@pytest.fixture(params=DTYPE, ids=lambda x: f"weights_dtype={x}", scope="module")
+def weights_dtype(request):
+    return request.param
+
+
 @pytest.fixture(scope="module")
 def sim(L, dtype):
     sim = Simulation(
@@ -67,11 +72,11 @@ def basis(L, dtype):
 
 
 @pytest.fixture(scope="module")
-def weights(sim):
+def weights(sim, weights_dtype):
     # Construct simple test weights;
     # one uniform positive and negative weighted volume respectively.
     r = 2  # Number of weighted volumes
-    weights = np.ones((sim.n, r)) / np.sqrt(sim.n)
+    weights = np.full((sim.n, r), 1 / np.sqrt(sim.n), dtype=weights_dtype)
     weights[:, 1] *= -1  # negate second weight vector
 
     return weights