From 55ee6e40121ffd904a79952adf7a0768b88ca3e6 Mon Sep 17 00:00:00 2001 From: Austin Sanders <arsanders@usgs.gov> Date: Wed, 11 Sep 2024 12:09:55 -0600 Subject: [PATCH] Added and updated docstrings (#610) * Added missing docstrings * Updated docstrings * Updated docstrings * Normalize docstring style * Normalize docstring style * Normalize docstring style --- ale/base/base.py | 135 +++++++++++++++++++++++++++++++--- ale/base/data_isis.py | 20 ++++- ale/base/data_naif.py | 9 +++ ale/base/label_isis.py | 15 +++- ale/base/label_pds3.py | 33 +++++++-- ale/base/type_distortion.py | 23 ++++++ ale/base/type_sensor.py | 33 +++++++++ ale/driver_verification.py | 141 ++++++++++++++++++++++++++++++++++-- ale/util.py | 113 ++++++++++++++++++++++++++++- 9 files changed, 498 insertions(+), 24 deletions(-) diff --git a/ale/base/base.py b/ale/base/base.py index 7bddcea..7c8a1ad 100644 --- a/ale/base/base.py +++ b/ale/base/base.py @@ -12,14 +12,18 @@ class Driver(): ---------- _file : str Reference to file path to be used by mixins for opening. + """ def __init__(self, file, num_ephem=909, num_quats=909, props={}, parsed_label=None): """ + Initialize a driver. + Parameters ---------- file : str path to file to be parsed + """ if not props: self._props = {} @@ -40,169 +44,220 @@ class Driver(): @property def image_lines(self): """ + Returns an integer containing the number of lines in the image. + Returns ------- : int Number of lines in image + """ raise NotImplementedError @property def image_samples(self): """ + Returns an integer containing the number of samples in an image. + Returns ------- : int Number of samples in image + """ raise NotImplementedError @property def usgscsm_distortion_model(self): """ + Returns distortion information for the USGSCSM sensor model. + Returns ------- : dict A dict containing the information about the distortion model for the usgscsm + """ raise NotImplementedError @property def detector_start_line(self): """ + Returns the zero-based detector line corresponding to the first image line. + Returns ------- : int Zero based Detector line corresponding to the first image line + """ return 0 @property def detector_start_sample(self): """ + Returns the zero-based detector line corresponding to the first image line. + Returns ------- : int Zero based Detector sample corresponding to the first image sample + """ return 0 @property def sample_summing(self): """ - Returns - ------- - : int - Sample summing + Returns the number of detector samples summed to produce each image sample. + + Returns + ------- + : int + Sample summing + """ raise NotImplementedError @property def line_summing(self): """ + Returns the number of detector samples summed to produce each image sample. + Returns ------- : int Line summing + """ raise NotImplementedError @property def platform_name(self): """ + Returns the name of the platform containing the sensor. This is usually the spacecraft name. + Returns ------- : str Name of the platform that the sensor is on + """ raise NotImplementedError @property def sensor_name(self): """ + Returns the name of the instrument. + Returns ------- : str Name of the sensor + """ raise NotImplementedError @property def target_body_radii(self): """ + The triaxial radii of the target body. + Returns ------- : list target body radii, first list element is semimajor axis, second is semiminor axis. + """ raise NotImplementedError @property def focal_length(self): """ + The focal length of the instrument. + Returns ------- : float focal length + """ raise NotImplementedError @property def detector_center_line(self): """ + The center line of the CCD in detector pixels. + Returns ------- : int The detector line of the principle point + """ raise NotImplementedError @property def detector_center_sample(self): """ + The center sample of the CCD in detector pixels. + Returns ------- : int The detector sample of the principle point + """ raise NotImplementedError @property def sensor_position(self): """ + Return the positions, velocities, and times for the sensor. + Returns ------- : (positions, velocities, times) a tuple containing a list of positions, a list of velocities, and a list of times + """ raise NotImplementedError @property def frame_chain(self): """ + Return the root node of the rotation frame tree/chain. + Returns ------- FrameNode The root node of the frame tree. This will always be the J2000 reference frame. + """ raise NotImplementedError @property def sun_position(self): """ + The sun position relative to the center of the target body in J2000 reference frame. + Returns ------- : (sun_positions, sun_velocities) a tuple containing a list of sun positions, a list of sun velocities + """ @property def target_name(self): """ - Returns + Return the target name. + + Returns ------- : int NAIF ID associated with the target body + """ raise NotImplementedError @@ -210,104 +265,134 @@ class Driver(): @property def target_frame_id(self): """ - Returns + The NAIF ID associated with the target body. + + Returns ------- : int NAIF ID associated with the target body + """ raise NotImplementedError @property def sensor_frame_id(self): """ - Returns + Returns the Naif ID code for the sensor reference frame. + + Returns ------- : int NAIF ID associated with the sensor frame + """ raise NotImplementedError @property def naif_keywords(self): """ - Returns + The NaifKeywords group from the file label that contains stored values from the original SPICE kernels. + + Returns ------- : dict dictionary containing the keys : values needed by Isis for the NaifKeywords group + """ raise NotImplementedError @property def sensor_model_version(self): """ - Returns + Return the version of the ISIS sensor model. + + Returns ------- : int version of the sensor model + """ raise NotImplementedError @property def focal2pixel_lines(self): """ + The line component of the affine transformation from focal plane coordinates to centered ccd pixels. + Returns ------- : list 3 element list containing affine transformation coefficient. The elements are as follows: constant, x coefficient, y coefficient + """ raise NotImplementedError @property def focal2pixel_samples(self): """ + The sample component of the affine transformation from focal plane coordinates to centered ccd pixels. + Returns ------- : list 3 element list containing affine transformation coefficients. The elements are as follows: constant, x coefficient, y coefficient + """ raise NotImplementedError @property def pixel2focal_x(self): """ + Convert from the detector to the focal plane x value. + Returns ------- : list 3 element list containing coefficients for the pixels to focal plane transformation. The elements are as follows: constant, sample, line + """ raise NotImplementedError @property def pixel2focal_y(self): """ + Convert from the detector to the focal plane y value. + Returns ------- : : list 3 element list containing coefficients for the pixels to focal plane transformation. The elements are as follows: constant, sample, line + """ raise NotImplementedError @property def ephemeris_start_time(self): """ - Returns + The image start time in ephemeris time. + + Returns ------- : double The start time of the image in ephemeris seconds past the J2000 epoch. + """ raise NotImplementedError @property def ephemeris_stop_time(self): """ - Returns + The image stop time in ephemeris time. + + Returns ------- : double The stop time of the image in ephemeris seconds past the J2000 epoch. + """ raise NotImplementedError @@ -320,15 +405,34 @@ class Driver(): ------- : double Center ephemeris time for an image + """ return (self.ephemeris_start_time + self.ephemeris_stop_time) / 2 @property def short_mission_name(self): + """ + Return short version of the mission name. + + Returns + ------- + str + Brief version of the mission name. + + """ return self.__module__.split('.')[-1].split('_')[0] @property def projection(self): + """ + Return projection information generated by osgeo. + + Returns + ------- + str + A string representation of the projection information. + + """ if not hasattr(self, "_projection"): try: from osgeo import gdal @@ -361,6 +465,15 @@ class Driver(): @property def geotransform(self): + """ + Return geotransform information generated by osgeo. + + Returns + ------- + tuple + Geotransform information + + """ if not hasattr(self, "_geotransform"): try: from osgeo import gdal diff --git a/ale/base/data_isis.py b/ale/base/data_isis.py index ec49ef7..b411ed6 100644 --- a/ale/base/data_isis.py +++ b/ale/base/data_isis.py @@ -148,7 +148,8 @@ def rotate_state(table, rotation): return rotated_pos, rotated_vel, ephemeris_times class IsisSpice(): - """Mixin class for reading from an ISIS cube that has been spiceinit'd + """ + Mixin class for reading from an ISIS cube that has been spiceinit'd Attributes ---------- @@ -602,6 +603,15 @@ class IsisSpice(): @property def sensor_frame_id(self): + """ + Returns the Naif ID code for the sensor reference frame. + + Returns + ------- + : int + NAIF ID associated with the sensor frame + + """ if 'ConstantFrames' in self.inst_pointing_table: return self.inst_pointing_table['ConstantFrames'][0] else: @@ -610,6 +620,14 @@ class IsisSpice(): @property def target_frame_id(self): + """ + The NAIF ID associated with the target body. + + Returns + ------- + : int + NAIF ID associated with the target body + """ if 'ConstantFrames' in self.body_orientation_table: return self.body_orientation_table['ConstantFrames'][0] else: diff --git a/ale/base/data_naif.py b/ale/base/data_naif.py index 0f73b14..df5c766 100644 --- a/ale/base/data_naif.py +++ b/ale/base/data_naif.py @@ -423,6 +423,15 @@ class NaifSpice(): @property def frame_chain(self): + """ + Return the root node of the rotation frame tree/chain. + + Returns + ------- + FrameNode + The root node of the frame tree. This will always be the J2000 reference frame. + """ + if not hasattr(self, '_frame_chain'): nadir = self._props.get('nadir', False) exact_ck_times = self._props.get('exact_ck_times', True) diff --git a/ale/base/label_isis.py b/ale/base/label_isis.py index 28d1642..7189874 100644 --- a/ale/base/label_isis.py +++ b/ale/base/label_isis.py @@ -7,6 +7,19 @@ class IsisLabel(): @property def label(self): + """ + Return the cube label. + + Returns + ------- + pvl.PVLModule + The cube label as a PVLModule object. + + Raises + ------ + ValueError + Raised when an invalid label is provided. + """ if not hasattr(self, "_label"): if isinstance(self._file, pvl.PVLModule): self._label = self._file @@ -137,7 +150,7 @@ class IsisLabel(): @property def line_summing(self): """ - the number of detector lines summed to produce each image sample + The number of detector lines summed to produce each image sample Returns ------- diff --git a/ale/base/label_pds3.py b/ale/base/label_pds3.py index c636e48..abe4cc4 100644 --- a/ale/base/label_pds3.py +++ b/ale/base/label_pds3.py @@ -7,6 +7,19 @@ class Pds3Label(): @property def label(self): + """ + Return the PDS3 label. + + Returns + ------- + pvl.PVLModule + The cube label as a PVLModule object. + + Raises + ------ + ValueError + Raised when an invalid label is provided. + """ if not hasattr(self, "_label"): if isinstance(self._file, pvl.PVLModule): self._label = self._file @@ -78,7 +91,7 @@ class Pds3Label(): """ Returns the full name of the instrument host - Returns + Returns ------- : str Full name of the instrument host @@ -104,7 +117,7 @@ class Pds3Label(): """ Returns the name of the spacecraft - Returns + Returns ------- : str Full name of the spacecraft @@ -117,7 +130,7 @@ class Pds3Label(): """ Returns the start time of the image as a UTC string - Returns + Returns ------- : str Start time of the image in UTC YYYY-MM-DDThh:mm:ss[.fff] @@ -130,7 +143,7 @@ class Pds3Label(): """ Returns the stop time of the image as a UTC string - Returns + Returns ------- : str Stop time of the image in UTC YYYY-MM-DDThh:mm:ss[.fff] @@ -141,7 +154,9 @@ class Pds3Label(): @property def image_lines(self): """ - Returns + Returns the number of lines in the image. + + Returns ------- : int Number of lines in the image @@ -152,6 +167,8 @@ class Pds3Label(): @property def image_samples(self): """ + Returns the number of samples in the image. + Returns ------- : int @@ -251,6 +268,9 @@ class Pds3Label(): @property def spacecraft_clock_start_count(self): """ + The spacecraft clock start count, frequently used to determine the start time + of the image. + Returns ------- : str @@ -262,6 +282,9 @@ class Pds3Label(): @property def spacecraft_clock_stop_count(self): """ + The spacecraft clock stop count, frequently used to determine the stop time + of the image. + Returns ------- : str diff --git a/ale/base/type_distortion.py b/ale/base/type_distortion.py index b4da718..15a1fc7 100644 --- a/ale/base/type_distortion.py +++ b/ale/base/type_distortion.py @@ -157,6 +157,15 @@ class CahvorDistortion(): class ChandrayaanMrffrDistortion(): @property def usgscsm_distortion_model(self): + """ + Returns the specification for ChandrayaanMrffr distortion in usgscsm. + + Returns + ------- + : dict + Dictionary containing the usgscsm specification for ChandrayaanMrffr distortion. + """ + transx = [-1* self.scaled_pixel_height, self.scaled_pixel_height, 0.0] transy = [0,0,0] transs = [1.0, 1.0 / self.scaled_pixel_height, 0.0] @@ -172,7 +181,14 @@ class ChandrayaanMrffrDistortion(): class LoDistortion(): @property def usgscsm_distortion_model(self): + """ + Returns the specification for Lo distortion in usgscsm. + Returns + ------- + : dict + Dictionary containing the usgscsm specification for Lo distortion. + """ # From ISIS LoHighDistortionMap::SetDistortion() # Get the perspective correction factors for x and y and the distortion # center (point of symmetry of distortion) @@ -197,7 +213,14 @@ class LoDistortion(): class ThemisIrDistortion(): @property def usgscsm_distortion_model(self): + """ + Returns the specification for ThemisIR distortion in usgscsm. + Returns + ------- + : dict + Dictionary containing the usgscsm specification for ThemisIR distortion. + """ return { "themisir":{ "p_alpha1" : 0.00447623, diff --git a/ale/base/type_sensor.py b/ale/base/type_sensor.py index 024a28f..ee57a14 100644 --- a/ale/base/type_sensor.py +++ b/ale/base/type_sensor.py @@ -120,21 +120,54 @@ class PushFrame(): @property def framelet_height(self): + """ + Return the number of lines in a framelet. + + Returns + ------- + int + The number of lines in each framelet. + """ return 1 @property def framelet_order_reversed(self): + """ + Return a boolean indicating if the framelets are reversed. + + Returns + ------- + Bool + A boolean indicating if the framelets are reversed. + """ return False @property def framelets_flipped(self): + """ + Return a boolean indicating if the framelets are flipped. + + Returns + ------- + Bool + A boolean indicating if the framelets are flipped. + """ + return False @property def num_frames(self): + """ + Return the number of frames in the image. + + Returns + ------- + int + The number of frames in the image. + """ return int(self.image_lines // self.framelet_height) @property diff --git a/ale/driver_verification.py b/ale/driver_verification.py index 57eaeb0..69f8cfa 100644 --- a/ale/driver_verification.py +++ b/ale/driver_verification.py @@ -23,8 +23,20 @@ class ReadIsis(IsisSpice, IsisLabel, Driver): def sensor_model_version(self): return 0 -# Define the function to run spiceinit with ISIS def run_spiceinit_isis(image_path): + """ + Run spiceinit on an image using ISIS. + + Parameters + ---------- + image_path : str + String path to the image on which spiceinit will be run + + Returns + ------- + None + + """ if ale_root is None: raise EnvironmentError("The environment variable 'ALEROOT' is not set.") # Move ALE drivers to a temporary subfolder @@ -45,12 +57,41 @@ def run_spiceinit_isis(image_path): shutil.move(str(driver), str(ale_drivers_path)) temp_folder.rmdir() -# Define the function to run spiceinit with ALE def run_spiceinit_ale(image_path): + """ + Run spiceinit on an image using ALE drivers. + + Parameters + ---------- + image_path : str + String path to the image on which spiceinit will be run + + Returns + ------- + None + + """ # Run spiceinit with ALE subprocess.run(['spiceinit', f'from={image_path}']) + def generate_body_rotation(driver, target_frame_id): + """ + Generate body rotation information from a driver. + + Parameters + ---------- + driver : :class:`ale.base.Driver` + The driver from which body rotation information will be generated. + target_frame_id : + The NAIF ID associated with the target body for which rotation information will be generated. + + Returns + ------- + dict + A dictionary containing body rotation information for the target body. + + """ frame_chain = driver.frame_chain target_frame = target_frame_id @@ -81,6 +122,21 @@ def generate_body_rotation(driver, target_frame_id): return body_rotation def generate_instrument_rotation(driver, sensor_frame_id): + """ + Generate instrument rotation information from a driver. + + Parameters + ---------- + driver : :class:`ale.base.Driver` + The driver from which to generate instrument rotation information. + sensor_frame_id : dict + The NAIF ID of the sensor frame for which to generate rotation information. + + Returns + ------- + dict + A dictionary containing instrument rotation information. + """ # sensor orientation frame_chain = driver.frame_chain sensor_frame = sensor_frame_id @@ -112,6 +168,19 @@ def generate_instrument_rotation(driver, sensor_frame_id): return instrument_pointing def generate_instrument_position(driver): + """ + Generate instrument position information from a driver. + + Parameters + ---------- + driver : :class:`ale.base.Driver` + The driver from which to generate instrument position information. + + Returns + ------- + dict + A dictionary containing instrument position information. + """ instrument_position = {} positions, velocities, times = driver.sensor_position instrument_position['spk_table_start_time'] = times[0] @@ -126,6 +195,19 @@ def generate_instrument_position(driver): return instrument_position def generate_sun_position(driver): + """ + Generate sun position information from a driver. + + Parameters + ---------- + driver : :class:`ale.base.Driver` + The driver from which to generate sun position information. + + Returns + ------- + dict + A dictionary containing sun position information. + """ sun_position = {} positions, velocities, times = driver.sun_position sun_position['spk_table_start_time'] = times[0] @@ -141,6 +223,23 @@ def generate_sun_position(driver): return sun_position def create_json_dump(driver, sensor_frame_id, target_frame_id): + """ + Convenience function for generating and merging instrument rotation, body rotation, instrument position, and sun position. + + Parameters + ---------- + driver : :class:`ale.base.Driver` + The driver from which to generate rotation and position information. + sensor_frame_id : dict + The NAIF ID of the sensor frame for which to generate rotation information. + target_frame_id : dict + The NAIF ID associated with the target body for which rotation information will be generated. + + Returns + ------- + dict + A dictionary containing instrument_rotation, body_rotation, instrument_position, and sun_position. + """ json_dump = {} json_dump["instrument_rotation"] = generate_instrument_rotation(driver, sensor_frame_id) json_dump["body_rotation"] = generate_body_rotation(driver, target_frame_id) @@ -149,14 +248,40 @@ def create_json_dump(driver, sensor_frame_id, target_frame_id): return json_dump def diff_and_describe(json1, json2, key_array): + """ + Compare two dictionaries and output differences. + + Parameters + ---------- + json1 : dict + The first dictionary for comparison. + json2 : dict + The second dictionary for comparison. + key_array : str + The key to be compared. + """ for key in key_array: json1 = json1[key] json2 = json2[key] diff = json1 - json2 print(" ".join(key_array) + "\nNum records:", len(diff), "\nMean:", np.mean(diff, axis=(0)), "\nMedian:", np.median(diff, axis=(0)), "\n") -# Define the function to compare ISDs def compare_isds(json1, json2): + """ + Compare two isds using :func:`driver_verification.diff_and_describe` + + Parameters + ---------- + json1 : dict + A dictionary containing a json-formatted ISD for comparison + json2 : dict + A dictionary containing a json-formatted ISD for comparison + + Returns + ------- + None + + """ diff_and_describe(json1, json2, ["instrument_position", "positions"]) diff_and_describe(json1, json2, ["instrument_position", "velocities"]) diff_and_describe(json1, json2, ["sun_position", "positions"]) @@ -166,9 +291,15 @@ def compare_isds(json1, json2): diff_and_describe(json1, json2, ["body_rotation", "quaternions"]) diff_and_describe(json1, json2, ["body_rotation", "angular_velocities"]) -# Define the main function def main(image): - + """ + Generate and compare an ALE ISD and an ISIS ISD. + + Parameters + ---------- + image : str + The name of the file for which to generate and compare ISDs. + """ # Duplicate the image for ALE and ISIS processing image_ale_path = Path(f"{image}_ALE.cub") image_isis_path = Path(f"{image}_ISIS.cub") diff --git a/ale/util.py b/ale/util.py index dd841c2..e3e3b65 100644 --- a/ale/util.py +++ b/ale/util.py @@ -117,6 +117,28 @@ def get_metakernels(spice_dir=spice_root, missions=set(), years=set(), versions= def find_latest_metakernel(path, year): + """ + Find the latest version of a metakernel. + + Parameters + ---------- + path : str + The string path of the directory in which to search for metakernels. + year : str|int + The year of the desired metakernel. + + Returns + ------- + str + The string filename of the latest metakernel. + + Raises + ------ + Exception + No metakernels were found in the given path. + Exception + No metakernels matching the specified year were found in the provided path. + """ metakernel = None mks = sorted(glob(os.path.join(path,'*.[Tt][Mm]'))) if not mks: @@ -131,6 +153,21 @@ def find_latest_metakernel(path, year): def dict_merge(dct, merge_dct): + """ + Merge the contents of two dictionaries. + + Parameters + ---------- + dct : dict + The first dictionary to merge. + merge_dct : dict + The second dictionary to merge. + + Returns + ------- + dict + The merged dictionary containing the contents of the two given dictionaries. + """ new_dct = dct.copy() for k, v in merge_dct.items(): if (k in dct and isinstance(dct[k], dict) @@ -175,10 +212,47 @@ def get_isis_preferences(isis_preferences=None): def dict_to_lower(d): + """ + Recursively convert all keys in the dictionary to lower-case. + + Parameters + ---------- + d : dict + The dictionary for which to convert the keys to lower-case. + + Returns + ------- + dict + The original dictionary with keys converted to lower-case. + """ return {k.lower():v if not isinstance(v, dict) else dict_to_lower(v) for k,v in d.items()} def expandvars(path, env_dict=os.environ, default=None, case_sensitive=True): + """ + Expand the environment variables in a given string. + + Parameters + ---------- + path : str + The string containing variables to be expanded. + env_dict : dict, optional + A dictionary containing environment variable definitions, by default os.environ + default : obj, optional + A default value for undefined environment variables, by default None + case_sensitive : bool, optional + Whether or not the output should match the case of the input, by default True + + Returns + ------- + str + The original string with expanded environment variables. + + Raises + ------ + KeyError + Raised if the value is not in the dictionary + """ if env_dict != os.environ: env_dict = dict_merge(env_dict, os.environ) @@ -240,6 +314,28 @@ def generate_kernels_from_cube(cube, expand=False, format_as='list'): return get_kernels_from_isis_pvl(kernel_group, expand, format_as) def get_kernels_from_isis_pvl(kernel_group, expand=True, format_as="list"): + """ + Extract kernels from ISIS PVL. + + Parameters + ---------- + kernel_group : str + The target kernel group to extract + expand : bool, optional + True if values of environment variables should be expanded, by default True + format_as : str, optional + Desired output format, by default "list" + + Returns + ------- + list|str|obj + The extracted kernels in the user-specified format + + Raises + ------ + Exception + Raised if the user specifies an invalid or unsupported format. + """ # enforce key order mk_paths = OrderedDict.fromkeys( ['TargetPosition', 'InstrumentPosition', @@ -310,6 +406,21 @@ def get_kernels_from_isis_pvl(kernel_group, expand=True, format_as="list"): raise Exception(f'{format_as} is not a valid return format') def write_metakernel_from_cube(cube, mkpath=None): + """ + Create a metakernel from a spiceinit'd cube. + + Parameters + ---------- + cube : str + The string filename of the cube from which to generate a metakernel. + mkpath : str, optional + The path to the output metakernel or None if not written to disk, by default None + + Returns + ------- + str + The text of the generated metakernel. + """ # add ISISPREF paths as path_symbols and path_values to avoid custom expand logic pvlprefs = get_isis_preferences() @@ -370,7 +481,7 @@ def get_ck_frames(kernel): def create_spk_dependency_tree(kernels): """ - construct the dependency tree for the body states in a set of kernels. + Construct the dependency tree for the body states in a set of kernels. Parameters ---------- -- GitLab