Fixed database typo and removed unnecessary class identifier.
This commit is contained in:
parent
00ad49a143
commit
45fb349a7d
5098 changed files with 952558 additions and 85 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load diff
|
@ -0,0 +1,169 @@
|
|||
import pytest
|
||||
|
||||
import numpy as np
|
||||
from numpy.testing import assert_array_almost_equal
|
||||
from scipy.spatial.transform import Rotation
|
||||
from scipy.optimize import linear_sum_assignment
|
||||
from scipy.spatial.distance import cdist
|
||||
from scipy.constants import golden as phi
|
||||
from scipy.spatial import cKDTree
|
||||
|
||||
|
||||
TOL = 1E-12
|
||||
NS = range(1, 13)
|
||||
NAMES = ["I", "O", "T"] + ["C%d" % n for n in NS] + ["D%d" % n for n in NS]
|
||||
SIZES = [60, 24, 12] + list(NS) + [2 * n for n in NS]
|
||||
|
||||
|
||||
def _calculate_rmsd(P, Q):
|
||||
"""Calculates the root-mean-square distance between the points of P and Q.
|
||||
The distance is taken as the minimum over all possible matchings. It is
|
||||
zero if P and Q are identical and non-zero if not.
|
||||
"""
|
||||
distance_matrix = cdist(P, Q, metric='sqeuclidean')
|
||||
matching = linear_sum_assignment(distance_matrix)
|
||||
return np.sqrt(distance_matrix[matching].sum())
|
||||
|
||||
|
||||
def _generate_pyramid(n, axis):
|
||||
thetas = np.linspace(0, 2 * np.pi, n + 1)[:-1]
|
||||
P = np.vstack([np.zeros(n), np.cos(thetas), np.sin(thetas)]).T
|
||||
P = np.concatenate((P, [[1, 0, 0]]))
|
||||
return np.roll(P, axis, axis=1)
|
||||
|
||||
|
||||
def _generate_prism(n, axis):
|
||||
thetas = np.linspace(0, 2 * np.pi, n + 1)[:-1]
|
||||
bottom = np.vstack([-np.ones(n), np.cos(thetas), np.sin(thetas)]).T
|
||||
top = np.vstack([+np.ones(n), np.cos(thetas), np.sin(thetas)]).T
|
||||
P = np.concatenate((bottom, top))
|
||||
return np.roll(P, axis, axis=1)
|
||||
|
||||
|
||||
def _generate_icosahedron():
|
||||
x = np.array([[0, -1, -phi],
|
||||
[0, -1, +phi],
|
||||
[0, +1, -phi],
|
||||
[0, +1, +phi]])
|
||||
return np.concatenate([np.roll(x, i, axis=1) for i in range(3)])
|
||||
|
||||
|
||||
def _generate_octahedron():
|
||||
return np.array([[-1, 0, 0], [+1, 0, 0], [0, -1, 0],
|
||||
[0, +1, 0], [0, 0, -1], [0, 0, +1]])
|
||||
|
||||
|
||||
def _generate_tetrahedron():
|
||||
return np.array([[1, 1, 1], [1, -1, -1], [-1, 1, -1], [-1, -1, 1]])
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name", [-1, None, True, np.array(['C3'])])
|
||||
def test_group_type(name):
|
||||
with pytest.raises(ValueError,
|
||||
match="must be a string"):
|
||||
Rotation.create_group(name)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name", ["Q", " ", "CA", "C ", "DA", "D ", "I2", ""])
|
||||
def test_group_name(name):
|
||||
with pytest.raises(ValueError,
|
||||
match="must be one of 'I', 'O', 'T', 'Dn', 'Cn'"):
|
||||
Rotation.create_group(name)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name", ["C0", "D0"])
|
||||
def test_group_order_positive(name):
|
||||
with pytest.raises(ValueError,
|
||||
match="Group order must be positive"):
|
||||
Rotation.create_group(name)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("axis", ['A', 'b', 0, 1, 2, 4, False, None])
|
||||
def test_axis_valid(axis):
|
||||
with pytest.raises(ValueError,
|
||||
match="`axis` must be one of"):
|
||||
Rotation.create_group("C1", axis)
|
||||
|
||||
|
||||
def test_icosahedral():
|
||||
"""The icosahedral group fixes the rotations of an icosahedron. Here we
|
||||
test that the icosahedron is invariant after application of the elements
|
||||
of the rotation group."""
|
||||
P = _generate_icosahedron()
|
||||
for g in Rotation.create_group("I"):
|
||||
g = Rotation.from_quat(g.as_quat())
|
||||
assert _calculate_rmsd(P, g.apply(P)) < TOL
|
||||
|
||||
|
||||
def test_octahedral():
|
||||
"""Test that the octahedral group correctly fixes the rotations of an
|
||||
octahedron."""
|
||||
P = _generate_octahedron()
|
||||
for g in Rotation.create_group("O"):
|
||||
assert _calculate_rmsd(P, g.apply(P)) < TOL
|
||||
|
||||
|
||||
def test_tetrahedral():
|
||||
"""Test that the tetrahedral group correctly fixes the rotations of a
|
||||
tetrahedron."""
|
||||
P = _generate_tetrahedron()
|
||||
for g in Rotation.create_group("T"):
|
||||
assert _calculate_rmsd(P, g.apply(P)) < TOL
|
||||
|
||||
|
||||
@pytest.mark.parametrize("n", NS)
|
||||
@pytest.mark.parametrize("axis", 'XYZ')
|
||||
def test_dicyclic(n, axis):
|
||||
"""Test that the dicyclic group correctly fixes the rotations of a
|
||||
prism."""
|
||||
P = _generate_prism(n, axis='XYZ'.index(axis))
|
||||
for g in Rotation.create_group("D%d" % n, axis=axis):
|
||||
assert _calculate_rmsd(P, g.apply(P)) < TOL
|
||||
|
||||
|
||||
@pytest.mark.parametrize("n", NS)
|
||||
@pytest.mark.parametrize("axis", 'XYZ')
|
||||
def test_cyclic(n, axis):
|
||||
"""Test that the cyclic group correctly fixes the rotations of a
|
||||
pyramid."""
|
||||
P = _generate_pyramid(n, axis='XYZ'.index(axis))
|
||||
for g in Rotation.create_group("C%d" % n, axis=axis):
|
||||
assert _calculate_rmsd(P, g.apply(P)) < TOL
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name, size", zip(NAMES, SIZES))
|
||||
def test_group_sizes(name, size):
|
||||
assert len(Rotation.create_group(name)) == size
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name, size", zip(NAMES, SIZES))
|
||||
def test_group_no_duplicates(name, size):
|
||||
g = Rotation.create_group(name)
|
||||
kdtree = cKDTree(g.as_quat())
|
||||
assert len(kdtree.query_pairs(1E-3)) == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name, size", zip(NAMES, SIZES))
|
||||
def test_group_symmetry(name, size):
|
||||
g = Rotation.create_group(name)
|
||||
q = np.concatenate((-g.as_quat(), g.as_quat()))
|
||||
distance = np.sort(cdist(q, q))
|
||||
deltas = np.max(distance, axis=0) - np.min(distance, axis=0)
|
||||
assert (deltas < TOL).all()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name", NAMES)
|
||||
def test_reduction(name):
|
||||
"""Test that the elements of the rotation group are correctly
|
||||
mapped onto the identity rotation."""
|
||||
g = Rotation.create_group(name)
|
||||
f = g.reduce(g)
|
||||
assert_array_almost_equal(f.magnitude(), np.zeros(len(g)))
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name", NAMES)
|
||||
def test_single_reduction(name):
|
||||
g = Rotation.create_group(name)
|
||||
f = g[-1].reduce(g)
|
||||
assert_array_almost_equal(f.magnitude(), 0)
|
||||
assert f.as_quat().shape == (4,)
|
|
@ -0,0 +1,161 @@
|
|||
from itertools import product
|
||||
import numpy as np
|
||||
from numpy.testing import assert_allclose
|
||||
from pytest import raises
|
||||
from scipy.spatial.transform import Rotation, RotationSpline
|
||||
from scipy.spatial.transform._rotation_spline import (
|
||||
_angular_rate_to_rotvec_dot_matrix,
|
||||
_rotvec_dot_to_angular_rate_matrix,
|
||||
_matrix_vector_product_of_stacks,
|
||||
_angular_acceleration_nonlinear_term,
|
||||
_create_block_3_diagonal_matrix)
|
||||
|
||||
|
||||
def test_angular_rate_to_rotvec_conversions():
|
||||
np.random.seed(0)
|
||||
rv = np.random.randn(4, 3)
|
||||
A = _angular_rate_to_rotvec_dot_matrix(rv)
|
||||
A_inv = _rotvec_dot_to_angular_rate_matrix(rv)
|
||||
|
||||
# When the rotation vector is aligned with the angular rate, then
|
||||
# the rotation vector rate and angular rate are the same.
|
||||
assert_allclose(_matrix_vector_product_of_stacks(A, rv), rv)
|
||||
assert_allclose(_matrix_vector_product_of_stacks(A_inv, rv), rv)
|
||||
|
||||
# A and A_inv must be reciprocal to each other.
|
||||
I_stack = np.empty((4, 3, 3))
|
||||
I_stack[:] = np.eye(3)
|
||||
assert_allclose(np.matmul(A, A_inv), I_stack, atol=1e-15)
|
||||
|
||||
|
||||
def test_angular_rate_nonlinear_term():
|
||||
# The only simple test is to check that the term is zero when
|
||||
# the rotation vector
|
||||
np.random.seed(0)
|
||||
rv = np.random.rand(4, 3)
|
||||
assert_allclose(_angular_acceleration_nonlinear_term(rv, rv), 0,
|
||||
atol=1e-19)
|
||||
|
||||
|
||||
def test_create_block_3_diagonal_matrix():
|
||||
np.random.seed(0)
|
||||
A = np.empty((4, 3, 3))
|
||||
A[:] = np.arange(1, 5)[:, None, None]
|
||||
|
||||
B = np.empty((4, 3, 3))
|
||||
B[:] = -np.arange(1, 5)[:, None, None]
|
||||
d = 10 * np.arange(10, 15)
|
||||
|
||||
banded = _create_block_3_diagonal_matrix(A, B, d)
|
||||
|
||||
# Convert the banded matrix to the full matrix.
|
||||
k, l = list(zip(*product(np.arange(banded.shape[0]),
|
||||
np.arange(banded.shape[1]))))
|
||||
k = np.asarray(k)
|
||||
l = np.asarray(l)
|
||||
|
||||
i = k - 5 + l
|
||||
j = l
|
||||
values = banded.ravel()
|
||||
mask = (i >= 0) & (i < 15)
|
||||
i = i[mask]
|
||||
j = j[mask]
|
||||
values = values[mask]
|
||||
full = np.zeros((15, 15))
|
||||
full[i, j] = values
|
||||
|
||||
zero = np.zeros((3, 3))
|
||||
eye = np.eye(3)
|
||||
|
||||
# Create the reference full matrix in the most straightforward manner.
|
||||
ref = np.block([
|
||||
[d[0] * eye, B[0], zero, zero, zero],
|
||||
[A[0], d[1] * eye, B[1], zero, zero],
|
||||
[zero, A[1], d[2] * eye, B[2], zero],
|
||||
[zero, zero, A[2], d[3] * eye, B[3]],
|
||||
[zero, zero, zero, A[3], d[4] * eye],
|
||||
])
|
||||
|
||||
assert_allclose(full, ref, atol=1e-19)
|
||||
|
||||
|
||||
def test_spline_2_rotations():
|
||||
times = [0, 10]
|
||||
rotations = Rotation.from_euler('xyz', [[0, 0, 0], [10, -20, 30]],
|
||||
degrees=True)
|
||||
spline = RotationSpline(times, rotations)
|
||||
|
||||
rv = (rotations[0].inv() * rotations[1]).as_rotvec()
|
||||
rate = rv / (times[1] - times[0])
|
||||
times_check = np.array([-1, 5, 12])
|
||||
dt = times_check - times[0]
|
||||
rv_ref = rate * dt[:, None]
|
||||
|
||||
assert_allclose(spline(times_check).as_rotvec(), rv_ref)
|
||||
assert_allclose(spline(times_check, 1), np.resize(rate, (3, 3)))
|
||||
assert_allclose(spline(times_check, 2), 0, atol=1e-16)
|
||||
|
||||
|
||||
def test_constant_attitude():
|
||||
times = np.arange(10)
|
||||
rotations = Rotation.from_rotvec(np.ones((10, 3)))
|
||||
spline = RotationSpline(times, rotations)
|
||||
|
||||
times_check = np.linspace(-1, 11)
|
||||
assert_allclose(spline(times_check).as_rotvec(), 1, rtol=1e-15)
|
||||
assert_allclose(spline(times_check, 1), 0, atol=1e-19)
|
||||
assert_allclose(spline(times_check, 2), 0, atol=1e-19)
|
||||
|
||||
assert_allclose(spline(5.5).as_rotvec(), 1, rtol=1e-15)
|
||||
assert_allclose(spline(5.5, 1), 0, atol=1e-19)
|
||||
assert_allclose(spline(5.5, 2), 0, atol=1e-19)
|
||||
|
||||
|
||||
def test_spline_properties():
|
||||
times = np.array([0, 5, 15, 27])
|
||||
angles = [[-5, 10, 27], [3, 5, 38], [-12, 10, 25], [-15, 20, 11]]
|
||||
|
||||
rotations = Rotation.from_euler('xyz', angles, degrees=True)
|
||||
spline = RotationSpline(times, rotations)
|
||||
|
||||
assert_allclose(spline(times).as_euler('xyz', degrees=True), angles)
|
||||
assert_allclose(spline(0).as_euler('xyz', degrees=True), angles[0])
|
||||
|
||||
h = 1e-8
|
||||
rv0 = spline(times).as_rotvec()
|
||||
rvm = spline(times - h).as_rotvec()
|
||||
rvp = spline(times + h).as_rotvec()
|
||||
assert_allclose(rv0, 0.5 * (rvp + rvm), rtol=1e-15)
|
||||
|
||||
r0 = spline(times, 1)
|
||||
rm = spline(times - h, 1)
|
||||
rp = spline(times + h, 1)
|
||||
assert_allclose(r0, 0.5 * (rm + rp), rtol=1e-14)
|
||||
|
||||
a0 = spline(times, 2)
|
||||
am = spline(times - h, 2)
|
||||
ap = spline(times + h, 2)
|
||||
assert_allclose(a0, am, rtol=1e-7)
|
||||
assert_allclose(a0, ap, rtol=1e-7)
|
||||
|
||||
|
||||
def test_error_handling():
|
||||
raises(ValueError, RotationSpline, [1.0], Rotation.random())
|
||||
|
||||
r = Rotation.random(10)
|
||||
t = np.arange(10).reshape(5, 2)
|
||||
raises(ValueError, RotationSpline, t, r)
|
||||
|
||||
t = np.arange(9)
|
||||
raises(ValueError, RotationSpline, t, r)
|
||||
|
||||
t = np.arange(10)
|
||||
t[5] = 0
|
||||
raises(ValueError, RotationSpline, t, r)
|
||||
|
||||
t = np.arange(10)
|
||||
|
||||
s = RotationSpline(t, r)
|
||||
raises(ValueError, s, 10, -1)
|
||||
|
||||
raises(ValueError, s, np.arange(10).reshape(5, 2))
|
Loading…
Add table
Add a link
Reference in a new issue