1111from imap_data_access import SPICEFilePath
1212from numpy .typing import NDArray
1313
14- from imap_processing .spice .geometry import SpiceFrame
14+ from imap_processing .spice .geometry import SpiceFrame , frame_transform
1515from imap_processing .spice .repoint import get_repoint_data
1616from imap_processing .spice .time import (
1717 TICK_DURATION ,
@@ -284,8 +284,8 @@ def calculate_pointing_attitude_segments(
284284 f"range: ({ et_to_utc (pointing_start_et )} , { et_to_utc (pointing_end_et )} )"
285285 )
286286
287- # 1 spin/15 seconds; 10 quaternions / spin.
288- num_samples = ( pointing_end_et - pointing_start_et ) / 15 * 10
287+ # Sample at 1Hz
288+ num_samples = pointing_end_et - pointing_start_et
289289 # There were rounding errors when using spiceypy.pxform
290290 # so np.ceil and np.floor were used to ensure the start
291291 # and end times were within the ck range.
@@ -295,11 +295,11 @@ def calculate_pointing_attitude_segments(
295295 int (num_samples ),
296296 )
297297
298- # Get the average quaternions for the pointing
299- q_avg = _average_quaternions (et_times )
298+ # Get the average spin-axis in HAE coordinates
299+ z_avg = _mean_spin_axis (et_times )
300300
301301 # Create a rotation matrix
302- rotation_matrix = _create_rotation_matrix (q_avg )
302+ rotation_matrix = _create_rotation_matrix (z_avg )
303303
304304 # Convert the rotation matrix to a quaternion.
305305 # https://spiceypy.readthedocs.io/en/main/documentation.html#spiceypy.spiceypy.m2q
@@ -317,9 +317,14 @@ def calculate_pointing_attitude_segments(
317317 return pointing_segments
318318
319319
320- def _average_quaternions (et_times : np .ndarray ) -> NDArray :
320+ def _mean_spin_axis (et_times : np .ndarray ) -> NDArray :
321321 """
322- Average the quaternions.
322+ Compute the mean spin axis for a given time range.
323+
324+ The mean spin-axis is computed by taking the mean of the spacecraft z-axis
325+ expressed in HAE Cartesian coordinates at each of the input et_times. The
326+ mean is computed by finding the mean of each component of the vector across
327+ time.
323328
324329 Parameters
325330 ----------
@@ -328,72 +333,52 @@ def _average_quaternions(et_times: np.ndarray) -> NDArray:
328333
329334 Returns
330335 -------
331- q_avg : np.ndarray
332- Average quaternion .
336+ z_avg : np.ndarray
337+ Mean spin-axis. Shape is (3,), a single 3D vector (x, y, z) .
333338 """
334- aggregate = np .zeros ((4 , 4 ))
335- for tdb in et_times :
336- # we use a quick and dirty method here for grabbing the quaternions
337- # from the attitude kernel. Depending on how well the kernel input
338- # data is built and sampled, there may or may not be aliasing with this
339- # approach. If it turns out that we need to pull the quaternions
340- # directly from the CK there are several routines that exist to do this
341- # but it's not straight forward. We'll revisit this if needed.
342-
343- # Rotation matrix from IMAP spacecraft frame to ECLIPJ2000.
344- # https://spiceypy.readthedocs.io/en/main/documentation.html#spiceypy.spiceypy.pxform
345- body_rots = spiceypy .pxform ("IMAP_SPACECRAFT" , "ECLIPJ2000" , tdb )
346- # Convert rotation matrix to quaternion.
347- # https://spiceypy.readthedocs.io/en/main/documentation.html#spiceypy.spiceypy.m2q
348- body_quat = spiceypy .m2q (body_rots )
349-
350- # Standardize the quaternion so that they may be compared.
351- body_quat = body_quat * np .sign (body_quat [0 ])
352- # Aggregate quaternions into a single matrix.
353- aggregate += np .outer (body_quat , body_quat )
354-
355- # Reference: "On Averaging Rotations".
356- # Link: https://link.springer.com/content/pdf/10.1023/A:1011129215388.pdf
357- aggregate /= len (et_times )
358-
359- # Compute eigen values and vectors of the matrix A
360- # Eigenvalues tell you how much "influence" each
361- # direction (eigenvector) has.
362- # The largest eigenvalue corresponds to the direction
363- # that has the most influence.
364- # The eigenvector corresponding to the largest
365- # eigenvalue points in the direction that has the most
366- # combined rotation influence.
367- eigvals , eigvecs = np .linalg .eig (aggregate )
368- # q0: The scalar part of the quaternion.
369- # q1, q2, q3: The vector part of the quaternion.
370- q_avg = eigvecs [:, np .argmax (eigvals )]
371-
372- return q_avg
373-
374-
375- def _create_rotation_matrix (q_avg : np .ndarray ) -> NDArray :
339+ # we use a quick and dirty method here for sampling the instantaneous
340+ # spin-axis. Depending on how well the kernel input
341+ # data is built and sampled, there may or may not be aliasing with this
342+ # approach. If it turns out that we need to pull the quaternions
343+ # directly from the CK there are several routines that exist to do this
344+ # but it's not straight forward. We'll revisit this if needed.
345+ z_inertial_hae = frame_transform (
346+ et_times , np .array ([0 , 0 , 1 ]), SpiceFrame .IMAP_SPACECRAFT , SpiceFrame .ECLIPJ2000
347+ )
348+
349+ # Compute the average spin axis by averaging each component across time
350+ z_avg = np .mean (z_inertial_hae , axis = 0 )
351+ # We don't need to worry about the magnitude being close to zero when
352+ # normalizing because the instantaneous spin-axes will always be close
353+ # to the same direction.
354+ z_avg /= np .linalg .norm (z_avg )
355+
356+ return z_avg
357+
358+
359+ def _create_rotation_matrix (z_avg : np .ndarray ) -> NDArray :
376360 """
377- Create a rotation matrix.
361+ Create a rotation matrix from the average spin axis .
378362
379363 Parameters
380364 ----------
381- q_avg : numpy.ndarray
382- Averaged quaternions for the pointing.
365+ z_avg : numpy.ndarray
366+ Average spin-axis that has been normalized to have unit length expressed
367+ in HAE coordinates.
383368
384369 Returns
385370 -------
386371 rotation_matrix : np.ndarray
387372 Rotation matrix.
388373 """
389- # Converts the averaged quaternion (q_avg) into a rotation matrix
390- # and get inertial z axis.
391- # https://spiceypy.readthedocs.io/en/main/documentation.html#spiceypy.spiceypy.q2m
392- z_avg = spiceypy .q2m (list (q_avg ))[:, 2 ]
393- # y_avg is perpendicular to both z_avg and the standard Z-axis.
374+ # y_avg is perpendicular to both z_avg and the HAE Z-axis.
375+ # Since z_avg will never point anywhere near the HAE Z-axis, this
376+ # cross-product will always work to define the Pointing Y-axis
394377 y_avg = np .cross (z_avg , [0 , 0 , 1 ])
378+ y_avg /= np .linalg .norm (y_avg )
395379 # x_avg is perpendicular to y_avg and z_avg.
396380 x_avg = np .cross (y_avg , z_avg )
381+ x_avg /= np .linalg .norm (x_avg )
397382
398383 # Construct the rotation matrix from x_avg, y_avg, z_avg
399384 rotation_matrix = np .asarray ([x_avg , y_avg , z_avg ])
0 commit comments