|
import numpy as np |
|
from numpy.testing import assert_allclose |
|
|
|
import pytest |
|
from scipy.spatial import geometric_slerp |
|
|
|
|
|
def _generate_spherical_points(ndim=3, n_pts=2): |
|
|
|
|
|
|
|
|
|
np.random.seed(123) |
|
points = np.random.normal(size=(n_pts, ndim)) |
|
points /= np.linalg.norm(points, axis=1)[:, np.newaxis] |
|
return points[0], points[1] |
|
|
|
|
|
class TestGeometricSlerp: |
|
|
|
|
|
@pytest.mark.parametrize("n_dims", [2, 3, 5, 7, 9]) |
|
@pytest.mark.parametrize("n_pts", [0, 3, 17]) |
|
def test_shape_property(self, n_dims, n_pts): |
|
|
|
|
|
|
|
start, end = _generate_spherical_points(n_dims, 2) |
|
|
|
actual = geometric_slerp(start=start, |
|
end=end, |
|
t=np.linspace(0, 1, n_pts)) |
|
|
|
assert actual.shape == (n_pts, n_dims) |
|
|
|
@pytest.mark.parametrize("n_dims", [2, 3, 5, 7, 9]) |
|
@pytest.mark.parametrize("n_pts", [3, 17]) |
|
def test_include_ends(self, n_dims, n_pts): |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
start, end = _generate_spherical_points(n_dims, 2) |
|
|
|
actual = geometric_slerp(start=start, |
|
end=end, |
|
t=np.linspace(0, 1, n_pts)) |
|
|
|
assert_allclose(actual[0], start) |
|
assert_allclose(actual[-1], end) |
|
|
|
@pytest.mark.parametrize("start, end", [ |
|
|
|
(np.zeros((1, 3)), np.ones((1, 3))), |
|
|
|
(np.zeros((1, 3)), np.ones(3)), |
|
|
|
(np.zeros(1), np.ones((3, 1))), |
|
]) |
|
def test_input_shape_flat(self, start, end): |
|
|
|
|
|
with pytest.raises(ValueError, match='one-dimensional'): |
|
geometric_slerp(start=start, |
|
end=end, |
|
t=np.linspace(0, 1, 10)) |
|
|
|
@pytest.mark.parametrize("start, end", [ |
|
|
|
(np.zeros(7), np.ones(3)), |
|
|
|
(np.zeros(2), np.ones(1)), |
|
|
|
(np.array([]), np.ones(3)), |
|
]) |
|
def test_input_dim_mismatch(self, start, end): |
|
|
|
|
|
|
|
with pytest.raises(ValueError, match='dimensions'): |
|
geometric_slerp(start=start, |
|
end=end, |
|
t=np.linspace(0, 1, 10)) |
|
|
|
@pytest.mark.parametrize("start, end", [ |
|
|
|
(np.array([]), np.array([])), |
|
]) |
|
def test_input_at_least1d(self, start, end): |
|
|
|
|
|
|
|
with pytest.raises(ValueError, match='at least two-dim'): |
|
geometric_slerp(start=start, |
|
end=end, |
|
t=np.linspace(0, 1, 10)) |
|
|
|
@pytest.mark.thread_unsafe |
|
@pytest.mark.parametrize("start, end, expected", [ |
|
|
|
|
|
(np.array([0, 0, 1.0]), np.array([0, 0, -1.0]), "warning"), |
|
|
|
|
|
|
|
|
|
|
|
|
|
(np.array([0.00000000e+00, |
|
-6.10865200e-04, |
|
9.99999813e-01]), np.array([0, 0, -1.0]), "warning"), |
|
|
|
|
|
|
|
|
|
|
|
(np.array([0.00000000e+00, |
|
-9.59930941e-04, |
|
9.99999539e-01]), np.array([0, 0, -1.0]), "success"), |
|
]) |
|
def test_handle_antipodes(self, start, end, expected): |
|
|
|
|
|
|
|
if expected == "warning": |
|
with pytest.warns(UserWarning, match='antipodes'): |
|
res = geometric_slerp(start=start, |
|
end=end, |
|
t=np.linspace(0, 1, 10)) |
|
else: |
|
res = geometric_slerp(start=start, |
|
end=end, |
|
t=np.linspace(0, 1, 10)) |
|
|
|
|
|
|
|
|
|
assert_allclose(np.linalg.norm(res, axis=1), 1.0) |
|
|
|
@pytest.mark.parametrize("start, end, expected", [ |
|
|
|
|
|
(np.array([1, 0]), |
|
np.array([0, 1]), |
|
np.array([[1, 0], |
|
[np.sqrt(3) / 2, 0.5], |
|
[0.5, np.sqrt(3) / 2], |
|
[0, 1]])), |
|
|
|
|
|
(np.array([1, 0, 0]), |
|
np.array([0, 1, 0]), |
|
np.array([[1, 0, 0], |
|
[np.sqrt(3) / 2, 0.5, 0], |
|
[0.5, np.sqrt(3) / 2, 0], |
|
[0, 1, 0]])), |
|
|
|
|
|
|
|
|
|
(np.array([1, 0, 0, 0, 0]), |
|
np.array([0, 1, 0, 0, 0]), |
|
np.array([[1, 0, 0, 0, 0], |
|
[np.sqrt(3) / 2, 0.5, 0, 0, 0], |
|
[0.5, np.sqrt(3) / 2, 0, 0, 0], |
|
[0, 1, 0, 0, 0]])), |
|
|
|
]) |
|
def test_straightforward_examples(self, start, end, expected): |
|
|
|
|
|
|
|
|
|
actual = geometric_slerp(start=start, |
|
end=end, |
|
t=np.linspace(0, 1, 4)) |
|
assert_allclose(actual, expected, atol=1e-16) |
|
|
|
@pytest.mark.parametrize("t", [ |
|
|
|
np.linspace(-20, 20, 300), |
|
|
|
np.linspace(-0.0001, 0.0001, 17), |
|
]) |
|
def test_t_values_limits(self, t): |
|
|
|
|
|
with pytest.raises(ValueError, match='interpolation parameter'): |
|
_ = geometric_slerp(start=np.array([1, 0]), |
|
end=np.array([0, 1]), |
|
t=t) |
|
|
|
@pytest.mark.parametrize("start, end", [ |
|
(np.array([1]), |
|
np.array([0])), |
|
(np.array([0]), |
|
np.array([1])), |
|
(np.array([-17.7]), |
|
np.array([165.9])), |
|
]) |
|
def test_0_sphere_handling(self, start, end): |
|
|
|
|
|
with pytest.raises(ValueError, match='at least two-dim'): |
|
_ = geometric_slerp(start=start, |
|
end=end, |
|
t=np.linspace(0, 1, 4)) |
|
|
|
@pytest.mark.parametrize("tol", [ |
|
|
|
5, |
|
|
|
"7", |
|
|
|
[5, 6, 7], np.array(9.0), |
|
]) |
|
def test_tol_type(self, tol): |
|
|
|
|
|
with pytest.raises(ValueError, match='must be a float'): |
|
_ = geometric_slerp(start=np.array([1, 0]), |
|
end=np.array([0, 1]), |
|
t=np.linspace(0, 1, 5), |
|
tol=tol) |
|
|
|
@pytest.mark.parametrize("tol", [ |
|
-5e-6, |
|
-7e-10, |
|
]) |
|
def test_tol_sign(self, tol): |
|
|
|
|
|
_ = geometric_slerp(start=np.array([1, 0]), |
|
end=np.array([0, 1]), |
|
t=np.linspace(0, 1, 5), |
|
tol=tol) |
|
|
|
@pytest.mark.parametrize("start, end", [ |
|
|
|
|
|
(np.array([1, 0]), np.array([0, 0])), |
|
|
|
|
|
|
|
(np.array([1 + 1e-6, 0, 0]), |
|
np.array([0, 1 - 1e-6, 0])), |
|
|
|
(np.array([1 + 1e-6, 0, 0, 0]), |
|
np.array([0, 1 - 1e-6, 0, 0])), |
|
]) |
|
def test_unit_sphere_enforcement(self, start, end): |
|
|
|
|
|
with pytest.raises(ValueError, match='unit n-sphere'): |
|
geometric_slerp(start=start, |
|
end=end, |
|
t=np.linspace(0, 1, 5)) |
|
|
|
@pytest.mark.parametrize("start, end", [ |
|
|
|
(np.array([1, 0]), |
|
np.array([np.sqrt(2) / 2., |
|
np.sqrt(2) / 2.])), |
|
|
|
(np.array([1, 0]), |
|
np.array([-np.sqrt(2) / 2., |
|
np.sqrt(2) / 2.])), |
|
]) |
|
@pytest.mark.parametrize("t_func", [ |
|
np.linspace, np.logspace]) |
|
def test_order_handling(self, start, end, t_func): |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
num_t_vals = 20 |
|
np.random.seed(789) |
|
forward_t_vals = t_func(0, 10, num_t_vals) |
|
|
|
forward_t_vals /= forward_t_vals.max() |
|
reverse_t_vals = np.flipud(forward_t_vals) |
|
shuffled_indices = np.arange(num_t_vals) |
|
np.random.shuffle(shuffled_indices) |
|
scramble_t_vals = forward_t_vals.copy()[shuffled_indices] |
|
|
|
forward_results = geometric_slerp(start=start, |
|
end=end, |
|
t=forward_t_vals) |
|
reverse_results = geometric_slerp(start=start, |
|
end=end, |
|
t=reverse_t_vals) |
|
scrambled_results = geometric_slerp(start=start, |
|
end=end, |
|
t=scramble_t_vals) |
|
|
|
|
|
assert_allclose(forward_results, np.flipud(reverse_results)) |
|
assert_allclose(forward_results[shuffled_indices], |
|
scrambled_results) |
|
|
|
@pytest.mark.parametrize("t", [ |
|
|
|
"15, 5, 7", |
|
|
|
|
|
|
|
]) |
|
def test_t_values_conversion(self, t): |
|
with pytest.raises(ValueError): |
|
_ = geometric_slerp(start=np.array([1]), |
|
end=np.array([0]), |
|
t=t) |
|
|
|
def test_accept_arraylike(self): |
|
|
|
|
|
actual = geometric_slerp([1, 0], [0, 1], [0, 1/3, 0.5, 2/3, 1]) |
|
|
|
|
|
|
|
|
|
expected = np.array([[1, 0], |
|
[np.sqrt(3) / 2, 0.5], |
|
[np.sqrt(2) / 2, |
|
np.sqrt(2) / 2], |
|
[0.5, np.sqrt(3) / 2], |
|
[0, 1]], dtype=np.float64) |
|
|
|
|
|
|
|
|
|
assert_allclose(actual, expected, atol=1e-16) |
|
|
|
def test_scalar_t(self): |
|
|
|
|
|
|
|
actual = geometric_slerp([1, 0], [0, 1], 0.5) |
|
expected = np.array([np.sqrt(2) / 2, |
|
np.sqrt(2) / 2], dtype=np.float64) |
|
assert actual.shape == (2,) |
|
assert_allclose(actual, expected) |
|
|
|
@pytest.mark.parametrize('start', [ |
|
np.array([1, 0, 0]), |
|
np.array([0, 1]), |
|
]) |
|
@pytest.mark.parametrize('t', [ |
|
np.array(1), |
|
np.array([1]), |
|
np.array([[1]]), |
|
np.array([[[1]]]), |
|
np.array([]), |
|
np.linspace(0, 1, 5), |
|
]) |
|
def test_degenerate_input(self, start, t): |
|
if np.asarray(t).ndim > 1: |
|
with pytest.raises(ValueError): |
|
geometric_slerp(start=start, end=start, t=t) |
|
else: |
|
|
|
shape = (t.size,) + start.shape |
|
expected = np.full(shape, start) |
|
|
|
actual = geometric_slerp(start=start, end=start, t=t) |
|
assert_allclose(actual, expected) |
|
|
|
|
|
|
|
non_degenerate = geometric_slerp(start=start, end=start[::-1], t=t) |
|
assert actual.size == non_degenerate.size |
|
|
|
@pytest.mark.parametrize('k', np.logspace(-10, -1, 10)) |
|
def test_numerical_stability_pi(self, k): |
|
|
|
|
|
|
|
angle = np.pi - k |
|
ts = np.linspace(0, 1, 100) |
|
P = np.array([1, 0, 0, 0]) |
|
Q = np.array([np.cos(angle), np.sin(angle), 0, 0]) |
|
|
|
|
|
|
|
with np.testing.suppress_warnings() as sup: |
|
sup.filter(UserWarning) |
|
result = geometric_slerp(P, Q, ts, 1e-18) |
|
norms = np.linalg.norm(result, axis=1) |
|
error = np.max(np.abs(norms - 1)) |
|
assert error < 4e-15 |
|
|
|
@pytest.mark.parametrize('t', [ |
|
[[0, 0.5]], |
|
[[[[[[[[[0, 0.5]]]]]]]]], |
|
]) |
|
def test_interpolation_param_ndim(self, t): |
|
|
|
arr1 = np.array([0, 1]) |
|
arr2 = np.array([1, 0]) |
|
|
|
with pytest.raises(ValueError): |
|
geometric_slerp(start=arr1, |
|
end=arr2, |
|
t=t) |
|
|
|
with pytest.raises(ValueError): |
|
geometric_slerp(start=arr1, |
|
end=arr1, |
|
t=t) |
|
|