123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460 |
- import numpy as np
- from scipy.linalg import solve_banded
- from ._rotation import Rotation
- def _create_skew_matrix(x):
- """Create skew-symmetric matrices corresponding to vectors.
- Parameters
- ----------
- x : ndarray, shape (n, 3)
- Set of vectors.
- Returns
- -------
- ndarray, shape (n, 3, 3)
- """
- result = np.zeros((len(x), 3, 3))
- result[:, 0, 1] = -x[:, 2]
- result[:, 0, 2] = x[:, 1]
- result[:, 1, 0] = x[:, 2]
- result[:, 1, 2] = -x[:, 0]
- result[:, 2, 0] = -x[:, 1]
- result[:, 2, 1] = x[:, 0]
- return result
- def _matrix_vector_product_of_stacks(A, b):
- """Compute the product of stack of matrices and vectors."""
- return np.einsum("ijk,ik->ij", A, b)
- def _angular_rate_to_rotvec_dot_matrix(rotvecs):
- """Compute matrices to transform angular rates to rot. vector derivatives.
- The matrices depend on the current attitude represented as a rotation
- vector.
- Parameters
- ----------
- rotvecs : ndarray, shape (n, 3)
- Set of rotation vectors.
- Returns
- -------
- ndarray, shape (n, 3, 3)
- """
- norm = np.linalg.norm(rotvecs, axis=1)
- k = np.empty_like(norm)
- mask = norm > 1e-4
- nm = norm[mask]
- k[mask] = (1 - 0.5 * nm / np.tan(0.5 * nm)) / nm**2
- mask = ~mask
- nm = norm[mask]
- k[mask] = 1/12 + 1/720 * nm**2
- skew = _create_skew_matrix(rotvecs)
- result = np.empty((len(rotvecs), 3, 3))
- result[:] = np.identity(3)
- result[:] += 0.5 * skew
- result[:] += k[:, None, None] * np.matmul(skew, skew)
- return result
- def _rotvec_dot_to_angular_rate_matrix(rotvecs):
- """Compute matrices to transform rot. vector derivatives to angular rates.
- The matrices depend on the current attitude represented as a rotation
- vector.
- Parameters
- ----------
- rotvecs : ndarray, shape (n, 3)
- Set of rotation vectors.
- Returns
- -------
- ndarray, shape (n, 3, 3)
- """
- norm = np.linalg.norm(rotvecs, axis=1)
- k1 = np.empty_like(norm)
- k2 = np.empty_like(norm)
- mask = norm > 1e-4
- nm = norm[mask]
- k1[mask] = (1 - np.cos(nm)) / nm ** 2
- k2[mask] = (nm - np.sin(nm)) / nm ** 3
- mask = ~mask
- nm = norm[mask]
- k1[mask] = 0.5 - nm ** 2 / 24
- k2[mask] = 1 / 6 - nm ** 2 / 120
- skew = _create_skew_matrix(rotvecs)
- result = np.empty((len(rotvecs), 3, 3))
- result[:] = np.identity(3)
- result[:] -= k1[:, None, None] * skew
- result[:] += k2[:, None, None] * np.matmul(skew, skew)
- return result
- def _angular_acceleration_nonlinear_term(rotvecs, rotvecs_dot):
- """Compute the non-linear term in angular acceleration.
- The angular acceleration contains a quadratic term with respect to
- the derivative of the rotation vector. This function computes that.
- Parameters
- ----------
- rotvecs : ndarray, shape (n, 3)
- Set of rotation vectors.
- rotvecs_dot : ndarray, shape (n, 3)
- Set of rotation vector derivatives.
- Returns
- -------
- ndarray, shape (n, 3)
- """
- norm = np.linalg.norm(rotvecs, axis=1)
- dp = np.sum(rotvecs * rotvecs_dot, axis=1)
- cp = np.cross(rotvecs, rotvecs_dot)
- ccp = np.cross(rotvecs, cp)
- dccp = np.cross(rotvecs_dot, cp)
- k1 = np.empty_like(norm)
- k2 = np.empty_like(norm)
- k3 = np.empty_like(norm)
- mask = norm > 1e-4
- nm = norm[mask]
- k1[mask] = (-nm * np.sin(nm) - 2 * (np.cos(nm) - 1)) / nm ** 4
- k2[mask] = (-2 * nm + 3 * np.sin(nm) - nm * np.cos(nm)) / nm ** 5
- k3[mask] = (nm - np.sin(nm)) / nm ** 3
- mask = ~mask
- nm = norm[mask]
- k1[mask] = 1/12 - nm ** 2 / 180
- k2[mask] = -1/60 + nm ** 2 / 12604
- k3[mask] = 1/6 - nm ** 2 / 120
- dp = dp[:, None]
- k1 = k1[:, None]
- k2 = k2[:, None]
- k3 = k3[:, None]
- return dp * (k1 * cp + k2 * ccp) + k3 * dccp
- def _compute_angular_rate(rotvecs, rotvecs_dot):
- """Compute angular rates given rotation vectors and its derivatives.
- Parameters
- ----------
- rotvecs : ndarray, shape (n, 3)
- Set of rotation vectors.
- rotvecs_dot : ndarray, shape (n, 3)
- Set of rotation vector derivatives.
- Returns
- -------
- ndarray, shape (n, 3)
- """
- return _matrix_vector_product_of_stacks(
- _rotvec_dot_to_angular_rate_matrix(rotvecs), rotvecs_dot)
- def _compute_angular_acceleration(rotvecs, rotvecs_dot, rotvecs_dot_dot):
- """Compute angular acceleration given rotation vector and its derivatives.
- Parameters
- ----------
- rotvecs : ndarray, shape (n, 3)
- Set of rotation vectors.
- rotvecs_dot : ndarray, shape (n, 3)
- Set of rotation vector derivatives.
- rotvecs_dot_dot : ndarray, shape (n, 3)
- Set of rotation vector second derivatives.
- Returns
- -------
- ndarray, shape (n, 3)
- """
- return (_compute_angular_rate(rotvecs, rotvecs_dot_dot) +
- _angular_acceleration_nonlinear_term(rotvecs, rotvecs_dot))
- def _create_block_3_diagonal_matrix(A, B, d):
- """Create a 3-diagonal block matrix as banded.
- The matrix has the following structure:
- DB...
- ADB..
- .ADB.
- ..ADB
- ...AD
- The blocks A, B and D are 3-by-3 matrices. The D matrices has the form
- d * I.
- Parameters
- ----------
- A : ndarray, shape (n, 3, 3)
- Stack of A blocks.
- B : ndarray, shape (n, 3, 3)
- Stack of B blocks.
- d : ndarray, shape (n + 1,)
- Values for diagonal blocks.
- Returns
- -------
- ndarray, shape (11, 3 * (n + 1))
- Matrix in the banded form as used by `scipy.linalg.solve_banded`.
- """
- ind = np.arange(3)
- ind_blocks = np.arange(len(A))
- A_i = np.empty_like(A, dtype=int)
- A_i[:] = ind[:, None]
- A_i += 3 * (1 + ind_blocks[:, None, None])
- A_j = np.empty_like(A, dtype=int)
- A_j[:] = ind
- A_j += 3 * ind_blocks[:, None, None]
- B_i = np.empty_like(B, dtype=int)
- B_i[:] = ind[:, None]
- B_i += 3 * ind_blocks[:, None, None]
- B_j = np.empty_like(B, dtype=int)
- B_j[:] = ind
- B_j += 3 * (1 + ind_blocks[:, None, None])
- diag_i = diag_j = np.arange(3 * len(d))
- i = np.hstack((A_i.ravel(), B_i.ravel(), diag_i))
- j = np.hstack((A_j.ravel(), B_j.ravel(), diag_j))
- values = np.hstack((A.ravel(), B.ravel(), np.repeat(d, 3)))
- u = 5
- l = 5
- result = np.zeros((u + l + 1, 3 * len(d)))
- result[u + i - j, j] = values
- return result
- class RotationSpline:
- """Interpolate rotations with continuous angular rate and acceleration.
- The rotation vectors between each consecutive orientation are cubic
- functions of time and it is guaranteed that angular rate and acceleration
- are continuous. Such interpolation are analogous to cubic spline
- interpolation.
- Refer to [1]_ for math and implementation details.
- Parameters
- ----------
- times : array_like, shape (N,)
- Times of the known rotations. At least 2 times must be specified.
- rotations : `Rotation` instance
- Rotations to perform the interpolation between. Must contain N
- rotations.
- Methods
- -------
- __call__
- References
- ----------
- .. [1] `Smooth Attitude Interpolation
- <https://github.com/scipy/scipy/files/2932755/attitude_interpolation.pdf>`_
- Examples
- --------
- >>> from scipy.spatial.transform import Rotation, RotationSpline
- >>> import numpy as np
- Define the sequence of times and rotations from the Euler angles:
- >>> times = [0, 10, 20, 40]
- >>> angles = [[-10, 20, 30], [0, 15, 40], [-30, 45, 30], [20, 45, 90]]
- >>> rotations = Rotation.from_euler('XYZ', angles, degrees=True)
- Create the interpolator object:
- >>> spline = RotationSpline(times, rotations)
- Interpolate the Euler angles, angular rate and acceleration:
- >>> angular_rate = np.rad2deg(spline(times, 1))
- >>> angular_acceleration = np.rad2deg(spline(times, 2))
- >>> times_plot = np.linspace(times[0], times[-1], 100)
- >>> angles_plot = spline(times_plot).as_euler('XYZ', degrees=True)
- >>> angular_rate_plot = np.rad2deg(spline(times_plot, 1))
- >>> angular_acceleration_plot = np.rad2deg(spline(times_plot, 2))
- On this plot you see that Euler angles are continuous and smooth:
- >>> import matplotlib.pyplot as plt
- >>> plt.plot(times_plot, angles_plot)
- >>> plt.plot(times, angles, 'x')
- >>> plt.title("Euler angles")
- >>> plt.show()
- The angular rate is also smooth:
- >>> plt.plot(times_plot, angular_rate_plot)
- >>> plt.plot(times, angular_rate, 'x')
- >>> plt.title("Angular rate")
- >>> plt.show()
- The angular acceleration is continuous, but not smooth. Also note that
- the angular acceleration is not a piecewise-linear function, because
- it is different from the second derivative of the rotation vector (which
- is a piecewise-linear function as in the cubic spline).
- >>> plt.plot(times_plot, angular_acceleration_plot)
- >>> plt.plot(times, angular_acceleration, 'x')
- >>> plt.title("Angular acceleration")
- >>> plt.show()
- """
- # Parameters for the solver for angular rate.
- MAX_ITER = 10
- TOL = 1e-9
- def _solve_for_angular_rates(self, dt, angular_rates, rotvecs):
- angular_rate_first = angular_rates[0].copy()
- A = _angular_rate_to_rotvec_dot_matrix(rotvecs)
- A_inv = _rotvec_dot_to_angular_rate_matrix(rotvecs)
- M = _create_block_3_diagonal_matrix(
- 2 * A_inv[1:-1] / dt[1:-1, None, None],
- 2 * A[1:-1] / dt[1:-1, None, None],
- 4 * (1 / dt[:-1] + 1 / dt[1:]))
- b0 = 6 * (rotvecs[:-1] * dt[:-1, None] ** -2 +
- rotvecs[1:] * dt[1:, None] ** -2)
- b0[0] -= 2 / dt[0] * A_inv[0].dot(angular_rate_first)
- b0[-1] -= 2 / dt[-1] * A[-1].dot(angular_rates[-1])
- for iteration in range(self.MAX_ITER):
- rotvecs_dot = _matrix_vector_product_of_stacks(A, angular_rates)
- delta_beta = _angular_acceleration_nonlinear_term(
- rotvecs[:-1], rotvecs_dot[:-1])
- b = b0 - delta_beta
- angular_rates_new = solve_banded((5, 5), M, b.ravel())
- angular_rates_new = angular_rates_new.reshape((-1, 3))
- delta = np.abs(angular_rates_new - angular_rates[:-1])
- angular_rates[:-1] = angular_rates_new
- if np.all(delta < self.TOL * (1 + np.abs(angular_rates_new))):
- break
- rotvecs_dot = _matrix_vector_product_of_stacks(A, angular_rates)
- angular_rates = np.vstack((angular_rate_first, angular_rates[:-1]))
- return angular_rates, rotvecs_dot
- def __init__(self, times, rotations):
- from scipy.interpolate import PPoly
- if rotations.single:
- raise ValueError("`rotations` must be a sequence of rotations.")
- if len(rotations) == 1:
- raise ValueError("`rotations` must contain at least 2 rotations.")
- times = np.asarray(times, dtype=float)
- if times.ndim != 1:
- raise ValueError("`times` must be 1-dimensional.")
- if len(times) != len(rotations):
- raise ValueError("Expected number of rotations to be equal to "
- "number of timestamps given, got {} rotations "
- "and {} timestamps."
- .format(len(rotations), len(times)))
- dt = np.diff(times)
- if np.any(dt <= 0):
- raise ValueError("Values in `times` must be in a strictly "
- "increasing order.")
- rotvecs = (rotations[:-1].inv() * rotations[1:]).as_rotvec()
- angular_rates = rotvecs / dt[:, None]
- if len(rotations) == 2:
- rotvecs_dot = angular_rates
- else:
- angular_rates, rotvecs_dot = self._solve_for_angular_rates(
- dt, angular_rates, rotvecs)
- dt = dt[:, None]
- coeff = np.empty((4, len(times) - 1, 3))
- coeff[0] = (-2 * rotvecs + dt * angular_rates
- + dt * rotvecs_dot) / dt ** 3
- coeff[1] = (3 * rotvecs - 2 * dt * angular_rates
- - dt * rotvecs_dot) / dt ** 2
- coeff[2] = angular_rates
- coeff[3] = 0
- self.times = times
- self.rotations = rotations
- self.interpolator = PPoly(coeff, times)
- def __call__(self, times, order=0):
- """Compute interpolated values.
- Parameters
- ----------
- times : float or array_like
- Times of interest.
- order : {0, 1, 2}, optional
- Order of differentiation:
- * 0 (default) : return Rotation
- * 1 : return the angular rate in rad/sec
- * 2 : return the angular acceleration in rad/sec/sec
- Returns
- -------
- Interpolated Rotation, angular rate or acceleration.
- """
- if order not in [0, 1, 2]:
- raise ValueError("`order` must be 0, 1 or 2.")
- times = np.asarray(times, dtype=float)
- if times.ndim > 1:
- raise ValueError("`times` must be at most 1-dimensional.")
- singe_time = times.ndim == 0
- times = np.atleast_1d(times)
- rotvecs = self.interpolator(times)
- if order == 0:
- index = np.searchsorted(self.times, times, side='right')
- index -= 1
- index[index < 0] = 0
- n_segments = len(self.times) - 1
- index[index > n_segments - 1] = n_segments - 1
- result = self.rotations[index] * Rotation.from_rotvec(rotvecs)
- elif order == 1:
- rotvecs_dot = self.interpolator(times, 1)
- result = _compute_angular_rate(rotvecs, rotvecs_dot)
- elif order == 2:
- rotvecs_dot = self.interpolator(times, 1)
- rotvecs_dot_dot = self.interpolator(times, 2)
- result = _compute_angular_acceleration(rotvecs, rotvecs_dot,
- rotvecs_dot_dot)
- else:
- assert False
- if singe_time:
- result = result[0]
- return result
|