Repository: teamtomo/torch-so3
Pull request #11: Cartesian Euler angle grid functionality
Author: login: jdickerson95
Created: 2025-03-31T21:39:41Z | Merged: 2025-04-23T00:53:16Z

Add option to do a basic s2 grid (just using arange).
Also fix error when min angle = max angle.


Diff:
diff --git a/examples/generating_so3_angles.ipynb b/examples/generating_so3_angles.ipynb
index 992c4e8..2d9e246 100644
--- a/examples/generating_so3_angles.ipynb
+++ b/examples/generating_so3_angles.ipynb
@@ -34,8 +34,8 @@
    ],
    "source": [
     "angles = get_uniform_euler_angles(\n",
-    "    in_plane_step=1.5,\n",
-    "    out_of_plane_step=2.5,\n",
+    "    psi_step=1.5,\n",
+    "    theta_step=2.5,\n",
     "    phi_min=0.0,\n",
     "    phi_max=360.0,\n",
     "    theta_min=0.0,\n",
diff --git a/examples/visualizing_s2_grids.ipynb b/examples/visualizing_s2_grids.ipynb
index e9532f5..e565fdc 100644
--- a/examples/visualizing_s2_grids.ipynb
+++ b/examples/visualizing_s2_grids.ipynb
@@ -6,7 +6,7 @@
    "source": [
     "# Visualizing base S2 grids\n",
     "\n",
-    "The `torch-so3` package uses the [Hopf Fibration](https://en.wikipedia.org/wiki/Hopf_fibration) to generate Euler angles that sample the 3D rotation group SO(3) uniformly. We roughly follow the methods outline in the paper [Generating Uniform Incremental Grids on SO(3) Using the Hopf Fibration](https://doi.org/10.1177/0278364909352700) by Yershova et al. to generate these grids."
+    "The `torch-so3` package uses the uniform sampling to generate Euler angles that sample the 3D rotation group SO(3) uniformly."
    ]
   },
   {
@@ -70,7 +70,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "out_of_plane_step = 2.5"
+    "theta_step = 2.5"
    ]
   },
   {
@@ -87,7 +87,7 @@
     }
    ],
    "source": [
-    "theta_phi_pairs_uniform = uniform_base_grid(out_of_plane_step)\n",
+    "theta_phi_pairs_uniform = uniform_base_grid(theta_step)\n",
     "print(\"Total points (uniform):\", theta_phi_pairs_uniform.shape[0])"
    ]
   },
@@ -105,7 +105,7 @@
     }
    ],
    "source": [
-    "theta_phi_pairs_healpix = healpix_base_grid(out_of_plane_step)\n",
+    "theta_phi_pairs_healpix = healpix_base_grid(theta_step)\n",
     "print(\"Total points (healpix):\", theta_phi_pairs_healpix.shape[0])"
    ]
   },
diff --git a/pyproject.toml b/pyproject.toml
index 2c3743e..fb6b21f 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -34,7 +34,11 @@ classifiers = [
     "Typing :: Typed",
 ]
 # add your package dependencies here
-dependencies = ["torch", "numpy", "healpy; platform_system != 'Windows'"]
+dependencies = [
+    "torch", 
+    "numpy", 
+    "healpy; platform_system != 'Windows'"
+]
 
 # https://peps.python.org/pep-0621/#dependencies-optional-dependencies
 # "extras" (e.g. for `pip install .[test]`)
diff --git a/src/torch_so3/__init__.py b/src/torch_so3/__init__.py
index 1256771..a4a8f2d 100644
--- a/src/torch_so3/__init__.py
+++ b/src/torch_so3/__init__.py
@@ -11,5 +11,16 @@
 
 from .local_so3_sampling import get_local_high_resolution_angles
 from .uniform_so3_sampling import get_uniform_euler_angles
+from .angular_ranges import SymmetryRanges, get_symmetry_ranges
+from .base_s2_grid import cartesian_base_grid, healpix_base_grid, uniform_base_grid
 
-__all__ = ["get_uniform_euler_angles", "get_local_high_resolution_angles"]
+
+__all__ = [
+    "get_uniform_euler_angles",
+    "get_local_high_resolution_angles",
+    "get_symmetry_ranges",
+    "SymmetryRanges",
+    "uniform_base_grid",
+    "healpix_base_grid",
+    "cartesian_base_grid",
+]
diff --git a/src/torch_so3/base_s2_grid.py b/src/torch_so3/base_s2_grid.py
index 262c7c7..1900170 100644
--- a/src/torch_so3/base_s2_grid.py
+++ b/src/torch_so3/base_s2_grid.py
@@ -3,16 +3,16 @@
 import platform
 import warnings
 
+import numpy as np
+import torch
 if platform.system() == "Windows":
     warnings.warn("healpy cannot be installed on Windows systems.", stacklevel=2)
 else:
     import healpy as hp
-import numpy as np
-import torch
 
 
 def uniform_base_grid(
-    out_of_plane_step: float = 2.5,
+    theta_step: float = 2.5,
     theta_min: float = 0.0,
     theta_max: float = 180.0,
     phi_min: float = 0.0,
@@ -20,13 +20,13 @@ def uniform_base_grid(
 ) -> torch.Tensor:
     """Generate a uniform base grid on the S^2 sphere.
 
+    phi step is calculated by the position on the sphere (sin(theta))
+
+
     Parameters
     ----------
-    in_plane_step : float, optional
-        Angular step for in-plane rotation (phi) in degrees. Default is 1.5 degrees.
-    out_of_plane_step : float, optional
-        Angular step for out-of-plane rotation (theta) in degrees. Default is 2.5
-        degrees.
+    theta_step : float, optional
+        Angular step for theta in degrees. Default is 2.5
     theta_min : float, optional
         Minimum value for theta in degrees. Default is 0.0.
     theta_max : float, optional
@@ -42,29 +42,35 @@ def uniform_base_grid(
         Tensor of shape (N, 2) containing theta and phi values in degrees, where N is
         the number of angles pairs generated.
     """
-    out_of_plane_step_rad = np.deg2rad(out_of_plane_step)
+    theta_step_rad = np.deg2rad(theta_step)
     phi_min_rad = np.deg2rad(phi_min)
     phi_max_rad = np.deg2rad(phi_max)
 
     # generate uniform set of theta values
     theta_all = np.arange(
-        theta_min, theta_max + out_of_plane_step, out_of_plane_step, dtype=np.float64
+        theta_min, theta_max + theta_step, theta_step, dtype=np.float64
     )
     theta_all = np.deg2rad(theta_all)
 
     # Phi step increment is modulated by the position on the sphere (sin(theta)), but
     # don't allow it to exceed the maximum step size
     phi_max_step_rad = phi_max_rad - phi_min_rad
-    phi_step_all = np.abs(out_of_plane_step_rad / (np.sin(theta_all) + 1e-6))
+    phi_step_all = np.abs(theta_step_rad / (np.sin(theta_all) + 1e-6))
     phi_step_all = np.clip(phi_step_all, a_min=None, a_max=phi_max_step_rad)
-    phi_step_all = phi_max_step_rad / np.round(phi_max_step_rad / phi_step_all)
+    if phi_max_step_rad > 0.0:
+        phi_step_all = phi_max_step_rad / np.round(phi_max_step_rad / phi_step_all)
+    else:
+        phi_step_all *= 0.0
 
     # Now generate the angle pairs
     angle_pairs = []
     for i, phi_step in enumerate(phi_step_all):
-        phi_values = np.arange(phi_min_rad, phi_max_rad, phi_step, dtype=np.float64)
+        if phi_min_rad >= phi_max_rad or phi_step <= 0.0:
+            phi_values = np.array([phi_min_rad], dtype=np.float64)
+        else:
+            phi_values = np.arange(phi_min_rad, phi_max_rad, phi_step, dtype=np.float64)
         theta_values = np.full_like(phi_values, theta_all[i])
-        angle_pairs.append(np.stack([theta_values, phi_values], axis=1))
+        angle_pairs.append(np.stack([phi_values, theta_values], axis=1))
 
     # Convert back to degrees
     angle_pairs = np.rad2deg(np.concatenate(angle_pairs))
@@ -73,7 +79,7 @@ def uniform_base_grid(
 
 
 def healpix_base_grid(
-    out_of_plane_step: float = 2.5,
+    theta_step: float = 2.5,
     theta_min: float = 0.0,
     theta_max: float = 180.0,
     phi_min: float = 0.0,
@@ -81,13 +87,12 @@ def healpix_base_grid(
 ) -> torch.Tensor:
     """Generate a base grid on the S^2 sphere using HEALPix.
 
+    phi step is calculated by the position on the sphere
+
     Parameters
     ----------
-    in_plane_step : float, optional
-        Angular step for in-plane rotation (phi) in degrees. Default is 1.5 degrees.
-    out_of_plane_step : float, optional
-        Angular step for out-of-plane rotation (theta) in degrees. Default is 2.5
-        degrees.
+    theta_step : float, optional
+        Angular step for theta in degrees. Default is 2.5
     theta_min : float, optional
         Minimum value for theta in degrees. Default is 0.0.
     theta_max : float, optional
@@ -106,11 +111,9 @@ def healpix_base_grid(
     if platform.system() == "Windows":
         raise ImportError("healpy cannot be installed on Windows systems.")
 
-    out_of_plane_step_rad = np.deg2rad(out_of_plane_step)
+    theta_step_rad = np.deg2rad(theta_step)
 
-    estimated_num_pixels = int(
-        4 * np.pi / (out_of_plane_step_rad * out_of_plane_step_rad)
-    )
+    estimated_num_pixels = int(4 * np.pi / (theta_step_rad * theta_step_rad))
 
     # Find the next largest npix value for a valid healpix grid
     exact_num_pixels = 0
@@ -152,6 +155,60 @@ def healpix_base_grid(
         theta_values = torch.cat(theta_result)
         phi_values = torch.cat(phi_result)
 
-    angle_pairs = torch.stack([theta_values, phi_values], dim=1)
+    angle_pairs = torch.stack([phi_values, theta_values], dim=1)
 
     return angle_pairs
+
+
+def cartesian_base_grid(
+    theta_step: float = 2.5,
+    phi_step: float = 1.5,
+    theta_min: float = 0.0,
+    theta_max: float = 180.0,
+    phi_min: float = 0.0,
+    phi_max: float = 360.0,
+) -> torch.Tensor:
+    """Generate a base grid on the S^2 sphere using a cartesian grid.
+
+    phi step is now set explicitly. This will oversample the poles.
+
+    Parameters
+    ----------
+    theta_step : float, optional
+        Angular step for theta in degrees. Default is 2.5
+    phi_step : float, optional
+        Angular step for phi in degrees. Default is 1.5 degrees.
+    theta_min : float, optional
+        Minimum value for theta in degrees. Default is 0.0.
+    theta_max : float, optional
+        Maximum value for theta in degrees. Default is 180.0.
+    phi_min : float, optional
+        Minimum value for phi in degrees. Default is 0.0.
+    phi_max : float, optional
+        Maximum value for phi in degrees. Default is 360.0.
+
+    Returns
+    -------
+    torch.Tensor
+        Tensor of shape (N, 2) containing theta and phi values in degrees, where N is
+        the number of angles pairs generated.
+    """
+    # Generate the base grid
+    phi_values = torch.arange(
+        phi_min,
+        phi_max,
+        phi_step,
+        dtype=torch.float64,
+    )
+
+    theta_values = torch.arange(
+        theta_min,
+        theta_max,
+        theta_step,
+        dtype=torch.float64,
+    )
+
+    grid = torch.meshgrid(phi_values, theta_values, indexing="ij")
+    euler_angles = torch.stack(grid, dim=-1).reshape(-1, 2)
+
+    return euler_angles
diff --git a/src/torch_so3/local_so3_sampling.py b/src/torch_so3/local_so3_sampling.py
index 0057091..7a6e966 100644
--- a/src/torch_so3/local_so3_sampling.py
+++ b/src/torch_so3/local_so3_sampling.py
@@ -1,5 +1,7 @@
 """Generate finer angular search around multiple selected Euler angles."""
 
+from typing import Literal
+
 import torch
 
 from torch_so3.uniform_so3_sampling import get_uniform_euler_angles
@@ -8,10 +10,13 @@
 
 
 def get_local_high_resolution_angles(
-    coarse_in_plane_step: float = 1.5,
-    coarse_out_of_plane_step: float = 2.5,
-    fine_in_plane_step: float = 0.1,
-    fine_out_of_plane_step: float = 0.1,
+    coarse_phi_step: float = 2.5,
+    coarse_theta_step: float = 2.5,
+    coarse_psi_step: float = 1.5,
+    fine_phi_step: float = 0.1,
+    fine_theta_step: float = 0.1,
+    fine_psi_step: float = 0.1,
+    base_grid_method: Literal["uniform", "healpix", "cartesian"] = "uniform",
 ) -> torch.Tensor:
     """Local orientation refinement from a coarse to fine grid.
 
@@ -21,14 +26,20 @@ def get_local_high_resolution_angles(
 
     Parameters
     ----------
-    coarse_in_plane_step : float
-        Coarse step size for in-plane angle (psi) in degrees.
-    coarse_out_of_plane_step : float
-        Coarse step size for out-of-plane angle (theta, phi) in degrees.
-    fine_in_plane_step : float
-        Finer step size for in-plane angles (psi) in degrees.
-    fine_out_of_plane_step : float
-        Finer step size for out-of-plane angle (theta, phi) in degrees.
+    coarse_phi_step : float
+        Coarse step size for phi in degrees.
+    coarse_theta_step : float
+        Coarse step size for theta in degrees.
+    coarse_psi_step : float
+        Coarse step size for psi in degrees.
+    fine_phi_step : float
+        Finer step size for phi in degrees.
+    fine_theta_step : float
+        Finer step size for theta in degrees.
+    fine_psi_step : float
+        Finer step size for psi in degrees.
+    base_grid_method : Literal["uniform", "healpix", "cartesian"]
+        Method to generate the base grid.
 
     Returns
     -------
@@ -37,15 +48,30 @@ def get_local_high_resolution_angles(
         number of angles generated. Angles exist around pole (0, 0, 0) and define grid
         to search over.
     """
-    euler_angles = get_uniform_euler_angles(
-        in_plane_step=fine_in_plane_step,
-        out_of_plane_step=fine_out_of_plane_step,
-        psi_min=-coarse_in_plane_step,
-        psi_max=coarse_in_plane_step + EPS,
-        theta_min=0.0,
-        theta_max=coarse_out_of_plane_step,
-        phi_min=0.0,  # Completely sample around the s2 sphere
-        phi_max=360.0,
-    )
+    if base_grid_method == "cartesian":
+        euler_angles = get_uniform_euler_angles(
+            phi_step=fine_phi_step,
+            theta_step=fine_theta_step,
+            psi_step=fine_psi_step,
+            phi_min=-coarse_phi_step,  # Completely sample for uniform base grid?
+            phi_max=coarse_phi_step,
+            theta_min=-coarse_theta_step,
+            theta_max=coarse_theta_step,
+            psi_min=-coarse_psi_step,
+            psi_max=coarse_psi_step + EPS,
+            base_grid_method=base_grid_method,
+        )
+    else:
+        euler_angles = get_uniform_euler_angles(
+            psi_step=fine_psi_step,
+            theta_step=fine_theta_step,
+            phi_min=-coarse_psi_step,  # Completely sample for uniform base grid?
+            phi_max=coarse_psi_step,
+            theta_min=-coarse_theta_step,
+            theta_max=coarse_theta_step,
+            psi_min=-coarse_psi_step,
+            psi_max=coarse_psi_step + EPS,
+            base_grid_method=base_grid_method,
+        )
 
     return euler_angles
diff --git a/src/torch_so3/uniform_so3_sampling.py b/src/torch_so3/uniform_so3_sampling.py
index 38298e6..46d8f95 100644
--- a/src/torch_so3/uniform_so3_sampling.py
+++ b/src/torch_so3/uniform_so3_sampling.py
@@ -1,32 +1,41 @@
 """Generates a set of Euler angles that uniformly samples SO(3) using Hopf fibration."""
 
-from typing import Literal
+import warnings
+from typing import Literal, Optional
 
 import torch
 
-from torch_so3.base_s2_grid import healpix_base_grid, uniform_base_grid
+from torch_so3.base_s2_grid import (
+    cartesian_base_grid,
+    healpix_base_grid,
+    uniform_base_grid,
+)
 
 
 def get_uniform_euler_angles(
-    in_plane_step: float = 1.5,
-    out_of_plane_step: float = 2.5,
+    psi_step: float = 1.5,
+    theta_step: float = 2.5,
+    phi_step: Optional[float] = None,
     phi_min: float = 0.0,
     phi_max: float = 360.0,
     theta_min: float = 0.0,
     theta_max: float = 180.0,
     psi_min: float = 0.0,
     psi_max: float = 360.0,
-    base_grid_method: Literal["uniform", "healpix"] = "uniform",
+    base_grid_method: Literal["uniform", "healpix", "cartesian"] = "uniform",
 ) -> torch.Tensor:
     """Generate sets of uniform Euler angles (ZYZ) using Hopf fibration.
 
     Parameters
     ----------
-    in_plane_step: float, optional
-        Angular step for in-plane rotation (phi) in degrees. Default is 1.5 degrees.
-    out_of_plane_step: float, optional
-        Angular step for out-of-plane rotation (theta) in degrees. Default is 2.5
+    psi_step: float, optional
+        Angular step for psi in degrees. Default is 1.5 degrees.
+    theta_step: float, optional
+        Angular step for theta in degrees. Default is 2.5
         degrees.
+    phi_step: float, optional
+        Angular step for phi rotation in degrees. Only used when base_grid_method is
+        "cartesian". Default is 2.5 degrees.
     phi_min: float, optional
         Minimum value for phi in degrees. Default is 0.0.
     phi_max: float, optional
@@ -41,7 +50,7 @@ def get_uniform_euler_angles(
         Maximum value for psi in degrees. Default is 360.0.
     base_grid_method: str, optional
         String literal specifying the method to generate the base grid. Default is
-        "uniform". Options are "uniform" and "healpix".
+        "uniform". Options are "uniform", "healpix", and "cartesian".
 
     Returns
     -------
@@ -51,27 +60,46 @@ def get_uniform_euler_angles(
     """
     # TODO: Validation of inputs, wrapping between zero and 2*pi, etc.
 
-    # Choose the method to generate the base grid from
-    if base_grid_method == "uniform":
-        base_grid_mth = uniform_base_grid
-    elif base_grid_method == "healpix":
-        base_grid_mth = healpix_base_grid
+    if base_grid_method == "cartesian":
+        # Handle cartesian_base_grid separately since it has a different signature
+        actual_phi_step = 2.5 if phi_step is None else phi_step
+        base_grid = cartesian_base_grid(
+            theta_step=theta_step,
+            phi_step=actual_phi_step,
+            theta_min=theta_min,
+            theta_max=theta_max,
+            phi_min=phi_min,
+            phi_max=phi_max,
+        )
     else:
-        raise ValueError(f"Invalid base grid method {base_grid_method}.")
+        # Handle uniform and healpix grids
+        if base_grid_method == "uniform":
+            base_grid_mth = uniform_base_grid
+        elif base_grid_method == "healpix":
+            base_grid_mth = healpix_base_grid
+        else:
+            raise ValueError(f"Invalid base grid method {base_grid_method}.")
 
-    base_grid = base_grid_mth(
-        out_of_plane_step=out_of_plane_step,
-        theta_min=theta_min,
-        theta_max=theta_max,
-        phi_min=phi_min,
-        phi_max=phi_max,
-    )
+        # Check if phi_step was specified for non-cartesian methods
+        if phi_step is not None:
+            warnings.warn(
+                f"phi_step is being ignored for {base_grid_method} method.",
+                stacklevel=2,
+            )
 
-    # Change order of base_grid from (theta, phi) to (phi, theta)
-    base_grid = base_grid[:, [1, 0]]
+        base_grid = base_grid_mth(
+            theta_step=theta_step,
+            theta_min=theta_min,
+            theta_max=theta_max,
+            phi_min=phi_min,
+            phi_max=phi_max,
+        )
 
     # Mesh-grid-like operation to include the in-plane rotation
-    psi_all = torch.arange(psi_min, psi_max, in_plane_step, dtype=torch.float64)
+    if psi_min >= psi_max:
+        psi_all = torch.tensor([psi_min], dtype=torch.float64)
+    else:
+        psi_all = torch.arange(psi_min, psi_max, psi_step, dtype=torch.float64)
 
     psi_mesh = psi_all.repeat_interleave(base_grid.size(0))
     base_grid = base_grid.repeat(psi_all.size(0), 1)
diff --git a/tests/test_torch_angular_search.py b/tests/test_torch_angular_search.py
index 92df2dc..0a11bf5 100644
--- a/tests/test_torch_angular_search.py
+++ b/tests/test_torch_angular_search.py
@@ -35,12 +35,12 @@ def test_get_uniform_euler_angles_healpix():
 
 def test_get_local_high_resolution_angles():
     local_angles = get_local_high_resolution_angles()
-    assert local_angles.shape == (63333, 3)
+    assert local_angles.shape == (1581, 3)
 
     # range tests for angles
-    assert (local_angles[:, 0] >= 0.0).all()
-    assert (local_angles[:, 0] < 360.0).all()
-    assert (local_angles[:, 1] >= 0.0).all()
-    assert (local_angles[:, 1] <= 2.5).all()
-    assert np.allclose(local_angles[:, 2].min().item(), -1.5)
-    assert np.allclose(local_angles[:, 2].max().item(), 1.5)
+    assert (local_angles[:, 0] >= -1.51).all()
+    assert (local_angles[:, 0] <= 1.51).all()
+    assert (local_angles[:, 1] >= -2.51).all()
+    assert (local_angles[:, 1] <= 2.51).all()
+    assert np.allclose(local_angles[:, 2].min().item(), -1.50)
+    assert np.allclose(local_angles[:, 2].max().item(), 1.50)


Reviews:
user: login: mgiammar
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-04-02T22:17:03Z
user: login: mgiammar
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-02T22:33:31Z
user: login: mgiammar
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-22T16:19:55Z

Review comments:
diff_hunk: @@ -22,9 +22,7 @@ def uniform_base_grid(
 
path: src/torch_so3/base_s2_grid.py
user: login: mgiammar
body: Add a note in the docstring mentioning that the phi step is automatically calculated based on the angular ranges and theta step size.
created_at: 2025-04-02T22:10:50Z
author_association: COLLABORATOR
diff_hunk: @@ -89,9 +87,7 @@ def healpix_base_grid(
 
     Parameters
     ----------
-    in_plane_step : float, optional
-        Angular step for in-plane rotation (phi) in degrees. Default is 1.5 degrees.
-    out_of_plane_step : float, optional
+    theta_step : float, optional
         Angular step for out-of-plane rotation (theta) in degrees. Default is 2.5
path: src/torch_so3/base_s2_grid.py
user: login: mgiammar
body: Remove reference to "out-of-plane" rotation
created_at: 2025-04-02T22:11:37Z
author_association: COLLABORATOR
diff_hunk: @@ -22,9 +22,7 @@ def uniform_base_grid(
 
     Parameters
     ----------
-    in_plane_step : float, optional
-        Angular step for in-plane rotation (phi) in degrees. Default is 1.5 degrees.
-    out_of_plane_step : float, optional
+    theta_step : float, optional
         Angular step for out-of-plane rotation (theta) in degrees. Default is 2.5
path: src/torch_so3/base_s2_grid.py
user: login: mgiammar
body: Remove reference to "out-of-plane" step
created_at: 2025-04-02T22:11:54Z
author_association: COLLABORATOR
diff_hunk: @@ -10,10 +10,12 @@
 
 
 def get_local_high_resolution_angles(
-    coarse_in_plane_step: float = 1.5,
-    coarse_out_of_plane_step: float = 2.5,
-    fine_in_plane_step: float = 0.1,
-    fine_out_of_plane_step: float = 0.1,
+    coarse_psi_step: float = 1.5,
path: src/torch_so3/local_so3_sampling.py
user: login: mgiammar
body: Should these parameters be ordered `coarse_phi_step`, `coarse_theta_step`, and `coarse_psi_step` to match how the tuple of angles are represented in the rest of the package? Not super set on formatting it this way, but seems to make sense
created_at: 2025-04-02T22:13:36Z
author_association: COLLABORATOR
diff_hunk: @@ -42,16 +48,30 @@ def get_local_high_resolution_angles(
         number of angles generated. Angles exist around pole (0, 0, 0) and define grid
         to search over.
     """
-    euler_angles = get_uniform_euler_angles(
-        in_plane_step=fine_in_plane_step,
-        out_of_plane_step=fine_out_of_plane_step,
-        phi_min=-coarse_in_plane_step,  # Completely sample for uniform base grid?
-        phi_max=coarse_in_plane_step,
-        theta_min=-coarse_out_of_plane_step,
-        theta_max=coarse_out_of_plane_step,
-        psi_min=-coarse_in_plane_step,
-        psi_max=coarse_in_plane_step + EPS,
-        base_grid_method=base_grid_method,
-    )
+    if base_grid_method == "cartesian":
path: src/torch_so3/local_so3_sampling.py
user: login: mgiammar
body: The function calls nested in the if/else statement here are the same (I think). If so, then should remove the if/else stmnt.
created_at: 2025-04-02T22:15:34Z
author_association: COLLABORATOR
diff_hunk: @@ -42,16 +48,30 @@ def get_local_high_resolution_angles(
         number of angles generated. Angles exist around pole (0, 0, 0) and define grid
         to search over.
     """
-    euler_angles = get_uniform_euler_angles(
-        in_plane_step=fine_in_plane_step,
-        out_of_plane_step=fine_out_of_plane_step,
-        phi_min=-coarse_in_plane_step,  # Completely sample for uniform base grid?
-        phi_max=coarse_in_plane_step,
-        theta_min=-coarse_out_of_plane_step,
-        theta_max=coarse_out_of_plane_step,
-        psi_min=-coarse_in_plane_step,
-        psi_max=coarse_in_plane_step + EPS,
-        base_grid_method=base_grid_method,
-    )
+    if base_grid_method == "cartesian":
path: src/torch_so3/local_so3_sampling.py
user: login: mgiammar
body: Function calls are actually different
created_at: 2025-04-02T22:33:31Z
author_association: COLLABORATOR

PR comments:
user: login: mgiammar
created_at: 2025-04-02T21:32:02Z
body: I think there is some ambiguity in what the `in_plane` and `out_of_plane` angular steps represent in how the code is dealing with angles (and downstream used to generate projections). In my mind, the base grid method should only be using the `out_of_plane` angular step, but the new cartesian grid method uses both:
```
        base_grid = cartesian_base_grid(
            out_of_plane_step=out_of_plane_step,
            in_plane_step=in_plane_step,
            theta_min=theta_min,
            theta_max=theta_max,
            phi_min=phi_min,
            phi_max=phi_max,
        )
```

Also, for the cartesian base grid it doesn't make sense to couple the `phi` angle to the step size of the `psi` angle. We should probably get some clarification and update the code accordingly to be more explicit.
author_association: COLLABORATOR
user: login: jdickerson95
created_at: 2025-04-08T06:00:34Z
body: For macOS it looks like we need to import torch before we import healpy. Not sure exactly why but worth noting moving forward.
author_association: COLLABORATOR
user: login: mgiammar
created_at: 2025-04-08T22:08:36Z
body: > For macOS it looks like we need to import torch before we import healpy. Not sure exactly why but worth noting moving forward.

Yeah I'm not sure the exact interaction between numpy/torch/healpy here that was breaking. I tried pulling the most recent commit and running the test on my mac, but still ran into the import error. Had to delete and reinstall my dev conda environment then everything passed... odd

For reference, this combination passed the tests:

- MacOS Sequoia 15.1.1
- Python 3.12.9
- numpy 2.2.4
- torch 2.6.0
- healpy 1.18.1
author_association: COLLABORATOR
user: login: jdickerson95
created_at: 2025-04-08T22:20:06Z
body: > > For macOS it looks like we need to import torch before we import healpy. Not sure exactly why but worth noting moving forward.
> 
> Yeah I'm not sure the exact interaction between numpy/torch/healpy here that was breaking. I tried pulling the most recent commit and running the test on my mac, but still ran into the import error. Had to delete and reinstall my dev conda environment then everything passed... odd
> 
> For reference, this combination passed the tests:
> 
> * MacOS Sequoia 15.1.1
> * Python 3.12.9
> * numpy 2.2.4
> * torch 2.6.0
> * healpy 1.18.1

Yeah the packages need to be initialized in a particular order for installing the environment. The runner is on macOS 14 but everything else is similar. If it's worth anything, this is what claude 3.7 thought:
"There's a platform check for Windows and a conditional import of the healpy module. This type of conditional import can sometimes lead to initialization issues, especially on macOS. You must ensure that PyTorch and its core functions are fully initialized before importing the module with the conditional external dependency. healpy depends on several C/C++ libraries, and on macOS, the way these are linked and loaded can be different from other platforms. The import order may prevent a memory issue during initialization."
author_association: COLLABORATOR
user: login: mgiammar
created_at: 2025-04-22T16:20:20Z
body: @jdickerson95 Is this PR ready to merge?
author_association: COLLABORATOR