FastQuat 1.0b2__tar.gz → 1.0b3__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.
- {fastquat-1.0b2 → fastquat-1.0b3}/PKG-INFO +1 -1
- {fastquat-1.0b2 → fastquat-1.0b3}/src/fastquat/quaternion.py +30 -32
- {fastquat-1.0b2 → fastquat-1.0b3}/tests/test_pow.py +48 -0
- {fastquat-1.0b2 → fastquat-1.0b3}/.github/workflows/ci.yml +0 -0
- {fastquat-1.0b2 → fastquat-1.0b3}/.github/workflows/release.yml +0 -0
- {fastquat-1.0b2 → fastquat-1.0b3}/.gitignore +0 -0
- {fastquat-1.0b2 → fastquat-1.0b3}/.pre-commit-config.yaml +0 -0
- {fastquat-1.0b2 → fastquat-1.0b3}/.readthedocs.yaml +0 -0
- {fastquat-1.0b2 → fastquat-1.0b3}/README.md +0 -0
- {fastquat-1.0b2 → fastquat-1.0b3}/docs/.gitignore +0 -0
- {fastquat-1.0b2 → fastquat-1.0b3}/docs/Makefile +0 -0
- {fastquat-1.0b2 → fastquat-1.0b3}/docs/README.md +0 -0
- {fastquat-1.0b2 → fastquat-1.0b3}/docs/source/api/quaternion.md +0 -0
- {fastquat-1.0b2 → fastquat-1.0b3}/docs/source/conf.py +0 -0
- {fastquat-1.0b2 → fastquat-1.0b3}/docs/source/development.md +0 -0
- {fastquat-1.0b2 → fastquat-1.0b3}/docs/source/index.md +0 -0
- {fastquat-1.0b2 → fastquat-1.0b3}/docs/source/user-guide/getting-started.ipynb +0 -0
- {fastquat-1.0b2 → fastquat-1.0b3}/docs/source/user-guide/installation.md +0 -0
- {fastquat-1.0b2 → fastquat-1.0b3}/docs/source/user-guide/tutorial-rotations.ipynb +0 -0
- {fastquat-1.0b2 → fastquat-1.0b3}/docs/source/user-guide/tutorial-slerp.ipynb +0 -0
- {fastquat-1.0b2 → fastquat-1.0b3}/pyproject.toml +0 -0
- {fastquat-1.0b2 → fastquat-1.0b3}/src/fastquat/__init__.py +0 -0
- {fastquat-1.0b2 → fastquat-1.0b3}/tests/conftest.py +0 -0
- {fastquat-1.0b2 → fastquat-1.0b3}/tests/test_base.py +0 -0
- {fastquat-1.0b2 → fastquat-1.0b3}/tests/test_indexing.py +0 -0
- {fastquat-1.0b2 → fastquat-1.0b3}/tests/test_math.py +0 -0
- {fastquat-1.0b2 → fastquat-1.0b3}/tests/test_rotation.py +0 -0
- {fastquat-1.0b2 → fastquat-1.0b3}/tests/test_tensor.py +0 -0
- {fastquat-1.0b2 → fastquat-1.0b3}/uv.lock +0 -0
|
@@ -455,34 +455,31 @@ class Quaternion:
|
|
|
455
455
|
def log(self) -> Self:
|
|
456
456
|
"""Compute quaternion logarithm.
|
|
457
457
|
|
|
458
|
-
For a quaternion q =
|
|
459
|
-
log(q) = log(
|
|
458
|
+
For a quaternion q = |q| * (cos(θ) + sin(θ)v), the logarithm is:
|
|
459
|
+
log(q) = log(|q|) + θ * v
|
|
460
460
|
|
|
461
461
|
For the zero quaternion, returns (-inf, 0, 0, 0).
|
|
462
462
|
|
|
463
463
|
Returns:
|
|
464
464
|
The logarithm of the quaternion
|
|
465
465
|
"""
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
#
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
# log(q) = log(|q|) + θ * v
|
|
484
|
-
log_norm = jnp.log(q_norm)
|
|
485
|
-
log_q_vector = theta[..., None] * unit_vector
|
|
466
|
+
scalar_part = self.w
|
|
467
|
+
vector_part = self.vector
|
|
468
|
+
vector_norm_sq = jnp.sum(vector_part**2, axis=-1)
|
|
469
|
+
is_real = vector_norm_sq == 0
|
|
470
|
+
|
|
471
|
+
# log(q) = log(|q|) + θ * v/|v|, with θ = atan2(|v|, s).
|
|
472
|
+
# The where guards keep the gradients finite for real quaternions (|v| = 0).
|
|
473
|
+
log_norm = 0.5 * jnp.log(scalar_part**2 + vector_norm_sq)
|
|
474
|
+
safe_vector_norm = jnp.sqrt(jnp.where(is_real, 1.0, vector_norm_sq))
|
|
475
|
+
safe_scalar_part = jnp.where(scalar_part == 0, 1.0, scalar_part)
|
|
476
|
+
# θ/|v| tends to 1/s when |v| → 0 (s > 0)
|
|
477
|
+
theta_over_vector_norm = jnp.where(
|
|
478
|
+
is_real,
|
|
479
|
+
1 / safe_scalar_part,
|
|
480
|
+
jnp.arctan2(safe_vector_norm, scalar_part) / safe_vector_norm,
|
|
481
|
+
)
|
|
482
|
+
log_q_vector = theta_over_vector_norm[..., None] * vector_part
|
|
486
483
|
|
|
487
484
|
return Quaternion.from_scalar_vector(log_norm, log_q_vector)
|
|
488
485
|
|
|
@@ -490,26 +487,27 @@ class Quaternion:
|
|
|
490
487
|
"""Compute quaternion exponential.
|
|
491
488
|
|
|
492
489
|
For a quaternion q = s + v, the exponential is:
|
|
493
|
-
exp(q) = exp(s) * (cos(
|
|
490
|
+
exp(q) = exp(s) * (cos(|v|) + sin(|v|) * v/|v|)
|
|
494
491
|
|
|
495
492
|
Returns:
|
|
496
493
|
The exponential of the quaternion
|
|
497
494
|
"""
|
|
498
495
|
scalar_part = self.w
|
|
499
496
|
vector_part = self.vector
|
|
500
|
-
|
|
497
|
+
vector_norm_sq = jnp.sum(vector_part**2, axis=-1)
|
|
498
|
+
is_real = vector_norm_sq == 0
|
|
501
499
|
|
|
502
|
-
#
|
|
500
|
+
# The where guard keeps the gradients finite for real quaternions (|v| = 0).
|
|
501
|
+
vector_norm = jnp.where(is_real, 0.0, jnp.sqrt(jnp.where(is_real, 1.0, vector_norm_sq)))
|
|
503
502
|
exp_scalar = jnp.exp(scalar_part)
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
unit_v = vector_part * jnp.expand_dims(inv_vector_norm, -1)
|
|
503
|
+
# sin(|v|)/|v| and cos(|v|) = 1 - |v|²/2 (sin(|v|/2)/(|v|/2))², written with sinc
|
|
504
|
+
# so that the first and second derivatives are exact at |v| = 0
|
|
505
|
+
sinc_vnorm = jnp.sinc(vector_norm / jnp.pi)
|
|
506
|
+
sinc_half_vnorm = jnp.sinc(vector_norm / (2 * jnp.pi))
|
|
507
|
+
cos_vnorm = 1 - 0.5 * vector_norm_sq * sinc_half_vnorm**2
|
|
510
508
|
|
|
511
509
|
result_w = exp_scalar * cos_vnorm
|
|
512
|
-
result_vector = exp_scalar *
|
|
510
|
+
result_vector = jnp.expand_dims(exp_scalar * sinc_vnorm, -1) * vector_part
|
|
513
511
|
|
|
514
512
|
return Quaternion.from_scalar_vector(result_w, result_vector)
|
|
515
513
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import jax
|
|
2
2
|
import jax.numpy as jnp
|
|
3
|
+
import jax.test_util
|
|
3
4
|
import pytest
|
|
4
5
|
|
|
5
6
|
from fastquat import Quaternion
|
|
@@ -386,3 +387,50 @@ def test_pow_zero_quaternion(cast_type, do_jit):
|
|
|
386
387
|
# 0^n for n > 0 should give 0
|
|
387
388
|
result_0_2 = func(zero_q, cast_type(2.0))
|
|
388
389
|
assert jnp.allclose(result_0_2.wxyz, zero_q.wxyz)
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
# Gradients of logarithm and exponential
|
|
393
|
+
@pytest.mark.parametrize(
|
|
394
|
+
'wxyz',
|
|
395
|
+
[
|
|
396
|
+
[0.0, 0.0, 0.0, 0.0], # pure zero: exp at the identity
|
|
397
|
+
[0.3, 0.0, 0.0, 0.0], # real
|
|
398
|
+
[0.0, 0.1, -0.2, 0.3], # pure imaginary
|
|
399
|
+
[0.5, 0.1, -0.2, 0.3],
|
|
400
|
+
],
|
|
401
|
+
)
|
|
402
|
+
def test_exp_grads(wxyz, enable_x64):
|
|
403
|
+
"""Test exp derivatives up to second order, including at a zero vector part."""
|
|
404
|
+
jax.test_util.check_grads(
|
|
405
|
+
lambda a: Quaternion.from_array(a).exp().wxyz, (jnp.array(wxyz),), order=2
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
@pytest.mark.parametrize(
|
|
410
|
+
'wxyz',
|
|
411
|
+
[
|
|
412
|
+
[1.0, 0.0, 0.0, 0.0], # identity
|
|
413
|
+
[2.0, 0.0, 0.0, 0.0], # real
|
|
414
|
+
[0.0, 0.1, -0.2, 0.3], # pure imaginary
|
|
415
|
+
[0.5, 0.1, -0.2, 0.3],
|
|
416
|
+
],
|
|
417
|
+
)
|
|
418
|
+
def test_log_grads(wxyz, enable_x64):
|
|
419
|
+
"""Test log derivatives, including at a zero vector part."""
|
|
420
|
+
jax.test_util.check_grads(
|
|
421
|
+
lambda a: Quaternion.from_array(a).log().wxyz, (jnp.array(wxyz),), order=1
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def test_exp_jacobian_at_zero():
|
|
426
|
+
"""Test that d exp(v)/dv at v = 0 maps the vector part onto itself."""
|
|
427
|
+
jac = jax.jacobian(lambda v: Quaternion.from_scalar_vector(0.0, v).exp().wxyz)(jnp.zeros(3))
|
|
428
|
+
expected = jnp.concatenate([jnp.zeros((1, 3)), jnp.eye(3)])
|
|
429
|
+
assert jnp.allclose(jac, expected)
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def test_log_jacobian_at_identity():
|
|
433
|
+
"""Test that d log(1 + v)/dv at v = 0 maps the vector part onto itself."""
|
|
434
|
+
jac = jax.jacobian(lambda v: Quaternion.from_scalar_vector(1.0, v).log().wxyz)(jnp.zeros(3))
|
|
435
|
+
expected = jnp.concatenate([jnp.zeros((1, 3)), jnp.eye(3)])
|
|
436
|
+
assert jnp.allclose(jac, expected)
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|