diff --git a/ale/__init__.py b/ale/__init__.py index f2d46ca2575f049a9d174327036200f3c0d83b73..b649ee82414e20bfe6ebad469da86f654dae8f38 100644 --- a/ale/__init__.py +++ b/ale/__init__.py @@ -1,2 +1,2 @@ from . import drivers -from .drivers import load +from .drivers import load, loads diff --git a/ale/drivers/__init__.py b/ale/drivers/__init__.py index d5f94b12912d999f4424cf0bce68f301f138cad9..9b816352ce5ea5f71720c5159906d73329501834 100644 --- a/ale/drivers/__init__.py +++ b/ale/drivers/__init__.py @@ -13,12 +13,15 @@ from datetime import datetime, date from abc import ABC +import datetime + # dynamically load drivers __all__ = [os.path.splitext(os.path.basename(d))[0] for d in glob(os.path.join(os.path.dirname(__file__), '*_driver.py'))] __driver_modules__ = [importlib.import_module('.'+m, package='ale.drivers') for m in __all__] drivers = dict(chain.from_iterable(inspect.getmembers(dmod, lambda x: inspect.isclass(x) and "_driver" in x.__module__) for dmod in __driver_modules__)) + def load(label): """ Attempt to load a given label from all possible drivers @@ -29,14 +32,29 @@ def load(label): String path to the given label file """ for name, driver in drivers.items(): - try: - print("TRYING:", driver) + print("Trying:", name) res = driver(label) - if res.is_valid(): - with res as r: - return res - - except Exception as e: - import traceback - traceback.print_exc() + try: + if res.is_valid(): + with res as r: + return res.to_dict() + except Exception as e: + import traceback + print("Driver Failed:", e) + traceback.print_exc() raise Exception('No Such Driver for Label') + + +def loads(label): + class JsonEncoder(json.JSONEncoder): + def default(self, obj): + if isinstance(obj, np.ndarray): + return obj.tolist() + if isinstance(obj, np.int64): + return int(obj) + if isinstance(obj, datetime.datetime): + return obj.__str__() + return json.JSONEncoder.default(self, obj) + + res = load(label) + return json.dumps(res, cls=JsonEncoder) diff --git a/ale/drivers/base.py b/ale/drivers/base.py index 743e782c6531bf4da23af7d39be7925fa7f6c568..e7fbca6fd2b5fd151b3ef2d5b4386fc234729a57 100644 --- a/ale/drivers/base.py +++ b/ale/drivers/base.py @@ -1,7 +1,211 @@ -from dateutil import parser +import os +import re +import struct +from glob import glob + import numpy as np +from dateutil import parser + import pvl import spiceypy as spice +from ale import config + + +def read_table_data(table_label, cube): + """ + Helper function to read all of the binary table data + + Parameters + ---------- + table_label : PVLModule + The ISIS table label + cube : file + The ISIS cube file + + Returns + ------- + bytes : + The binary portion of the table data + """ + cubehandle = open(cube, "rb") + cubehandle.seek(table_label['StartByte']) + return cubehandle.read(table_label['Bytes']) + +def field_size(field_label): + """ + Helper function to determine the size of a binary + table field + + Parameters + ---------- + field_label : PVLModule + The field label + + Returns + ------- + int : + The size of the one entry in bytes + """ + data_sizes = { + 'Integer' : 4, + 'Double' : 8, + 'Real' : 4, + 'Text' : 1 + } + return data_sizes[field_label['Type']] * field_label['Size'] + +def field_format(field_label): + """ + Helper function to get the format string for a + single entry in a table field + + Parameters + ---------- + field_label : PVLModule + The field label + + Returns + ------- + str : + The format string for the entry binary + """ + data_formats = { + 'Integer' : 'i', + 'Double' : 'd', + 'Real' : 'f' + } + return data_formats[field_label['Type']] * field_label['Size'] + +def parse_field(field_label, data, encoding='latin_1'): + """ + Parses a binary table field entry and converts it into + an in memory data type + + Parameters + ---------- + field_label : PVLModule + The field label + + data : bytes + The binary data for the field entry + + Returns + ------- + Union[int, float, str, list] : + The table field entry converted to a native python + type + """ + if field_label['Type'] == 'Text': + field_data = data[:field_label['Size']].decode(encoding=encoding) + else: + data_format = field_format(field_label) + field_data = struct.unpack_from(data_format, data) + if len(field_data) == 1: + field_data = field_data[0] + return field_data + +def parse_table_data(table_label, data): + """ + Parses an ISIS table into a dict where the keys are the + field names and the values are lists of entries. + + Parameters + ---------- + table_label : PVLModule + The table label + + data : bytes + The binary data for the entire table + + Returns + ------- + dict : + The table as a dict + """ + fields = table_label.getlist('Field') + results = {field['Name']:[] for field in fields} + offset = 0 + for record in range(table_label['Records']): + for field in fields: + field_data = parse_field(field, data[offset:]) + results[field['Name']].append(field_data) + offset += field_size(field) + return results + +def parse_rotation_table(label, field_data): + """ + Parses ISIS rotation table data. + + Parameters + ---------- + table_label : PVLModule + The table label + + field_data : dict + The table data as a dict with field names + as keys and lists of entries as values + + Returns + ------- + dict : + The rotation data + """ + results = {} + if all (key in field_data for key in ('J2000Q0','J2000Q1','J2000Q2','J2000Q3')): + results['Rotations'] = [ [q0, q1, q2, q3] for q0, q1, q2, q3 in zip(field_data['J2000Q0'],field_data['J2000Q1'],field_data['J2000Q2'],field_data['J2000Q3']) ] + if all (key in field_data for key in ('AV1','AV2','AV3')): + results['AngularVelocities'] = np.array( [ [av1, av2, av3] for av1, av2, av3 in zip(field_data['AV1'],field_data['AV2'],field_data['AV3']) ] ) + if 'ET' in field_data: + results['Times'] = np.array(field_data['ET']) + if all (key in field_data for key in ('J2000Ang1','J2000Ang2','J2000Ang3')): + results['EulerCoefficients'] = np.array([field_data['J2000Ang1'],field_data['J2000Ang2'],field_data['J2000Ang3']]) + results['BaseTime'] = field_data['J2000Ang1'][-1] + results['TimeScale'] = field_data['J2000Ang2'][-1] + + if 'TimeDependentFrames' in label: + results['TimeDependentFrames'] = np.array(label['TimeDependentFrames']) + if all (key in label for key in ('ConstantRotation','ConstantFrames')): + const_rotation_mat = np.array(label['ConstantRotation']) + results['ConstantRotation'] = np.reshape(const_rotation_mat, (3, 3)) + results['ConstantFrames'] = np.array(label['ConstantFrames']) + if all (key in label for key in ('PoleRa','PoleDec','PrimeMeridian')): + results['BodyRotationCoefficients'] = np.array( [label['PoleRa'],label['PoleDec'],label['PrimeMeridian']] ) + if all (key in label for key in ('PoleRaNutPrec','PoleDecNutPrec','PmNutPrec','SysNutPrec0','SysNutPrec1')): + results['SatelliteNutationPrecessionCoefficients'] = np.array( [label['PoleRaNutPrec'],label['PoleDecNutPrec'],label['PmNutPrec']] ) + results['PlanetNutationPrecessionAngleCoefficients'] = np.array( [label['SysNutPrec0'],label['SysNutPrec1']] ) + return results + +def parse_position_table(field_data): + """ + Parses ISIS position table data. + + Parameters + ---------- + table_label : PVLModule + The table label + + field_data : dict + The table data as a dict with field names as keys + and lists of entries as values + + Returns + ------- + dict : + The position data + """ + results = {} + if all (key in field_data for key in ('J2000X','J2000Y','J2000Z')): + results['Positions'] = np.array( [ [x, y, z] for x, y, z in zip(field_data['J2000X'],field_data['J2000Y'],field_data['J2000Z']) ] ) + if 'ET' in field_data: + results['Times'] = np.array(field_data['ET']) + if all (key in field_data for key in ('J2000XV','J2000YV','J2000ZV')): + results['Velocities'] = np.array( [ [x, y, z] for x, y, z in zip(field_data['J2000XV'],field_data['J2000YV'],field_data['J2000ZV']) ] ) + if all (key in field_data for key in ('J2000SVX','J2000SVY','J2000SVZ')): + results['PositionCoefficients'] = np.array( [field_data['J2000SVX'][:-1],field_data['J2000SVY'][:-1],field_data['J2000SVZ'][:-1]] ) + results['BaseTime'] = field_data['J2000SVX'][-1] + results['TimeScale'] = field_data['J2000SVY'][-1] + return results + class Driver(): """ @@ -12,13 +216,15 @@ class Driver(): _file : str Reference to file path to be used by mixins for opening. """ - def __init__(self, file): + def __init__(self, file, num_ephem=909, num_quats=909): """ Parameters ---------- file : str path to file to be parsed """ + self._num_quaternions = num_quats + self._num_ephem = num_ephem self._file = file def __str__(self): @@ -45,6 +251,7 @@ class Driver(): iid = self.instrument_id return True except Exception as e: + print(e) return False def to_dict(self): @@ -57,10 +264,111 @@ class Driver(): dict Dictionary of key, attribute pairs """ - keys = set(dir(self)) & self.required_keys - return {p:getattr(self, p) for p in keys} + keys = set() + return {p:getattr(self, p) for p in dir(self) if p[0] != "_" and isinstance(getattr(type(self), p), property)} + + + @property + def label(self): + if not hasattr(self, "_label"): + if isinstance(self._file, pvl.PVLModule): + self._label = self._file + try: + self._label = pvl.loads(self._file) + except Exception: + self._label = pvl.load(self._file) + except: + raise ValueError("{} is not a valid label".format(self._file)) + return self._label + + @property + def file(self): + return self._file + + @property + def interpolation_method(self): + return "lagrange" + + @property + def starting_detector_line(self): + return 1 + + @property + def starting_detector_sample(self): + return 1 + + @property + def detector_sample_summing(self): + return 1 + + @property + def detector_line_summing(self): + return 1 + + @property + def name_platform(self): + return "Generic Platform" + + @property + def name_sensor(self): + return "Generic Sensor" + + @property + def radii(self): + return { + "semimajor" : self._semimajor, + "semiminor" : self._semiminor, + "unit" : "m" # default to meters + } + + @property + def reference_height(self): + # TODO: This should be a reasonable # + return { + "minheight" : 0, + "maxheight": 1000, + "unit": "m" + } + + @property + def focal_length_model(self): + return { + "focal_length" : self._focal_length + } + + @property + def detector_center(self): + if not hasattr(self, '_detector_center'): + self._detector_center = { + "line" : self._detector_center_line, + "sample" : self._detector_center_sample + } + return self._detector_center + + @property + def sensor_position(self): + return { + "positions" : self._sensor_position, + "velocities" : self._sensor_velocity, + "unit" : "m" + } + + @property + def sensor_orientation(self): + return { + "quaternions" : self._sensor_orientation + } + + @property + def sun_position(self): + return { + "positions" : self._sun_position, + "velocities" : self._sun_velocity, + "unit" : "m" + } + -class LineScanner(Driver): +class LineScanner(): @property def name_model(self): """ @@ -88,7 +396,7 @@ class LineScanner(Driver): @property def dt_quaternion(self): - return (self.ending_ephemeris_time - self.starting_ephemeris_time) / self.number_of_ephemerides + return (self.ending_ephemeris_time - self.starting_ephemeris_time) / self.number_of_quaternions @property def line_scan_rate(self): @@ -102,16 +410,27 @@ class LineScanner(Driver): @property def number_of_ephemerides(self): - #TODO: Not make this hardcoded - return 909 + return self._num_ephem @property def number_of_quaternions(self): #TODO: Not make this hardcoded - return 909 + return self._num_quaternions + + @property + def ending_ephemeris_time(self): + return (self.image_lines * self.line_exposure_duration) + self.starting_ephemeris_time + + @property + def center_ephemeris_time(self): + return (self.starting_ephemeris_time + self.ending_ephemeris_time)/2 + +class Framer(): + @property + def name_sensor(self): + return "Generic Framer" -class Framer(Driver): @property def name_model(self): """ @@ -150,54 +469,10 @@ class PDS3(): Dict-like object with PVL keys """ - def _compute_ephemerides(self): - """ - Helper function to pull position and velocity in one pass - so that the results can then be cached in the associated - properties. - """ - eph = np.empty((self.number_of_ephemerides, 3)) - eph_rates = np.empty(eph.shape) - current_et = self.starting_ephemeris_time - for i in range(self.number_of_ephemerides): - state, _ = spice.spkezr(self.spacecraft_name, - current_et, - self.reference_frame, - 'NONE', - self.target_name,) # If this is the sensor, insufficient, if this is the spacecraft, it works? Huh? - eph[i] = state[:3] - eph_rates[i] = state[3:] - current_et += getattr(self, 'dt_ephemeris', 0) - # By default, spice works in km - eph *= 1000 - eph_rates *= 1000 - self._sensor_velocity = eph_rates - self._sensor_position = eph - - @property - def focal_plane_tempature(self): - return self.label['FOCAL_PLANE_TEMPERATURE'].value @property - def label(self): - """ - Loads a PVL from from the _file attribute. - - Returns - ------- - : PVLModule - Dict-like object with PVL keys - """ - if not hasattr(self, "_label"): - if isinstance(self._file, pvl.PVLModule): - self._label = label - try: - self._label = pvl.loads(self._file, strict=False) - except AttributeError: - self._label = pvl.load(self._file, strict=False) - except: - raise Exception("{} is not a valid label".format(label)) - return self._label + def _focal_plane_tempature(self): + return self.label['FOCAL_PLANE_TEMPERATURE'].value @property def line_exposure_duration(self): @@ -271,17 +546,12 @@ class PDS3(): return sc @property - def ending_ephemeris_time(self): - return (self.image_lines * self.line_exposure_duration) + self.starting_ephemeris_time - - @property - def center_ephemeris_time(self): - return (self.starting_ephemeris_time + self.ending_ephemeris_time)/2 - + def _detector_center_line(self): + return spice.gdpool('INS{}_CCD_CENTER'.format(self.ikid), 0, 2)[0] @property - def detector_center(self): - return list(spice.gdpool('INS{}_CCD_CENTER'.format(self.ikid), 0, 2)) + def _detector_center_sample(self): + return spice.gdpool('INS{}_CCD_CENTER'.format(self.ikid), 0, 2)[1] @property def spacecraft_name(self): @@ -296,18 +566,6 @@ class PDS3(): """ return self.label['MISSION_NAME'] - @property - def starting_detector_line(self): - return 1 - - @property - def starting_detector_sample(self): - return 1 - - @property - def detector_sample_summing(self): - return 1 - @property def detector_line_summing(self): return self.label.get('SAMPLING_FACTOR', 1) @@ -337,7 +595,7 @@ class Spice(): spice.unload(self.metakernel) @property - def odtx(self): + def _odtx(self): """ Returns ------- @@ -347,7 +605,7 @@ class Spice(): return spice.gdpool('INS{}_OD_T_X'.format(self.ikid),0, 10).tolist() @property - def odty(self): + def _odty(self): """ Returns ------- @@ -357,7 +615,7 @@ class Spice(): return spice.gdpool('INS{}_OD_T_Y'.format(self.ikid), 0, 10).tolist() @property - def odtk(self): + def _odtk(self): """ Returns ------- @@ -389,11 +647,11 @@ class Spice(): return list(spice.gdpool('INS{}_ITRANSS'.format(self.fikid), 0, 3)) @property - def focal_length(self): + def _focal_length(self): return float(spice.gdpool('INS{}_FOCAL_LENGTH'.format(self.ikid), 0, 1)[0]) @property - def semimajor(self): + def _semimajor(self): """ Returns ------- @@ -404,7 +662,7 @@ class Spice(): return rad[1][1] @property - def semiminor(self): + def _semiminor(self): """ Returns ------- @@ -419,7 +677,7 @@ class Spice(): return 'IAU_{}'.format(self.target_name) @property - def sun_position(self): + def _sun_position(self): sun_state, _ = spice.spkezr("SUN", self.center_ephemeris_time, self.reference_frame, @@ -429,7 +687,7 @@ class Spice(): return [sun_state[:4].tolist()] @property - def sun_velocity(self): + def _sun_velocity(self): sun_state, lt = spice.spkezr("SUN", self.center_ephemeris_time, self.reference_frame, @@ -439,21 +697,42 @@ class Spice(): return [sun_state[3:6].tolist()] @property - def sensor_position(self): - - if not hasattr(self, '_sensor_position'): - self._compute_ephemerides() - return self._sensor_position.tolist() + def _sensor_position(self): + if not hasattr(self, '_position'): + eph = [] + current_et = self.starting_ephemeris_time + for i in range(self.number_of_ephemerides): + state, _ = spice.spkezr(self.spacecraft_name, + current_et, + self.reference_frame, + 'NONE', + self.target_name,) + eph.append(state[:3]) + current_et += getattr(self, "dt_ephemeris", 0) + # By default, spice works in km + self._position = [e * 1000 for e in eph] + return self._position @property - def sensor_velocity(self): - if not hasattr(self, '_sensor_velocity'): - self._compute_ephemerides() - return self._sensor_velocity.tolist() + def _sensor_velocity(self): + if not hasattr(self, '_velocity'): + eph_rates = [] + current_et = self.starting_ephemeris_time + for i in range(self.number_of_ephemerides): + state, _ = spice.spkezr(self.spacecraft_name, + current_et, + self.reference_frame, + 'NONE', + self.target_name,) + eph_rates.append(state[3:]) + current_et += getattr(self, "dt_ephemeris", 0) + # By default, spice works in km + self._velocity = [e*1000 for e in eph_rates] + return self._velocity @property - def sensor_orientation(self): - if not hasattr(self, '_sensor_orientation'): + def _sensor_orientation(self): + if not hasattr(self, '_orientation'): current_et = self.starting_ephemeris_time qua = np.empty((self.number_of_ephemerides, 4)) for i in range(self.number_of_quaternions): @@ -465,21 +744,18 @@ class Spice(): qua[i,:3] = q[1:] qua[i,3] = q[0] current_et += getattr(self, 'dt_quaternion', 0) - self._sensor_orientation = qua - return self._sensor_orientation.tolist() + self._orientation = qua + print(len(self._orientation)) + return self._orientation.tolist() @property - def reference_height(self): - # TODO: This should be a reasonable # - return 0, 100 + def _detector_center_sample(self): + return float(spice.gdpool('INS{}_BORESIGHT_SAMPLE'.format(self.ikid), 0, 1)[0]) + @property - def detector_center(self): - if not hasattr(self, '_detector_center'): - center_line = float(spice.gdpool('INS{}_BORESIGHT_LINE'.format(self.ikid), 0, 1)[0]) - center_sample = float(spice.gdpool('INS{}_BORESIGHT_SAMPLE'.format(self.ikid), 0, 1)[0]) - self._detector_center = [center_line, center_sample] - return self._detector_center + def _detector_center_line(self): + return float(spice.gdpool('INS{}_BORESIGHT_LINE'.format(self.ikid), 0, 1)[0]) @property def center_ephemeris_time(self): @@ -505,18 +781,6 @@ class Spice(): class Isis3(): - @property - def label(self): - if not hasattr(self, "_label"): - if isinstance(self._file, pvl.PVLModule): - self._label = self._file - try: - self._label = pvl.loads(self._file, strict=False) - except AttributeError: - self._label = pvl.load(self._file, strict=False) - except: - raise ValueError("{} is not a valid label".format(self._file)) - return self._label @property def start_time(self): @@ -576,5 +840,447 @@ class Isis3(): def starting_ephemeris_time(self): if not hasattr(self, '_starting_ephemeris_time'): sclock = self.label['IsisCube']['Archive']['SpacecraftClockStartCount'] - self._starting_ephemeris_time = spice.scs2e(self.spacecraft_id, sclock) + self._starting_ephemeris_time = spice.scs2e(self.spacecraft_id, sclock).value return self._starting_ephemeris_time + + +class IsisSpice(Isis3): + """Mixin class for reading from an ISIS cube that has been spiceinit'd + + Attributes + ---------- + _label : PVLModule + Dict-like object with PVL keys + + _inst_pointing_table : dict + Dictionary that contains information about the + rotation from J2000 to the sensor reference frame. + All of the values for each property, such as angular + velocity, are stored in a list or numpy array where + each entry is the property at a different time. + + _body_orientation_table : dict + Dictionary that contains information about the + rotation from J2000 to the body fixed reference + frame. All of the values for each property, such + as angular velocity, are stored in a list or + numpy array where each entry is the property at a + different time. + + _inst_position_table : dict + Dictionary that contains information about the + location of the sensor relative to the center of the + target body. All of the values for each property, + such as velocity, are stored in a list or numpy + array where each entry is the property at a + different time. + + _sun_position_table : dict + Dictionary that contains information about the + location of the sun relative to the center of the + target body. All of the values for each property, + such as velocity, are stored in a list or numpy + array where each entry is the property at a + different time. + + """ + + def _init_tables(self): + # init tables + for table in self.label.getlist('Table'): + binary_data = read_table_data(table, self._file) + field_data = parse_table_data(table, binary_data) + if table['Name'] == 'InstrumentPointing': + self._inst_pointing_table = parse_rotation_table(table, field_data) + elif table['Name'] == 'BodyRotation': + self._body_orientation_table = parse_rotation_table(table, field_data) + elif table['Name'] == 'InstrumentPosition': + self._inst_position_table = parse_position_table(field_data) + elif table['Name'] == 'SunPosition': + self._sun_position_table = parse_position_table(field_data) + + @property + def label(self): + """ + Loads a PVL from from the _file attribute and + parses the binary table data. + + Returns + ------- + PVLModule : + Dict-like object with PVL keys + """ + if not hasattr(self, "_label"): + try: + self._label = pvl.load(self.file) + except: + raise ValueError("{} is not a valid label".format(self.file)) + return self._label + + def __enter__(self): + """ + Stub method to conform with how other driver mixins + are used. + """ + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + """ + Stub method to conform with how other driver mixins + are used. + """ + pass + + @property + def number_of_quaternions(self): + """ + The number of instrument rotation quaternions + + Returns + ------- + int : + The number of quaternions + """ + return len(self._sensor_orientation) + + @property + def number_of_ephemerides(self): + """ + The number of instrument position states. These may + be just positions or positions and vbelocities. + + Returns + ------- + int : + The number of states + """ + return len(self._sensor_position) + + @property + def _sclock_hex_string(self): + """ + The hex encoded image start time computed from the + spacecraft clock count + + Returns + ------- + str : + The hex string representation of the image + start time as a double + """ + for key in self.naif_keywords: + if re.match('CLOCK_ET_.*_COMPUTED', key[0]): + # If the hex string is only numbers and contains leading 0s, + # the PVL library strips them off (ie. 0000000000002040 becomes + # 2040). Pad to 16 in case this happens. + return str(key[1]).zfill(16) + raise ValueError("No computed spacecraft clock time found in NaifKeywords.") + + @property + def starting_ephemeris_time(self): + """ + The image start time in ephemeris time + + Returns + ------- + float : + The image start ephemeris time + """ + return struct.unpack('d', bytes.fromhex(self._sclock_hex_string))[0] + + @property + def _detector_center_sample(self): + """ + The center of the CCD in detector pixels + + Returns + ------- + list : + The center of the CCD formatted as line, sample + """ + return self.naif_keywords.get('INS{}_BORESIGHT_SAMPLE'.format(self.ikid), None) + + @property + def _detector_center_line(self): + """ + The center of the CCD in detector pixels + + Returns + ------- + list : + The center of the CCD formatted as line, sample + """ + return self.naif_keywords.get('INS{}_BORESIGHT_LINE'.format(self.ikid), None) + + @property + def _cube_label(self): + """ + The ISIS cube label portion of the file label + + Returns + ------- + PVLModule : + The ISIS cube label + + """ + if 'IsisCube' not in self.label: + raise ValueError("Could not find ISIS cube label.") + return self.label['IsisCube'] + + @property + def _kernels_group(self): + """ + The Kernels group from the ISIS cube label. + This is where the original SPICE kernels are listed. + + Returns + ------- + PVLModule : + The kernels group + """ + if 'Kernels' not in self._cube_label: + raise ValueError("Could not find Kernels group in ISIS cube label.") + return self._cube_label['Kernels'] + + @property + def ikid(self): + """ + The NAIF id for the instrument + + Returns + ------- + int : + The instrument id + """ + if 'NaifIkCode' not in self._kernels_group: + raise ValueError("Could not find Instrument NAIF ID in Kernels group.") + return self._kernels_group['NaifIkCode'] + + @property + def focal2pixel_lines(self): + """ + The line component of the affine transformation + from focal plane coordinates to centered ccd pixels + + Returns + ------- + list : + The coefficients of the affine transformation + formatted as constant, x, y + """ + return self.naif_keywords.get('INS{}_ITRANSL'.format(self.ikid), None) + + @property + def focal2pixel_samples(self): + """ + The sample component of the affine transformation + from focal plane coordinates to centered ccd pixels + + Returns + ------- + list : + The coefficients of the affine transformation + formatted as constant, x, y + """ + return self.naif_keywords.get('INS{}_ITRANSS'.format(self.ikid), None) + + @property + def _focal_length(self): + """ + The focal length of the instrument + + Returns + ------- + float : + The focal length in millimeters + """ + return self.naif_keywords.get('INS{}_FOCAL_LENGTH'.format(self.ikid), None) + + @property + def _body_radii(self): + """ + The triaxial radii of the target body + + Returns + ------- + list : + The body radii in kilometers. For most bodies, + this is formatted as semimajor, semimajor, + semiminor + """ + for key in self.naif_keywords: + if re.match('BODY-?\d*_RADII', key[0]): + return self.naif_keywords[key[0]] + + @property + def _semimajor(self): + """ + The radius of the target body at its widest + diameter + + Returns + ------- + float : + The radius in kilometers + """ + return self._body_radii[0] + + @property + def _semiminor(self): + """ + The radius of the target body perpendicular to its + widest diameter + + Returns + ------- + float : + The radius in kilometers + """ + return self._body_radii[2] + + @property + def _body_time_dependent_frames(self): + """ + List of time dependent reference frames between the + target body reference frame and the J2000 frame. + + Returns + ------- + list : + The list of frames starting with the body + reference frame and ending with the final time + dependent frame. + """ + if not hasattr(self, "_body_orientation_table"): + self._init_tables() + if 'TimeDependentFrames' not in self._body_orientation_table: + raise ValueError("Could not find body time dependent frames.") + return self._body_orientation_table['TimeDependentFrames'] + + @property + def reference_frame(self): + """ + The NAIF ID for the target body reference frame + + Returns + ------- + int : + The frame ID + """ + return self._body_time_dependent_frames[0] + + @property + def _sun_position(self): + """ + The sun position + + Returns + ------- + array : + The sun position vectors relative to the center + of the target body in the J2000 reference frame + as a 2d numpy array + """ + if not hasattr(self, "_sun_position_table"): + self._init_tables() + return self._sun_position_table.get('Positions', 'None') + + @property + def _sun_velocity(self): + """ + The sun velocity + + Returns + ------- + array : + The sun velocity vectors in the J2000 reference + frame as a 2d numpy array + """ + if not hasattr(self, "_sun_position_table"): + self._init_tables() + return self._sun_position_table.get('Velocities', None) + + @property + def _sensor_position(self): + """ + """ + if not hasattr(self, "_inst_position_table"): + self._init_tables() + return self._inst_position_table.get('Positions', None) + + @property + def _sensor_velocity(self): + """ + The sensor velocity + + Returns + ------- + array : + The sensor velocity vectors in the J2000 + reference frame as a 2d numpy array + """ + if not hasattr(self, "_inst_position_table"): + self._init_tables() + return self._inst_position_table.get('Velocities', None) + + @property + def _sensor_orientation(self): + """ + The rotation from J2000 to the sensor reference + frame + + Returns + ------- + array : + The sensor rotation quaternions as a numpy + quaternion array + """ + if not hasattr(self, "_inst_pointing_table"): + self._init_tables() + return self._inst_pointing_table.get('Rotations', None) + + @property + def body_orientation(self): + """ + The rotation from J2000 to the target body + reference frame + + Returns + ------- + array : + The body rotation quaternions as a numpy + quaternion array + """ + if not hasattr(self, "_body_orientation_table"): + self._init_tables() + return self._body_orientation_table.get('Rotations', None) + + @property + def naif_keywords(self): + """ + The NaifKeywords group from the file label that + contains stored values from the original SPICE + kernels + + Returns + ------- + PVLModule : + The stored NAIF keyword values + """ + if 'NaifKeywords' not in self.label: + raise ValueError("Could not find NaifKeywords in label.") + return self.label['NaifKeywords'] + + @property + def _odtk(self): + return self.label["NaifKeywords"]["INS{}_OD_K".format(self.ikid)] + + +class RadialDistortion(): + @property + def optical_distortion(self): + return { + "Radial": { + "coefficients" : self._odtk + } + } diff --git a/ale/drivers/cassini_driver.py b/ale/drivers/cassini_driver.py index c584bd197756f91b387ecb4d8e248f7007ab6c29..408beebacc6b6d2627b7ca9f4a4b7a6b4cc11538 100644 --- a/ale/drivers/cassini_driver.py +++ b/ale/drivers/cassini_driver.py @@ -6,21 +6,18 @@ import spiceypy as spice import numpy as np from ale import config -from ale.drivers.base import Framer -from ale.drivers import keys +from ale.drivers.base import Framer, RadialDistortion, Driver -class CassiniISS(Framer): +class CassiniISS(Driver, Framer, RadialDistortion): """ - Cassini mixin class for defining snowflake Spice calls. + Cassini mixin class for defining snowflake Spice calls. """ id_lookup = { "ISSNA" : "CASSINI_ISS_NAC", "ISSWA" : "CASSINI_ISS_WAC" } - required_keys = keys.base | keys.framer | keys.radial_distortion - @property def metakernel(self): """ diff --git a/ale/drivers/isis_spice_driver.py b/ale/drivers/isis_spice_driver.py deleted file mode 100644 index a8c5f2312977c97d3ae0f13c5fcb115cb6643f2b..0000000000000000000000000000000000000000 --- a/ale/drivers/isis_spice_driver.py +++ /dev/null @@ -1,627 +0,0 @@ -from glob import glob -import os -import struct -import re - -import pvl -import numpy as np -import quaternion - -from ale import config -from ale.drivers.base import Driver, Isis3 - -def read_table_data(table_label, cube): - """ - Helper function to read all of the binary table data - - Parameters - ---------- - table_label : PVLModule - The ISIS table label - cube : file - The ISIS cube file - - Returns - ------- - bytes : - The binary portion of the table data - """ - cube.seek(label['StartByte']-1) # This -1 is straight out of ISIS - return cube.read(label['Bytes']) - -def field_size(field_label): - """ - Helper function to determine the size of a binary - table field - - Parameters - ---------- - field_label : PVLModule - The field label - - Returns - ------- - int : - The size of the one entry in bytes - """ - data_sizes = { - 'Integer' : 4, - 'Double' : 8, - 'Real' : 4, - 'Text' : 1 - } - return data_sizes[field_label['Type']] * field_label['Size'] - -def field_format(field_label): - """ - Helper function to get the format string for a - single entry in a table field - - Parameters - ---------- - field_label : PVLModule - The field label - - Returns - ------- - str : - The format string for the entry binary - """ - data_formats = { - 'Integer' : 'i', - 'Double' : 'd', - 'Real' : 'f' - } - return data_formats[field_label['Type']] * field_label['Size'] - -def parse_field(field_label, data, encoding='latin_1'): - """ - Parses a binary table field entry and converts it into - an in memory data type - - Parameters - ---------- - field_label : PVLModule - The field label - - data : bytes - The binary data for the field entry - - Returns - ------- - Union[int, float, str, list] : - The table field entry converted to a native python - type - """ - if field_label['Type'] == 'Text': - field_data = data[:field_label['Size']].decode(encoding=encoding) - else: - data_format = field_format(field_label) - field_data = struct.unpack_from(data_format, data) - if len(field_data) == 1: - field_data = field_data[0] - return field_data - -def parse_table_data(table_label, data): - """ - Parses an ISIS table into a dict where the keys are the - field names and the values are lists of entries. - - Parameters - ---------- - table_label : PVLModule - The table label - - data : bytes - The binary data for the entire table - - Returns - ------- - dict : - The table as a dict - """ - fields = table_label.getlist('Field') - results = {field['Name']:[] for field in fields} - offset = 0 - for record in range(table_label['Records']): - for field in fields: - field_data = parse_field(field, data[offset:]) - results[field['Name']].append(field_data) - offset += field_size(field) - return results - -def parse_rotation_table(label, field_data): - """ - Parses ISIS rotation table data. - - Parameters - ---------- - table_label : PVLModule - The table label - - field_data : dict - The table data as a dict with field names - as keys and lists of entries as values - - Returns - ------- - dict : - The rotation data - """ - results = {} - if all (key in field_data for key in ('J2000Q0','J2000Q1','J2000Q2','J2000Q3')): - results['Rotations'] = quaternion.as_quat_array( [ [q0, q1, q2, q3] for q0, q1, q2, q3 in zip(field_data['J2000Q0'],field_data['J2000Q1'],field_data['J2000Q2'],field_data['J2000Q3']) ] ) - if all (key in field_data for key in ('AV1','AV2','AV3')): - results['AngularVelocities'] = np.array( [ [av1, av2, av3] for av1, av2, av3 in zip(field_data['AV1'],field_data['AV2'],field_data['AV3']) ] ) - if 'ET' in field_data: - results['Times'] = np.array(field_data['ET']) - if all (key in field_data for key in ('J2000Ang1','J2000Ang2','J2000Ang3')): - results['EulerCoefficients'] = np.array([field_data['J2000Ang1'],field_data['J2000Ang2'],field_data['J2000Ang3']]) - results['BaseTime'] = field_data['J2000Ang1'][-1] - results['TimeScale'] = field_data['J2000Ang2'][-1] - - if 'TimeDependentFrames' in label: - results['TimeDependentFrames'] = np.array(label['TimeDependentFrames']) - if all (key in label for key in ('ConstantRotation','ConstantFrames')): - const_rotation_mat = np.array(label['ConstantRotation']) - results['ConstantRotation'] = quaternion.from_rotation_matrix(np.reshape(const_rotation_mat, (3, 3))) - results['ConstantFrames'] = np.array(label['ConstantFrames']) - if all (key in label for key in ('PoleRa','PoleDec','PrimeMeridian')): - results['BodyRotationCoefficients'] = np.array( [label['PoleRa'],label['PoleDec'],label['PrimeMeridian']] ) - if all (key in label for key in ('PoleRaNutPrec','PoleDecNutPrec','PmNutPrec','SysNutPrec0','SysNutPrec1')): - results['SatelliteNutationPrecessionCoefficients'] = np.array( [label['PoleRaNutPrec'],label['PoleDecNutPrec'],label['PmNutPrec']] ) - results['PlanetNutationPrecessionAngleCoefficients'] = np.array( [label['SysNutPrec0'],label['SysNutPrec1']] ) - return results - -def parse_position_table(field_data): - """ - Parses ISIS position table data. - - Parameters - ---------- - table_label : PVLModule - The table label - - field_data : dict - The table data as a dict with field names as keys - and lists of entries as values - - Returns - ------- - dict : - The position data - """ - results = {} - if all (key in field_data for key in ('J2000X','J2000Y','J2000Z')): - results['Positions'] = np.array( [ [x, y, z] for x, y, z in zip(field_data['J2000X'],field_data['J2000Y'],field_data['J2000Z']) ] ) - if 'ET' in field_data: - results['Times'] = np.array(field_data['ET']) - if all (key in field_data for key in ('J2000XV','J2000YV','J2000ZV')): - results['Velocities'] = np.array( [ [x, y, z] for x, y, z in zip(field_data['J2000XV'],field_data['J2000YV'],field_data['J2000ZV']) ] ) - if all (key in field_data for key in ('J2000SVX','J2000SVY','J2000SVZ')): - results['PositionCoefficients'] = np.array( [field_data['J2000SVX'][:-1],field_data['J2000SVY'][:-1],field_data['J2000SVZ'][:-1]] ) - results['BaseTime'] = field_data['J2000SVX'][-1] - results['TimeScale'] = field_data['J2000SVY'][-1] - return results - -class IsisSpice(Isis3): - """Mixin class for reading from an ISIS cube that has been spiceinit'd - - Attributes - ---------- - _label : PVLModule - Dict-like object with PVL keys - - _inst_pointing_table : dict - Dictionary that contains information about the - rotation from J2000 to the sensor reference frame. - All of the values for each property, such as angular - velocity, are stored in a list or numpy array where - each entry is the property at a different time. - - _body_orientation_table : dict - Dictionary that contains information about the - rotation from J2000 to the body fixed reference - frame. All of the values for each property, such - as angular velocity, are stored in a list or - numpy array where each entry is the property at a - different time. - - _inst_position_table : dict - Dictionary that contains information about the - location of the sensor relative to the center of the - target body. All of the values for each property, - such as velocity, are stored in a list or numpy - array where each entry is the property at a - different time. - - _sun_position_table : dict - Dictionary that contains information about the - location of the sun relative to the center of the - target body. All of the values for each property, - such as velocity, are stored in a list or numpy - array where each entry is the property at a - different time. - - """ - - @property - def label(self): - """ - Loads a PVL from from the _file attribute and - parses the binary table data. - - Returns - ------- - PVLModule : - Dict-like object with PVL keys - """ - if not hasattr(self, "_label"): - try: - self._label = pvl.load(self._file) - except: - raise ValueError("{} is not a valid label".format(self._file)) - for table in self._label.getlist('Table'): - binary_data = read_table_data(table, self._file) - field_data = parse_table_data(table, binary_data) - if table['Name'] == 'InstrumentPointing': - self._inst_pointing_table = parse_rotation_table(table, field_data) - elif table['Name'] == 'BodyRotation': - self._body_orientation_table = parse_rotation_table(table, field_data) - elif table['Name'] == 'InstrumentPosition': - self._inst_position_table = parse_position_table(field_data) - elif table['Name'] == 'SunPosition': - self._sun_position_table = parse_position_table(field_data) - return self._label - - def __enter__(self): - """ - Stub method to conform with how other driver mixins - are used. - """ - return self - - def __exit__(self, exc_type, exc_val, exc_tb): - """ - Stub method to conform with how other driver mixins - are used. - """ - pass - - @property - def number_of_quaternions(self): - """ - The number of instrument rotation quaternions - - Returns - ------- - int : - The number of quaternions - """ - return len(self.sensor_orientation) - - @property - def number_of_ephemerides(self): - """ - The number of instrument position states. These may - be just positions or positions and vbelocities. - - Returns - ------- - int : - The number of states - """ - return len(self.sensor_position) - - @property - def _sclock_hex_string(self): - """ - The hex encoded image start time computed from the - spacecraft clock count - - Returns - ------- - str : - The hex string representation of the image - start time as a double - """ - for key in self.naif_keywords: - if re.match('CLOCK_ET_.*_COMPUTED', key[0]): - # If the hex string is only numbers and contains leading 0s, - # the PVL library strips them off (ie. 0000000000002040 becomes - # 2040). Pad to 16 in case this happens. - return str(key[1]).zfill(16) - raise ValueError("No computed spacecraft clock time found in NaifKeywords.") - - @property - def starting_ephemeris_time(self): - """ - The image start time in ephemeris time - - Returns - ------- - float : - The image start ephemeris time - """ - return struct.unpack('d', bytes.fromhex(self._sclock_hex_string))[0] - - @property - def detector_center(self): - """ - The center of the CCD in detector pixels - - Returns - ------- - list : - The center of the CCD formatted as line, sample - """ - return [ - self.naif_keywords.get('INS{}_BORESIGHT_LINE'.format(self.ikid), None), - self.naif_keywords.get('INS{}_BORESIGHT_SAMPLE'.format(self.ikid), None) - ] - - @property - def _cube_label(self): - """ - The ISIS cube label portion of the file label - - Returns - ------- - PVLModule : - The ISIS cube label - """ - if 'IsisCube' not in self.label: - raise ValueError("Could not find ISIS cube label.") - return self.label['IsisCube'] - - @property - def _kernels_group(self): - """ - The Kernels group from the ISIS cube label. - This is where the original SPICE kernels are listed. - - Returns - ------- - PVLModule : - The kernels group - """ - if 'Kernels' not in self._cube_label: - raise ValueError("Could not find Kernels group in ISIS cube label.") - return self._cube_label['Kernels'] - - @property - def ikid(self): - """ - The NAIF id for the instrument - - Returns - ------- - int : - The instrument id - """ - if 'NaifIkCode' not in self._kernels_group: - raise ValueError("Could not find Instrument NAIF ID in Kernels group.") - return self._kernels_group['NaifIkCode'] - - @property - def focal2pixel_lines(self): - """ - The line component of the affine transformation - from focal plane coordinates to centered ccd pixels - - Returns - ------- - list : - The coefficients of the affine transformation - formatted as constant, x, y - """ - return self.naif_keywords.get('INS{}_ITRANSL'.format(self.ikid), None) - - @property - def focal2pixel_samples(self): - """ - The sample component of the affine transformation - from focal plane coordinates to centered ccd pixels - - Returns - ------- - list : - The coefficients of the affine transformation - formatted as constant, x, y - """ - return self.naif_keywords.get('INS{}_ITRANSS'.format(self.ikid), None) - - @property - def focal_length(self): - """ - The focal length of the instrument - - Returns - ------- - float : - The focal length in millimeters - """ - return self.naif_keywords.get('INS{}_FOCAL_LENGTH'.format(self.ikid), None) - - @property - def body_radii(self): - """ - The triaxial radii of the target body - - Returns - ------- - list : - The body radii in kilometers. For most bodies, - this is formatted as semimajor, semimajor, - semiminor - """ - for key in self.naif_keywords: - if re.match('BODY-?\d*_RADII', key[0]): - return self.naif_keywords[key[0]] - - @property - def semimajor(self): - """ - The radius of the target body at its widest - diameter - - Returns - ------- - float : - The radius in kilometers - """ - return self.body_radii[0] - - @property - def semiminor(self): - """ - The radius of the target body perpendicular to its - widest diameter - - Returns - ------- - float : - The radius in kilometers - """ - return self.body_radii[2] - - @property - def _body_time_dependent_frames(self): - """ - List of time dependent reference frames between the - target body reference frame and the J2000 frame. - - Returns - ------- - list : - The list of frames starting with the body - reference frame and ending with the final time - dependent frame. - """ - if not hasattr(self, "_body_orientation_table"): - self.label - if 'TimeDependentFrames' not in self._body_orientation_table: - raise ValueError("Could not find body time dependent frames.") - return self._body_orientation_table['TimeDependentFrames'] - - @property - def reference_frame(self): - """ - The NAIF ID for the target body reference frame - - Returns - ------- - int : - The frame ID - """ - return self._body_time_dependent_frames[0] - - @property - def sun_position(self): - """ - The sun position - - Returns - ------- - array : - The sun position vectors relative to the center - of the target body in the J2000 reference frame - as a 2d numpy array - """ - if not hasattr(self, "_sun_position_table"): - self.label - return self._sun_position_table.get('Positions', 'None') - - @property - def sun_velocity(self): - """ - The sun velocity - - Returns - ------- - array : - The sun velocity vectors in the J2000 reference - frame as a 2d numpy array - """ - if not hasattr(self, "_sun_position_table"): - self.label - return self._sun_position_table.get('Velocities', None) - - @property - def sensor_position(self): - """ - The sensor position - - Returns - ------- - array : - The sensor position vectors relative to the - center of the target body in the J2000 - reference frame as a 2d numpy array - """ - if not hasattr(self, "_inst_position_table"): - self.label - return self._inst_position_table.get('Positions', None) - - @property - def sensor_velocity(self): - """ - The sensor velocity - - Returns - ------- - array : - The sensor velocity vectors in the J2000 - reference frame as a 2d numpy array - """ - if not hasattr(self, "_inst_position_table"): - self.label - return self._inst_position_table.get('Velocities', None) - - @property - def sensor_orientation(self): - """ - The rotation from J2000 to the sensor reference - frame - - Returns - ------- - array : - The sensor rotation quaternions as a numpy - quaternion array - """ - if not hasattr(self, "_inst_pointing_table"): - self.label - return self._inst_pointing_table.get('Rotations', None) - - @property - def body_orientation(self): - """ - The rotation from J2000 to the target body - reference frame - - Returns - ------- - array : - The body rotation quaternions as a numpy - quaternion array - """ - if not hasattr(self, "_body_orientation_table"): - self.label - return self._body_orientation_table.get('Rotations', None) - - @property - def naif_keywords(self): - """ - The NaifKeywords group from the file label that - contains stored values from the original SPICE - kernels - - Returns - ------- - PVLModule : - The stored NAIF keyword values - """ - if 'NaifKeywords' not in self.label: - raise ValueError("Could not find NaifKeywords in label.") - return self.label['NaifKeywords'] diff --git a/ale/drivers/keys.py b/ale/drivers/keys.py deleted file mode 100644 index 94cf987c7e744dcb3362fc59015155a94f8a0165..0000000000000000000000000000000000000000 --- a/ale/drivers/keys.py +++ /dev/null @@ -1,67 +0,0 @@ -base = { - 'name_model', - 'center_ephemeris_time', - 'detector_center', - 'detector_line_summing', - 'detector_sample_summing', - 'ending_ephemeris_time', - 'exposure_duration', - 'focal2pixel_lines', - 'focal2pixel_samples', - 'focal_epsilon', - 'focal_length', - 'ikid', - 'image_lines', - 'image_samples', - 'instrument_id', - 'interpolation_method', - 'reference_frame', - 'reference_height', - 'semimajor', - 'semiminor', - 'sensor_orientation', - 'sensor_position', - 'sensor_velocity', - 'spacecraft_clock_stop_count', - 'spacecraft_id', - 'spacecraft_name', - 'start_time', - 'starting_detector_line', - 'starting_detector_sample', - 'starting_ephemeris_time', - 'sun_position', - 'sun_velocity', - 'target_name', - 'number_of_ephemerides', - 'number_of_ephemerides' -} - -filter = { - 'fikid' -} - -transverse_distortion = { - 'odtk', - 'odtx' -} - -radial_distortion = { - 'odty' -} - -framer = { - 'filter_number', -} - -temp_dep_focal_legth = { - 'focal_plane_tempature' -} - -linescanner = { - 'line_exposure_duration', - 'line_scan_rate', - 't0_ephemeris', - 't0_quaternion', - 'dt_ephemeris', - 'dt_quaternion', -} diff --git a/ale/drivers/lro_driver.py b/ale/drivers/lro_driver.py index a97cab4507a1188c918ea3783ed8bd4851c73320..78c752dae6ce35d956f78b03723d36bca19adedf 100644 --- a/ale/drivers/lro_driver.py +++ b/ale/drivers/lro_driver.py @@ -6,15 +6,13 @@ import pvl import spiceypy as spice from ale.util import get_metakernels -from ale.drivers.base import LineScanner, Spice, PDS3, Isis3 -from ale.drivers import keys +from ale.drivers.base import LineScanner, Spice, PDS3, Isis3, Driver -class LrocSpice(Spice, LineScanner): +class LrocSpice(Driver, Spice, LineScanner): """ Lroc mixin class for defining snowflake Spice calls. """ - required_keys = keys.base | keys.linescanner @property def metakernel(self): diff --git a/ale/drivers/mdis_driver.py b/ale/drivers/mdis_driver.py index 1091f8a6e01e2b9e040109387dbd29127b69a10d..4453980a4306352184801deef3abe72a3e43a202 100644 --- a/ale/drivers/mdis_driver.py +++ b/ale/drivers/mdis_driver.py @@ -6,11 +6,10 @@ import spiceypy as spice import numpy as np from ale import config -from ale.drivers.base import Framer, Spice, PDS3, Isis3 -from ale.drivers import keys +from ale.drivers.base import Framer, Spice, PDS3, Isis3, Driver -class MdisSpice(Spice, Framer): +class MdisSpice(Driver, Spice, Framer): """ MDIS mixin class for defining snowflake Spice calls. Since MDIS has unique Spice keys, those are defined here as an intermediate mixin for MDIS drivers @@ -24,7 +23,6 @@ class MdisSpice(Spice, Framer): 'MERCURY DUAL IMAGING SYSTEM WIDE ANGLE CAMERA':'MSGR_MDIS_WAC' } - required_keys = keys.base | keys.framer | keys.filter | keys.transverse_distortion | keys.temp_dep_focal_legth @property def metakernel(self): @@ -45,7 +43,7 @@ class MdisSpice(Spice, Framer): return self._metakernel @property - def focal_length(self): + def _focal_length(self): """ Computes Focal Length from Kernels @@ -65,7 +63,7 @@ class MdisSpice(Spice, Framer): f_t = np.poly1d(coeffs[::-1]) # eval at the focal_plane_tempature - return f_t(self.focal_plane_tempature) + return f_t(self._focal_plane_tempature) @property def starting_detector_sample(self): diff --git a/ale/drivers/mro_driver.py b/ale/drivers/mro_driver.py index 2cf539b564460845b2db631472e99c521975286e..78d759ce6216af1d10f59e3d80c511e5fc3ec6f3 100644 --- a/ale/drivers/mro_driver.py +++ b/ale/drivers/mro_driver.py @@ -6,11 +6,38 @@ import pvl import spiceypy as spice from ale import config -from ale.drivers.base import LineScanner, Spice, PDS3, Isis3 -from ale.drivers import keys +from ale.drivers.base import LineScanner, Spice, PDS3, Isis3, IsisSpice, Driver, RadialDistortion +class CtxIsisSpice(Driver, IsisSpice, LineScanner, RadialDistortion): -class CtxSpice(Spice, LineScanner): + @property + def instrument_id(self): + """ + Returns an instrument id for uniquely identifying the instrument, but often + also used to be piped into Spice Kernels to acquire IKIDs. Therefore they + the same ID the Spice expects in bods2c calls. + + Returns + ------- + : str + instrument id + """ + return "N/A" + + @property + def spacecraft_id(self): + return "N/A" + + @property + def ikid(self): + return int(self.label["IsisCube"]["Kernels"]["NaifFrameCode"]) + + @property + def line_exposure_duration(self): + return self.label["IsisCube"]["Instrument"]["LineExposureDuration"].value * 0.001 # Scale to seconds + + +class CtxSpice(Driver, Spice, LineScanner, RadialDistortion): """ Spice mixins that defines MRO CTX specific snowflake Spice calls. """ @@ -18,8 +45,6 @@ class CtxSpice(Spice, LineScanner): 'CONTEXT CAMERA':'MRO_CTX' } - required_keys = keys.base | keys.linescanner | keys.radial_distortion - @property def metakernel(self): """ diff --git a/src/ale.cpp b/src/ale.cpp index 5cf73cd190d8562f841b7500e94614449be3e2c0..e63db09fe624f128a1c32bac7d854a8b0cd0723c 100644 --- a/src/ale.cpp +++ b/src/ale.cpp @@ -355,7 +355,7 @@ namespace ale { PyObject *pDict = PyModule_GetDict(pModule); // Get the add method from the dictionary. - PyObject *pFunc = PyDict_GetItemString(pDict, "load"); + PyObject *pFunc = PyDict_GetItemString(pDict, "loads"); if(!pFunc) { // import errors do not set a PyError flag, need to use a custom // error message instead. @@ -382,15 +382,13 @@ namespace ale { throw invalid_argument(getPyTraceback()); } - std::string cResult; - - // use PyObject_Str to ensure return is always a string PyObject *pResultStr = PyObject_Str(pResult); PyObject *temp_bytes = PyUnicode_AsUTF8String(pResultStr); // Owned reference + if(!temp_bytes){ throw invalid_argument(getPyTraceback()); } - + std::string cResult; char *temp_str = PyBytes_AS_STRING(temp_bytes); // Borrowed pointer cResult = temp_str; // copy into std::string diff --git a/tests/pytests/test_isis_spice_drivers.py b/tests/pytests/test_isis_spice_drivers.py index 551e0d6bed269a700b5d5ab12ef21ef58c38310b..481f508de8506c88caab5302e1980f165e8527ba 100644 --- a/tests/pytests/test_isis_spice_drivers.py +++ b/tests/pytests/test_isis_spice_drivers.py @@ -3,8 +3,7 @@ from collections import namedtuple import pytest import ale -from ale.drivers import isis_spice_driver -from ale.drivers.isis_spice_driver import IsisSpice +from ale.drivers import base from ale import util import pvl @@ -431,14 +430,14 @@ End count = table_label['Records'] * len(table_label.getlist('Field')) doubles = list(range(count)) return struct.pack('d' * count, *doubles) - monkeypatch.setattr(isis_spice_driver, 'read_table_data', test_table_data) + monkeypatch.setattr(base, 'read_table_data', test_table_data) def test_label(file): return pvl.loads(label) monkeypatch.setattr(pvl, 'load', test_label) - test_image = IsisSpice() - test_image._file = 'testfile.cub' + test_image = type('TestCubeDriver', (base.Driver, base.IsisSpice), {})(label) + # test_image._file = 'testfile.cub' return test_image def test_read(test_cube): @@ -454,7 +453,7 @@ def test_starting_ephemeris_time(test_cube): assert test_cube.starting_ephemeris_time == 8.0 def test_detector_center(test_cube): - assert test_cube.detector_center == [512.5, 512.5] + assert [test_cube._detector_center_line, test_cube._detector_center_sample] == [512.5, 512.5] def test_ikid(test_cube): assert test_cube.ikid == -236820 @@ -466,37 +465,37 @@ def test_focal2pixel_samples(test_cube): assert test_cube.focal2pixel_samples == [0.0, 71.42857143, 0.0] def test_focal_length(test_cube): - assert test_cube.focal_length == 549.11781953727 + assert test_cube._focal_length == 549.11781953727 def test_body_radii(test_cube): - assert test_cube.body_radii == [6051.8, 6051.8, 6051.8] + assert test_cube._body_radii == [6051.8, 6051.8, 6051.8] def test_semimajor(test_cube): - assert test_cube.semimajor == 6051.8 + assert test_cube._semimajor == 6051.8 def test_semiminor(test_cube): - assert test_cube.semiminor == 6051.8 + assert test_cube._semiminor == 6051.8 def test_reference_frame(test_cube): assert test_cube.reference_frame == 10012 def test_sun_position(test_cube): - assert np.array_equal(test_cube.sun_position, np.array([[0, 1, 2]])) + assert np.array_equal(test_cube._sun_position, np.array([[0, 1, 2]])) def test_sun_velocity(test_cube): - assert np.array_equal(test_cube.sun_velocity, np.array([[3, 4, 5]])) + assert np.array_equal(test_cube._sun_velocity, np.array([[3, 4, 5]])) def test_sensor_position(test_cube): - assert np.array_equal(test_cube.sensor_position, np.array([[0, 1, 2]])) + assert np.array_equal(test_cube._sensor_position, np.array([[0, 1, 2]])) def test_sensor_velocity(test_cube): - assert np.array_equal(test_cube.sensor_velocity, np.array([[3, 4, 5]])) + assert np.array_equal(test_cube._sensor_velocity, np.array([[3, 4, 5]])) def test_sensor_orientation(test_cube): - assert np.array_equal(test_cube.sensor_orientation, quaternion.as_quat_array([[0, 1, 2, 3]])) + assert np.array_equal(test_cube._sensor_orientation, np.asarray([[0, 1, 2, 3]])) def test_body_orientation(test_cube): - assert np.array_equal(test_cube.body_orientation, quaternion.as_quat_array([[0, 1, 2, 3]])) + assert np.array_equal(test_cube.body_orientation, np.asarray([[0, 1, 2, 3]])) def test_naif_keywords(test_cube): assert isinstance(test_cube.naif_keywords, pvl.PVLObject) diff --git a/tests/pytests/test_lro_drivers.py b/tests/pytests/test_lro_drivers.py index 56c81a783290db69d1d90f934a5ab1fe5d1afe85..4930672c8f51ffeeb43dedd252bde2ad03d5081d 100644 --- a/tests/pytests/test_lro_drivers.py +++ b/tests/pytests/test_lro_drivers.py @@ -115,4 +115,3 @@ def test_lro_creation(lro_lroclabel): with LrocPds3Driver(lro_lroclabel) as m: d = m.to_dict() assert isinstance(d, dict) - assert(set(d.keys()) == m.required_keys) diff --git a/tests/pytests/test_mdis_driver.py b/tests/pytests/test_mdis_driver.py index a290173041896bc4e9902652d102f8c5bb14ecc0..efcbd00a3425eeec72f5280a70e34fe900d14ad9 100644 --- a/tests/pytests/test_mdis_driver.py +++ b/tests/pytests/test_mdis_driver.py @@ -247,4 +247,3 @@ def test_mdis_creation(mdislabel): with MdisPDS3Driver(mdislabel) as m: d = m.to_dict() assert isinstance(d, dict) - assert(set(d.keys()) == m.required_keys) diff --git a/tests/pytests/test_mro_drivers.py b/tests/pytests/test_mro_drivers.py index a18b153593d260e6faa40471bea43b5c19892106..543d9cce77e0cd05517d4702d73969c381872068 100644 --- a/tests/pytests/test_mro_drivers.py +++ b/tests/pytests/test_mro_drivers.py @@ -66,4 +66,3 @@ def test_ctx_creation(mroctx_label): with CtxPds3Driver(mroctx_label) as m: d = m.to_dict() assert isinstance(d, dict) - assert(set(d.keys()) == m.required_keys)