Commit 36217e1d authored by AustinSanders's avatar AustinSanders Committed by Jesse Mapel
Browse files

LRO WAC drivers, pushframe model, and tests (#447)

* Added initial lro_wac driver

* Added lrolrocwacisislabelnaifspicedriver tests

* Updated LRO WAC drivers, added PushFrame model + formatter

* Changed interframe_delay unit to seconds

* changed sensor name to spacecraft name instead of inst id

* Added and updated lro wac tests

* Updated test to stop breaking failure on binary kernel conversion

* Address PR feedback

Added radial distortion mixin
Adjusted comments
parent 08d3c702
Loading
Loading
Loading
Loading
+25 −0
Original line number Diff line number Diff line
@@ -274,3 +274,28 @@ class IsisLabel():
            # if no units are available, assume the exposure duration is given in milliseconds
            line_exposure_duration = line_exposure_duration * 0.001
        return line_exposure_duration


    @property
    def interframe_delay(self):
        """
        The interframe delay in seconds

        Returns
        -------
        : float
          interframe delay in seconds
        """
        interframe_delay = self.label['IsisCube']['Instrument']['InterframeDelay']
        if isinstance(interframe_delay, pvl.collections.Quantity):
            units = interframe_delay.units
            if "ms" in units.lower():
                interframe_delay = interframe_delay.value * 0.001
            else:
                # if not milliseconds, the units are probably seconds
                interframe_delay = interframe_delay.value
        else:
            # if no units are available, assume the interframe delay is given in milliseconds
            interframe_delay = interframe_delay * 0.001

        return interframe_delay
+75 −0
Original line number Diff line number Diff line
@@ -72,6 +72,81 @@ class LineScanner():
        """
        return self.ephemeris_start_time + (self.image_lines * self.exposure_duration)


class PushFrame():

    @property
    def name_model(self):
        """
        Returns Key used to define the sensor type. Primarily
        used for generating camera models.

        Returns
        -------
        : str
          USGS Frame model
        """
        return "USGS_ASTRO_PUSH_FRAME_SENSOR_MODEL"


    @property
    def ephemeris_time(self):
        """
        Returns an array of times between the start/stop ephemeris times
        based on the number of lines in the image.
        Expects ephemeris start/stop times to be defined. These should be
        floating point numbers containing the start and stop times of the
        images.
        Expects image_lines to be defined. This should be an integer containing
        the number of lines in the image.

        Returns
        -------
        : ndarray
          ephemeris times split based on image lines
        """

        return np.arange(self.ephemeris_start_time + (.5 * self.exposure_duration), self.ephemeris_stop_time + self.interframe_delay, self.interframe_delay)


    @property
    def framelet_height(self):
        return 1


    @property
    def framelet_order_reversed(self):
        return False


    @property
    def framelets_flipped(self):
        return False


    @property
    def num_frames(self):
        return int(self.image_lines // self.framelet_height)


    @property
    def ephemeris_stop_time(self):
        """
        Returns the sum of the starting ephemeris time and the number of lines
        times the exposure duration. Expects ephemeris start time, exposure duration
        and image lines to be defined. These should be double precision numbers
        containing the ephemeris start, exposure duration and number of lines of
        the image.

        Returns
        -------
        : double
          Center ephemeris time for an image
        """
        return self.ephemeris_start_time + (self.interframe_delay) * (self.num_frames - 1) + self.exposure_duration



class Framer():
    """
    Mix-in for framing sensors.
+207 −6
Original line number Diff line number Diff line
@@ -5,16 +5,16 @@ import pvl
import spiceypy as spice
from glob import glob

from ale.util import get_metakernels
from ale.util import get_metakernels, query_kernel_pool
from ale.base import Driver
from ale.base.data_naif import NaifSpice
from ale.base.data_isis import IsisSpice
from ale.base.label_pds3 import Pds3Label
from ale.base.label_isis import IsisLabel
from ale.base.type_sensor import LineScanner, Radar
from ale.base.type_sensor import LineScanner, Radar, PushFrame
from ale.base.type_distortion import RadialDistortion


class LroLrocPds3LabelNaifSpiceDriver(LineScanner, NaifSpice, Pds3Label, Driver):
class LroLrocNacPds3LabelNaifSpiceDriver(LineScanner, NaifSpice, Pds3Label, Driver):
    """
    Driver for reading LROC NACL, NACR (not WAC, it is a push frame) labels. Requires a Spice mixin to
    acquire additional ephemeris and instrument data located exclusively in SPICE kernels, A PDS3 label,
@@ -278,7 +278,7 @@ class LroLrocPds3LabelNaifSpiceDriver(LineScanner, NaifSpice, Pds3Label, Driver)



class LroLrocIsisLabelNaifSpiceDriver(LineScanner, NaifSpice, IsisLabel, Driver):
class LroLrocNacIsisLabelNaifSpiceDriver(LineScanner, NaifSpice, IsisLabel, Driver):
    @property
    def instrument_id(self):
        """
@@ -513,7 +513,7 @@ class LroLrocIsisLabelNaifSpiceDriver(LineScanner, NaifSpice, IsisLabel, Driver)
        return rotated_velocity[0]


class LroLrocIsisLabelIsisSpiceDriver(LineScanner, IsisSpice, IsisLabel, Driver):
class LroLrocNacIsisLabelIsisSpiceDriver(LineScanner, IsisSpice, IsisLabel, Driver):
    @property
    def instrument_id(self):
        """
@@ -849,3 +849,204 @@ class LroMiniRfIsisLabelNaifSpiceDriver(Radar, NaifSpice, IsisLabel, Driver):
          Naif ID code for the sensor frame
        """
        return self.target_frame_id



class LroLrocWacIsisLabelIsisSpiceDriver(PushFrame, IsisLabel, IsisSpice, RadialDistortion, Driver):
    @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
        expect the same ID the Spice expects in bods2c calls.
        Expects instrument_id to be defined in the IsisLabel mixin. This should be
        a string of the form 'WAC-UV' or 'WAC-VIS'

        Returns
        -------
        : str
          instrument id
        """
        id_lookup = {
        "WAC-UV" : "LRO_LROCWAC_UV",
        "WAC-VIS" : "LRO_LROCWAC_VIS"
        }
        return id_lookup[super().instrument_id]


    @property
    def sensor_name(self):
        return self.label['IsisCube']['Instrument']['SpacecraftName']


    @property
    def sensor_model_version(self):
        """
        Returns ISIS instrument sensor model version number

        Returns
        -------
        : int
          ISIS sensor model version
        """
        return 2


    @property
    def odtk(self):
        """
        The coefficients for the distortion model

        Returns
        -------
        : list
          Radial distortion coefficients.
        """
        return [self.naif_keywords.get('INS{}_OD_K'.format(self.ikid), None)]


    @property
    def framelet_height(self):
        if self.instrument_id == "LRO_LROCWAC_UV":
            return 16
        elif self.instrument_id == "LRO_LROCWAC_VIS":
            return 14

class LroLrocWacIsisLabelNaifSpiceDriver(PushFrame, IsisLabel, NaifSpice, RadialDistortion, Driver):
    """
    Driver for Lunar Reconnaissance Orbiter WAC ISIS cube
    """
    @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
        expect the same ID the Spice expects in bods2c calls.
        Expects instrument_id to be defined in the IsisLabel mixin. This should be
        a string of the form 'WAC-UV' or 'WAC-VIS'

        Returns
        -------
        : str
          instrument id
        """
        id_lookup = {
            "WAC-UV" : "LRO_LROCWAC_UV",
            "WAC-VIS" : "LRO_LROCWAC_VIS"
        }
        return id_lookup[super().instrument_id]

    @property
    def sensor_model_version(self):
        return 2


    @property
    def ephemeris_start_time(self):
        """
        Returns the ephemeris start time of the image.
        Expects spacecraft_id to be defined. This should be the integer
        Naif ID code for the spacecraft.

        Returns
        -------
        : float
          ephemeris start time of the image
        """
        if not hasattr(self, '_ephemeris_start_time'):
            sclock = self.label['IsisCube']['Instrument']['SpacecraftClockStartCount']
            self._ephemeris_start_time = spice.scs2e(self.spacecraft_id, sclock)
        return self._ephemeris_start_time


    @property
    def detector_center_line(self):
        """
        The center of the CCD in detector pixels
        ISIS uses 0.5 based CCD lines, so we need to convert to 0 based.

        Returns
        -------
        float :
            The center line of the CCD
        """
        return super().detector_center_line - 0.5


    @property
    def detector_center_sample(self):
        """
        The center of the CCD in detector pixels
        ISIS uses 0.5 based CCD samples, so we need to convert to 0 based.

        Returns
        -------
        float :
            The center sample of the CCD
        """
        return super().detector_center_sample - 0.5


    @property
    def sensor_name(self):
        return self.label['IsisCube']['Instrument']['SpacecraftName']


    @property
    def odtk(self):
        """
        The coefficients for the distortion model

        Returns
        -------
        : list
          Radial distortion coefficients.
        """
        return spice.gdpool('INS{}_OD_K'.format(self.ikid), 0, 3).tolist()


    @property
    def naif_keywords(self):
        """
        Updated set of naif keywords containing the NaifIkCode for the specific
        WAC filter used when taking the image.

        Returns
        -------
        : dict
          Dictionary of keywords and values that ISIS creates and attaches to the label
        """
        _naifKeywords = {**super().naif_keywords,
                         **query_kernel_pool("*_FOCAL_LENGTH"),
                         **query_kernel_pool("*_BORESIGHT_SAMPLE"),
                         **query_kernel_pool("*_BORESIGHT_LINE"),
                         **query_kernel_pool("*_TRANS*"),
                         **query_kernel_pool("*_ITRANS*"),
                         **query_kernel_pool("*_OD_K")}
        return _naifKeywords


    @property
    def framelets_flipped(self):
        return self.label['IsisCube']['Instrument']['SpacecraftName'] == "Yes"


    @property
    def sampling_factor(self):
        if self.instrument_id == "LRO_LROCWAC_UV":
            return 4
        elif self.instrument_id == "LRO_LROCWAC_VIS":
            return 1


    @property
    def num_frames(self):
        return self.image_lines // (self.framelet_height // self.sampling_factor)


    @property
    def framelet_height(self):
        if self.instrument_id == "LRO_LROCWAC_UV":
            return 16
        elif self.instrument_id == "LRO_LROCWAC_VIS":
            return 14
+13 −2
Original line number Diff line number Diff line
@@ -5,7 +5,7 @@ from scipy.interpolate import interp1d, BPoly
from networkx.algorithms.shortest_paths.generic import shortest_path

from ale.transformation import FrameChain
from ale.base.type_sensor import LineScanner, Framer, Radar
from ale.base.type_sensor import LineScanner, Framer, Radar, PushFrame
from ale.rotation import ConstantRotation, TimeDependentRotation

def to_isd(driver):
@@ -55,6 +55,17 @@ def to_isd(driver):
        meta_data['name_model'] = 'USGS_ASTRO_FRAME_SENSOR_MODEL'
        meta_data['center_ephemeris_time'] = driver.center_ephemeris_time

    if isinstance(driver, PushFrame):
        meta_data['name_model'] = 'USGS_ASTRO_PUSH_FRAME_SENSOR_MODEL'
        meta_data['starting_ephemeris_time'] = driver.ephemeris_start_time
        meta_data['ending_ephemeris_time'] = driver.ephemeris_stop_time
        meta_data['center_ephemeris_time'] = driver.center_ephemeris_time
        meta_data['exposure_duration'] = driver.exposure_duration
        meta_data['interframe_delay'] = driver.interframe_delay
        meta_data['framelet_order_reversed'] = driver.framelet_order_reversed
        meta_data['framelets_flipped'] = driver.framelets_flipped
        meta_data['framelet_height'] = driver.framelet_height

    # SAR sensor model specifics
    if isinstance(driver, Radar):
        meta_data['name_model'] = 'USGS_ASTRO_SAR_SENSOR_MODEL'
@@ -104,7 +115,7 @@ def to_isd(driver):
    body_rotation["reference_frame"] = destination_frame
    meta_data['body_rotation'] = body_rotation

    if isinstance(driver, LineScanner) or isinstance(driver, Framer):
    if isinstance(driver, LineScanner) or isinstance(driver, Framer) or isinstance(driver, PushFrame):
        # sensor orientation
        sensor_frame = driver.sensor_frame_id

+11 −8
Original line number Diff line number Diff line
@@ -191,6 +191,7 @@ def convert_kernels(kernels):
            # Get the full path to the kernel then truncate it to the relative path
            path = os.path.join(data_root, kernel)
            path = os.path.relpath(path)
            try:
                bin_output = subprocess.run(['tobin', path],
                                            capture_output=True, check=True)
                matches = re.search(r'To: (.*\.b\w*)', str(bin_output.stdout))
@@ -199,5 +200,7 @@ def convert_kernels(kernels):
                else:
                    kernel = matches.group(1)
                    binary_kernels.append(kernel)
            except:
                warnings.warn(f"Unable to convert {path} to binary kernel")
        updated_kernels.append(kernel)
    return updated_kernels, binary_kernels
Loading