Skip to content
Snippets Groups Projects
Unverified Commit 3aff4592 authored by Jesse Mapel's avatar Jesse Mapel Committed by GitHub
Browse files

Adds CassiniIssNac/Wac IsisLabel NaifData Driver (#470)


* stuff

* Fixes cassini iss wac/nac drivers and adds tests for isis label naif spice

* Address warnings in tests

* Remove print statement

* Removed notebooks/notebook changes

* Added caching to focal length property in cassiniIss IsisLabel NaifSpice

* Addressed failing tests

Co-authored-by: default avataracpaquette <acp263@nau.edu>
parent 78253de6
No related branches found
No related tags found
No related merge requests found
...@@ -62,7 +62,7 @@ def parse_table(table_label, data): ...@@ -62,7 +62,7 @@ def parse_table(table_label, data):
'Real' : 'f'} 'Real' : 'f'}
# Parse the binary data # Parse the binary data
fields = table_label.getlist('Field') fields = table_label.getall('Field')
results = {field['Name']:[] for field in fields} results = {field['Name']:[] for field in fields}
offset = 0 offset = 0
for record in range(table_label['Records']): for record in range(table_label['Records']):
...@@ -200,7 +200,11 @@ class IsisSpice(): ...@@ -200,7 +200,11 @@ class IsisSpice():
Instrument pointing table Instrument pointing table
""" """
if not hasattr(self, "_inst_pointing_table"): if not hasattr(self, "_inst_pointing_table"):
for table in self.label.getlist('Table'): tables = []
if "Table" in self.label:
tables = self.label.getall('Table')
for table in tables:
if table['Name'] == 'InstrumentPointing': if table['Name'] == 'InstrumentPointing':
binary_data = read_table_data(table, self._file) binary_data = read_table_data(table, self._file)
self._inst_pointing_table = parse_table(table, binary_data) self._inst_pointing_table = parse_table(table, binary_data)
...@@ -220,7 +224,11 @@ class IsisSpice(): ...@@ -220,7 +224,11 @@ class IsisSpice():
Body orientation table Body orientation table
""" """
if not hasattr(self, "_body_orientation_table"): if not hasattr(self, "_body_orientation_table"):
for table in self.label.getlist('Table'): tables = []
if "Table" in self.label:
tables = self.label.getall('Table')
for table in tables:
if table['Name'] == 'BodyRotation': if table['Name'] == 'BodyRotation':
binary_data = read_table_data(table, self._file) binary_data = read_table_data(table, self._file)
self._body_orientation_table = parse_table(table, binary_data) self._body_orientation_table = parse_table(table, binary_data)
...@@ -240,7 +248,11 @@ class IsisSpice(): ...@@ -240,7 +248,11 @@ class IsisSpice():
Instrument position table Instrument position table
""" """
if not hasattr(self, "_inst_position_table"): if not hasattr(self, "_inst_position_table"):
for table in self.label.getlist('Table'): tables = []
if "Table" in self.label:
tables = self.label.getall('Table')
for table in tables:
if table['Name'] == 'InstrumentPosition': if table['Name'] == 'InstrumentPosition':
binary_data = read_table_data(table, self._file) binary_data = read_table_data(table, self._file)
self._inst_position_table = parse_table(table, binary_data) self._inst_position_table = parse_table(table, binary_data)
...@@ -260,7 +272,11 @@ class IsisSpice(): ...@@ -260,7 +272,11 @@ class IsisSpice():
Sun position table Sun position table
""" """
if not hasattr(self, "_sun_position_table"): if not hasattr(self, "_sun_position_table"):
for table in self.label.getlist('Table'): tables = []
if "Table" in self.label:
tables = self.label.getall('Table')
for table in tables:
if table['Name'] == 'SunPosition': if table['Name'] == 'SunPosition':
binary_data = read_table_data(table, self._file) binary_data = read_table_data(table, self._file)
self._sun_position_table = parse_table(table, binary_data) self._sun_position_table = parse_table(table, binary_data)
......
...@@ -110,6 +110,188 @@ wac_filter_to_focal_length = { ...@@ -110,6 +110,188 @@ wac_filter_to_focal_length = {
("T3","IRP90"):201.07 ("T3","IRP90"):201.07
} }
class CassiniIssIsisLabelNaifSpiceDriver(Framer, IsisLabel, NaifSpice, RadialDistortion, Driver):
@property
def instrument_id(self):
"""
Returns an instrument id for unquely identifying the instrument, but often
also used to be piped into Spice Kernels to acquire instrument kernel (IK) NAIF IDs.
Therefore they use the same NAIF ID asin bods2c calls. Expects instrument_id to be
defined from a mixin class. This should return a string containing either 'ISSNA' or
'ISSWA'
Returns
-------
: str
instrument id
"""
return id_lookup[super().instrument_id]
@property
def spacecraft_name(self):
"""
Spacecraft name used in various Spice calls to acquire
ephemeris data.
Returns
-------
: str
Name of the spacecraft
"""
return 'CASSINI'
@property
def sensor_name(self):
"""
Returns the name of the instrument
Returns
-------
: str
Name of the sensor
"""
return name_lookup[super().instrument_id]
@property
def ephemeris_start_time(self):
"""
Returns the start and stop ephemeris times for the image.
Returns
-------
: float
start time
"""
return spice.str2et(self.utc_start_time.strftime("%Y-%m-%d %H:%M:%S.%f"))[0]
@property
def center_ephemeris_time(self):
"""
Returns the starting ephemeris time as the ssi framers center is the
start.
Returns
-------
: double
Center ephemeris time for an image
"""
center_time = self.ephemeris_start_time + (self.exposure_duration / 2.0)
return center_time
@property
def odtk(self):
"""
The radial distortion coeffs are not defined in the ik kernels, instead
they are defined in the ISS Data User Guide (Knowles). Therefore, we
manually specify the codes here.
Expects instrument_id to be defined. This should be a string containing either
CASSINI_ISS_WAC or CASSINI_ISIS_NAC
Returns
-------
: list<float>
radial distortion coefficients
"""
if self.instrument_id == 'CASSINI_ISS_WAC':
# WAC
return [0, float('-6.2e-5'), 0]
elif self.instrument_id == 'CASSINI_ISS_NAC':
# NAC
return [0, float('-8e-6'), 0]
@property
def focal_length(self):
"""
NAC uses multiple filter pairs, each filter combination has a different focal length.
NAIF's Cassini kernels do not contain focal lengths for NAC filters and
so we aquired updated NAC filter data from ISIS's IAK kernel.
"""
# default focal defined by IAK kernel
if not hasattr(self, "_focal_length"):
try:
default_focal_len = super(CassiniIssPds3LabelNaifSpiceDriver, self).focal_length
except:
default_focal_len = float(spice.gdpool('INS{}_DEFAULT_FOCAL_LENGTH'.format(self.ikid), 0, 2)[0])
filters = tuple(self.label["IsisCube"]["BandBin"]['FilterName'].split("/"))
if self.instrument_id == "CASSINI_ISS_NAC":
self._focal_length = nac_filter_to_focal_length.get(filters, default_focal_len)
elif self.instrument_id == "CASSINI_ISS_WAC":
self._focal_length = wac_filter_to_focal_length.get(filters, default_focal_len)
return self._focal_length
@property
def _original_naif_sensor_frame_id(self):
"""
Original sensor frame ID as defined in Cassini's IK kernel. This
is the frame ID you want to default to for WAC. For NAC, this Frame ID
sits between J2000 and an extra 180 rotation since NAC was mounted
upside down.
Returns
-------
: int
sensor frame code from NAIF's IK kernel
"""
return self.ikid
@property
def sensor_frame_id(self):
"""
Overwrite sensor frame id to return fake frame ID for NAC representing a
mounting point with a 180 degree rotation. ID was taken from ISIS's IAK
kernel for Cassini. This is because NAC requires an extra rotation not
in NAIF's Cassini kernels. Wac does not require an extra rotation so
we simply return original sensor frame id for Wac.
Returns
-------
: int
NAIF's Wac sensor frame ID, or ALE's Nac sensor frame ID
"""
if self.instrument_id == "CASSINI_ISS_NAC":
return 14082360
elif self.instrument_id == "CASSINI_ISS_WAC":
return 14082361
@property
def frame_chain(self):
"""
Construct the initial frame chain using the original sensor_frame_id
obtained from the ikid. Then tack on the ISIS iak rotation.
Returns
-------
: Object
Custom Cassini ALE Frame Chain object for rotation computation and application
"""
if not hasattr(self, '_frame_chain'):
try:
# Call frinfo to check if the ISIS iak has been loaded with the
# additional reference frame. Otherwise, Fail and add it manually
_ = spice.frinfo(self.sensor_frame_id)
self._frame_chain = super().frame_chain
except spice.utils.exceptions.NotFoundError as e:
self._frame_chain = FrameChain.from_spice(sensor_frame=self._original_naif_sensor_frame_id,
target_frame=self.target_frame_id,
center_ephemeris_time=self.center_ephemeris_time,
ephemeris_times=self.ephemeris_time,)
rotation = ConstantRotation([[0, 0, 1, 0]], self.sensor_frame_id, self._original_naif_sensor_frame_id)
self._frame_chain.add_edge(rotation=rotation)
return self._frame_chain
@property
def sensor_model_version(self):
return 1
class CassiniIssPds3LabelNaifSpiceDriver(Framer, Pds3Label, NaifSpice, RadialDistortion, Driver): class CassiniIssPds3LabelNaifSpiceDriver(Framer, Pds3Label, NaifSpice, RadialDistortion, Driver):
""" """
Cassini mixin class for defining Spice calls. Cassini mixin class for defining Spice calls.
...@@ -374,3 +556,20 @@ class CassiniIssIsisLabelIsisSpiceDriver(Framer, IsisLabel, IsisSpice, NoDistort ...@@ -374,3 +556,20 @@ class CassiniIssIsisLabelIsisSpiceDriver(Framer, IsisLabel, IsisSpice, NoDistort
: float : float
""" """
return self.inst_position_table['SpkTableStartTime'] return self.inst_position_table['SpkTableStartTime']
@property
def focal_length(self):
"""
The focal length of the instrument
Expects naif_keywords to be defined. This should be a dict containing
Naif keyworkds from the label.
Expects ikid to be defined. This should be the integer Naif ID code
for the instrument.
Returns
-------
float :
The focal length in millimeters
"""
filters = self.label["IsisCube"]["BandBin"]['FilterName'].split("/")
return self.naif_keywords.get('INS{}_{}_{}_FOCAL_LENGTH'.format(self.ikid, filters[0], filters[1]), None)
...@@ -559,7 +559,7 @@ def get_isis_mission_translations(isis_data): ...@@ -559,7 +559,7 @@ def get_isis_mission_translations(isis_data):
""" """
mission_translation_file = read_pvl(os.path.join(isis_data, "base", "translations", "MissionName2DataDir.trn")) mission_translation_file = read_pvl(os.path.join(isis_data, "base", "translations", "MissionName2DataDir.trn"))
# For some reason this file takes the form [value, key] for mission name -> data dir # For some reason this file takes the form [value, key] for mission name -> data dir
lookup = [l[::-1] for l in mission_translation_file["MissionName"].getlist("Translation")] lookup = [l[::-1] for l in mission_translation_file["MissionName"].getall("Translation")]
return dict(lookup) return dict(lookup)
...@@ -693,15 +693,19 @@ def search_isis_db(dbobj, labelobj, isis_data): ...@@ -693,15 +693,19 @@ def search_isis_db(dbobj, labelobj, isis_data):
# Flag is set when kernels encapsulating the entire image time is found # Flag is set when kernels encapsulating the entire image time is found
full_match = False full_match = False
for selection in dbobj.getlist("Selection"): for selection in dbobj.getall("Selection"):
files = selection.getlist("File") files = selection.getall("File")
if not files:
raise Exception(f"No File found in {selection}")
# selection criteria # selection criteria
matches = selection.getlist("Match") matches = []
times = selection.getlist("Time") if "Match" in selection:
matches = selection.getall("Match")
if not files: times = []
raise Exception(f"No File found in {selection}") if "Time" in selection:
times = selection.getall("Time")
files = [path.join(*file) if isinstance(file, list) else file for file in files] files = [path.join(*file) if isinstance(file, list) else file for file in files]
......
...@@ -125,7 +125,7 @@ ...@@ -125,7 +125,7 @@
"detector_sample_summing": 1, "detector_sample_summing": 1,
"detector_line_summing": 1, "detector_line_summing": 1,
"focal_length_model": { "focal_length_model": {
"focal_length": null "focal_length": 2003.09
}, },
"detector_center": { "detector_center": {
"line": 512.5, "line": 512.5,
......
...@@ -10,7 +10,7 @@ from unittest.mock import PropertyMock, patch ...@@ -10,7 +10,7 @@ from unittest.mock import PropertyMock, patch
import json import json
from conftest import get_image_label, get_image_kernels, get_isd, convert_kernels, compare_dicts, get_table_data from conftest import get_image_label, get_image_kernels, get_isd, convert_kernels, compare_dicts, get_table_data
from ale.drivers.co_drivers import CassiniIssPds3LabelNaifSpiceDriver from ale.drivers.co_drivers import CassiniIssPds3LabelNaifSpiceDriver, CassiniIssIsisLabelNaifSpiceDriver
@pytest.fixture() @pytest.fixture()
def test_kernels(scope="module", autouse=True): def test_kernels(scope="module", autouse=True):
...@@ -27,7 +27,8 @@ def test_load_pds(test_kernels): ...@@ -27,7 +27,8 @@ def test_load_pds(test_kernels):
isd_str = ale.loads(label_file, props={'kernels': test_kernels}) isd_str = ale.loads(label_file, props={'kernels': test_kernels})
isd_obj = json.loads(isd_str) isd_obj = json.loads(isd_str)
print(json.dumps(isd_obj, indent=2)) print(json.dumps(isd_obj, indent=2))
assert compare_dicts(isd_obj, compare_dict) == [] x = compare_dicts(isd_obj, compare_dict)
assert x == []
def test_load_isis(): def test_load_isis():
label_file = get_image_label("N1702360370_1", label_type="isis3") label_file = get_image_label("N1702360370_1", label_type="isis3")
...@@ -40,7 +41,18 @@ def test_load_isis(): ...@@ -40,7 +41,18 @@ def test_load_isis():
isd_str = ale.loads(label_file) isd_str = ale.loads(label_file)
isd_obj = json.loads(isd_str) isd_obj = json.loads(isd_str)
print(json.dumps(isd_obj, indent=2)) print(json.dumps(isd_obj, indent=2))
assert compare_dicts(isd_obj, compare_dict) == [] x = compare_dicts(isd_obj, compare_dict)
assert x == []
def test_load_isis_naif(test_kernels):
label_file = get_image_label("N1702360370_1")
compare_dict = get_isd("cassiniiss")
isd_str = ale.loads(label_file, props={"kernels": test_kernels})
isd_obj = json.loads(isd_str)
print(json.dumps(isd_obj, indent=2))
x = compare_dicts(isd_obj, compare_dict)
assert x == []
# ========= Test cassini pds3label and naifspice driver ========= # ========= Test cassini pds3label and naifspice driver =========
class test_cassini_pds3_naif(unittest.TestCase): class test_cassini_pds3_naif(unittest.TestCase):
...@@ -130,3 +142,75 @@ class test_cassini_pds3_naif(unittest.TestCase): ...@@ -130,3 +142,75 @@ class test_cassini_pds3_naif(unittest.TestCase):
frame_chain = self.driver.frame_chain frame_chain = self.driver.frame_chain
assert len(frame_chain.nodes()) == 0 assert len(frame_chain.nodes()) == 0
from_spice.assert_called_with(center_ephemeris_time=2.4, ephemeris_times=[2.4], nadir=False, sensor_frame=14082360, target_frame=-800) from_spice.assert_called_with(center_ephemeris_time=2.4, ephemeris_times=[2.4], nadir=False, sensor_frame=14082360, target_frame=-800)
# ========= Test cassini isislabel and naifspice driver =========
class test_cassini_isis_naif(unittest.TestCase):
def setUp(self):
label = get_image_label("N1702360370_1", "isis3")
self.driver = CassiniIssIsisLabelNaifSpiceDriver(label)
def test_instrument_id(self):
assert self.driver.instrument_id == "CASSINI_ISS_NAC"
def test_spacecraft_name(self):
assert self.driver.spacecraft_name == "CASSINI"
def test_sensor_name(self):
assert self.driver.sensor_name == "Imaging Science Subsystem Narrow Angle Camera"
def test_ephemeris_start_time(self):
with patch('ale.drivers.co_drivers.spice.str2et', return_value=[12345]) as str2et:
assert self.driver.ephemeris_start_time == 12345
str2et.assert_called_with('2011-12-12 05:02:19.773000')
def test_center_ephemeris_time(self):
with patch('ale.drivers.co_drivers.spice.str2et', return_value=[12345]) as str2et:
print(self.driver.exposure_duration)
assert self.driver.center_ephemeris_time == 12347.3
str2et.assert_called_with('2011-12-12 05:02:19.773000')
def test_odtk(self):
assert self.driver.odtk == [0, -8e-06, 0]
def test_focal_length(self):
# This value isn't used for anything in the test, as it's only used for the
# default focal length calculation if the filter can't be found.
with patch('ale.drivers.co_drivers.spice.gdpool', return_value=[10.0]) as gdpool, \
patch('ale.base.data_naif.spice.bods2c', return_value=-12345) as bods2c:
assert self.driver.focal_length == 2003.09
def test_sensor_model_version(self):
assert self.driver.sensor_model_version == 1
def test_sensor_frame_id(self):
assert self.driver.sensor_frame_id == 14082360
@patch('ale.transformation.FrameChain.from_spice', return_value=ale.transformation.FrameChain())
def test_custom_frame_chain(self, from_spice):
with patch('ale.drivers.co_drivers.spice.bods2c', return_value=-12345) as bods2c, \
patch('ale.drivers.co_drivers.CassiniIssIsisLabelNaifSpiceDriver.target_frame_id', \
new_callable=PropertyMock) as target_frame_id, \
patch('ale.drivers.co_drivers.CassiniIssIsisLabelNaifSpiceDriver.ephemeris_start_time', \
new_callable=PropertyMock) as ephemeris_start_time:
ephemeris_start_time.return_value = .1
target_frame_id.return_value = -800
frame_chain = self.driver.frame_chain
assert len(frame_chain.nodes()) == 2
assert 14082360 in frame_chain.nodes()
assert -12345 in frame_chain.nodes()
from_spice.assert_called_with(center_ephemeris_time=2.4000000000000004, ephemeris_times=[2.4000000000000004], sensor_frame=-12345, target_frame=-800)
@patch('ale.transformation.FrameChain.from_spice', return_value=ale.transformation.FrameChain())
def test_custom_frame_chain_iak(self, from_spice):
with patch('ale.drivers.co_drivers.spice.bods2c', return_value=-12345) as bods2c, \
patch('ale.drivers.co_drivers.CassiniIssIsisLabelNaifSpiceDriver.target_frame_id', \
new_callable=PropertyMock) as target_frame_id, \
patch('ale.drivers.co_drivers.CassiniIssIsisLabelNaifSpiceDriver.ephemeris_start_time', \
new_callable=PropertyMock) as ephemeris_start_time, \
patch('ale.drivers.co_drivers.spice.frinfo', return_value=True) as frinfo:
ephemeris_start_time.return_value = .1
target_frame_id.return_value = -800
frame_chain = self.driver.frame_chain
assert len(frame_chain.nodes()) == 0
from_spice.assert_called_with(center_ephemeris_time=2.4000000000000004, ephemeris_times=[2.4000000000000004], nadir=False, sensor_frame=14082360, target_frame=-800, )
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment