From 269fdb6af7e2e3de8ddef58572aeb5977f3d6d82 Mon Sep 17 00:00:00 2001
From: acpaquette <acpaquette@usgs.gov>
Date: Fri, 29 Sep 2023 09:20:26 -0700
Subject: [PATCH] MSL Nadir Pointing (#564)

* Enabled nadir pointing in MSL/CAHVOR driver

* Removed commented out position changes

* Fix MSL test

* More msl fiddling

* Update for MSL Nadir pointing rotation in the cahvor mixin

* Fixed msl tests and added nadir test
---
 ale/base/type_sensor.py                    |  34 ++-
 ale/drivers/msl_drivers.py                 |  10 +-
 ale/isd_generate.py                        |  17 +-
 tests/pytests/data/isds/msl_nadir_isd.json | 295 +++++++++++++++++++++
 tests/pytests/test_msl_drivers.py          |  14 +-
 5 files changed, 359 insertions(+), 11 deletions(-)
 create mode 100644 tests/pytests/data/isds/msl_nadir_isd.json

diff --git a/ale/base/type_sensor.py b/ale/base/type_sensor.py
index 87b4394..613bb48 100644
--- a/ale/base/type_sensor.py
+++ b/ale/base/type_sensor.py
@@ -1,10 +1,11 @@
 import math
 
 import numpy as np
+import spiceypy as spice
 from scipy.spatial.transform import Rotation
 
 from ale.transformation import FrameChain
-from ale.transformation import ConstantRotation
+from ale.transformation import ConstantRotation, TimeDependentRotation
 
 class LineScanner():
     """
@@ -468,7 +469,7 @@ class Cahvor():
             H_prime = (self.cahvor_camera_dict['H'] - h_c * self.cahvor_camera_dict['A'])/h_s
             V_prime = (self.cahvor_camera_dict['V'] - v_c * self.cahvor_camera_dict['A'])/v_s
             if self._props.get("landed", False):
-              self._cahvor_rotation_matrix = np.array([H_prime, -V_prime, -self.cahvor_camera_dict['A']])
+              self._cahvor_rotation_matrix = np.array([-H_prime, -V_prime, self.cahvor_camera_dict['A']])
             else:
               self._cahvor_rotation_matrix = np.array([H_prime, V_prime, self.cahvor_camera_dict['A']])
         return self._cahvor_rotation_matrix
@@ -485,12 +486,39 @@ class Cahvor():
           A networkx frame chain object
         """
         if not hasattr(self, '_frame_chain'):
+            nadir = self._props.get("nadir", False)
             self._frame_chain = FrameChain.from_spice(sensor_frame=self.final_inst_frame,
                                                       target_frame=self.target_frame_id,
                                                       center_ephemeris_time=self.center_ephemeris_time,
                                                       ephemeris_times=self.ephemeris_time,
-                                                      nadir=False, exact_ck_times=False)
+                                                      nadir=nadir, exact_ck_times=False)
             cahvor_quats = Rotation.from_matrix(self.cahvor_rotation_matrix).as_quat()
+            
+            if nadir:
+                # Logic for nadir calculation was taken from ISIS3
+                #  SpiceRotation::setEphemerisTimeNadir
+                rotation = self._frame_chain.compute_rotation(self.target_frame_id, 1)
+                p_vec, v_vec, times = self.sensor_position
+                rotated_positions = rotation.apply_at(p_vec, times)
+                rotated_velocities = rotation.rotate_velocity_at(p_vec, v_vec, times)
+
+                p_vec = rotated_positions
+                v_vec = rotated_velocities
+
+                velocity_axis = 2
+                # Get the default line translation with no potential flipping
+                # from the driver
+                trans_x = np.array(self.focal2pixel_lines)
+
+                if (trans_x[0] < trans_x[1]):
+                    velocity_axis = 1
+
+                quats = [spice.m2q(spice.twovec(-p_vec[i], 3, v_vec[i], velocity_axis)) for i, time in enumerate(times)]
+                quats = np.array(quats)[:,[1,2,3,0]]
+
+                rotation = TimeDependentRotation(quats, times, 1, self.final_inst_frame)
+                self._frame_chain.add_edge(rotation)
+
             # If we are landed we only care about the final cahvor frame relative to the target
             if self._props.get("landed", False):
               cahvor_rotation = ConstantRotation(cahvor_quats, self.target_frame_id, self.sensor_frame_id)
diff --git a/ale/drivers/msl_drivers.py b/ale/drivers/msl_drivers.py
index b06677b..f61402f 100644
--- a/ale/drivers/msl_drivers.py
+++ b/ale/drivers/msl_drivers.py
@@ -108,7 +108,10 @@ class MslMastcamPds3NaifSpiceDriver(Cahvor, Framer, Pds3Label, NaifSpice, Cahvor
         : list<double>
           focal plane to detector lines
         """
-        return [0, 0, 1/self.pixel_size]
+        if self._props.get("landed", False):
+            return [0, 0, -1/self.pixel_size]
+        else:
+            return [0, 0, 1/self.pixel_size]
     
     @property
     def focal2pixel_samples(self):
@@ -120,7 +123,10 @@ class MslMastcamPds3NaifSpiceDriver(Cahvor, Framer, Pds3Label, NaifSpice, Cahvor
         : list<double>
           focal plane to detector samples
         """
-        return [0, -1/self.pixel_size, 0]
+        if (self._props.get("nadir", False)):
+            return [0, 1/self.pixel_size, 0]
+        else:
+            return [0, -1/self.pixel_size, 0]
 
     @property
     def sensor_model_version(self):
diff --git a/ale/isd_generate.py b/ale/isd_generate.py
index 103d18f..6b9695b 100755
--- a/ale/isd_generate.py
+++ b/ale/isd_generate.py
@@ -70,9 +70,15 @@ def main():
     parser.add_argument(
         "-l", "--local",
         action="store_true",
-        help="Generate local spice data, or image that is unaware of itself relative to "
+        help="Generate local spice data, an isd that is unaware of itself relative to "
              "target body. This is largely used for landed/rover data."
     )
+    parser.add_argument(
+        "-N", "--nadir",
+        action="store_true",
+        help="Generate nadir spice pointing, an isd that has pointing directly towards "
+             "the center of the target body."
+    )
     parser.add_argument(
         '--version',
         action='version',
@@ -117,7 +123,8 @@ def main():
                                        "log_level": log_level, 
                                        "only_isis_spice": args.only_isis_spice, 
                                        "only_naif_spice": args.only_naif_spice,
-                                       "local": args.local}
+                                       "local": args.local,
+                                       "nadir": args.nadir}
                 ): f for f in args.input
             }
             for f in concurrent.futures.as_completed(futures):
@@ -138,7 +145,8 @@ def file_to_isd(
     log_level=logging.WARNING,
     only_isis_spice=False,
     only_naif_spice=False,
-    local=False
+    local=False,
+    nadir=False
 ):
     """
     Returns nothing, but acts as a thin wrapper to take the *file* and generate
@@ -167,6 +175,9 @@ def file_to_isd(
     if local:
         props['landed'] = local
 
+    if nadir:
+        props['nadir'] = nadir
+
     if kernels is not None:
         kernels = [str(PurePath(p)) for p in kernels]
         props["kernels"] = kernels
diff --git a/tests/pytests/data/isds/msl_nadir_isd.json b/tests/pytests/data/isds/msl_nadir_isd.json
new file mode 100644
index 0000000..c131a90
--- /dev/null
+++ b/tests/pytests/data/isds/msl_nadir_isd.json
@@ -0,0 +1,295 @@
+{
+  "isis_camera_version": 1,
+  "image_lines": 1193,
+  "image_samples": 1338,
+  "name_platform": "MARS SCIENCE LABORATORY",
+  "name_sensor": "MAST CAMERA LEFT",
+  "reference_height": {
+    "maxheight": 1000,
+    "minheight": -1000,
+    "unit": "m"
+  },
+  "name_model": "USGS_ASTRO_FRAME_SENSOR_MODEL",
+  "center_ephemeris_time": 598494669.4412209,
+  "radii": {
+    "semimajor": 3396.19,
+    "semiminor": 3376.2,
+    "unit": "km"
+  },
+  "body_rotation": {
+    "time_dependent_frames": [
+      10014,
+      1
+    ],
+    "ck_table_start_time": 598494669.4412209,
+    "ck_table_end_time": 598494669.4412209,
+    "ck_table_original_size": 1,
+    "ephemeris_times": [
+      598494669.4412209
+    ],
+    "quaternions": [
+      [
+        -0.31921039676060065,
+        0.2937636389682398,
+        -0.12264933637077348,
+        0.8926168199781396
+      ]
+    ],
+    "angular_velocities": [
+      [
+        3.1623010827381965e-05,
+        -2.881378599775597e-05,
+        5.651578887273642e-05
+      ]
+    ],
+    "reference_frame": 1
+  },
+  "instrument_pointing": {
+    "time_dependent_frames": [
+      -76205,
+      1
+    ],
+    "ck_table_start_time": 598494669.4412209,
+    "ck_table_end_time": 598494669.4412209,
+    "ck_table_original_size": 1,
+    "ephemeris_times": [
+      598494669.4412209
+    ],
+    "quaternions": [
+      [
+        0.7419897630883615,
+        0.4156401046800741,
+        0.25055247114123014,
+        0.4625126528632889
+      ]
+    ],
+    "angular_velocities": null,
+    "reference_frame": 1,
+    "constant_frames": [
+      -76573,
+      -76205
+    ],
+    "constant_rotation": [
+      0.4165667270357225,
+      0.908831179877854,
+      -0.02231699819809352,
+      -0.7809014963237929,
+      0.37028293957648445,
+      0.503073948538243,
+      0.4654728939111354,
+      -0.19213649091316684,
+      0.8639551804888768
+    ]
+  },
+  "naif_keywords": {
+    "BODY499_RADII": [
+      3396.19,
+      3396.19,
+      3376.2
+    ],
+    "BODY_FRAME_CODE": 10014,
+    "BODY_CODE": 499,
+    "FRAME_-76210_NAME": "MSL_MASTCAM_LEFT",
+    "INS-76210_CAHVOR_H": [
+      712.373106,
+      4664.465028,
+      33.182389
+    ],
+    "TKFRAME_-76210_UNITS": "DEGREES",
+    "INS-76210_IFOV_VERTICAL": 0.01230196,
+    "INS-76210_CAHVOR_O": [
+      0.999627,
+      0.026908,
+      0.004759
+    ],
+    "INS-76210_CAHVOR_R": [
+      -0.000151,
+      -0.139189,
+      -1.250336
+    ],
+    "INS-76210_CAHVOR_V": [
+      570.612488,
+      -14.279011,
+      4648.733195
+    ],
+    "INS-76210_CAHVOR_FILE": "MSL_CAL_003_SN_3003_FILTER_0_FOCUS_02315-MCAML-FLIGHT.cahvor",
+    "INS-76210_DISTORTION_PIXEL": [
+      837.77915717,
+      592.14046615
+    ],
+    "TKFRAME_-76210_AXES": [
+      2.0,
+      1.0,
+      3.0
+    ],
+    "TKFRAME_-76210_SPEC": "ANGLES",
+    "INS-76210_FOCAL_LENGTH": 34.0,
+    "INS-76210_CAHVOR_QUAT": [
+      1e-05,
+      -0.00325,
+      -0.00104,
+      0.99999
+    ],
+    "INS-76210_CAHVOR_POS": [
+      0.80436,
+      0.55942,
+      -1.90608
+    ],
+    "INS-76210_FOV_BOUNDARY": [
+      0.17483767,
+      0.12730492,
+      0.97633255,
+      -0.0,
+      0.12834274,
+      0.99172987,
+      -0.17476887,
+      0.12719345,
+      0.9763594,
+      -0.17553059
+    ],
+    "INS-76210_PIXEL_LINES": 1200.0,
+    "INS-76210_CAHVOR_MODEL": " CAHVOR",
+    "INS-76210_PIXEL_SIZE": 0.0074,
+    "INS-76210_CAHVOR_HC": 829.187822,
+    "INS-76210_IFOV_NOMINAL": 0.01247026,
+    "INS-76210_CAHVOR_HS": 4645.242086,
+    "INS-76210_BORESIGHT_PIXEL": [
+      829.18782212,
+      601.33514402
+    ],
+    "FRAME_-76210_CLASS_ID": -76210.0,
+    "INS-76210_FOV_CENTER_PIXEL": [
+      823.5,
+      599.5
+    ],
+    "INS-76210_FOV_CLASS_SPEC": "CORNERS",
+    "INS-76210_BORESIGHT": [
+      -0.0,
+      -0.0,
+      1.0
+    ],
+    "INS-76210_CAHVOR_THETA": -1.5710039999999998,
+    "INS-76210_IFOV": 0.01228988,
+    "TKFRAME_-76210_RELATIVE": "MSL_RSM_HEAD",
+    "FRAME_-76210_CLASS": 4.0,
+    "INS-76210_CAHVOR_DIMS": [
+      1648.0,
+      1200.0
+    ],
+    "INS-76210_FOV_SHAPE": "POLYGON",
+    "INS-76210_PIXEL_SAMPLES": 1648.0,
+    "INS-76210_IFOV_HORIZONTAL": 0.0122778,
+    "INS-76210_CAHVOR_VC": 601.335144,
+    "INS-76210_CAHVOR_VS": 4644.882626,
+    "TKFRAME_-76210_ANGLES": [
+      -90.01,
+      1.484,
+      89.655
+    ],
+    "FRAME_-76210_CENTER": -76.0,
+    "INS-76210_CAHVOR_A": [
+      0.999664,
+      0.025047,
+      0.006727
+    ],
+    "INS-76210_CAHVOR_C": [
+      0.767151,
+      0.433709,
+      -1.971648
+    ],
+    "INS-76210_FOV_FRAME": "MSL_MASTCAM_LEFT",
+    "BODY499_POLE_DEC": [
+      52.8865,
+      -0.0609,
+      0.0
+    ],
+    "BODY499_POLE_RA": [
+      317.68143,
+      -0.1061,
+      0.0
+    ],
+    "BODY499_PM": [
+      176.63,
+      350.89198226,
+      0.0
+    ]
+  },
+  "detector_sample_summing": 1,
+  "detector_line_summing": 1,
+  "focal_length_model": {
+    "focal_length": 34.0
+  },
+  "detector_center": {
+    "line": 576.4026068104001,
+    "sample": 680.1442422028802
+  },
+  "focal2pixel_lines": [
+    0,
+    0,
+    136.49886775101945
+  ],
+  "focal2pixel_samples": [
+    0,
+    136.49886775101945,
+    0
+  ],
+  "optical_distortion": {
+    "cahvor": {
+      "coefficients": [
+        0,
+        0,
+        0,
+        0,
+        0
+      ]
+    }
+  },
+  "starting_detector_line": 0,
+  "starting_detector_sample": 0,
+  "instrument_position": {
+    "spk_table_start_time": 598494669.4412209,
+    "spk_table_end_time": 598494669.4412209,
+    "spk_table_original_size": 1,
+    "ephemeris_times": [
+      598494669.4412209
+    ],
+    "positions": [
+      [
+        -42.94908602840011,
+        -2878.1178384691016,
+        -1794.0007301446622
+      ]
+    ],
+    "velocities": [
+      [
+        0.2143509476698853,
+        0.054297300199827,
+        -0.09225670467004271
+      ]
+    ],
+    "reference_frame": 1
+  },
+  "sun_position": {
+    "spk_table_start_time": 598494669.4412209,
+    "spk_table_end_time": 598494669.4412209,
+    "spk_table_original_size": 1,
+    "ephemeris_times": [
+      598494669.4412209
+    ],
+    "positions": [
+      [
+        -178112289.0644448,
+        -111686023.67244285,
+        -46420243.18796099
+      ]
+    ],
+    "velocities": [
+      [
+        12.688344510456547,
+        -19.98128446099907,
+        -9.507348850576207
+      ]
+    ],
+    "reference_frame": 1
+  }
+}
\ No newline at end of file
diff --git a/tests/pytests/test_msl_drivers.py b/tests/pytests/test_msl_drivers.py
index 3590016..0b25ee1 100644
--- a/tests/pytests/test_msl_drivers.py
+++ b/tests/pytests/test_msl_drivers.py
@@ -5,7 +5,7 @@ import pytest
 import unittest
 
 import ale
-from conftest import get_image, get_image_label, get_isd, get_image_kernels, convert_kernels, compare_dicts
+from conftest import get_image_label, get_isd, get_image_kernels, convert_kernels, compare_dicts
 from ale.drivers.msl_drivers import MslMastcamPds3NaifSpiceDriver
 
 from conftest import get_image_label
@@ -20,11 +20,19 @@ def test_mastcam_kernels():
     for kern in binary_kernels:
         os.remove(kern)
 
-def test_msl_mastcam_load(test_mastcam_kernels):
+def test_msl_mastcam_load_local(test_mastcam_kernels):
     label_file = get_image_label('2264ML0121141200805116C00_DRCL', "pds3")
     compare_dict = get_isd("msl")
 
-    isd_str = ale.loads(label_file, props={'kernels': test_mastcam_kernels})
+    isd_str = ale.loads(label_file, props={'kernels': test_mastcam_kernels, 'local': True})
+    isd_obj = json.loads(isd_str)
+    assert compare_dicts(isd_obj, compare_dict) == []
+
+def test_msl_mastcam_load_nadir(test_mastcam_kernels):
+    label_file = get_image_label('2264ML0121141200805116C00_DRCL', "pds3")
+    compare_dict = get_isd("msl_nadir")
+
+    isd_str = ale.loads(label_file, props={'kernels': test_mastcam_kernels, 'nadir': True})
     isd_obj = json.loads(isd_str)
     assert compare_dicts(isd_obj, compare_dict) == []
 
-- 
GitLab