From 3d2b4aa4c33961f6963e17125cab6dfe2fac6902 Mon Sep 17 00:00:00 2001
From: Jesse Mapel <jmapel@usgs.gov>
Date: Fri, 20 Sep 2019 11:34:46 -0700
Subject: [PATCH] Updated how Angular Velocity is stored and computed in
 rotations (#275)

* Added angular velocity to rotations

* In progress commit

* Updated new AV calculations

* In progress commit

* More in progress

* In progress

* Updated rotation

* indexing error fix

* more clean up

* Fixed velocity rotation

* Removed commented line
---
 ale/formatters/isis_formatter.py      |   2 +
 ale/rotation.py                       | 112 +++++++++++++++++---------
 ale/transformation.py                 |  12 +--
 tests/pytests/test_cassini_drivers.py |   3 +-
 tests/pytests/test_data_isis.py       |  12 +--
 tests/pytests/test_mro_drivers.py     |  30 ++++---
 tests/pytests/test_rotation.py        |  89 +++++++++++++-------
 tests/pytests/test_voyager_drivers.py |   8 +-
 8 files changed, 174 insertions(+), 94 deletions(-)

diff --git a/ale/formatters/isis_formatter.py b/ale/formatters/isis_formatter.py
index 13944fa..9d530d4 100644
--- a/ale/formatters/isis_formatter.py
+++ b/ale/formatters/isis_formatter.py
@@ -40,6 +40,7 @@ def to_isis(driver):
     instrument_pointing['CkTableOriginalSize'] = len(time_dependent_rotation.times)
     instrument_pointing['EphemerisTimes'] = time_dependent_rotation.times
     instrument_pointing['Quaternions'] = time_dependent_rotation.quats[:, [3, 0, 1, 2]]
+    instrument_pointing['AngularVelocity'] = time_dependent_rotation.av
 
     # Reverse the frame order because ISIS orders frames as
     # (destination, intermediate, ..., intermediate, source)
@@ -61,6 +62,7 @@ def to_isis(driver):
         body_rotation['CkTableOriginalSize'] = len(time_dependent_rotation.times)
         body_rotation['EphemerisTimes'] = time_dependent_rotation.times
         body_rotation['Quaternions'] = time_dependent_rotation.quats[:, [3, 0, 1, 2]]
+        body_rotation['AngularVelocity'] = time_dependent_rotation.av
 
     if source_frame != target_frame:
         # Reverse the frame order because ISIS orders frames as
diff --git a/ale/rotation.py b/ale/rotation.py
index 9db50d6..cba6f04 100644
--- a/ale/rotation.py
+++ b/ale/rotation.py
@@ -110,7 +110,7 @@ class ConstantRotation:
             new_rot = self._rot * other._rot
             return ConstantRotation(new_rot.as_quat(), other.source, self.dest)
         elif isinstance(other, TimeDependentRotation):
-            return TimeDependentRotation((self._rot * other._rots).as_quat(), other.times, other.source, self.dest)
+            return TimeDependentRotation((self._rot * other._rots).as_quat(), other.times, other.source, self.dest, av=self._rot.apply(other.av))
         else:
             raise TypeError("Rotations can only be composed with other rotations.")
 
@@ -155,7 +155,7 @@ class TimeDependentRotation:
         rot = Rotation.from_euler(sequence, np.asarray(euler), degrees=degrees)
         return TimeDependentRotation(rot.as_quat(), times, source, dest)
 
-    def __init__(self, quats, times, source, dest):
+    def __init__(self, quats, times, source, dest, av=None):
         """
         Construct a time dependent rotation
 
@@ -173,14 +173,22 @@ class TimeDependentRotation:
                  The NAIF ID code for the source frame
         dest : int
                The NAIF ID code for the destination frame
+        av : 2darray
+             The angular velocity of the rotation at each time as a 2d numpy array.
+             If not entered, then angular velocity will be computed by assuming constant
+             angular velocity between times.
         """
         self.source = source
         self.dest = dest
-        self.quats = np.asarray(quats)
-        self.times = np.asarray(times)
+        self.quats = quats
+        self.times = np.atleast_1d(times)
+        if av is not None:
+            self.av = np.asarray(av)
+        else:
+            self.av = av
 
     def __repr__(self):
-        return f'Time Dependent Rotation Source: {self.source}, Destination: {self.dest}, Quat: {self.quats}'
+        return f'Time Dependent Rotation Source: {self.source}, Destination: {self.dest}, Quats: {self.quats}, AV: {self.av}, Times: {self.times}'
 
     @property
     def quats(self):
@@ -202,25 +210,30 @@ class TimeDependentRotation:
                     The new quaternions as a 2d array. The quaternions must be
                     in scalar last format (x, y, z, w).
         """
-        self._rots = Rotation.from_quat(np.asarray(new_quats))
+        self._rots = Rotation.from_quat(new_quats)
 
     def inverse(self):
         """
         Get the inverse rotation, that is the rotation from the destination
         reference frame to the source reference frame.
         """
-        return TimeDependentRotation(self._rots.inv().as_quat(), self.times, self.dest, self.source)
+        new_rots = self._rots.inv()
+        if self.av is not None:
+            new_av = -new_rots.apply(self.av)
+        else:
+            new_av = None
+        return TimeDependentRotation(new_rots.as_quat(), self.times, self.dest, self.source, av=new_av)
 
     def _slerp(self, times):
         """
-        Using SLERP interpolate the rotation and angular velocity at specific times
-
-        This uses the same code as scipy SLERP, except it extrapolates
-        assuming constant angular velocity before and after the first
-        and last intervals.
+        Using SLERP interpolate the rotation and angular velocity at
+        specific times.
 
-        If the this rotation is only defined at one time, then the rotation is
-        assumed to be constant.
+        Times outside of the range covered by this rotation are extrapolated
+        assuming constant angular velocity. If the rotation has angular velocities
+        stored, then the first and last angular velocity are used for extrapolation.
+        Otherwise, the angular velocities from the first and last interpolation
+        interval are used for extrapolation.
 
         Parameters
         ----------
@@ -234,23 +247,47 @@ class TimeDependentRotation:
          : 2darray
            The angular velocity vectors
         """
-        vec_times = np.asarray(times)
-        if vec_times.ndim < 1:
-            vec_times = np.asarray([times])
-        elif vec_times.ndim > 1:
+        # Convert non-vector input to vector and check input
+        vec_times = np.atleast_1d(times)
+        if vec_times.ndim > 1:
             raise ValueError('Input times must be either a float or a 1d iterable of floats')
-        if len(self.times) < 2:
-            return Rotation.from_quat(np.repeat(self.quats, len(vec_times), 0)), np.zeros((len(vec_times), 3))
+
+        # Compute constant angular velocity for interpolation intervals
+        avs = np.zeros((len(self.times) + 1, 3))
+        if len(self.times) > 1:
+            steps = self.times[1:] - self.times[:-1]
+            rotvecs = (self._rots[1:] * self._rots[:-1].inv()).as_rotvec()
+            avs[1:-1] = rotvecs / steps[:, None]
+
+        # If available use actual angular velocity for extrapolation
+        # Otherwise use the adjacent interpolation interval
+        if self.av is not None:
+            avs[0] = self.av[0]
+            avs[-1] = self.av[-1]
         else:
-            idx = np.searchsorted(self.times, vec_times) - 1
-            idx[idx >= len(self.times) - 1] = len(self.times) - 2
-            idx[idx < 0] = 0
-            steps = self.times[idx+1] - self.times[idx]
-            rotvecs = (self._rots[idx + 1] * self._rots[idx].inv()).as_rotvec()
-            alpha = (vec_times - self.times[idx]) / steps
-            interp_rots = Rotation.from_rotvec(rotvecs * alpha[:, None]) * self._rots[idx]
-            interp_av = rotvecs / steps[:, None]
-            return interp_rots, interp_av
+            avs[0] = avs[1]
+            avs[-1] = avs[-2]
+
+        # Determine interpolation intervals for input times
+        av_idx = np.searchsorted(self.times, vec_times)
+        rot_idx = av_idx - 1
+        rot_idx[rot_idx < 0] = 0
+
+        # Interpolate/extrapolate rotations
+        time_diffs = vec_times - self.times[rot_idx]
+        interp_av = avs[av_idx]
+        interp_rots = Rotation.from_rotvec(interp_av * time_diffs[:, None]) * self._rots[rot_idx]
+
+        # If actual angular velocities are available, linearly interpolate them
+        if self.av is not None:
+            av_diff = np.zeros((len(self.times), 3))
+            if len(self.times) > 1:
+                av_diff[:-1] = self.av[1:] - self.av[:-1]
+                av_diff[-1] = av_diff[-2]
+            interp_av = self.av[rot_idx] + (av_diff[rot_idx] * time_diffs[:, None])
+
+        return interp_rots, interp_av
+
 
     def reinterpolate(self, times):
         """
@@ -264,10 +301,10 @@ class TimeDependentRotation:
         Returns
         -------
          : TimeDependentRotation
-           The new rotation that the input times
+           The new rotation at the input times
         """
-        new_rots, _ = self._slerp(times)
-        return TimeDependentRotation(new_rots.as_quat(), times, self.source, self.dest)
+        new_rots, av = self._slerp(times)
+        return TimeDependentRotation(new_rots.as_quat(), times, self.source, self.dest, av=av)
 
     def __mul__(self, other):
         """
@@ -289,11 +326,14 @@ class TimeDependentRotation:
         if self.source != other.dest:
             raise ValueError("Destination frame of first rotation {} is not the same as source frame of second rotation {}.".format(other.dest, self.source))
         if isinstance(other, ConstantRotation):
-            return TimeDependentRotation((self._rots * other._rot).as_quat(), self.times, other.source, self.dest)
+            return TimeDependentRotation((self._rots * other._rot).as_quat(), self.times, other.source, self.dest, av=self.av)
         elif isinstance(other, TimeDependentRotation):
             merged_times = np.union1d(np.asarray(self.times), np.asarray(other.times))
-            new_quats = (self.reinterpolate(merged_times)._rots * other.reinterpolate(merged_times)._rots).as_quat()
-            return TimeDependentRotation(new_quats, merged_times, other.source, self.dest)
+            reinterp_self = self.reinterpolate(merged_times)
+            reinterp_other = other.reinterpolate(merged_times)
+            new_quats = (reinterp_self._rots * reinterp_other._rots).as_quat()
+            new_av = reinterp_self.av + reinterp_self._rots.apply(reinterp_other.av)
+            return TimeDependentRotation(new_quats, merged_times, other.source, self.dest, av=new_av)
         else:
             raise TypeError("Rotations can only be composed with other rotations.")
 
@@ -326,7 +366,7 @@ class TimeDependentRotation:
             skew = np.array([[0, -avs[indx, 2], avs[indx, 1]],
                              [avs[indx, 2], 0, -avs[indx, 0]],
                              [-avs[indx, 1], avs[indx, 0], 0]])
-            rot_deriv = np.dot(skew, rots[indx].as_dcm())
+            rot_deriv = np.dot(skew, rots[indx].as_dcm().T).T
             rotated_vel[indx] = rots[indx].apply(vec_vel[indx])
             rotated_vel[indx] += np.dot(rot_deriv, vec_pos[indx])
 
diff --git a/ale/transformation.py b/ale/transformation.py
index 6b1adb8..2d0f269 100644
--- a/ale/transformation.py
+++ b/ale/transformation.py
@@ -105,21 +105,21 @@ class FrameChain(nx.DiGraph):
         time_dependent_frames.extend(target_time_dependent_frames)
         constant_frames.extend(target_constant_frames)
 
-        quats = np.zeros((len(times), 4))
-
         for s, d in time_dependent_frames:
+            quats = np.zeros((len(times), 4))
+            avs = np.zeros((len(times), 3))
             for j, time in enumerate(times):
-                rotation_matrix = spice.pxform(spice.frmnam(s), spice.frmnam(d), time)
+                state_matrix = spice.sxform(spice.frmnam(s), spice.frmnam(d), time)
+                rotation_matrix, avs[j] = spice.xf2rav(state_matrix)
                 quat_from_rotation = spice.m2q(rotation_matrix)
                 quats[j,:3] = quat_from_rotation[1:]
                 quats[j,3] = quat_from_rotation[0]
 
-            rotation = TimeDependentRotation(quats, times, s, d)
+            rotation = TimeDependentRotation(quats, times, s, d, av=avs)
             frame_chain.add_edge(rotation=rotation)
 
-        quats = np.zeros(4)
-
         for s, d in constant_frames:
+            quats = np.zeros(4)
             rotation_matrix = spice.pxform(spice.frmnam(s), spice.frmnam(d), times[0])
             quat_from_rotation = spice.m2q(rotation_matrix)
             quats[:3] = quat_from_rotation[1:]
diff --git a/tests/pytests/test_cassini_drivers.py b/tests/pytests/test_cassini_drivers.py
index 5c43446..805221d 100644
--- a/tests/pytests/test_cassini_drivers.py
+++ b/tests/pytests/test_cassini_drivers.py
@@ -61,8 +61,7 @@ def test_kernels(scope="module", autouse=True):
 def test_cassini_load(test_kernels, usgscsm_compare_dict):
     label_file = get_image_label("N1702360370_1")
     usgscsm_isd = ale.load(label_file, props={'kernels': test_kernels}, formatter='usgscsm')
-    print(usgscsm_isd['sensor_position'])
-    print(usgscsm_isd['sensor_orientation'])
+    print(usgscsm_isd)
     assert compare_dicts(usgscsm_isd, usgscsm_compare_dict) == []
 
 # ========= Test cassini pds3label and naifspice driver =========
diff --git a/tests/pytests/test_data_isis.py b/tests/pytests/test_data_isis.py
index 5afe3e4..76489e0 100644
--- a/tests/pytests/test_data_isis.py
+++ b/tests/pytests/test_data_isis.py
@@ -636,8 +636,8 @@ def test_sun_position_cache(testdata):
     sun_pos, sun_vel, sun_times = testdata.sun_position
     np.testing.assert_almost_equal(sun_pos, [[1000, 0, 0], [0, 0, 1000]])
     np.testing.assert_almost_equal(sun_vel,
-                                   [[-1000, 2000*np.pi*np.sqrt(3)/9, -2000*np.pi*np.sqrt(3)/9],
-                                    [2000*np.pi*np.sqrt(3)/9, -2000*np.pi*np.sqrt(3)/9, -1000]])
+                                   [[-1000, -2000*np.pi*np.sqrt(3)/9, 2000*np.pi*np.sqrt(3)/9],
+                                    [-2000*np.pi*np.sqrt(3)/9, 2000*np.pi*np.sqrt(3)/9, -1000]])
     np.testing.assert_equal(sun_times, [0, 1])
 
 def test_sun_position_polynomial(testdata):
@@ -666,8 +666,8 @@ def test_sun_position_polynomial(testdata):
     sun_pos, sun_vel, sun_times = testdata.sun_position
     np.testing.assert_almost_equal(sun_pos, [[1000, 0, 0], [-1000, 0, 1000]])
     np.testing.assert_almost_equal(sun_vel,
-                                   [[-500, 500 + 1000*np.pi*np.sqrt(3)/9, -500 - 1000*np.pi*np.sqrt(3)/9],
-                                    [-500 + 1000*np.pi*np.sqrt(3)/9, -500 - 2000*np.pi*np.sqrt(3)/9, 500 + 1000*np.pi*np.sqrt(3)/9]])
+                                   [[-500, 500 - 1000*np.pi*np.sqrt(3)/9, -500 + 1000*np.pi*np.sqrt(3)/9],
+                                    [-500 - 1000*np.pi*np.sqrt(3)/9, -500 + 2000*np.pi*np.sqrt(3)/9, 500 - 1000*np.pi*np.sqrt(3)/9]])
     np.testing.assert_equal(sun_times, [2, 4])
 
 def test_inst_position_cache(testdata):
@@ -696,6 +696,6 @@ def test_inst_position_cache(testdata):
     sensor_pos, sensor_vel, sensor_times = testdata.sensor_position
     np.testing.assert_almost_equal(sensor_pos, [[1000, 0, 0], [0, 0, 1000]])
     np.testing.assert_almost_equal(sensor_vel,
-                                   [[-1000, 2000*np.pi*np.sqrt(3)/9, -2000*np.pi*np.sqrt(3)/9],
-                                    [2000*np.pi*np.sqrt(3)/9, -2000*np.pi*np.sqrt(3)/9, -1000]])
+                                   [[-1000, -2000*np.pi*np.sqrt(3)/9, 2000*np.pi*np.sqrt(3)/9],
+                                    [-2000*np.pi*np.sqrt(3)/9, 2000*np.pi*np.sqrt(3)/9, -1000]])
     np.testing.assert_equal(sensor_times, [0, 1])
diff --git a/tests/pytests/test_mro_drivers.py b/tests/pytests/test_mro_drivers.py
index 93bedff..77c5b31 100644
--- a/tests/pytests/test_mro_drivers.py
+++ b/tests/pytests/test_mro_drivers.py
@@ -120,7 +120,13 @@ def isis_compare_dict():
                                            [0.42063547, 0.18618438, -0.23973759, 0.85494273],
                                            [0.42064763, 0.18624551, -0.2397057 , 0.85493237],
                                            [0.42065923, 0.18630667, -0.23967382, 0.85492228],
-                                           [0.42067144, 0.18636687, -0.23964185, 0.85491211]]},
+                                           [0.42067144, 0.18636687, -0.23964185, 0.85491211]],
+                           'AngularVelocity': [[-6.157101679378247e-06, 0.0009352034475077121, -0.00011941673357358669],
+                                               [-6.887942908783687e-06, 0.0009356318276404807, -0.00011800670313459697],
+                                               [-1.0577832791680011e-05, 0.000937142301898857, -0.0001101520178027217],
+                                               [-8.088753082470754e-06, 0.0009320299674209734, -0.00011098361545849514],
+                                               [-8.410068339567782e-06, 0.0009332739373169815, -0.0001116179282972322],
+                                               [-7.579826199031287e-06, 0.000938872225704521, -0.00011122572722581592]]},
     'BodyRotation': {'TimeDependentFrames': [10014, 1],
                      'CkTableStartTime': 297088762.24158406,
                      'CkTableEndTime': 297088762.9923841,
@@ -131,7 +137,13 @@ def isis_compare_dict():
                                      [-0.8371162107293473, 0.2996940355045328, 0.10720441474371896, 0.44489004065791765],
                                      [-0.8371138430875174, 0.2996946060241849, 0.1072028198209324, 0.44489449564328926],
                                      [-0.8371114754203602, 0.2996951765357392, 0.10720122489401934, 0.44489895061910595],
-                                     [-0.8371091077303039, 0.29969574703861046, 0.10719962996461516, 0.4449034055807993]]},
+                                     [-0.8371091077303039, 0.29969574703861046, 0.10719962996461516, 0.4449034055807993]],
+                     'AngularVelocity': [[6.178371695520371e-06, 4.2327129042393283e-05, 5.652013216046471e-05],
+                                         [6.178822211046791e-06, 4.232706327946714e-05, 5.652013216046188e-05],
+                                         [6.179272726027232e-06, 4.232699751172339e-05, 5.652013216045904e-05],
+                                         [6.179723239999644e-06, 4.232693173922947e-05, 5.652013216045621e-05],
+                                         [6.18017375357995e-06, 4.2326865961895454e-05, 5.6520132160453345e-05],
+                                         [6.180624266306148e-06, 4.2326800179788824e-05, 5.6520132160450506e-05]]},
     'InstrumentPosition': {'SpkTableStartTime': 297088762.24158406,
                            'SpkTableEndTime': 297088762.9923841,
                            'SpkTableOriginalSize': 6,
@@ -142,18 +154,18 @@ def isis_compare_dict():
                                          [-1886.18215477, 911.90037749, -2961.79786985],
                                          [-1886.47677475, 911.47872522, -2961.7414312],
                                          [-1886.77135665, 911.05705456, -2961.68493293]],
-                           'Velocities': [[-1.96292377, -2.80759072, 0.37446658],
-                                          [-1.96267123, -2.80771348, 0.37486368],
-                                          [-1.96241863, -2.80783619, 0.37526077],
-                                          [-1.96216602, -2.80795883, 0.37565786],
-                                          [-1.96191334, -2.80808143, 0.37605493],
-                                          [-1.96166065, -2.80820396, 0.376452]]},
+                           'Velocities':  [[-1.9629237646703683, -2.80759072221274, 0.37446657801485306],
+                                           [-1.9626712192798401, -2.807713482051373, 0.3748636774173111],
+                                           [-1.9624186346660286, -2.807836185534424, 0.3752607691067297],
+                                           [-1.9621660109346446, -2.8079588326107823, 0.37565785291714804],
+                                           [-1.9619133478903363, -2.8080814233753033, 0.37605492915558875],
+                                           [-1.961660645638678, -2.8082039577768683, 0.37645199765665144]]},
     'SunPosition': {'SpkTableStartTime': 297088762.61698407,
                     'SpkTableEndTime': 297088762.61698407,
                     'SpkTableOriginalSize': 1,
                     'EphemerisTimes': [297088762.61698407],
                     'Positions': [[-208246643.00357282, -7677078.093689713, 2103553.070434019]],
-                    'Velocities': [[-0.21024935169058154, -23.900370491465555, -10.956673494157382]]}}
+                    'Velocities': [[-0.21020163267146563, -23.901883517440407, -10.957471339412034]]}}
 
 @pytest.fixture(scope='module')
 def test_kernels():
diff --git a/tests/pytests/test_rotation.py b/tests/pytests/test_rotation.py
index c2018f2..249cf2d 100644
--- a/tests/pytests/test_rotation.py
+++ b/tests/pytests/test_rotation.py
@@ -5,87 +5,102 @@ from scipy.spatial.transform import Rotation
 from ale.rotation import ConstantRotation, TimeDependentRotation
 
 def test_constant_constant_composition():
-    # Two 90 degree rotation about the X-axis
     rot1_2 = ConstantRotation([1.0/np.sqrt(2), 0, 0, 1.0/np.sqrt(2)], 1, 2)
-    rot2_3 = ConstantRotation([1.0/np.sqrt(2), 0, 0, 1.0/np.sqrt(2)], 2, 3)
-    # compose to get a 180 degree rotation about the X-axis
+    rot2_3 = ConstantRotation([0, 1.0/np.sqrt(2), 0, 1.0/np.sqrt(2)], 2, 3)
     rot1_3 = rot2_3*rot1_2
     assert isinstance(rot1_3, ConstantRotation)
     assert rot1_3.source == 1
     assert rot1_3.dest == 3
-    np.testing.assert_equal(rot1_3.quat, np.array([1, 0, 0, 0]))
+    np.testing.assert_equal(rot1_3.quat, [0.5, 0.5, -0.5, 0.5])
 
 def test_constant_time_dependent_composition():
-    # 90 degree rotation about the X-axis to a 180 degree rotation about the X-axis
     quats = [[1.0/np.sqrt(2), 0, 0, 1.0/np.sqrt(2)],[1, 0, 0, 0]]
     times = [0, 1]
-    rot1_2 = TimeDependentRotation(quats, times, 1, 2)
-    # 90 degree rotation about the X-axis
-    rot2_3 = ConstantRotation([1.0/np.sqrt(2), 0, 0, 1.0/np.sqrt(2)], 2, 3)
-    # compose to get a 180 degree rotation about the X-axis to a 270 degree rotation about the X-axis
+    av = [[np.pi/2, 0, 0], [np.pi/2, 0, 0]]
+    rot1_2 = TimeDependentRotation(quats, times, 1, 2, av=av)
+    rot2_3 = ConstantRotation([0, 1.0/np.sqrt(2), 0, 1.0/np.sqrt(2)], 2, 3)
     rot1_3 = rot2_3*rot1_2
     assert isinstance(rot1_3, TimeDependentRotation)
     assert rot1_3.source == 1
     assert rot1_3.dest == 3
-    expected_quats = np.array([[1, 0, 0, 0],[1.0/np.sqrt(2), 0, 0, -1.0/np.sqrt(2)]])
-    np.testing.assert_equal(rot1_3.times, np.array(times))
+    expected_quats = [[0.5, 0.5, -0.5, 0.5],[1.0/np.sqrt(2), 0, -1.0/np.sqrt(2), 0]]
+    expected_av = [[0, 0, -np.pi/2], [0, 0, -np.pi/2]]
+    np.testing.assert_equal(rot1_3.times, times)
     np.testing.assert_almost_equal(rot1_3.quats, expected_quats)
+    np.testing.assert_almost_equal(rot1_3.av, expected_av)
 
 def test_time_dependent_constant_composition():
-    # 90 degree rotation about the X-axis
     rot1_2 = ConstantRotation([1.0/np.sqrt(2), 0, 0, 1.0/np.sqrt(2)], 1, 2)
-    # 90 degree rotation about the X-axis to a 180 degree rotation about the X-axis
     quats = [[1.0/np.sqrt(2), 0, 0, 1.0/np.sqrt(2)],[1, 0, 0, 0]]
     times = [0, 1]
-    rot2_3 = TimeDependentRotation(quats, times, 2, 3)
-    # compose to get a 180 degree rotation about the X-axis to a 270 degree rotation about the X-axis
+    av = [[np.pi/2, 0, 0], [np.pi/2, 0, 0]]
+    rot2_3 = TimeDependentRotation(quats, times, 2, 3, av=av)
     rot1_3 = rot2_3*rot1_2
     assert isinstance(rot1_3, TimeDependentRotation)
     assert rot1_3.source == 1
     assert rot1_3.dest == 3
-    expected_quats = np.array([[1, 0, 0, 0],[1.0/np.sqrt(2), 0, 0, -1.0/np.sqrt(2)]])
-    np.testing.assert_equal(rot1_3.times, np.array(times))
+    expected_quats = [[1, 0, 0, 0],[1.0/np.sqrt(2), 0, 0, -1.0/np.sqrt(2)]]
+    np.testing.assert_equal(rot1_3.times, times)
     np.testing.assert_almost_equal(rot1_3.quats, expected_quats)
+    np.testing.assert_almost_equal(rot1_3.av, av)
 
 def test_time_dependent_time_dependent_composition():
     # 90 degree rotation about the X-axis to a 180 degree rotation about the X-axis
     quats1_2 = [[1.0/np.sqrt(2), 0, 0, 1.0/np.sqrt(2)],[1, 0, 0, 0]]
     times1_2 = [0, 1]
-    rot1_2 = TimeDependentRotation(quats1_2, times1_2, 1, 2)
+    av1_2 = [[np.pi/2, 0, 0], [np.pi/2, 0, 0]]
+    rot1_2 = TimeDependentRotation(quats1_2, times1_2, 1, 2, av=av1_2)
     # -90 degree rotation about the X-axis to a 90 degree rotation about the X-axis
     quats2_3 = [[1.0/np.sqrt(2), 0, 0, -1.0/np.sqrt(2)],[1.0/np.sqrt(2), 0, 0, 1.0/np.sqrt(2)]]
     times2_3 = [0, 2]
-    rot2_3 = TimeDependentRotation(quats2_3, times2_3, 2, 3)
-
+    av2_3 = [[np.pi/2, 0, 0], [np.pi/2, 0, 0]]
+    rot2_3 = TimeDependentRotation(quats2_3, times2_3, 2, 3, av=av2_3)
 
     # compose to get no rotation to a 180 degree rotation about the X-axis to no rotation
     rot1_3 = rot2_3*rot1_2
     assert isinstance(rot1_3, TimeDependentRotation)
     assert rot1_3.source == 1
     assert rot1_3.dest == 3
-    expected_times = np.array([0, 1, 2])
-    expected_quats = np.array([[0, 0, 0, -1], [-1, 0, 0, 0], [0, 0, 0, 1]])
+    expected_times = [0, 1, 2]
+    expected_quats = [[0, 0, 0, -1], [-1, 0, 0, 0], [0, 0, 0, 1]]
+    expected_av = [[np.pi, 0, 0], [np.pi, 0, 0], [np.pi, 0, 0]]
     np.testing.assert_equal(rot1_3.times, expected_times)
     np.testing.assert_almost_equal(rot1_3.quats, expected_quats)
+    np.testing.assert_almost_equal(rot1_3.av, expected_av)
 
 def test_constant_inverse():
     rot1_2 = ConstantRotation([1.0/np.sqrt(2), 0, 0, 1.0/np.sqrt(2)], 1, 2)
     rot2_1 = rot1_2.inverse()
     assert rot2_1.source == 2
     assert rot2_1.dest == 1
-    expected_quats = np.array([1.0/np.sqrt(2), 0, 0, -1.0/np.sqrt(2)])
+    expected_quats = [1.0/np.sqrt(2), 0, 0, -1.0/np.sqrt(2)]
     np.testing.assert_almost_equal(rot2_1.quat, expected_quats)
 
 def test_time_dependent_inverse():
+    quats1_2 = [[1.0/np.sqrt(2), 0, 0, 1.0/np.sqrt(2)],[1, 0, 0, 0]]
+    times1_2 = [0, 1]
+    av1_2 = [[np.pi/2, 0, 0], [np.pi/2, 0, 0]]
+    rot1_2 = TimeDependentRotation(quats1_2, times1_2, 1, 2, av=av1_2)
+    rot2_1 = rot1_2.inverse()
+    assert rot2_1.source == 2
+    assert rot2_1.dest == 1
+    expected_quats = [[1.0/np.sqrt(2), 0, 0, -1.0/np.sqrt(2)],[1, 0, 0, 0]]
+    expected_av = [[-np.pi/2, 0, 0], [-np.pi/2, 0, 0]]
+    np.testing.assert_equal(rot2_1.times, times1_2)
+    np.testing.assert_almost_equal(rot2_1.quats, expected_quats)
+    np.testing.assert_almost_equal(rot2_1.av, expected_av)
+
+def test_time_dependent_inverse_no_av():
     quats1_2 = [[1.0/np.sqrt(2), 0, 0, 1.0/np.sqrt(2)],[1, 0, 0, 0]]
     times1_2 = [0, 1]
     rot1_2 = TimeDependentRotation(quats1_2, times1_2, 1, 2)
     rot2_1 = rot1_2.inverse()
     assert rot2_1.source == 2
     assert rot2_1.dest == 1
-    expected_quats = np.array([[1.0/np.sqrt(2), 0, 0, -1.0/np.sqrt(2)],[1, 0, 0, 0]])
-    np.testing.assert_equal(rot2_1.times, np.array(times1_2))
+    expected_quats = [[1.0/np.sqrt(2), 0, 0, -1.0/np.sqrt(2)],[1, 0, 0, 0]]
+    np.testing.assert_equal(rot2_1.times, times1_2)
     np.testing.assert_almost_equal(rot2_1.quats, expected_quats)
+    assert rot2_1.av is None
 
 def test_rotation_matrix():
     rot = ConstantRotation([0, 0, 0, 1], 1, 2)
@@ -99,9 +114,10 @@ def test_from_euler():
     times = [0, 1]
     seq = 'XYZ'
     rot = TimeDependentRotation.from_euler(seq, angles, times, 0, 1)
-    expected_quats = np.asarray([[0.5, 0.5, 0.5, 0.5], [-0.5, -0.5, 0.5, 0.5]])
+    expected_quats = [[0.5, 0.5, 0.5, 0.5], [-0.5, -0.5, 0.5, 0.5]]
     np.testing.assert_almost_equal(rot.quats, expected_quats)
-    np.testing.assert_equal(rot.times, np.asarray(times))
+    assert rot.av is None
+    np.testing.assert_equal(rot.times, times)
     assert rot.source == 0
     assert rot.dest == 1
 
@@ -113,6 +129,7 @@ def test_from_euler_degrees():
     rad_rot = TimeDependentRotation.from_euler('XYZ', rad_angles, [0, 1], 0, 1)
     degree_rot = TimeDependentRotation.from_euler('XYZ', degree_angles, [0, 1], 0, 1, degrees=True)
     np.testing.assert_almost_equal(rad_rot.quats, degree_rot.quats)
+    assert degree_rot.av is None
 
 def test_from_matrix():
     mat = [[0, 0, 1],
@@ -136,7 +153,7 @@ def test_slerp():
     np.testing.assert_almost_equal(np.degrees(new_avs),
                                    np.repeat([[90, 0, 0]], 8, 0))
 
-def test_slerp_single_time():
+def test_slerp_constant_rotation():
     rot = TimeDependentRotation([[0, 0, 0, 1]], [0], 1, 2)
     new_rot, new_avs = rot._slerp([-1, 3])
     np.testing.assert_equal(new_rot.as_quat(),
@@ -144,6 +161,16 @@ def test_slerp_single_time():
     np.testing.assert_equal(new_avs,
                             [[0, 0, 0], [0, 0, 0]])
 
+def test_slerp_single_time():
+    rot = TimeDependentRotation([[0, 0, 0, 1]], [0], 1, 2, av=[[np.pi/2, 0, 0]])
+    new_rot, new_avs = rot._slerp([-1, 3])
+    expected_quats = [[-1/np.sqrt(2), 0, 0, 1/np.sqrt(2)], [1/np.sqrt(2), 0, 0, -1/np.sqrt(2)]]
+    expected_av = [[np.pi/2, 0, 0], [np.pi/2, 0, 0]]
+    np.testing.assert_almost_equal(new_rot.as_quat(),
+                                   expected_quats)
+    np.testing.assert_equal(new_avs,
+                            expected_av)
+
 def test_slerp_variable_velocity():
     test_quats = Rotation.from_euler('xyz',
                                      [[0, 0, 0],
@@ -203,6 +230,6 @@ def test_rotate_velocity_at():
     input_times = [1, 2, 3]
     rot_vel = rot.rotate_velocity_at(input_pos, input_vel, input_times)
     np.testing.assert_almost_equal(rot_vel,
-                                   [[-1, -3 - np.pi, 2 - 3*np.pi/2],
-                                    [1 + 2*np.pi, -3, -2 + np.pi],
-                                    [3 + np.pi/2, 1 - 3*np.pi/2, -2]])
+                                   [[-1, -3 + np.pi, 2 + 3*np.pi/2],
+                                    [1 + 3*np.pi, -3 + np.pi, -2],
+                                    [3, 1 - np.pi, -2 - np.pi/2]])
diff --git a/tests/pytests/test_voyager_drivers.py b/tests/pytests/test_voyager_drivers.py
index 7c054f6..40fab5b 100644
--- a/tests/pytests/test_voyager_drivers.py
+++ b/tests/pytests/test_voyager_drivers.py
@@ -51,13 +51,15 @@ def isis_compare_dict():
                            'CkTableEndTime': -646346832.89712,
                            'CkTableOriginalSize': 1,
                            'EphemerisTimes': [-646346832.89712],
-                           'Quaternions': [[0.34057881936764,0.085849252725072,0.69748691965044,-0.62461825983655]]},
+                           'Quaternions': [[0.34057881936764,0.085849252725072,0.69748691965044,-0.62461825983655]],
+                           'AngularVelocity': [[0.0, 0.0, 0.0]]},
     'BodyRotation': {'TimeDependentFrames': [10024, 1],
                      'CkTableStartTime': -646346832.89712,
                      'CkTableEndTime': -646346832.89712,
                      'CkTableOriginalSize': 1,
                      'EphemerisTimes': [-646346832.89712],
-                     'Quaternions': [[-0.029536576144092695, 0.010097306192904288, 0.22183794661925513, -0.9745837883512549]]},
+                     'Quaternions': [[-0.029536576144092695, 0.010097306192904288, 0.22183794661925513, -0.9745837883512549]],
+                     'AngularVelocity': [[-6.713536787324419e-07, -8.842601122842458e-06, 1.845852958470088e-05]]},
     'InstrumentPosition': {'SpkTableStartTime': -646346832.89712,
                            'SpkTableEndTime': -646346832.89712,
                            'SpkTableOriginalSize': 1,
@@ -81,12 +83,10 @@ def test_kernels():
 
 @pytest.mark.parametrize("label_type", ['isis3'])
 @pytest.mark.parametrize("formatter", ['isis'])
-@pytest.mark.skip(reason="Fails due to angular velocity problems")
 def test_voyager_load(test_kernels, label_type, formatter, isis_compare_dict):
     label_file = get_image_label('c2065022', label_type)
 
     usgscsm_isd_str = ale.loads(label_file, props={'kernels': test_kernels}, formatter=formatter)
     usgscsm_isd_obj = json.loads(usgscsm_isd_str)
     print(usgscsm_isd_obj)
-
     assert compare_dicts(usgscsm_isd_obj, isis_compare_dict) == []
-- 
GitLab