From bcf4f21b1a740516c502fc87d87353821990fd9f Mon Sep 17 00:00:00 2001 From: Eraldo Marques <119956342+VoxLeone@users.noreply.github.com> Date: Wed, 14 May 2025 18:13:34 -0300 Subject: [PATCH 01/22] Create CHANGELOG.md --- CHANGELOG.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..40321fe --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,27 @@ +# Changelog + +All notable changes to this project are documented in this file. + +This project follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) +and uses [Semantic Versioning](https://semver.org/). + +--- + +## [Unreleased] + +### Added +- MIT license/authorship headers to all `.py` files ([features/cuda]) +- CUDA feature branch for GPU support + +### Changed +- Restructured branch strategy: `main`, `dev`, `features/cuda` +- Clarified setup instructions in `README.md` + +--- + +## [0.1.0] – 2025-05-14 + +### Added +- Initial codebase and Git setup +- Created branches: `main`, `dev`, and `feature/cuda` +- Basic Python project scaffold From 8353ccca95cc8fa639d86abd79bc229eee324d23 Mon Sep 17 00:00:00 2001 From: Eraldo Marques <119956342+VoxLeone@users.noreply.github.com> Date: Wed, 14 May 2025 19:31:00 -0300 Subject: [PATCH 02/22] Create ph.py --- spinstep/utils/ph.py | 1 + 1 file changed, 1 insertion(+) create mode 100644 spinstep/utils/ph.py diff --git a/spinstep/utils/ph.py b/spinstep/utils/ph.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/spinstep/utils/ph.py @@ -0,0 +1 @@ + From 91d0f5d95557c8b99f297e083320cb7a4191ca2f Mon Sep 17 00:00:00 2001 From: Eraldo Marques <119956342+VoxLeone@users.noreply.github.com> Date: Wed, 14 May 2025 19:32:10 -0300 Subject: [PATCH 03/22] Add files via upload --- spinstep/utils/array_backend.py | 13 +++++++++++++ spinstep/utils/quaternion_math.py | 16 ++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 spinstep/utils/array_backend.py create mode 100644 spinstep/utils/quaternion_math.py diff --git a/spinstep/utils/array_backend.py b/spinstep/utils/array_backend.py new file mode 100644 index 0000000..fd11cad --- /dev/null +++ b/spinstep/utils/array_backend.py @@ -0,0 +1,13 @@ +# array_backend.py — MIT License +# Author: Eraldo B. Marques — Created: 2025-05-14 +# See LICENSE.txt for full terms. This header must be retained in redistributions. + +def get_array_module(use_cuda=False): + if use_cuda: + try: + import cupy as cp + return cp + except ImportError: + print("[SpinStep] CuPy not found, falling back to NumPy.") + import numpy as np + return np diff --git a/spinstep/utils/quaternion_math.py b/spinstep/utils/quaternion_math.py new file mode 100644 index 0000000..ad6338c --- /dev/null +++ b/spinstep/utils/quaternion_math.py @@ -0,0 +1,16 @@ +# quaternion_math.py — MIT License +# Author: Eraldo B. Marques — Created: 2025-05-14 +# See LICENSE.txt for full terms. This header must be retained in redistributions. + +def batch_quaternion_angle(qs1, qs2, xp): + """ + qs1: (N, 4) array + qs2: (M, 4) array + xp: array module (np or cp) + Returns (N, M) array of angular distances. + """ + # Quaternion inner product: angle = 2*arccos(|dot(q1, q2)|) + dots = xp.abs(xp.dot(qs1, qs2.T)) + dots = xp.clip(dots, -1.0, 1.0) + angles = 2 * xp.arccos(dots) + return angles From 8d70b531c04aa7931ba93d8bc26e66f5f8f0d648 Mon Sep 17 00:00:00 2001 From: Eraldo Marques <119956342+VoxLeone@users.noreply.github.com> Date: Wed, 14 May 2025 19:33:23 -0300 Subject: [PATCH 04/22] Delete spinstep/utils/ph.py --- spinstep/utils/ph.py | 1 - 1 file changed, 1 deletion(-) delete mode 100644 spinstep/utils/ph.py diff --git a/spinstep/utils/ph.py b/spinstep/utils/ph.py deleted file mode 100644 index 8b13789..0000000 --- a/spinstep/utils/ph.py +++ /dev/null @@ -1 +0,0 @@ - From 802584445b4ce8d0999c7c8db20126864fd84f8c Mon Sep 17 00:00:00 2001 From: Eraldo Marques <119956342+VoxLeone@users.noreply.github.com> Date: Wed, 14 May 2025 19:49:17 -0300 Subject: [PATCH 05/22] Update discrete.py --- spinstep/discrete.py | 62 +++++++++++++++++++++++++++++--------------- 1 file changed, 41 insertions(+), 21 deletions(-) diff --git a/spinstep/discrete.py b/spinstep/discrete.py index d54b520..fe6b0c3 100644 --- a/spinstep/discrete.py +++ b/spinstep/discrete.py @@ -1,36 +1,56 @@ -import numpy as np -from scipy.spatial.transform import Rotation as R -from sklearn.neighbors import BallTree +from spinstep.utils.array_backend import get_array_module class DiscreteOrientationSet: - def __init__(self, orientations): - arr = np.array(orientations) + def __init__(self, orientations, use_cuda=False): + xp = get_array_module(use_cuda) + arr = xp.array(orientations) if arr.ndim != 2 or arr.shape[1] != 4: raise ValueError("Each orientation must be a quaternion [x, y, z, w]") - norms = np.linalg.norm(arr, axis=1) - if np.any(norms < 1e-8): + norms = xp.linalg.norm(arr, axis=1) + if xp.any(norms < 1e-8): raise ValueError("Zero or near-zero quaternion in orientation set") arr = arr / norms[:, None] self.orientations = arr + self.xp = xp + self.use_cuda = use_cuda - # Precompute rotation vectors for BallTree - self.rotvecs = R.from_quat(arr).as_rotvec() - if len(arr) > 100: - self._balltree = BallTree(self.rotvecs) - else: - self._balltree = None + # Only for CPU/NumPy mode: BallTree for fast queries + self._balltree = None + if not use_cuda: + from scipy.spatial.transform import Rotation as R + from sklearn.neighbors import BallTree + self.rotvecs = R.from_quat(arr).as_rotvec() + if len(arr) > 100: + self._balltree = BallTree(self.rotvecs) + else: + self._balltree = None def query_within_angle(self, quat, angle): """Return indices of orientations within the given angle of quat.""" - rv = R.from_quat(quat).as_rotvec().reshape(1, -1) - if self._balltree is not None: - # BallTree in rotation vector space, Euclidean distance ≈ angle for small rotations - inds = self._balltree.query_radius(rv, r=angle)[0] + if self.use_cuda: + # Brute force: batch math on GPU + # Convert quat to rotvec on CPU, then broadcast to GPU + import numpy as np + from scipy.spatial.transform import Rotation as R + rv = R.from_quat(np.array(quat)).as_rotvec().reshape(1, -1) + rv_gpu = self.xp.array(rv) + dists = self.xp.linalg.norm(self.orientations - rv_gpu, axis=1) + inds = self.xp.where(dists < angle)[0] + return inds else: - # Brute force for small sets - dists = np.linalg.norm(self.rotvecs - rv, axis=1) - inds = np.where(dists < angle)[0] - return inds + from scipy.spatial.transform import Rotation as R + rv = R.from_quat(quat).as_rotvec().reshape(1, -1) + if self._balltree is not None: + inds = self._balltree.query_radius(rv, r=angle)[0] + else: + dists = self.xp.linalg.norm(self.rotvecs - rv, axis=1) + inds = self.xp.where(dists < angle)[0] + return inds + + def as_numpy(self): + if self.use_cuda: + return self.xp.asnumpy(self.orientations) + return self.orientations # ... rest unchanged ... From 86b03879a2240067fdb27360299ba37be6e00d4f Mon Sep 17 00:00:00 2001 From: Eraldo Marques <119956342+VoxLeone@users.noreply.github.com> Date: Wed, 14 May 2025 19:51:53 -0300 Subject: [PATCH 06/22] Update discrete.py --- spinstep/discrete.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/spinstep/discrete.py b/spinstep/discrete.py index fe6b0c3..58f2899 100644 --- a/spinstep/discrete.py +++ b/spinstep/discrete.py @@ -1,3 +1,7 @@ +# discrete.py — MIT License +# Author: Eraldo B. Marques — Created: 2025-05-14 +# See LICENSE.txt for full terms. This header must be retained in redistributions. + from spinstep.utils.array_backend import get_array_module class DiscreteOrientationSet: From dc3563a5591e3fdaa02474b218c715e600ccc4ac Mon Sep 17 00:00:00 2001 From: Eraldo Marques <119956342+VoxLeone@users.noreply.github.com> Date: Wed, 14 May 2025 20:05:57 -0300 Subject: [PATCH 07/22] Update __init.py__ --- spinstep/__init.py__ | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/spinstep/__init.py__ b/spinstep/__init.py__ index 8442e36..c0cc448 100644 --- a/spinstep/__init.py__ +++ b/spinstep/__init.py__ @@ -1,3 +1,7 @@ +# __init__.py — MIT License +# Author: Eraldo B. Marques — Created: 2025-05-14 +# See LICENSE.txt for full terms. This header must be retained in redistributions. + from .node import Node from .traversal import QuaternionDepthIterator from .discrete import DiscreteOrientationSet From f6cd07ac1a6618805fac0b4a5bcfaf1d5eccac0c Mon Sep 17 00:00:00 2001 From: Eraldo Marques <119956342+VoxLeone@users.noreply.github.com> Date: Wed, 14 May 2025 20:09:13 -0300 Subject: [PATCH 08/22] Update demo.py --- spinstep/demo.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/spinstep/demo.py b/spinstep/demo.py index 02e1e7a..8b96118 100644 --- a/spinstep/demo.py +++ b/spinstep/demo.py @@ -1,3 +1,7 @@ +# demo.py — MIT License +# Author: Eraldo Marques — Created: 2025-05-14 +# See LICENSE.txt for full terms. This header must be retained in redistributions. + from spinstep.node import Node from spinstep.traversal import QuaternionDepthIterator from spinstep.quaternion_utils import quaternion_from_euler @@ -7,8 +11,6 @@ # .Applies a quaternion-based depth-first traversal. # .Only visits nodes that lie within a given angular threshold (like aiming a "cone" of rotation). - Only visits nodes that lie within a given angular threshold (like aiming a "cone" of rotation). - def build_demo_tree(): """Build a small tree with varied 3D orientations (yaw, pitch, roll).""" root = Node("root", [0, 0, 0, 1], [ From 7cea844b71b90da54192c654eabfb01b418ea999 Mon Sep 17 00:00:00 2001 From: Eraldo Marques <119956342+VoxLeone@users.noreply.github.com> Date: Wed, 14 May 2025 20:11:03 -0300 Subject: [PATCH 09/22] Update demo1_tree_traversal.py --- spinstep/demo1_tree_traversal.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/spinstep/demo1_tree_traversal.py b/spinstep/demo1_tree_traversal.py index 40285f0..789b16a 100644 --- a/spinstep/demo1_tree_traversal.py +++ b/spinstep/demo1_tree_traversal.py @@ -1,3 +1,7 @@ +# demo1_tree_traversal.py — MIT License +# Author: Eraldo Marques — Created: 2025-05-14 +# See LICENSE.txt for full terms. This header must be retained in redistributions. + import numpy as np from scipy.spatial.transform import Rotation as R From 3753073a1d896bea787084a1d1d2673c56369178 Mon Sep 17 00:00:00 2001 From: Eraldo Marques <119956342+VoxLeone@users.noreply.github.com> Date: Wed, 14 May 2025 20:13:16 -0300 Subject: [PATCH 10/22] Update demo2_full_depth_traversal.py --- spinstep/demo2_full_depth_traversal.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/spinstep/demo2_full_depth_traversal.py b/spinstep/demo2_full_depth_traversal.py index 959926e..c632e07 100644 --- a/spinstep/demo2_full_depth_traversal.py +++ b/spinstep/demo2_full_depth_traversal.py @@ -1,3 +1,7 @@ +# demo2_full_depth_traversal.py — MIT License +# Author: Eraldo Marques — Created: 2025-05-14 +# See LICENSE.txt for full terms. This header must be retained in redistributions. + import numpy as np from scipy.spatial.transform import Rotation as R From 482540ac927783438d0d13376c101e47c2c438e3 Mon Sep 17 00:00:00 2001 From: Eraldo Marques <119956342+VoxLeone@users.noreply.github.com> Date: Wed, 14 May 2025 20:14:13 -0300 Subject: [PATCH 11/22] Update demo3_spatial_traversal.py --- spinstep/demo3_spatial_traversal.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/spinstep/demo3_spatial_traversal.py b/spinstep/demo3_spatial_traversal.py index 5fe0756..320f262 100644 --- a/spinstep/demo3_spatial_traversal.py +++ b/spinstep/demo3_spatial_traversal.py @@ -1,3 +1,7 @@ +# demo3_spatial_traversal.py — MIT License +# Author: Eraldo Marques — Created: 2025-05-14 +# See LICENSE.txt for full terms. This header must be retained in redistributions. + import numpy as np from scipy.spatial.transform import Rotation as R From ddf88a6a96516c35cdb28baec2dabf169be2225d Mon Sep 17 00:00:00 2001 From: Eraldo Marques <119956342+VoxLeone@users.noreply.github.com> Date: Wed, 14 May 2025 20:15:50 -0300 Subject: [PATCH 12/22] Update demo4_discrete_traversal.py --- spinstep/demo4_discrete_traversal.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/spinstep/demo4_discrete_traversal.py b/spinstep/demo4_discrete_traversal.py index 33ab863..f67080c 100644 --- a/spinstep/demo4_discrete_traversal.py +++ b/spinstep/demo4_discrete_traversal.py @@ -1,3 +1,7 @@ +# demo4_discrete_traversal.py — MIT License +# Author: Eraldo Marques — Created: 2025-05-14 +# See LICENSE.txt for full terms. This header must be retained in redistributions. + import numpy as np from scipy.spatial.transform import Rotation as R from spinstep.node import Node @@ -25,4 +29,4 @@ visited_nodes = [] for node in iterator: visited_nodes.append(node.name) - print("Visited:", node.name) \ No newline at end of file + print("Visited:", node.name) From 0a9862fbe7e010282901550e9d6d8ee00c7a4a18 Mon Sep 17 00:00:00 2001 From: Eraldo Marques <119956342+VoxLeone@users.noreply.github.com> Date: Wed, 14 May 2025 20:17:21 -0300 Subject: [PATCH 13/22] Update discrete_iterator.py --- spinstep/discrete_iterator.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/spinstep/discrete_iterator.py b/spinstep/discrete_iterator.py index 2a37334..d6fc61a 100644 --- a/spinstep/discrete_iterator.py +++ b/spinstep/discrete_iterator.py @@ -1,3 +1,7 @@ +# discrete_iterator.py — MIT License +# Author: Eraldo Marques — Created: 2025-05-14 +# See LICENSE.txt for full terms. This header must be retained in redistributions. + import numpy as np from scipy.spatial.transform import Rotation as R From eb27c571301a10dbc040fe9312c5b326a59b44d5 Mon Sep 17 00:00:00 2001 From: Eraldo Marques <119956342+VoxLeone@users.noreply.github.com> Date: Wed, 14 May 2025 20:19:18 -0300 Subject: [PATCH 14/22] Update node.py --- spinstep/node.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/spinstep/node.py b/spinstep/node.py index 83f6a13..eb8bd73 100644 --- a/spinstep/node.py +++ b/spinstep/node.py @@ -1,3 +1,7 @@ +# demo1_tree_traversal.py — MIT License +# Author: Eraldo Marques — Created: 2025-05-14 +# See LICENSE.txt for full terms. This header must be retained in redistributions. + import numpy as np class Node: From cdc884698656df65895fb55c0c2decd9599f8b62 Mon Sep 17 00:00:00 2001 From: Eraldo Marques <119956342+VoxLeone@users.noreply.github.com> Date: Wed, 14 May 2025 20:21:35 -0300 Subject: [PATCH 15/22] Update quaternion_utils.py --- spinstep/quaternion_utils.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/spinstep/quaternion_utils.py b/spinstep/quaternion_utils.py index 4d4d836..2c01c7c 100644 --- a/spinstep/quaternion_utils.py +++ b/spinstep/quaternion_utils.py @@ -1,3 +1,7 @@ +# quaternion_utils.py — MIT License +# Author: Eraldo Marques — Created: 2025-05-14 +# See LICENSE.txt for full terms. This header must be retained in redistributions. + import numpy as np from scipy.spatial.transform import Rotation as R From 383782999165c06d0cc7923758eea75955d07f65 Mon Sep 17 00:00:00 2001 From: Eraldo Marques <119956342+VoxLeone@users.noreply.github.com> Date: Wed, 14 May 2025 20:22:13 -0300 Subject: [PATCH 16/22] Update node.py --- spinstep/node.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spinstep/node.py b/spinstep/node.py index eb8bd73..8b4d545 100644 --- a/spinstep/node.py +++ b/spinstep/node.py @@ -1,4 +1,4 @@ -# demo1_tree_traversal.py — MIT License +# node.py — MIT License # Author: Eraldo Marques — Created: 2025-05-14 # See LICENSE.txt for full terms. This header must be retained in redistributions. From 39ecfd6e4e85b5cd230bba47a303c3093c8986ea Mon Sep 17 00:00:00 2001 From: Eraldo Marques <119956342+VoxLeone@users.noreply.github.com> Date: Wed, 14 May 2025 20:22:57 -0300 Subject: [PATCH 17/22] Update traversal.py --- spinstep/traversal.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/spinstep/traversal.py b/spinstep/traversal.py index 3ac3b78..bd75dfa 100644 --- a/spinstep/traversal.py +++ b/spinstep/traversal.py @@ -1,3 +1,7 @@ +# traversal.py — MIT License +# Author: Eraldo Marques — Created: 2025-05-14 +# See LICENSE.txt for full terms. This header must be retained in redistributions. + from scipy.spatial.transform import Rotation as R class QuaternionDepthIterator: From a4c98fa3571275fb410663940abc3d3eed68f1e5 Mon Sep 17 00:00:00 2001 From: Eraldo Marques <119956342+VoxLeone@users.noreply.github.com> Date: Wed, 14 May 2025 20:26:10 -0300 Subject: [PATCH 18/22] Update test.py --- tests/test.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test.py b/tests/test.py index 8b13789..2e4eccf 100644 --- a/tests/test.py +++ b/tests/test.py @@ -1 +1,3 @@ - +# test.py — MIT License +# Author: Eraldo Marques — Created: 2025-05-14 +# See LICENSE.txt for full terms. This header must be retained in redistributions. From 3588bba6a47b8fb4a4ffa788e3cf294ebdf4aa4f Mon Sep 17 00:00:00 2001 From: Eraldo Marques <119956342+VoxLeone@users.noreply.github.com> Date: Thu, 15 May 2025 12:03:30 -0300 Subject: [PATCH 19/22] Update and rename test.py to test_spinstep.py --- tests/test.py | 3 -- tests/test_spinstep.py | 98 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+), 3 deletions(-) delete mode 100644 tests/test.py create mode 100644 tests/test_spinstep.py diff --git a/tests/test.py b/tests/test.py deleted file mode 100644 index 2e4eccf..0000000 --- a/tests/test.py +++ /dev/null @@ -1,3 +0,0 @@ -# test.py — MIT License -# Author: Eraldo Marques — Created: 2025-05-14 -# See LICENSE.txt for full terms. This header must be retained in redistributions. diff --git a/tests/test_spinstep.py b/tests/test_spinstep.py new file mode 100644 index 0000000..ee5b828 --- /dev/null +++ b/tests/test_spinstep.py @@ -0,0 +1,98 @@ +# test_spinstep.py — MIT License +# Author: Eraldo Marques — Created: 2025-05-14 +# See LICENSE.txt for full terms. This header must be retained in redistributions. + +import pytest +import numpy as np + +try: + import cupy as cp + HAS_CUPY = True +except ImportError: + HAS_CUPY = False + +from spinstep.discrete import DiscreteOrientationSet +# If you have continuous traversal classes, import them here +# from spinstep.continuous import QuaternionDepthIterator +from spinstep.node import Node + +@pytest.fixture +def simple_tree(): + return Node("root", [0, 0, 0, 1], [ + Node("A", [0.707, 0, 0, 0.707]), + Node("B", [0, 0.707, 0, 0.707]) + ]) + +def test_discrete_orientation_set_cpu(): + arr = np.eye(4) + dset = DiscreteOrientationSet(arr) + assert len(dset) == 4 + assert np.allclose(np.linalg.norm(dset.orientations, axis=1), 1) + +def test_discrete_orientation_set_gpu(): + if not HAS_CUPY: + pytest.skip("CuPy not installed") + arr = np.eye(4) + dset = DiscreteOrientationSet(arr, use_cuda=True) + assert len(dset) == 4 + norms = dset.xp.linalg.norm(dset.orientations, axis=1) + assert dset.xp.allclose(norms, 1) + +def test_query_within_angle_cpu(): + arr = np.array([ + [0, 0, 0, 1], + [0.707, 0, 0, 0.707], + [0, 0.707, 0, 0.707] + ]) + dset = DiscreteOrientationSet(arr) + inds = dset.query_within_angle([0, 0, 0, 1], angle=1.0) + assert 0 in inds + +def test_query_within_angle_gpu(): + if not HAS_CUPY: + pytest.skip("CuPy not installed") + arr = np.array([ + [0, 0, 0, 1], + [0.707, 0, 0, 0.707], + [0, 0.707, 0, 0.707] + ]) + dset = DiscreteOrientationSet(arr, use_cuda=True) + inds = dset.query_within_angle([0, 0, 0, 1], angle=1.0) + assert 0 in dset.xp.asnumpy(inds) + +def test_from_cube_and_icosahedron(): + dset_cube = DiscreteOrientationSet.from_cube() + dset_ico = DiscreteOrientationSet.from_icosahedron() + assert len(dset_cube) == 24 + assert len(dset_ico) == 60 + +def test_from_custom(): + arr = np.eye(4) + dset = DiscreteOrientationSet.from_custom(arr) + assert len(dset) == 4 + +def test_from_sphere_grid(): + dset = DiscreteOrientationSet.from_sphere_grid(10) + assert len(dset) == 10 + +def test_invalid_quaternion(): + arr = np.zeros((3, 4)) + with pytest.raises(ValueError): + DiscreteOrientationSet(arr) + +def test_as_numpy_gpu(): + if not HAS_CUPY: + pytest.skip("CuPy not installed") + arr = np.eye(4) + dset = DiscreteOrientationSet(arr, use_cuda=True) + arr2 = dset.as_numpy() + assert isinstance(arr2, np.ndarray) + assert arr2.shape == (4, 4) + +# Example for continuous traversal (if implemented) +# def test_continuous_traversal(simple_tree): +# # Replace with your actual traversal class and logic +# it = QuaternionDepthIterator(simple_tree, angle_threshold=0.2) +# names = [node.name for node in it] +# assert "root" in names and "A" in names and "B" in names + From ec2d77566a832aa43d62fa5d1e7856b917a5a2fd Mon Sep 17 00:00:00 2001 From: Eraldo Marques <119956342+VoxLeone@users.noreply.github.com> Date: Thu, 15 May 2025 15:00:44 -0300 Subject: [PATCH 20/22] Delete pyproject.toml --- pyproject.toml | 19 ------------------- 1 file changed, 19 deletions(-) delete mode 100644 pyproject.toml diff --git a/pyproject.toml b/pyproject.toml deleted file mode 100644 index 10716d5..0000000 --- a/pyproject.toml +++ /dev/null @@ -1,19 +0,0 @@ -[project] -name = "spinstep" -version = "0.1.0" -description = "A quaternion-based tree traversal framework for orientation-aware data structures." -authors = [ - { name="Your Name", email="your@email.com" } -] -license = "MIT" -readme = "README.md" -requires-python = ">=3.8" - -dependencies = [ - "numpy >= 1.22", - "scipy >= 1.8" -] - -[build-system] -requires = ["setuptools>=61.0"] -build-backend = "setuptools.build_meta" From f58e1b95680ebeff76235c7b3fe2761dc3dc552f Mon Sep 17 00:00:00 2001 From: Eraldo Marques <119956342+VoxLeone@users.noreply.github.com> Date: Fri, 16 May 2025 09:08:38 -0300 Subject: [PATCH 21/22] Add files via upload --- tests/test_discrete_traversal_Version2.py | 272 ++++++++++++++++++++++ 1 file changed, 272 insertions(+) create mode 100644 tests/test_discrete_traversal_Version2.py diff --git a/tests/test_discrete_traversal_Version2.py b/tests/test_discrete_traversal_Version2.py new file mode 100644 index 0000000..4cf3b39 --- /dev/null +++ b/tests/test_discrete_traversal_Version2.py @@ -0,0 +1,272 @@ +# test_discrete_traversal.py — SpinStep Test Suite — MIT License +# Author: Eraldo B. Marques — Created: 2025-05-16 +# See LICENSE.txt for full terms. + +import numpy as np +import pytest +from scipy.spatial.transform import Rotation as R + +# Import the modules under test +from spinstep.orientations.discrete import DiscreteOrientationSet +from spinstep.traversal.discrete_iterator import DiscreteQuaternionIterator + +# Simple node class for testing +class Node: + def __init__(self, id, orientation, children=None): + self.id = id + self.orientation = orientation # [x,y,z,w] quaternion + self.children = children or [] + + def add_child(self, child): + self.children.append(child) + return child + + def __repr__(self): + return f"Node({self.id})" + + +# ===== DiscreteOrientationSet Tests ===== + +class TestDiscreteOrientationSet: + def test_initialization(self): + # Test with valid quaternions + quats = [ + [0, 0, 0, 1], # Identity + [0, 1, 0, 0], # 180° around Y + [0, 0, 1, 0], # 180° around Z + ] + orientation_set = DiscreteOrientationSet(quats) + assert len(orientation_set) == 3 + + # Test normalization + unnormalized = [ + [0, 0, 0, 2], # Non-unit quaternion + ] + orientation_set = DiscreteOrientationSet(unnormalized) + assert np.allclose(orientation_set.orientations[0], [0, 0, 0, 1]) + + # Test error cases + with pytest.raises(ValueError): + DiscreteOrientationSet([[0, 0, 0]]) # Wrong shape + + with pytest.raises(ValueError): + DiscreteOrientationSet([[0, 0, 0, 0]]) # Zero quaternion + + def test_predefined_sets(self): + # Test cube (octahedral) set - should have 24 orientations + cube_set = DiscreteOrientationSet.from_cube() + assert len(cube_set) == 24 + + # Test icosahedral set - should have 60 orientations + icosa_set = DiscreteOrientationSet.from_icosahedron() + assert len(icosa_set) == 60 + + # Test custom set + custom_quats = [[0, 0, 0, 1], [1, 0, 0, 0]] + custom_set = DiscreteOrientationSet.from_custom(custom_quats) + assert len(custom_set) == 2 + + # Test sphere grid with different point counts + sphere_set_small = DiscreteOrientationSet.from_sphere_grid(n_points=10) + assert len(sphere_set_small) == 10 + + sphere_set_large = DiscreteOrientationSet.from_sphere_grid(n_points=100) + assert len(sphere_set_large) == 100 + + def test_query_within_angle(self): + # Create a set with known angles + quats = [ + [0, 0, 0, 1], # Identity + [0, np.sin(np.pi/8), 0, np.cos(np.pi/8)], # 45° around Y + [0, np.sin(np.pi/4), 0, np.cos(np.pi/4)], # 90° around Y + ] + orientation_set = DiscreteOrientationSet(quats) + + # Query within small angle - should only find identity + results = orientation_set.query_within_angle([0, 0, 0, 1], np.pi/16) + assert len(results) == 1 + + # Query within medium angle - should find identity and 45° + results = orientation_set.query_within_angle([0, 0, 0, 1], np.pi/6) + assert len(results) == 2 + + # Query within large angle - should find all three + results = orientation_set.query_within_angle([0, 0, 0, 1], np.pi/3) + assert len(results) == 3 + + @pytest.mark.skipif(not np.array([True]), reason="CUDA not available") + def test_cuda_support(self): + try: + cuda_set = DiscreteOrientationSet([[0, 0, 0, 1]], use_cuda=True) + # If this succeeds, basic CUDA import worked + assert cuda_set.use_cuda == True + + # Test as_numpy() method for GPU->CPU transfer + cpu_array = cuda_set.as_numpy() + assert isinstance(cpu_array, np.ndarray) + except (ImportError, ModuleNotFoundError): + pytest.skip("CUDA/CuPy not available") + + +# ===== DiscreteQuaternionIterator Tests ===== + +class TestDiscreteQuaternionIterator: + def setup_method(self): + # Create a simple tree structure with specific orientations + # root (identity orientation) + # / \ + # A B + # / \ / \ + # C D E F + # + # Where A is 45° rotation from root around Y + # B is 90° rotation from root around Y + # Others have further rotations + + self.root = Node("root", [0, 0, 0, 1]) + + # 45° around Y + self.node_a = self.root.add_child( + Node("A", [0, np.sin(np.pi/8), 0, np.cos(np.pi/8)]) + ) + + # 90° around Y + self.node_b = self.root.add_child( + Node("B", [0, np.sin(np.pi/4), 0, np.cos(np.pi/4)]) + ) + + # A's children - small variations + self.node_c = self.node_a.add_child( + Node("C", [0, np.sin(np.pi/8+0.1), 0, np.cos(np.pi/8+0.1)]) + ) + + self.node_d = self.node_a.add_child( + Node("D", [0, np.sin(np.pi/8-0.1), 0, np.cos(np.pi/8-0.1)]) + ) + + # B's children - small variations + self.node_e = self.node_b.add_child( + Node("E", [0, np.sin(np.pi/4+0.1), 0, np.cos(np.pi/4+0.1)]) + ) + + self.node_f = self.node_b.add_child( + Node("F", [0, np.sin(np.pi/4-0.1), 0, np.cos(np.pi/4-0.1)]) + ) + + # Create orientation set with basic steps + steps = [ + [0, 0, 0, 1], # Identity (no rotation) + [0, np.sin(np.pi/8), 0, np.cos(np.pi/8)], # 45° step around Y + [0, np.sin(-np.pi/8), 0, np.cos(np.pi/8)], # -45° step around Y + ] + self.orientation_set = DiscreteOrientationSet(steps) + + def test_iterator_creation(self): + # Test basic creation + iterator = DiscreteQuaternionIterator( + self.root, + self.orientation_set, + angle_threshold=np.pi/6 + ) + assert iterator is not None + + # Test error with invalid node + class InvalidNode: + pass + + with pytest.raises(AttributeError): + DiscreteQuaternionIterator( + InvalidNode(), + self.orientation_set + ) + + def test_traversal(self): + # With large angle threshold, should visit all nodes + iterator = DiscreteQuaternionIterator( + self.root, + self.orientation_set, + angle_threshold=np.pi/2, # Very permissive + max_depth=3 + ) + + visited_nodes = list(iterator) + visited_ids = [node.id for node in visited_nodes] + + # Should have visited all nodes in some order + for node_id in ["root", "A", "B", "C", "D", "E", "F"]: + assert node_id in visited_ids + + # With tiny threshold, should only visit root + iterator = DiscreteQuaternionIterator( + self.root, + self.orientation_set, + angle_threshold=np.pi/64, # Very restrictive + max_depth=3 + ) + + visited_nodes = list(iterator) + assert len(visited_nodes) == 1 + assert visited_nodes[0].id == "root" + + def test_max_depth(self): + # With max_depth=1, should only visit root and its direct children + iterator = DiscreteQuaternionIterator( + self.root, + self.orientation_set, + angle_threshold=np.pi/2, # Very permissive + max_depth=1 + ) + + visited_nodes = list(iterator) + visited_ids = [node.id for node in visited_nodes] + + # Should have visited only root and direct children + for node_id in ["root", "A", "B"]: + assert node_id in visited_ids + + # Shouldn't have visited grandchildren + for node_id in ["C", "D", "E", "F"]: + assert node_id not in visited_ids + + +# ===== Integration Tests ===== + +def test_full_pipeline(): + """Test the complete pipeline from orientation set creation to traversal""" + + # Create a custom orientation set + custom_steps = [ + [0, 0, 0, 1], # Identity + [1, 0, 0, 0], # 180° around X + [0, 1, 0, 0], # 180° around Y + [0, 0, 1, 0], # 180° around Z + ] + + orientation_set = DiscreteOrientationSet.from_custom(custom_steps) + + # Create a simple tree + root = Node("root", [0, 0, 0, 1]) + node_a = root.add_child(Node("A", [1, 0, 0, 0])) + node_b = root.add_child(Node("B", [0, 1, 0, 0])) + + # Create iterator and traverse + iterator = DiscreteQuaternionIterator( + root, + orientation_set, + angle_threshold=np.pi/4 + ) + + # Check that we can iterate and get nodes + visited = [] + for node in iterator: + visited.append(node.id) + + # We expect to visit all nodes since our steps include direct rotations + # to each child's orientation + assert "root" in visited + assert "A" in visited + assert "B" in visited + + +if __name__ == "__main__": + pytest.main(["-xvs", __file__]) \ No newline at end of file From 74d43bf948f76f6d3d02ca7b9ede23e36806e091 Mon Sep 17 00:00:00 2001 From: Eraldo Marques <119956342+VoxLeone@users.noreply.github.com> Date: Fri, 16 May 2025 09:09:37 -0300 Subject: [PATCH 22/22] Rename test_discrete_traversal_Version2.py to test_discrete_traversal.py --- ...iscrete_traversal_Version2.py => test_discrete_traversal.py} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename tests/{test_discrete_traversal_Version2.py => test_discrete_traversal.py} (99%) diff --git a/tests/test_discrete_traversal_Version2.py b/tests/test_discrete_traversal.py similarity index 99% rename from tests/test_discrete_traversal_Version2.py rename to tests/test_discrete_traversal.py index 4cf3b39..9d0a77c 100644 --- a/tests/test_discrete_traversal_Version2.py +++ b/tests/test_discrete_traversal.py @@ -269,4 +269,4 @@ def test_full_pipeline(): if __name__ == "__main__": - pytest.main(["-xvs", __file__]) \ No newline at end of file + pytest.main(["-xvs", __file__])