FastQuat 1.0b1__tar.gz → 1.0b2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. {fastquat-1.0b1 → fastquat-1.0b2}/PKG-INFO +22 -7
  2. {fastquat-1.0b1 → fastquat-1.0b2}/README.md +20 -5
  3. fastquat-1.0b2/docs/README.md +27 -0
  4. {fastquat-1.0b1 → fastquat-1.0b2}/docs/source/index.md +17 -13
  5. {fastquat-1.0b1 → fastquat-1.0b2}/src/fastquat/quaternion.py +6 -0
  6. fastquat-1.0b2/tests/test_indexing.py +208 -0
  7. fastquat-1.0b1/docs/README.md +0 -96
  8. {fastquat-1.0b1 → fastquat-1.0b2}/.github/workflows/ci.yml +0 -0
  9. {fastquat-1.0b1 → fastquat-1.0b2}/.github/workflows/release.yml +0 -0
  10. {fastquat-1.0b1 → fastquat-1.0b2}/.gitignore +0 -0
  11. {fastquat-1.0b1 → fastquat-1.0b2}/.pre-commit-config.yaml +0 -0
  12. {fastquat-1.0b1 → fastquat-1.0b2}/.readthedocs.yaml +0 -0
  13. {fastquat-1.0b1 → fastquat-1.0b2}/docs/.gitignore +0 -0
  14. {fastquat-1.0b1 → fastquat-1.0b2}/docs/Makefile +0 -0
  15. {fastquat-1.0b1 → fastquat-1.0b2}/docs/source/api/quaternion.md +0 -0
  16. {fastquat-1.0b1 → fastquat-1.0b2}/docs/source/conf.py +0 -0
  17. {fastquat-1.0b1 → fastquat-1.0b2}/docs/source/development.md +0 -0
  18. {fastquat-1.0b1 → fastquat-1.0b2}/docs/source/user-guide/getting-started.ipynb +0 -0
  19. {fastquat-1.0b1 → fastquat-1.0b2}/docs/source/user-guide/installation.md +0 -0
  20. {fastquat-1.0b1 → fastquat-1.0b2}/docs/source/user-guide/tutorial-rotations.ipynb +0 -0
  21. {fastquat-1.0b1 → fastquat-1.0b2}/docs/source/user-guide/tutorial-slerp.ipynb +0 -0
  22. {fastquat-1.0b1 → fastquat-1.0b2}/pyproject.toml +0 -0
  23. {fastquat-1.0b1 → fastquat-1.0b2}/src/fastquat/__init__.py +0 -0
  24. {fastquat-1.0b1 → fastquat-1.0b2}/tests/conftest.py +0 -0
  25. {fastquat-1.0b1 → fastquat-1.0b2}/tests/test_base.py +0 -0
  26. {fastquat-1.0b1 → fastquat-1.0b2}/tests/test_math.py +0 -0
  27. {fastquat-1.0b1 → fastquat-1.0b2}/tests/test_pow.py +0 -0
  28. {fastquat-1.0b1 → fastquat-1.0b2}/tests/test_rotation.py +0 -0
  29. {fastquat-1.0b1 → fastquat-1.0b2}/tests/test_tensor.py +0 -0
  30. {fastquat-1.0b1 → fastquat-1.0b2}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: FastQuat
3
- Version: 1.0b1
3
+ Version: 1.0b2
4
4
  Summary: High-performance quaternions with JAX support
5
5
  Project-URL: homepage, https://fastquat.readthedocs.io
6
6
  Project-URL: repository, https://github.com/CMBSciPol/fastquat
@@ -55,11 +55,26 @@ from fastquat import Quaternion
55
55
  # Create quaternions
56
56
  q1 = Quaternion(1) # Identity quaternion
57
57
  q2 = Quaternion(0.7071, 0.7071, 0.0, 0.0) # 90° rotation around x-axis
58
-
59
- # Quaternion operations
60
- q3 = q1 * q2 # Multiplication
61
- q_inv = 1 / q1 # Inverse, or q1 ** -1
62
- q_norm = q1.normalize() # Normalization
58
+ q = Quaternion(1.0, 0.1, 0.2, 0.3)
59
+ p = 2
60
+
61
+ # Arithmetic
62
+ q_sum = q1 + q2
63
+ q_diff = q1 - q2
64
+ q_product = q1 * q2
65
+ q_power = q**p
66
+
67
+ # Normalization
68
+ norm = abs(q) # Quaternion norm
69
+ q_unit = q.normalize() # Unit quaternion
70
+
71
+ # Conjugation and inverse
72
+ q_conj = q.conj() # Conjugate
73
+ q_inv = 1 / q # Inverse, or q ** -1
74
+
75
+ # Other operations
76
+ q_log = q.log()
77
+ q_exp = q.exp()
63
78
 
64
79
  # Rotate vectors
65
80
  vector = jnp.array([1.0, 0.0, 0.0])
@@ -32,11 +32,26 @@ from fastquat import Quaternion
32
32
  # Create quaternions
33
33
  q1 = Quaternion(1) # Identity quaternion
34
34
  q2 = Quaternion(0.7071, 0.7071, 0.0, 0.0) # 90° rotation around x-axis
35
-
36
- # Quaternion operations
37
- q3 = q1 * q2 # Multiplication
38
- q_inv = 1 / q1 # Inverse, or q1 ** -1
39
- q_norm = q1.normalize() # Normalization
35
+ q = Quaternion(1.0, 0.1, 0.2, 0.3)
36
+ p = 2
37
+
38
+ # Arithmetic
39
+ q_sum = q1 + q2
40
+ q_diff = q1 - q2
41
+ q_product = q1 * q2
42
+ q_power = q**p
43
+
44
+ # Normalization
45
+ norm = abs(q) # Quaternion norm
46
+ q_unit = q.normalize() # Unit quaternion
47
+
48
+ # Conjugation and inverse
49
+ q_conj = q.conj() # Conjugate
50
+ q_inv = 1 / q # Inverse, or q ** -1
51
+
52
+ # Other operations
53
+ q_log = q.log()
54
+ q_exp = q.exp()
40
55
 
41
56
  # Rotate vectors
42
57
  vector = jnp.array([1.0, 0.0, 0.0])
@@ -0,0 +1,27 @@
1
+ # Documentation Build Instructions
2
+
3
+ This documentation uses **PEP 735 dependency groups** instead of separate requirements files to avoid duplication.
4
+
5
+
6
+ ## Building Documentation Locally
7
+
8
+ The dependency group `docs` is included in the group `dev` which is installed by default when using ̀uv` (recommended).
9
+ No extra step other than setting up and activating the virtual environment is required:
10
+ ```bash
11
+ # From the docs/ directory
12
+ uv sync --locked --group cuda12
13
+ source .venv/bin/activate
14
+ ```
15
+
16
+ If you're not using uv, you can install the docs dependencies manually:
17
+ ```bash
18
+ # Install the project with docs dependencies (pip >= 25.1)
19
+ pip install --upgrade pip
20
+ pip install -e . --group dev
21
+ ```
22
+
23
+ Then build the docs
24
+ ```bash
25
+ cd docs
26
+ make html
27
+ ```
@@ -19,26 +19,30 @@ FastQuat provides optimized quaternion operations with full JAX compatibility, f
19
19
  ## Quick Start
20
20
 
21
21
  ```python
22
- import jax.numpy as jnp
23
22
  from fastquat import Quaternion
24
23
 
25
24
  # Create quaternions
26
25
  q1 = Quaternion(1.0) # Identity quaternion
27
26
  q2 = Quaternion(0.7071, 0.7071, 0.0, 0.0) # 90° rotation around x-axis
28
-
29
- # Quaternion operations
30
- q3 = q1 * q2 # Multiplication
31
- q_inv = 1 / q1 # Inverse
32
- q_norm = q1.normalize() # Normalization
33
-
34
- # Rotate vectors
35
- vector = jnp.array([1.0, 0.0, 0.0])
36
- rotated = q2.rotate_vector(vector)
37
-
38
- # Spherical interpolation (SLERP)
39
- interpolated = q1.slerp(q2, t=0.5) # Halfway between q1 and q2
40
27
  ```
41
28
 
29
+ ## Quaternion Operations
30
+
31
+ | Operation | Syntax | Description |
32
+ |-----------|--------|-------------|
33
+ | Addition | `q1 + q2` | Component-wise addition |
34
+ | Subtraction | `q1 - q2` | Component-wise subtraction |
35
+ | Multiplication | `q1 * q2` | Hamilton product |
36
+ | Exponentiation | `q ** p` | Quaternion power |
37
+ | Inverse | `1 / q` | Multiplicative inverse |
38
+ | Norm | `abs(q)` | Quaternion magnitude |
39
+ | Normalization | `q.normalize()` | Unit quaternion |
40
+ | Conjugate | `q.conj()` | Quaternion conjugate |
41
+ | Rotation | `q.rotate_vector(v)` | Rotate 3D vector |
42
+ | SLERP | `q1.slerp(q2, t)` | Spherical interpolation |
43
+ | Log | `q.log()` | Quaternion logarithm |
44
+ | Exp | `q.exp()` | Quaternion exponential |
45
+
42
46
  ```{toctree}
43
47
  :maxdepth: 2
44
48
  :caption: User Guide
@@ -292,6 +292,12 @@ class Quaternion:
292
292
  for i in range(self.shape[0]):
293
293
  yield Quaternion.from_array(self.wxyz[i])
294
294
 
295
+ def __getitem__(self, idx: Any) -> Self:
296
+ """Index or slice the tensor of quaternions."""
297
+ if not isinstance(idx, tuple):
298
+ idx = (idx,)
299
+ return Quaternion.from_array(self.wxyz[(*idx, slice(None))])
300
+
295
301
  def __pos__(self) -> Self:
296
302
  """Quaternion positive."""
297
303
  return self
@@ -0,0 +1,208 @@
1
+ """Indexing tests for Quaternion class.
2
+
3
+ Tests for __getitem__: integers, slices, boolean masks, fancy integer arrays, None
4
+ (newaxis), Ellipsis, and combinations thereof. The component axis (w, x, y, z) must
5
+ never be reachable by these indices -- only the batch shape is indexed.
6
+ """
7
+
8
+ import jax
9
+ import jax.numpy as jnp
10
+ import numpy as np
11
+ import pytest
12
+
13
+ from fastquat.quaternion import Quaternion
14
+
15
+
16
+ def _arange_quat(*shape: int) -> Quaternion:
17
+ """A Quaternion whose wxyz array is `jnp.arange(prod(shape) * 4).reshape(*shape, 4)`."""
18
+ size = 1
19
+ for dim in shape:
20
+ size *= dim
21
+ return Quaternion.from_array(jnp.arange(size * 4, dtype=jnp.float32).reshape(*shape, 4))
22
+
23
+
24
+ @pytest.mark.parametrize(
25
+ 'idx',
26
+ [0, -1, 2, slice(1, 4), slice(None, None, 2), slice(None, None, -1), slice(2, None)],
27
+ )
28
+ @pytest.mark.parametrize('do_jit', [False, True])
29
+ def test_getitem_int_and_slice(idx, do_jit):
30
+ """Basic and strided/negative slicing over the leading axis."""
31
+
32
+ # `idx` is a Python-level constant closed over by `func`, not a traced argument.
33
+ def func(q):
34
+ return q[idx]
35
+
36
+ if do_jit:
37
+ func = jax.jit(func)
38
+
39
+ q = _arange_quat(6)
40
+ result = func(q)
41
+ expected = q.wxyz[idx]
42
+ assert result.wxyz.shape == expected.shape
43
+ assert jnp.allclose(result.wxyz, expected)
44
+
45
+
46
+ def test_getitem_boolean_mask():
47
+ """Boolean-mask indexing preserves the component axis.
48
+
49
+ Not parametrized over `do_jit`: a traced boolean mask is non-concrete under `jit`, exactly
50
+ as it would be for a plain JAX array (`arr[mask]`) -- this is a JAX limitation, not
51
+ something `Quaternion.__getitem__` needs to work around.
52
+ """
53
+ mask = jnp.array([True, False, True, True, False])
54
+ q = _arange_quat(5)
55
+ result = q[mask]
56
+ assert result.shape == (3,)
57
+ assert jnp.allclose(result.wxyz, q.wxyz[mask])
58
+
59
+
60
+ @pytest.mark.parametrize('do_jit', [False, True])
61
+ def test_getitem_fancy_integer_array(do_jit):
62
+ """Gather with a dynamic integer index array (e.g. batched detector selection)."""
63
+ idx = jnp.array([3, 0, 0, 4])
64
+
65
+ def func(q, i):
66
+ return q[i]
67
+
68
+ if do_jit:
69
+ func = jax.jit(func)
70
+
71
+ q = _arange_quat(5)
72
+ result = func(q, idx)
73
+ assert result.shape == (4,)
74
+ assert jnp.allclose(result.wxyz, q.wxyz[idx])
75
+
76
+
77
+ @pytest.mark.parametrize('do_jit', [False, True])
78
+ def test_getitem_fancy_integer_array_out_of_range_clamps(do_jit):
79
+ """Out-of-range indices clamp like default JAX gather semantics (no IndexError)."""
80
+ idx = jnp.array([0, 10, -10])
81
+
82
+ def func(q, i):
83
+ return q[i]
84
+
85
+ if do_jit:
86
+ func = jax.jit(func)
87
+
88
+ q = _arange_quat(5)
89
+ result = func(q, idx)
90
+ assert jnp.allclose(result.wxyz, q.wxyz[idx])
91
+
92
+
93
+ @pytest.mark.parametrize(
94
+ 'idx,expected_shape',
95
+ [
96
+ ((None, slice(None)), (1, 5)),
97
+ ((slice(None), None), (5, 1)),
98
+ ((None, slice(None), None), (1, 5, 1)),
99
+ ],
100
+ )
101
+ @pytest.mark.parametrize('do_jit', [False, True])
102
+ def test_getitem_none_newaxis(idx, expected_shape, do_jit):
103
+ """None/newaxis inserts a batch axis without disturbing the components."""
104
+
105
+ def func(q):
106
+ return q[idx]
107
+
108
+ if do_jit:
109
+ func = jax.jit(func)
110
+
111
+ q = _arange_quat(5)
112
+ result = func(q)
113
+ assert result.shape == expected_shape
114
+ assert jnp.allclose(result.wxyz, q.wxyz[idx])
115
+
116
+
117
+ @pytest.mark.parametrize('do_jit', [False, True])
118
+ def test_getitem_ellipsis_with_none(do_jit):
119
+ """Ellipsis combined with None on a multi-dimensional batch shape."""
120
+
121
+ def func(q):
122
+ return q[None, ...]
123
+
124
+ if do_jit:
125
+ func = jax.jit(func)
126
+
127
+ q = _arange_quat(2, 3)
128
+ result = func(q)
129
+ assert result.shape == (1, 2, 3)
130
+ assert jnp.allclose(result.wxyz, q.wxyz[None, ...])
131
+
132
+
133
+ @pytest.mark.parametrize('do_jit', [False, True])
134
+ def test_getitem_multidim_tuple(do_jit):
135
+ """A tuple index spanning multiple batch dimensions."""
136
+
137
+ def func(q):
138
+ return q[1:, 0]
139
+
140
+ if do_jit:
141
+ func = jax.jit(func)
142
+
143
+ q = _arange_quat(3, 4)
144
+ result = func(q)
145
+ assert result.shape == (2,)
146
+ assert jnp.allclose(result.wxyz, q.wxyz[1:, 0])
147
+
148
+
149
+ def test_getitem_scalar_index_reduces_ndim():
150
+ """Indexing with a plain int drops that axis, exactly like a JAX array."""
151
+ q = _arange_quat(6)
152
+ result = q[2]
153
+ assert result.shape == ()
154
+ assert jnp.allclose(result.wxyz, q.wxyz[2])
155
+
156
+
157
+ def test_getitem_never_indexes_component_axis():
158
+ """A full-rank slice tuple must still leave all 4 components intact."""
159
+ q = _arange_quat(3)
160
+ # `q[:]` only has one explicit index; the hidden component axis must stay whole.
161
+ result = q[:]
162
+ assert result.wxyz.shape == (3, 4)
163
+ assert jnp.allclose(result.wxyz, q.wxyz)
164
+
165
+
166
+ @pytest.mark.parametrize('do_jit', [False, True])
167
+ def test_getitem_broadcast_multiplication(do_jit):
168
+ """The pattern furax needs: q1[None, :] * q2[:, None] broadcasts like raw arrays would."""
169
+
170
+ def func(q1, q2):
171
+ return q1[None, :] * q2[:, None]
172
+
173
+ if do_jit:
174
+ func = jax.jit(func)
175
+
176
+ q1 = Quaternion.random(jax.random.key(0), shape=(5,))
177
+ q2 = Quaternion.random(jax.random.key(1), shape=(3,))
178
+ result = func(q1, q2)
179
+ assert result.shape == (3, 5)
180
+
181
+ # Cross-check against the equivalent reshape-based broadcast (the old workaround).
182
+ expected = q1.reshape((1, 5)) * q2.reshape((3, 1))
183
+ assert jnp.allclose(result.wxyz, expected.wxyz)
184
+
185
+
186
+ def test_getitem_preserves_dtype():
187
+ q = Quaternion.from_array(jnp.ones((4, 4), dtype=jnp.float16))
188
+ assert q[1:3].dtype == jnp.float16
189
+
190
+
191
+ def test_getitem_pytree_leaf_indexing_under_vmap():
192
+ """Quaternion indexing composes with jax.vmap, exercising the pytree registration."""
193
+ q = _arange_quat(3, 4)
194
+
195
+ def first_row(qi: Quaternion) -> Quaternion:
196
+ return qi[0]
197
+
198
+ result = jax.vmap(first_row)(q)
199
+ assert result.shape == (3,)
200
+ assert jnp.allclose(result.wxyz, q.wxyz[:, 0])
201
+
202
+
203
+ def test_getitem_boolean_mask_matches_numpy_reference():
204
+ rng = np.random.default_rng(0)
205
+ mask = rng.random(7) > 0.5
206
+ q = _arange_quat(7)
207
+ result = q[jnp.asarray(mask)]
208
+ assert jnp.allclose(result.wxyz, q.wxyz[mask])
@@ -1,96 +0,0 @@
1
- # Documentation Build Instructions
2
-
3
- This documentation uses **PEP 735 dependency groups** instead of separate requirements files to avoid duplication.
4
-
5
- ## Building Documentation Locally
6
-
7
- ### Using uv (recommended)
8
-
9
- ```bash
10
- # From the docs/ directory
11
- cd docs/
12
- uv run --group docs make html
13
-
14
- # Or from the root directory
15
- uv run --group docs sphinx-build docs/source docs/build/html
16
- ```
17
-
18
- ### Using other tools
19
-
20
- If you're not using uv, you can install the docs dependencies manually:
21
-
22
- ```bash
23
- # Install the project with docs dependencies
24
- pip install -e .[docs]
25
-
26
- # Then build the docs
27
- cd docs/
28
- make html
29
- ```
30
-
31
- ## What Changed
32
-
33
- ### ✅ Before (duplicated dependencies)
34
- - Dependencies listed in both `pyproject.toml` and `docs/requirements.txt`
35
- - ReadTheDocs config pointed to `docs/requirements.txt`
36
-
37
- ### ✅ After (PEP 735 dependency groups)
38
- - Dependencies only in `pyproject.toml` under `[dependency-groups.docs]`
39
- - ReadTheDocs config uses `extra_requirements: [docs]`
40
- - No more `docs/requirements.txt` file
41
-
42
- ## PEP 735 Benefits
43
-
44
- 1. **Single source of truth** - dependencies defined once in `pyproject.toml`
45
- 2. **No duplication** - eliminates sync issues between files
46
- 3. **Modern standard** - follows PEP 735 specification
47
- 4. **Tool support** - works with uv, pip, and ReadTheDocs
48
-
49
- ## ReadTheDocs Configuration
50
-
51
- The `.readthedocs.yaml` configuration now uses:
52
-
53
- ```yaml
54
- python:
55
- install:
56
- - method: pip
57
- path: .
58
- extra_requirements:
59
- - docs
60
- ```
61
-
62
- This automatically installs the project with the `docs` dependency group.
63
-
64
- ## Available Commands
65
-
66
- ```bash
67
- # Build HTML documentation
68
- uv run --group docs make html
69
-
70
- # Clean and rebuild
71
- uv run --group docs make clean html
72
-
73
- # Auto-rebuild on changes (requires sphinx-autobuild)
74
- uv run --group docs sphinx-autobuild source build/html
75
-
76
- # View available make targets
77
- uv run --group docs make help
78
- ```
79
-
80
- ## Dependencies in the `docs` Group
81
-
82
- The documentation dependencies are defined in `pyproject.toml`:
83
-
84
- ```toml
85
- [dependency-groups]
86
- docs = [
87
- "sphinx>=7.0.0",
88
- "sphinx-rtd-theme>=1.3.0",
89
- "myst-parser>=2.0.0",
90
- "nbsphinx>=0.9.0",
91
- "jupyter>=1.0.0",
92
- "matplotlib>=3.7.0",
93
- "numpy>=1.24.0",
94
- "pillow>=9.0.0",
95
- ]
96
- ```
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes