From 418723f15a0043beb7a2d551df227091d523d805 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 24 Mar 2026 18:26:30 +0000
Subject: [PATCH 1/3] Initial plan
From 9e37fd5efe504e593a2f7e73ec74cad5f1725297 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 24 Mar 2026 18:36:54 +0000
Subject: [PATCH 2/3] Complete documentation overhaul: README, docstrings, and
usage guides
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Stage 2: README restructured with Overview, Installation, Quick Start,
Core Concepts, Examples, Optional Dependencies, Development, License.
Stage 3: All public class docstrings converted to Google-style format
with Args/Returns/Raises/Example sections.
Stage 4: Created docs/getting-started.md, docs/continuous-traversal.md,
docs/discrete-traversal.md, docs/faq.md. Fixed docs/09-api-reference.md
with correct signatures and module paths. Updated docs/index.md.
Stage 5: All examples validated — every code snippet runs without error.
Co-authored-by: VoxleOne <119956342+VoxleOne@users.noreply.github.com>
Agent-Logs-Url: https://github.com/VoxleOne/SpinStep/sessions/9c6faf98-3686-45a3-9659-1c604bb18a00
---
README.md | 283 ++++++++++++++++------------------
docs/09-api-reference.md | 202 +++++++++++++++---------
docs/continuous-traversal.md | 102 ++++++++++++
docs/discrete-traversal.md | 128 +++++++++++++++
docs/faq.md | 126 +++++++++++++++
docs/getting-started.md | 63 ++++++++
docs/index.md | 54 ++++---
spinstep/discrete.py | 66 ++++----
spinstep/discrete_iterator.py | 39 +++--
spinstep/node.py | 34 ++--
spinstep/traversal.py | 33 ++--
11 files changed, 812 insertions(+), 318 deletions(-)
create mode 100644 docs/continuous-traversal.md
create mode 100644 docs/discrete-traversal.md
create mode 100644 docs/faq.md
create mode 100644 docs/getting-started.md
diff --git a/README.md b/README.md
index b60087a..652f15b 100644
--- a/README.md
+++ b/README.md
@@ -1,221 +1,200 @@
-# SpinStep - [Read the Docs](https://github.com/VoxLeone/SpinStep/tree/main/docs/index.md)
+# SpinStep
-**SpinStep** is a proof-of-concept quaternion-driven traversal framework for trees and orientation-based data structures.
-
-By leveraging the power of 3D rotation math, SpinStep enables traversal based not on position or order, but on orientation. This makes it ideal for spatial reasoning, robotics, 3D scene graphs, and anywhere quaternion math naturally applies.
+**SpinStep** is a quaternion-driven traversal framework for trees and orientation-based data structures. Instead of traversing by position or order, SpinStep uses 3D rotation math to navigate trees based on orientation — making it ideal for robotics, spatial reasoning, 3D scene graphs, and anywhere quaternion math naturally applies.
----
+## Overview
+
+SpinStep provides two traversal modes:
+
+- **Continuous traversal** — apply a single rotation step at each node and visit children whose orientation falls within an angular threshold (`QuaternionDepthIterator`).
+- **Discrete traversal** — try every rotation from a predefined symmetry group or custom set and visit reachable children (`DiscreteQuaternionIterator` with `DiscreteOrientationSet`).
+
+Both modes are built on a simple `Node` class that stores a name, a unit quaternion orientation `[x, y, z, w]`, and a list of children.
-## Features
+## Installation
-- Quaternion-based stepping and branching
-- Full support for yaw, pitch, and roll rotations
-- Configurable angular thresholds for precision control
-- Easily extendable to N-ary trees or orientation graphs
-- Written in Python with SciPy's rotation engine
+**Requirements:** Python 3.9+
----
+Install from source:
-## Example Use Case
+```bash
+git clone https://github.com/VoxleOne/SpinStep.git
+cd SpinStep
+pip install .
+```
+
+Or install in editable mode for development:
+
+```bash
+pip install -e ".[dev]"
+```
+
+## Quick Start
```python
from spinstep import Node, QuaternionDepthIterator
-# Define your tree with orientation quaternions
+# Build a small tree with quaternion orientations [x, y, z, w]
root = Node("root", [0, 0, 0, 1], [
- Node("child", [0.2588, 0, 0, 0.9659]) # ~30 degrees around Z
+ Node("child", [0.2588, 0, 0, 0.9659]) # ~30° rotation around Z
])
-# Step using a 30-degree rotation
+# Traverse using a 30° rotation step
iterator = QuaternionDepthIterator(root, [0.2588, 0, 0, 0.9659])
for node in iterator:
print("Visited:", node.name)
+# Output:
+# Visited: root
+# Visited: child
```
----
+## Core Concepts
-## Requirements
+### Node
-- Python 3.9+
-- `numpy`
-- `scipy`
-- `scikit-learn`
-
-Install dependencies via pip:
+A tree node with a quaternion-based orientation. Each node stores a name, a unit quaternion `[x, y, z, w]` (automatically normalised), and a list of children.
-```bash
-pip install numpy scipy scikit-learn
-```
-## Node Requirements
-
-- `.orientation`: Quaternion as `[x, y, z, w]`, always normalized.
-- `.children`: Iterable of nodes.
-- Node constructor and orientation set utilities always normalize quaternions and check for zero-norm.
-- `angle_threshold` parameters are always in radians.
+```python
+from spinstep import Node
-All core functions will raise `ValueError` or `AttributeError` if these invariants are violated.
+root = Node("root", [0, 0, 0, 1])
+child = Node("child", [0, 0, 0.3827, 0.9239]) # ~45° around Z
+root.children.append(child)
+```
----
+### QuaternionDepthIterator
-## Concepts
+Depth-first iterator that applies a continuous rotation step at each node. Only children within an angular threshold of the rotated state are visited.
-SpinStep uses quaternion rotation to determine if a child node is reachable from a given orientation. Only children whose orientations lie within a defined angular threshold (default: 45°) of the current rotation state are traversed.
+```python
+from spinstep import Node, QuaternionDepthIterator
-This mimics rotational motion or attention in physical and virtual spaces. Ideal for:
+root = Node("root", [0, 0, 0, 1], [
+ Node("close", [0.2588, 0, 0, 0.9659]), # ~30° — will be visited
+ Node("far", [0.7071, 0, 0, 0.7071]), # ~90° — too far
+])
-- Orientation trees
-- 3D pose search
-- Animation graph traversal
-- Spatial AI and robotics
----
+for node in QuaternionDepthIterator(root, [0.2588, 0, 0, 0.9659]):
+ print(node.name)
+# Output:
+# root
+# close
+```
-
-

-
+### DiscreteOrientationSet
----
+A set of discrete quaternion orientations with spatial querying. Comes with factory methods for common symmetry groups.
-## What Would It Mean to “Rotate into Branches”?
+```python
+from spinstep import DiscreteOrientationSet
-### 1. Quaternion as a Branch Selector
+cube_set = DiscreteOrientationSet.from_cube() # 24 orientations
+icosa_set = DiscreteOrientationSet.from_icosahedron() # 60 orientations
+grid_set = DiscreteOrientationSet.from_sphere_grid(200) # 200 Fibonacci-sampled
+```
-- Each node in a graph or tree encodes a rotational state (quaternion)
-- Traversal is guided by a current quaternion state
-- At each step, you rotate your state and select the next node based on geometric orientation
+### DiscreteQuaternionIterator
-🔸 *Use Cases*: Scene graphs, spatial indexing, directional AI traversal, robot path planning
+Depth-first iterator that tries every rotation in a `DiscreteOrientationSet` at each node. Children reachable by any rotation within the angular threshold are visited.
----
+```python
+import numpy as np
+from spinstep import Node, DiscreteOrientationSet, DiscreteQuaternionIterator
-### 2. Quaternion-Based Traversal Heuristics
+root = Node("root", [0, 0, 0, 1], [
+ Node("child1", [0, 0, 0.3827, 0.9239]),
+ Node("child2", [0, 0.7071, 0, 0.7071]),
+])
-Instead of:
+orientation_set = DiscreteOrientationSet.from_cube()
+it = DiscreteQuaternionIterator(root, orientation_set, angle_threshold=np.pi / 4)
-```python
-next = left or right
+for node in it:
+ print(node.name)
+# Output:
+# root
+# child1
+# child2
```
-You define:
+## Examples
-```python
-next_node = rotate(current_orientation, branch_orientation)
-```
+### Continuous Traversal with Custom Threshold
-- Rotation (quaternion multiplication) becomes the “step” function
-- Orientation and direction are first-class traversal parameters
+```python
+import numpy as np
+from spinstep import Node, QuaternionDepthIterator
-🔸 *Use Cases*: Game engines, camera control, 3D modeling, procedural generation
+root = Node("origin", [0, 0, 0, 1], [
+ Node("alpha", [0.1305, 0, 0, 0.9914]), # ~15°
+])
----
+# Use explicit angle threshold of 20° (in radians)
+it = QuaternionDepthIterator(root, [0.1305, 0, 0, 0.9914], angle_threshold=np.deg2rad(20))
+print([n.name for n in it])
+# Output: ['origin', 'alpha']
+```
-### 3. Multi-Dimensional Trees with Quaternion Keys
+### Query Orientations by Angle
-- Use quaternion distance (angle) to decide which branches to explore or when to stop
-- Think of it like a quaternion-aware k-d tree
+```python
+import numpy as np
+from spinstep import DiscreteOrientationSet
----
+dos = DiscreteOrientationSet.from_cube()
+indices = dos.query_within_angle([0, 0, 0, 1], np.deg2rad(10))
+print(f"Found {len(indices)} orientations within 10° of identity")
+```
-### Visual Metaphor
+## Optional Dependencies
-Imagine walking through a tree **not** left/right — but by **rotating** in space:
+SpinStep works out of the box with NumPy, SciPy, and scikit-learn. Two optional dependencies unlock additional features:
-- Rotate **pitch** to descend to one child
-- Rotate **yaw** to reach another
-- Traverse hierarchies by change in orientation, not position
+| Package | Install | Feature |
+|---------|---------|---------|
+| [CuPy](https://cupy.dev/) | `pip install cupy-cuda12x` | GPU-accelerated orientation storage and angular distance computation |
+| [healpy](https://healpy.readthedocs.io/) | `pip install healpy` | HEALPix-based unique relative spin detection via `get_unique_relative_spins()` |
----
+GPU example:
-## Project Structure
+```python
+import numpy as np
+from spinstep import DiscreteOrientationSet
+orientations = np.random.randn(1000, 4)
+# Store orientations on GPU (requires CuPy)
+gpu_set = DiscreteOrientationSet(orientations, use_cuda=True)
```
-spinstep/
-├── __init__.py
-├── discrete.py
-├── discrete_iterator.py
-├── node.py
-├── traversal.py
-└── utils/
- ├── array_backend.py
- ├── quaternion_math.py
- └── quaternion_utils.py
-
-benchmark/
-├── edge-cases.md
-├── INSTRUCTIONS.md
-├── qgnn_example.py
-├── README.md
-├── references.md
-└── test_qgnn_benchmark.py
-
-demos/
-├── demo1_tree_traversal.py
-├── demo2_full_depth_traversal.py
-├── demo3_spatial_traversal.py
-├── demo4_discrete_traversal.py
-└── demo.py
-
-examples/
-└── gpu_orientation_matching.py
-
-tests/
-├── test_discrete_traversal.py
-└── test_spinstep.py
-
-docs/
-├── assets/
-│ ├── img/
-│ │ └── (...) # Images go here
-│ └── notebooks/
-│ └── (...) # Jupyter notebooks or rendered outputs
-├── CONTRIBUTING.md
-└── index.md
-
-# Root files
-CHANGELOG.md
-LICENSE
-MANIFEST.in
-pyproject.toml
-README.md
-requirements.txt
-dev-requirements.txt
-```
-
----
-## To Build and Install Locally
-
-First, clone the repository:
+## Development
```bash
-git clone https://github.com/VoxLeone/spinstep.git
-cd spinstep
-```
+# Install with development dependencies
+pip install -e ".[dev]"
-Then, install it:
+# Run tests
+python -m pytest tests/ -v
-```bash
-pip install .
+# Run linter
+ruff check spinstep/
```
-To build a wheel distribution:
-
-```bash
-python -m build
-```
----
-## [Benchmark Instructions](benchmark/INSTRUCTIONS.md)
----
+## Documentation
-## License
+Full documentation is available in the [docs/](docs/index.md) directory:
-MIT — free to use, fork, and adapt.
+- [Getting Started](docs/getting-started.md)
+- [Continuous Traversal Guide](docs/continuous-traversal.md)
+- [Discrete Traversal Guide](docs/discrete-traversal.md)
+- [FAQ](docs/faq.md)
+- [API Reference](docs/09-api-reference.md)
+- [Contributing](docs/CONTRIBUTING.md)
-## 💬 Feedback & Contributions
+## License
-PRs and issues are welcome!
-If you're using SpinStep in a cool project, let us know.
+MIT — see [LICENSE](LICENSE) for details.
diff --git a/docs/09-api-reference.md b/docs/09-api-reference.md
index 7b13b6d..7e0f0e2 100644
--- a/docs/09-api-reference.md
+++ b/docs/09-api-reference.md
@@ -1,140 +1,188 @@
# SpinStep API Reference
-This document details the main public API of the SpinStep library.
-For usage examples, see the [Examples & Tutorials](examples.md).
+This document details the public API of the SpinStep library.
---
## Table of Contents
-- [spinstep.node.Node](#spinstepnodenode)
-- [spinstep.discrete.DiscreteOrientationSet](#spinstepdiscretediscreteorientationset)
-- [spinstep.discrete_iterator.DiscreteQuaternionIterator](#spinstepdiscrete_iteratordiscretequaternioniterator)
-- [spinstep.continuous.QuaternionDepthIterator](#spinstepcontinuousquaterniondepthiterator)
+- [spinstep.node.Node](#node)
+- [spinstep.traversal.QuaternionDepthIterator](#quaterniondepthiterator)
+- [spinstep.discrete.DiscreteOrientationSet](#discreteorientationset)
+- [spinstep.discrete_iterator.DiscreteQuaternionIterator](#discretequaternioniterator)
- [Exceptions](#exceptions)
---
-## spinstep.node.Node
+## Node
```python
+from spinstep import Node
+
class Node:
- def __init__(self, name: str, orientation: Sequence[float], children: Optional[Iterable["Node"]] = None)
+ def __init__(
+ self,
+ name: str,
+ orientation: ArrayLike,
+ children: Optional[Sequence[Node]] = None,
+ ) -> None
```
-- **name** (`str`): Node identifier (any string).
-- **orientation** (`[x, y, z, w]`): Quaternion representing the orientation. Automatically normalized.
-- **children** (`Iterable[Node]`, optional): List or iterable of child nodes.
+A tree node with a quaternion-based orientation.
-#### Attributes
+**Args:**
-- **name**: Node name.
-- **orientation**: Normalized quaternion (`numpy.ndarray`, shape `(4,)`).
-- **children**: List of child nodes.
+- **name** (`str`): Human-readable identifier.
+- **orientation** (`[x, y, z, w]`): Quaternion. Automatically normalised. Must be non-zero.
+- **children** (`Sequence[Node]`, optional): Initial child nodes.
----
+**Attributes:**
+
+- **name** (`str`): Node identifier.
+- **orientation** (`numpy.ndarray`, shape `(4,)`): Normalised quaternion.
+- **children** (`list[Node]`): Child nodes.
+
+**Raises:**
+
+- `ValueError`: If orientation is not a 4-element vector or has near-zero norm.
-## spinstep.discrete.DiscreteOrientationSet
+---
-A container for a set of discrete orientations (quaternions).
+## QuaternionDepthIterator
```python
-class DiscreteOrientationSet:
- def __init__(self, orientations: Sequence[Sequence[float]])
-```
+from spinstep import QuaternionDepthIterator
-#### Constructor
+class QuaternionDepthIterator:
+ def __init__(
+ self,
+ start_node: Node,
+ rotation_step_quat: ArrayLike,
+ angle_threshold: Optional[float] = None,
+ ) -> None
+```
-- **orientations**: List or array of normalized quaternions (`[x, y, z, w]`).
+Depth-first tree iterator driven by a continuous quaternion rotation step.
-#### Class Methods
+**Args:**
-- `from_cube()`: Returns a set of cube group orientations (24 elements).
-- `from_icosahedron()`: Returns a set of icosahedral group orientations (60 elements).
-- `from_custom(orientations)`: Create from user-specified list of quaternions.
-- `from_sphere_grid(N: int)`: Approximate a uniform grid of `N` orientations on the sphere.
+- **start_node** (`Node`): Root node of the tree.
+- **rotation_step_quat** (`[x, y, z, w]`): Quaternion rotation applied at each step.
+- **angle_threshold** (`float`, optional): Maximum angular distance in radians for a child to be visited. When `None`, defaults to 30% of the step angle (minimum 1°).
-#### Methods
+**Class Variables:**
-- `query_within_angle(quat: Sequence[float], angle: float) -> np.ndarray`:
- Returns indices of orientations within `angle` (radians) of `quat`.
+- `DEFAULT_DYNAMIC_THRESHOLD_FACTOR` (`float`): `0.3`
-#### Attributes
+**Iterator Protocol:**
-- **orientations**: Array of normalized quaternions (shape `(n,4)`).
+Implements `__iter__` and `__next__`. Yields `Node` instances in depth-first order.
---
-## spinstep.discrete_iterator.DiscreteQuaternionIterator
-
-Depth-first traversal over a tree of Nodes using a discrete orientation set.
+## DiscreteOrientationSet
```python
-class DiscreteQuaternionIterator:
+from spinstep import DiscreteOrientationSet
+
+class DiscreteOrientationSet:
def __init__(
self,
- start_node: Node,
- orientation_set: DiscreteOrientationSet,
- angle_threshold: float = np.pi/8,
- max_depth: int = 100
- )
+ orientations: ArrayLike,
+ use_cuda: bool = False,
+ ) -> None
```
-- **start_node**: The root `Node` to start traversal from.
-- **orientation_set**: Instance of `DiscreteOrientationSet`.
-- **angle_threshold**: Maximum allowed angular distance (in radians) to consider two orientations "matching."
-- **max_depth**: Maximum recursion depth.
+A set of discrete quaternion orientations with spatial querying.
-#### Usage
+**Args:**
-```python
-it = DiscreteQuaternionIterator(root_node, orientation_set, angle_threshold=0.2)
-for node in it:
- print(node.name)
-```
+- **orientations**: Array of shape `(N, 4)` — one quaternion `[x, y, z, w]` per row.
+- **use_cuda** (`bool`): When `True`, store on GPU via CuPy.
----
+**Attributes:**
+
+- **orientations**: Normalised quaternion array of shape `(N, 4)`.
+- **use_cuda** (`bool`): Whether GPU storage is active.
+- **xp**: The array module in use (`numpy` or `cupy`).
+
+### Methods
+
+#### `query_within_angle(quat, angle) -> numpy.ndarray`
+
+Return indices of orientations within `angle` radians of `quat`.
-## spinstep.continuous.QuaternionDepthIterator
+- **quat**: Query quaternion `[x, y, z, w]` or batch `(N, 4)`.
+- **angle** (`float`): Maximum angular distance in radians.
+- **Returns**: Integer index array.
-Depth-first traversal for continuous (non-discrete) orientation search.
+#### `as_numpy() -> numpy.ndarray`
+
+Convert orientations to a NumPy array (transfers from GPU if needed).
+
+#### `__len__() -> int`
+
+Return the number of orientations.
+
+### Factory Class Methods
+
+#### `from_cube() -> DiscreteOrientationSet`
+
+24 orientations from the octahedral symmetry group.
+
+#### `from_icosahedron() -> DiscreteOrientationSet`
+
+60 orientations from the icosahedral symmetry group.
+
+#### `from_custom(quat_list) -> DiscreteOrientationSet`
+
+Create from a user-supplied array of quaternions `(N, 4)`.
+
+#### `from_sphere_grid(n_points=100) -> DiscreteOrientationSet`
+
+Fibonacci-sphere sampling of `n_points` orientations.
+
+---
+
+## DiscreteQuaternionIterator
```python
-class QuaternionDepthIterator:
+from spinstep import DiscreteQuaternionIterator
+
+class DiscreteQuaternionIterator:
def __init__(
self,
start_node: Node,
- angle_threshold: float = np.pi/8,
- max_depth: int = 100
- )
+ orientation_set: DiscreteOrientationSet,
+ angle_threshold: float = np.pi / 8,
+ max_depth: int = 100,
+ ) -> None
```
-- **start_node**: The root `Node`.
-- **angle_threshold**: Maximum allowed angular distance (in radians) for matching.
-- **max_depth**: Maximum recursion depth.
+Depth-first tree iterator using a discrete set of orientation steps.
-#### Usage
+**Args:**
-```python
-it = QuaternionDepthIterator(root_node, angle_threshold=0.1)
-for node in it:
- print(node.name)
-```
+- **start_node** (`Node`): Root node of the tree.
+- **orientation_set** (`DiscreteOrientationSet`): Candidate rotation steps.
+- **angle_threshold** (`float`): Maximum angular distance in radians. Default: `π/8` (22.5°).
+- **max_depth** (`int`): Maximum traversal depth. Default: `100`.
----
+**Raises:**
-## Exceptions
+- `AttributeError`: If `start_node` lacks `.orientation` or `.children`.
-- **ValueError**: Raised for invalid or non-normalized quaternions, or malformed orientation sets.
-- **AttributeError**: Raised if a node lacks required `.orientation` or `.children` attributes.
+**Iterator Protocol:**
+
+Implements `__iter__` and `__next__`. Yields `Node` instances in depth-first order. Tracks visited nodes to avoid cycles.
---
-## See Also
+## Exceptions
-- [Orientation Sets](05_orientation_sets.md)
-- [Discrete Traversal Guide](06_discrete_traversal.md)
-- [Troubleshooting & FAQ](07_troubleshooting.md)
+- **ValueError**: Raised for invalid quaternions (zero-norm, wrong shape) or malformed orientation sets.
+- **AttributeError**: Raised if a node lacks required `.orientation` or `.children` attributes.
---
-[⬅️ 08. Troubleshooting](08-troubleshooting.md) | [🏠 Home](index.md)
+
+[⬅️ Troubleshooting](08-troubleshooting.md) | [🏠 Home](index.md)
diff --git a/docs/continuous-traversal.md b/docs/continuous-traversal.md
new file mode 100644
index 0000000..5941ae9
--- /dev/null
+++ b/docs/continuous-traversal.md
@@ -0,0 +1,102 @@
+# Continuous Traversal Guide
+
+The `QuaternionDepthIterator` performs depth-first traversal of a quaternion-oriented tree using a single continuous rotation step.
+
+## How It Works
+
+At each visited node the iterator:
+
+1. Applies the rotation step to the current orientation state.
+2. Computes the angular distance between the rotated state and each child's orientation.
+3. Pushes children within the `angle_threshold` onto the traversal stack.
+4. Returns the current node to the caller.
+
+## Basic Usage
+
+```python
+from spinstep import Node, QuaternionDepthIterator
+
+root = Node("root", [0, 0, 0, 1], [
+ Node("child_a", [0.2588, 0, 0, 0.9659]), # ~30° around Z
+ Node("child_b", [0.7071, 0, 0, 0.7071]), # ~90° around Z
+])
+
+# Step with a 30° rotation — only child_a is close enough
+for node in QuaternionDepthIterator(root, [0.2588, 0, 0, 0.9659]):
+ print(node.name)
+# Output:
+# root
+# child_a
+```
+
+## Angle Threshold
+
+By default, the threshold is set dynamically to 30 % of the step angle (minimum 1°).
+You can override it with an explicit value in radians:
+
+```python
+import numpy as np
+from spinstep import Node, QuaternionDepthIterator
+
+root = Node("root", [0, 0, 0, 1], [
+ Node("child", [0.2588, 0, 0, 0.9659]),
+])
+
+# Explicit threshold of 45° (in radians)
+it = QuaternionDepthIterator(root, [0.2588, 0, 0, 0.9659], angle_threshold=np.deg2rad(45))
+visited = [n.name for n in it]
+print(visited)
+# Output: ['root', 'child']
+```
+
+### Dynamic Threshold Behaviour
+
+When `angle_threshold` is not specified:
+
+- The step angle is computed from `rotation_step_quat`.
+- The threshold is set to `step_angle * 0.3`.
+- If the step angle is near zero (identity rotation), the threshold defaults to 1° (`np.deg2rad(1.0)`).
+
+## Deep Trees
+
+The iterator naturally handles multi-level trees. Children are traversed depth-first:
+
+```python
+from spinstep import Node, QuaternionDepthIterator
+
+grandchild = Node("grandchild", [0.5, 0, 0, 0.866])
+child = Node("child", [0.2588, 0, 0, 0.9659], [grandchild])
+root = Node("root", [0, 0, 0, 1], [child])
+
+for node in QuaternionDepthIterator(root, [0.2588, 0, 0, 0.9659], angle_threshold=1.0):
+ print(node.name)
+# Output:
+# root
+# child
+# grandchild
+```
+
+## Iterator Protocol
+
+`QuaternionDepthIterator` implements Python's iterator protocol (`__iter__` and `__next__`), so you can use it in `for` loops, `list()` calls, or `next()`:
+
+```python
+from spinstep import Node, QuaternionDepthIterator
+
+root = Node("root", [0, 0, 0, 1])
+it = QuaternionDepthIterator(root, [0, 0, 0, 1])
+
+first = next(it)
+print(first.name)
+# Output: root
+```
+
+## Edge Cases
+
+- **Zero-norm child orientation:** Gracefully skipped (not visited, no error).
+- **Identity rotation step:** Threshold defaults to 1°.
+- **No matching children:** The iterator simply yields the root and stops.
+
+---
+
+[⬅️ Getting Started](getting-started.md) | [🏠 Home](index.md) | [Discrete Traversal ➡️](discrete-traversal.md)
diff --git a/docs/discrete-traversal.md b/docs/discrete-traversal.md
new file mode 100644
index 0000000..f304b34
--- /dev/null
+++ b/docs/discrete-traversal.md
@@ -0,0 +1,128 @@
+# Discrete Traversal Guide
+
+SpinStep supports traversal using discrete sets of quaternion rotations via `DiscreteOrientationSet` and `DiscreteQuaternionIterator`.
+
+## When to Use Discrete Traversal
+
+Use discrete traversal when:
+
+- You want to explore orientations from a symmetry group (cube, icosahedron).
+- You have a fixed set of allowed rotation steps (e.g. robot actuator positions).
+- You want to search over a grid of orientations.
+
+## DiscreteOrientationSet
+
+A container for a set of normalised quaternion orientations with angular querying.
+
+### Creating Orientation Sets
+
+```python
+from spinstep import DiscreteOrientationSet
+
+# Predefined symmetry groups
+cube_set = DiscreteOrientationSet.from_cube() # 24 orientations (octahedral group)
+icosa_set = DiscreteOrientationSet.from_icosahedron() # 60 orientations (icosahedral group)
+
+# Fibonacci-sphere sampling
+grid_set = DiscreteOrientationSet.from_sphere_grid(200) # 200 orientations
+
+# Custom quaternions
+import numpy as np
+custom_quats = np.array([
+ [0, 0, 0, 1],
+ [0.7071, 0, 0, 0.7071],
+ [0, 0.7071, 0, 0.7071],
+])
+custom_set = DiscreteOrientationSet.from_custom(custom_quats)
+```
+
+### Querying by Angle
+
+Find all orientations within a given angular distance of a query quaternion:
+
+```python
+import numpy as np
+from spinstep import DiscreteOrientationSet
+
+dos = DiscreteOrientationSet.from_cube()
+identity = [0, 0, 0, 1]
+indices = dos.query_within_angle(identity, np.deg2rad(10))
+print(f"Found {len(indices)} orientations within 10° of identity")
+```
+
+### GPU Support
+
+Pass `use_cuda=True` to store orientations on GPU (requires [CuPy](https://cupy.dev/)):
+
+```python
+import numpy as np
+from spinstep import DiscreteOrientationSet
+
+orientations = np.random.randn(10000, 4)
+gpu_set = DiscreteOrientationSet(orientations, use_cuda=True)
+```
+
+## DiscreteQuaternionIterator
+
+Depth-first iterator that tries every orientation in the set as a rotation step at each node.
+
+### Basic Usage
+
+```python
+import numpy as np
+from spinstep import Node, DiscreteOrientationSet, DiscreteQuaternionIterator
+
+root = Node("root", [0, 0, 0, 1], [
+ Node("child1", [0, 0, 0.3827, 0.9239]), # ~45° around Z
+ Node("child2", [0, 0.7071, 0, 0.7071]), # 90° around Y
+])
+
+orientation_set = DiscreteOrientationSet.from_cube()
+it = DiscreteQuaternionIterator(root, orientation_set, angle_threshold=np.pi / 4)
+
+for node in it:
+ print(node.name)
+# Output:
+# root
+# child1
+# child2
+```
+
+### Parameters
+
+- `angle_threshold` — maximum angular distance (radians) for a child to be reachable. Default: `π/8` (22.5°).
+- `max_depth` — maximum traversal depth. Default: `100`.
+
+### Depth Limiting
+
+```python
+import numpy as np
+from spinstep import Node, DiscreteOrientationSet, DiscreteQuaternionIterator
+
+deep_child = Node("deep", [0.5, 0, 0, 0.866])
+child = Node("child", [0, 0, 0.3827, 0.9239], [deep_child])
+root = Node("root", [0, 0, 0, 1], [child])
+
+dos = DiscreteOrientationSet.from_cube()
+it = DiscreteQuaternionIterator(root, dos, angle_threshold=1.0, max_depth=1)
+
+visited = [n.name for n in it]
+print(visited)
+# 'deep' is not visited because max_depth=1 limits traversal
+```
+
+## Use Cases
+
+- **Robot planning:** Restrict traversal to valid actuator orientations.
+- **Crystal / molecular modelling:** Traverse only symmetry-equivalent orientations.
+- **3D search / AI:** Explore all directions in a grid.
+- **Procedural content:** Generate structures along discrete orientation paths.
+
+## Performance Tips
+
+- For large orientation sets (>1000), increase `angle_threshold` or decrease `max_depth` to limit the search space.
+- The iterator tracks visited nodes by identity to avoid cycles.
+
+---
+
+[⬅️ Continuous Traversal](continuous-traversal.md) | [🏠 Home](index.md) | [FAQ ➡️](faq.md)
diff --git a/docs/faq.md b/docs/faq.md
new file mode 100644
index 0000000..19c82bc
--- /dev/null
+++ b/docs/faq.md
@@ -0,0 +1,126 @@
+# FAQ
+
+## Installation & Setup
+
+### How do I install SpinStep?
+
+```bash
+git clone https://github.com/VoxleOne/SpinStep.git
+cd SpinStep
+pip install .
+```
+
+### What Python versions are supported?
+
+Python 3.9, 3.10, 3.11, and 3.12.
+
+### What are the required dependencies?
+
+- `numpy>=1.22`
+- `scipy>=1.10`
+- `scikit-learn>=1.2`
+
+These are installed automatically when you run `pip install .`.
+
+## Common Pitfalls
+
+### ValueError about quaternions?
+
+All quaternions must be non-zero. SpinStep normalises them automatically, but zero vectors (e.g. `[0, 0, 0, 0]`) will raise a `ValueError`:
+
+```python
+from spinstep import Node
+Node("bad", [0, 0, 0, 0]) # Raises ValueError
+```
+
+### AttributeError about .orientation or .children?
+
+`DiscreteQuaternionIterator` requires nodes with `.orientation` and `.children` attributes. Use the `Node` class or provide objects with these attributes.
+
+### Why is my traversal only visiting the root?
+
+The `angle_threshold` may be too small. Try increasing it:
+
+```python
+import numpy as np
+from spinstep import Node, QuaternionDepthIterator
+
+root = Node("root", [0, 0, 0, 1], [
+ Node("child", [0.2588, 0, 0, 0.9659])
+])
+
+# Use a larger threshold
+for node in QuaternionDepthIterator(root, [0.2588, 0, 0, 0.9659], angle_threshold=np.deg2rad(45)):
+ print(node.name)
+```
+
+## GPU vs CPU Behaviour
+
+### When should I use `use_cuda=True`?
+
+Only when you have CuPy installed and a CUDA-capable GPU. GPU acceleration benefits large orientation sets (thousands of quaternions) where batch angular distance computation is the bottleneck.
+
+```python
+from spinstep import DiscreteOrientationSet
+
+# CPU (default)
+cpu_set = DiscreteOrientationSet.from_cube()
+
+# GPU (requires CuPy)
+import numpy as np
+gpu_set = DiscreteOrientationSet(np.random.randn(10000, 4), use_cuda=True)
+```
+
+### What happens if CuPy is not installed?
+
+If `use_cuda=True` is specified but CuPy is not available, SpinStep falls back to NumPy. No error is raised.
+
+### Does the tree traversal itself run on GPU?
+
+No. The traversal logic (stack operations, depth tracking) always runs on CPU. GPU acceleration applies only to orientation storage and batch angular distance computations in `DiscreteOrientationSet`.
+
+## Optional Dependencies
+
+### What is CuPy used for?
+
+[CuPy](https://cupy.dev/) enables GPU-accelerated storage and computation for `DiscreteOrientationSet`. Install with:
+
+```bash
+pip install cupy-cuda12x
+```
+
+### What is healpy used for?
+
+[healpy](https://healpy.readthedocs.io/) enables the `get_unique_relative_spins()` utility function for HEALPix-based unique relative spin detection. Install with:
+
+```bash
+pip install healpy
+```
+
+## Quaternion Format
+
+### What quaternion convention does SpinStep use?
+
+SpinStep uses `[x, y, z, w]` format (scalar-last), matching [SciPy's convention](https://docs.scipy.org/doc/scipy/reference/generated/scipy.spatial.transform.Rotation.from_quat.html).
+
+### Can I use Euler angles or axis-angle?
+
+Yes. Convert to quaternions first using SciPy:
+
+```python
+from scipy.spatial.transform import Rotation as R
+
+# From Euler angles (in degrees)
+quat = R.from_euler("xyz", [30, 0, 0], degrees=True).as_quat()
+
+# From axis-angle
+quat = R.from_rotvec([0.5236, 0, 0]).as_quat() # 30° around X
+```
+
+### Are quaternions automatically normalised?
+
+Yes. Both `Node` and `DiscreteOrientationSet` normalise quaternions on construction.
+
+---
+
+[⬅️ Discrete Traversal](discrete-traversal.md) | [🏠 Home](index.md) | [API Reference ➡️](09-api-reference.md)
diff --git a/docs/getting-started.md b/docs/getting-started.md
new file mode 100644
index 0000000..a86df4d
--- /dev/null
+++ b/docs/getting-started.md
@@ -0,0 +1,63 @@
+# Getting Started
+
+## Installation
+
+**Requirements:** Python 3.9+
+
+Install SpinStep from source:
+
+```bash
+git clone https://github.com/VoxleOne/SpinStep.git
+cd SpinStep
+pip install .
+```
+
+This installs SpinStep and its core dependencies (`numpy`, `scipy`, `scikit-learn`).
+
+For development (includes `pytest`, `ruff`, `mypy`, `black`):
+
+```bash
+pip install -e ".[dev]"
+```
+
+## First Example
+
+SpinStep uses quaternions `[x, y, z, w]` to represent orientations.
+Here is a minimal working example using continuous traversal:
+
+```python
+from spinstep import Node, QuaternionDepthIterator
+
+# Identity quaternion: [0, 0, 0, 1] (no rotation)
+root = Node("root", [0, 0, 0, 1], [
+ Node("child", [0.2588, 0, 0, 0.9659]) # ~30° around Z
+])
+
+# Traverse with a 30° rotation step
+for node in QuaternionDepthIterator(root, [0.2588, 0, 0, 0.9659]):
+ print("Visited:", node.name)
+# Output:
+# Visited: root
+# Visited: child
+```
+
+### How It Works
+
+1. You create `Node` objects, each with a quaternion orientation.
+2. You choose a traversal mode:
+ - `QuaternionDepthIterator` for continuous single-step traversal.
+ - `DiscreteQuaternionIterator` for multi-step discrete traversal.
+3. The iterator visits nodes whose orientations are reachable from the current state.
+
+## Key Concepts
+
+- **Quaternion format:** SpinStep uses `[x, y, z, w]` format (scalar-last), matching SciPy's convention.
+- **Automatic normalisation:** All quaternions are normalised on construction. You do not need to pre-normalise.
+- **Angle threshold:** Controls how close a child's orientation must be to the rotated state to be visited. Measured in radians.
+
+## Next Steps
+
+- [Continuous Traversal Guide](continuous-traversal.md) — detailed `QuaternionDepthIterator` usage.
+- [Discrete Traversal Guide](discrete-traversal.md) — using `DiscreteOrientationSet` and `DiscreteQuaternionIterator`.
+- [FAQ](faq.md) — common pitfalls and tips.
+- [API Reference](09-api-reference.md) — full class and method documentation.
diff --git a/docs/index.md b/docs/index.md
index ba78fc9..72ba6c3 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -1,8 +1,19 @@
# Welcome to the SpinStep Documentation
-This documentation provides insight into the motivation, technology, and future of the SpinStep engine.
+SpinStep is a quaternion-driven traversal framework for trees and orientation-based data structures.
-## Table of Contents
+## Getting Started
+
+- [Getting Started](getting-started.md) — installation and first example
+- [Continuous Traversal Guide](continuous-traversal.md) — using `QuaternionDepthIterator`
+- [Discrete Traversal Guide](discrete-traversal.md) — using `DiscreteOrientationSet` and `DiscreteQuaternionIterator`
+- [FAQ](faq.md) — common pitfalls, GPU vs CPU, optional dependencies
+
+## Reference
+
+- [API Reference](09-api-reference.md) — full class and method documentation
+
+## In-Depth Topics
diff --git a/spinstep/discrete.py b/spinstep/discrete.py
index f84b07b..b4fae53 100644
--- a/spinstep/discrete.py
+++ b/spinstep/discrete.py
@@ -25,12 +25,25 @@ class DiscreteOrientationSet:
:class:`~sklearn.neighbors.BallTree` is used for large sets; an optional
CUDA path is available via CuPy.
- Parameters
- ----------
- orientations:
- Array of shape ``(N, 4)`` — one quaternion per row.
- use_cuda:
- When *True*, store orientations on the GPU using CuPy.
+ Args:
+ orientations: Array of shape ``(N, 4)`` — one quaternion per row.
+ use_cuda: When ``True``, store orientations on the GPU using CuPy.
+
+ Raises:
+ ValueError: If *orientations* is ``None``, not of shape ``(N, 4)``,
+ or all quaternions are zero vectors.
+
+ Attributes:
+ orientations: Normalised quaternion array of shape ``(N, 4)``.
+ use_cuda: Whether GPU storage is enabled.
+ xp: The array module in use (``numpy`` or ``cupy``).
+
+ Example::
+
+ from spinstep import DiscreteOrientationSet
+
+ cube = DiscreteOrientationSet.from_cube() # 24 orientations
+ icosa = DiscreteOrientationSet.from_icosahedron() # 60 orientations
"""
def __init__(
@@ -90,16 +103,12 @@ def query_within_angle(
) -> np.ndarray:
"""Return indices of orientations within *angle* radians of *quat*.
- Parameters
- ----------
- quat:
- Query quaternion ``[x, y, z, w]`` or batch of shape ``(N, 4)``.
- angle:
- Maximum angular distance in radians.
+ Args:
+ quat: Query quaternion ``[x, y, z, w]`` or batch of shape
+ ``(N, 4)``.
+ angle: Maximum angular distance in radians.
- Returns
- -------
- numpy.ndarray
+ Returns:
Integer indices into :attr:`orientations`.
"""
query_quat_np = np.asarray(quat)
@@ -161,10 +170,11 @@ def from_icosahedron(cls) -> "DiscreteOrientationSet":
def from_custom(cls, quat_list: ArrayLike) -> "DiscreteOrientationSet":
"""Create orientation set from a user-supplied list of quaternions.
- Parameters
- ----------
- quat_list:
- Array of shape ``(N, 4)`` — quaternions ``[x, y, z, w]``.
+ Args:
+ quat_list: Array of shape ``(N, 4)`` — quaternions ``[x, y, z, w]``.
+
+ Returns:
+ A new :class:`DiscreteOrientationSet`.
"""
return cls(quat_list)
@@ -172,10 +182,11 @@ def from_custom(cls, quat_list: ArrayLike) -> "DiscreteOrientationSet":
def from_sphere_grid(cls, n_points: int = 100) -> "DiscreteOrientationSet":
"""Create orientation set by Fibonacci-sphere sampling.
- Parameters
- ----------
- n_points:
- Number of orientations to generate.
+ Args:
+ n_points: Number of orientations to generate.
+
+ Returns:
+ A new :class:`DiscreteOrientationSet`.
"""
if n_points <= 0:
return cls(np.empty((0, 4)))
@@ -196,11 +207,10 @@ def from_sphere_grid(cls, n_points: int = 100) -> "DiscreteOrientationSet":
def as_numpy(self) -> np.ndarray:
"""Convert orientations to a NumPy array.
- Returns
- -------
- numpy.ndarray
- The orientations as a NumPy array. If stored as a CuPy array
- on GPU, transfers to CPU first.
+ If stored as a CuPy array on GPU, transfers to CPU first.
+
+ Returns:
+ The orientations as a NumPy array of shape ``(N, 4)``.
"""
if hasattr(self.orientations, "get"): # CuPy array
return self.orientations.get()
diff --git a/spinstep/discrete_iterator.py b/spinstep/discrete_iterator.py
index 5632a37..3b446f4 100644
--- a/spinstep/discrete_iterator.py
+++ b/spinstep/discrete_iterator.py
@@ -25,22 +25,29 @@ class DiscreteQuaternionIterator:
*angle_threshold* of any resulting candidate orientation are pushed onto
the traversal stack.
- Parameters
- ----------
- start_node:
- Root node of the tree.
- orientation_set:
- :class:`DiscreteOrientationSet` providing the candidate rotation steps.
- angle_threshold:
- Maximum angular distance (radians) for a child to be considered
- reachable. Defaults to π/8 (22.5°).
- max_depth:
- Maximum traversal depth.
-
- Raises
- ------
- AttributeError
- If *start_node* lacks ``.orientation`` or ``.children`` attributes.
+ Args:
+ start_node: Root node of the tree.
+ orientation_set: :class:`DiscreteOrientationSet` providing the
+ candidate rotation steps.
+ angle_threshold: Maximum angular distance (radians) for a child to
+ be considered reachable. Defaults to π/8 (22.5°).
+ max_depth: Maximum traversal depth. Defaults to 100.
+
+ Raises:
+ AttributeError: If *start_node* lacks ``.orientation`` or
+ ``.children`` attributes.
+
+ Example::
+
+ import numpy as np
+ from spinstep import Node, DiscreteOrientationSet, DiscreteQuaternionIterator
+
+ root = Node("root", [0, 0, 0, 1], [
+ Node("child", [0, 0, 0.3827, 0.9239])
+ ])
+ dos = DiscreteOrientationSet.from_cube()
+ for node in DiscreteQuaternionIterator(root, dos, angle_threshold=np.pi / 4):
+ print(node.name)
"""
def __init__(
diff --git a/spinstep/node.py b/spinstep/node.py
index a98846d..d7d9e20 100644
--- a/spinstep/node.py
+++ b/spinstep/node.py
@@ -21,19 +21,27 @@ class Node:
and an optional list of child nodes. The orientation is automatically
normalised on construction.
- Parameters
- ----------
- name:
- Human-readable identifier for this node.
- orientation:
- Quaternion as ``[x, y, z, w]``. Must have non-zero norm.
- children:
- Optional initial child nodes.
-
- Raises
- ------
- ValueError
- If *orientation* is not a 4-element vector or has near-zero norm.
+ Args:
+ name: Human-readable identifier for this node.
+ orientation: Quaternion as ``[x, y, z, w]``. Must have non-zero norm.
+ children: Optional initial child nodes.
+
+ Raises:
+ ValueError: If *orientation* is not a 4-element vector or has
+ near-zero norm.
+
+ Attributes:
+ name: Node identifier string.
+ orientation: Normalised quaternion as a NumPy array of shape ``(4,)``.
+ children: List of child :class:`Node` instances.
+
+ Example::
+
+ from spinstep import Node
+
+ root = Node("root", [0, 0, 0, 1])
+ child = Node("child", [0.2588, 0, 0, 0.9659])
+ root.children.append(child)
"""
name: str
diff --git a/spinstep/traversal.py b/spinstep/traversal.py
index 5a61851..77a274c 100644
--- a/spinstep/traversal.py
+++ b/spinstep/traversal.py
@@ -25,16 +25,29 @@ class QuaternionDepthIterator:
*angle_threshold* of the rotated state are pushed onto the traversal
stack.
- Parameters
- ----------
- start_node:
- Root node of the tree to traverse.
- rotation_step_quat:
- Quaternion ``[x, y, z, w]`` applied at every step.
- angle_threshold:
- Maximum angular distance (radians) between the rotated state and a
- child's orientation for the child to be visited. When *None* the
- threshold is set to 30 % of the step angle (minimum 1°).
+ Args:
+ start_node: Root node of the tree to traverse.
+ rotation_step_quat: Quaternion ``[x, y, z, w]`` applied at every step.
+ angle_threshold: Maximum angular distance (radians) between the
+ rotated state and a child's orientation for the child to be
+ visited. When ``None`` the threshold is set to 30 % of the
+ step angle (minimum 1°).
+
+ Attributes:
+ rotation_step: The rotation step as a
+ :class:`~scipy.spatial.transform.Rotation` instance.
+ angle_threshold: Angular distance threshold in radians.
+ stack: Internal traversal stack of ``(Node, Rotation)`` pairs.
+
+ Example::
+
+ from spinstep import Node, QuaternionDepthIterator
+
+ root = Node("root", [0, 0, 0, 1], [
+ Node("child", [0.2588, 0, 0, 0.9659])
+ ])
+ for node in QuaternionDepthIterator(root, [0.2588, 0, 0, 0.9659]):
+ print(node.name)
"""
DEFAULT_DYNAMIC_THRESHOLD_FACTOR: float = 0.3
From 8c3735f7ab3a2cf4b9532ecf91ca125255c4fb44 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 24 Mar 2026 18:40:31 +0000
Subject: [PATCH 3/3] Fix broken cross-reference links in
docs/02-architecture.md
Co-authored-by: VoxleOne <119956342+VoxleOne@users.noreply.github.com>
Agent-Logs-Url: https://github.com/VoxleOne/SpinStep/sessions/9c6faf98-3686-45a3-9659-1c604bb18a00
---
docs/02-architecture.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/02-architecture.md b/docs/02-architecture.md
index a8cb0fa..f45fdc1 100644
--- a/docs/02-architecture.md
+++ b/docs/02-architecture.md
@@ -82,7 +82,7 @@ for node in iterator:
- **General-purpose:** Works for kinematic chains, scene graphs, spatial search, and more.
- **Mathematically robust:** By using quaternions, SpinStep avoids gimbal lock and enables smooth, consistent orientation operations.
-For deeper rationale and use cases, see [docs/01_rationale.md](01_rationale.md) and [docs/04_use_cases.md](04_use_cases.md).
+For deeper rationale and use cases, see [docs/01-rationale.md](01-rationale.md) and [docs/04-use-cases.md](04-use-cases.md).
---
[⬅️ 01. Rationale](01-rationale.md) | [🏠 Home](index.md) | [03. Basics ➡️](03-basics.md)