diff --git a/ale/base/type_sensor.py b/ale/base/type_sensor.py index 6c70eec580b1bcb452a609440c52b98300d78946..87b4394d932f556b3164b72d23a993a2e45834bc 100644 --- a/ale/base/type_sensor.py +++ b/ale/base/type_sensor.py @@ -386,6 +386,22 @@ class Cahvor(): """ raise NotImplementedError + @property + def final_inst_frame(self): + """ + Defines the final frame before cahvor frame in the frame chain + """ + raise NotImplementedError + + @property + def sensor_position(self): + positions, velocities, times = super().sensor_position + positions += self.cahvor_camera_dict["C"] + if self._props.get("landed", False): + positions = np.array([[0, 0, 0]] * len(times)) + velocities = np.array([[0, 0, 0]] * len(times)) + return positions, velocities, times + def compute_h_c(self): """ Computes the h_c element of a cahvor model for the conversion @@ -451,7 +467,10 @@ class Cahvor(): v_s = self.compute_v_s() 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 - self._cahvor_rotation_matrix = np.array([H_prime, -V_prime, -self.cahvor_camera_dict['A']]) + if self._props.get("landed", False): + 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 @property @@ -466,13 +485,17 @@ class Cahvor(): A networkx frame chain object """ if not hasattr(self, '_frame_chain'): - self._frame_chain = FrameChain.from_spice(sensor_frame=self.spacecraft_id * 1000, + 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) cahvor_quats = Rotation.from_matrix(self.cahvor_rotation_matrix).as_quat() - cahvor_rotation = ConstantRotation(cahvor_quats, self.spacecraft_id * 1000, self.sensor_frame_id) + # 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) + else: + cahvor_rotation = ConstantRotation(cahvor_quats, self.final_inst_frame, self.sensor_frame_id) self._frame_chain.add_edge(rotation = cahvor_rotation) return self._frame_chain @@ -487,7 +510,9 @@ class Cahvor(): : float The detector center line/boresight center line """ - return self.compute_v_c() + # Add here 0.5 for consistency with the CSM convention that the + # upper-left image pixel is at (0.5, 0.5). + return self.compute_v_c() + 0.5 @property def detector_center_sample(self): @@ -500,7 +525,9 @@ class Cahvor(): : float The detector center sample/boresight center sample """ - return self.compute_h_c() + # Add here 0.5 for consistency with the CSM convention that the + # upper-left image pixel is at (0.5, 0.5). + return self.compute_h_c() + 0.5 @property def pixel_size(self): diff --git a/ale/drivers/msl_drivers.py b/ale/drivers/msl_drivers.py index 7af3d1ef3546a90c8c28ab4b25e2bd0a726784e0..b06677b8a800ad7e7738ea12d50751119f80e0fb 100644 --- a/ale/drivers/msl_drivers.py +++ b/ale/drivers/msl_drivers.py @@ -69,6 +69,18 @@ class MslMastcamPds3NaifSpiceDriver(Cahvor, Framer, Pds3Label, NaifSpice, Cahvor self._cahvor_camera_params['R'] = np.array(camera_model_group["MODEL_COMPONENT_6"]) return self._cahvor_camera_params + @property + def final_inst_frame(self): + """ + Defines MSLs last naif frame before the cahvor model frame + + Returns + ------- + : int + Naif frame code for MSL_RSM_HEAD + """ + return spice.bods2c("MSL_RSM_HEAD") + @property def sensor_frame_id(self): """ @@ -96,7 +108,7 @@ class MslMastcamPds3NaifSpiceDriver(Cahvor, Framer, Pds3Label, NaifSpice, Cahvor : list<double> focal plane to detector lines """ - return [0, 1/self.pixel_size, 0] + return [0, 0, 1/self.pixel_size] @property def focal2pixel_samples(self): @@ -108,23 +120,7 @@ class MslMastcamPds3NaifSpiceDriver(Cahvor, Framer, Pds3Label, NaifSpice, Cahvor : list<double> focal plane to detector samples """ - return [0, 0, 1/self.pixel_size] - - @property - def detector_center_line(self): - return self.label["INSTRUMENT_STATE_PARMS"]["DETECTOR_LINES"]/2 - - @property - def detector_center_sample(self): - return self.label["INSTRUMENT_STATE_PARMS"]["MSL:DETECTOR_SAMPLES"]/2 - - @property - def detector_start_line(self): - return self.label["IMAGE_REQUEST_PARMS"]["FIRST_LINE"] - - @property - def detector_start_sample(self): - return self.label["IMAGE_REQUEST_PARMS"]["FIRST_LINE_SAMPLE"] + return [0, -1/self.pixel_size, 0] @property def sensor_model_version(self): diff --git a/ale/formatters/formatter.py b/ale/formatters/formatter.py index 6de1913700912c9f400346627e455c495b05abed..73a9d644435eb621eca0c11f83cc97ec61df8aac 100644 --- a/ale/formatters/formatter.py +++ b/ale/formatters/formatter.py @@ -118,7 +118,7 @@ def to_isd(driver): sensor_frame = driver.sensor_frame_id instrument_pointing = {} - source_frame, destination_frame, time_dependent_sensor_frame = frame_chain.last_time_dependent_frame_between(1, sensor_frame) + source_frame, destination_frame, _ = frame_chain.last_time_dependent_frame_between(1, sensor_frame) # Reverse the frame order because ISIS orders frames as # (destination, intermediate, ..., intermediate, source) diff --git a/ale/isd_generate.py b/ale/isd_generate.py index a5a43812a8b10192e16165585eba737ee5d40341..103d18f7f9882668400207a20aebc29fdf2119a3 100755 --- a/ale/isd_generate.py +++ b/ale/isd_generate.py @@ -67,6 +67,12 @@ def main(): action="store_true", help="Only use drivers that generate fresh spice data" ) + parser.add_argument( + "-l", "--local", + action="store_true", + help="Generate local spice data, or image that is unaware of itself relative to " + "target body. This is largely used for landed/rover data." + ) parser.add_argument( '--version', action='version', @@ -97,7 +103,7 @@ def main(): if len(args.input) == 1: try: - file_to_isd(args.input[0], args.out, kernels=k, log_level=log_level, only_isis_spice=args.only_isis_spice, only_naif_spice=args.only_naif_spice) + file_to_isd(args.input[0], args.out, kernels=k, log_level=log_level, only_isis_spice=args.only_isis_spice, only_naif_spice=args.only_naif_spice, local=args.local) except Exception as err: # Seriously, this just throws a generic Exception? sys.exit(f"File {args.input[0]}: {err}") @@ -107,7 +113,11 @@ def main(): ) as executor: futures = { executor.submit( - file_to_isd, f, **{"kernels": k, "log_level": log_level, "only_isis_spice": args.only_isis_spice, "only_naif_spice": args.only_naif_spice} + file_to_isd, f, **{"kernels": k, + "log_level": log_level, + "only_isis_spice": args.only_isis_spice, + "only_naif_spice": args.only_naif_spice, + "local": args.local} ): f for f in args.input } for f in concurrent.futures.as_completed(futures): @@ -127,7 +137,8 @@ def file_to_isd( kernels: list = None, log_level=logging.WARNING, only_isis_spice=False, - only_naif_spice=False + only_naif_spice=False, + local=False ): """ Returns nothing, but acts as a thin wrapper to take the *file* and generate @@ -152,6 +163,10 @@ def file_to_isd( logger.info(f"Reading: {file}") props = {} + + if local: + props['landed'] = local + if kernels is not None: kernels = [str(PurePath(p)) for p in kernels] props["kernels"] = kernels diff --git a/tests/pytests/test_cahvor_mixin.py b/tests/pytests/test_cahvor_mixin.py index 26eee41f9aad1ec1838e47686f1e9f7133cb077e..b3ad22f4b41c2181c3328af7fcb791008cb8fa69 100644 --- a/tests/pytests/test_cahvor_mixin.py +++ b/tests/pytests/test_cahvor_mixin.py @@ -27,6 +27,7 @@ class test_cahvor_sensor(unittest.TestCase): self.driver.target_frame_id = 10014 self.driver.center_ephemeris_time = 0 self.driver.ephemeris_time = [0] + self.driver._props = {} @patch("ale.base.type_sensor.Cahvor.cahvor_camera_dict", new_callable=PropertyMock, return_value=cahvor_camera_dict()) def test_compute_functions(self, cahvor_camera_dict): @@ -39,26 +40,28 @@ class test_cahvor_sensor(unittest.TestCase): def test_cahvor_model_elements(self, cahvor_camera_dict): cahvor_matrix = self.driver.cahvor_rotation_matrix np.testing.assert_allclose(cahvor_matrix, [[-0.82067034, -0.57129702, 0.01095033], - [-0.43920248, 0.6184257, -0.65165238], - [ 0.3655151, -0.5396012, -0.7584387 ]]) + [0.43920248, -0.6184257, 0.65165238], + [ -0.3655151, 0.5396012, 0.7584387 ]]) @patch('ale.transformation.FrameChain.from_spice', return_value=ale.transformation.FrameChain()) @patch("ale.base.type_sensor.Cahvor.cahvor_camera_dict", new_callable=PropertyMock, return_value=cahvor_camera_dict()) - def test_cahvor_frame_chain(self, cahvor_camera_dict, from_spice): + @patch("ale.base.type_sensor.Cahvor.final_inst_frame", new_callable=PropertyMock, return_value=-76000) + def test_cahvor_frame_chain(self, final_inst_frame, cahvor_camera_dict, from_spice): frame_chain = self.driver.frame_chain assert len(frame_chain.nodes()) == 2 assert -76000 in frame_chain.nodes() assert -76562 in frame_chain.nodes() from_spice.assert_called_with(center_ephemeris_time=0, ephemeris_times=[0], sensor_frame=-76000, target_frame=10014, nadir=False, exact_ck_times=False) - np.testing.assert_allclose(frame_chain[-76562][-76000]['rotation'].quat, [-0.28255205, 0.8940826, -0.33309383, 0.09914206]) + print(frame_chain[-76562][-76000]['rotation'].quat[3]) + np.testing.assert_allclose(frame_chain[-76562][-76000]['rotation'].quat, [-0.09914206260782343, 0.3330938313054434, 0.8940825953723198, -0.28255205470925904]) @patch("ale.base.type_sensor.Cahvor.cahvor_camera_dict", new_callable=PropertyMock, return_value=cahvor_camera_dict()) def test_cahvor_detector_center_line(self, cahvor_camera_dict): - np.testing.assert_almost_equal(self.driver.detector_center_line, 590.1933422831007) + np.testing.assert_almost_equal(self.driver.detector_center_line, 590.6933422831007) @patch("ale.base.type_sensor.Cahvor.cahvor_camera_dict", new_callable=PropertyMock, return_value=cahvor_camera_dict()) def test_cahvor_detector_center_sample(self, cahvor_camera_dict): - np.testing.assert_almost_equal(self.driver.detector_center_sample, 673.4306859859296) + np.testing.assert_almost_equal(self.driver.detector_center_sample, 673.9306859859296) @patch("ale.base.type_sensor.Cahvor.cahvor_camera_dict", new_callable=PropertyMock, return_value=cahvor_camera_dict()) def test_cahvor_pixel_size(self, cahvor_camera_dict): diff --git a/tests/pytests/test_msl_drivers.py b/tests/pytests/test_msl_drivers.py index 986dbd2094b72e0f348d2a6c987b9069d319585b..84fb73be88a97c7054234b7609cc0390cec52150 100644 --- a/tests/pytests/test_msl_drivers.py +++ b/tests/pytests/test_msl_drivers.py @@ -20,6 +20,11 @@ class test_mastcam_pds_naif(unittest.TestCase): def test_exposure_duration(self): np.testing.assert_almost_equal(self.driver.exposure_duration, 0.0102) + def test_final_inst_frame(self): + with patch('ale.drivers.msl_drivers.spice.bods2c', new_callable=PropertyMock, return_value=-76562) as bods2c: + assert self.driver.final_inst_frame == -76562 + bods2c.assert_called_with("MSL_RSM_HEAD") + def test_cahvor_camera_dict(self): cahvor_camera_dict = self.driver.cahvor_camera_dict assert len(cahvor_camera_dict) == 4 @@ -36,13 +41,13 @@ class test_mastcam_pds_naif(unittest.TestCase): def test_focal2pixel_lines(self): with patch('ale.drivers.msl_drivers.spice.bods2c', new_callable=PropertyMock, return_value=-76220) as bods2c, \ patch('ale.drivers.msl_drivers.spice.gdpool', new_callable=PropertyMock, return_value=[100]) as gdpool: - np.testing.assert_allclose(self.driver.focal2pixel_lines, [0, 137.96844341513602, 0]) + np.testing.assert_allclose(self.driver.focal2pixel_lines, [0, 0, 137.96844341513602]) bods2c.assert_called_with('MSL_MASTCAM_RIGHT') gdpool.assert_called_with('INS-76220_FOCAL_LENGTH', 0, 1) def test_focal2pixel_samples(self): with patch('ale.drivers.msl_drivers.spice.bods2c', new_callable=PropertyMock, return_value=-76220) as bods2c, \ patch('ale.drivers.msl_drivers.spice.gdpool', new_callable=PropertyMock, return_value=[100]) as gdpool: - np.testing.assert_allclose(self.driver.focal2pixel_samples, [0, 0, 137.96844341513602]) + np.testing.assert_allclose(self.driver.focal2pixel_samples, [0, -137.96844341513602, 0]) bods2c.assert_called_with('MSL_MASTCAM_RIGHT') gdpool.assert_called_with('INS-76220_FOCAL_LENGTH', 0, 1)