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