diff --git a/CMakeLists.txt b/CMakeLists.txt
index 700968bf30b41e2d4a0a26392e48f6ab4be494c0..62ed6767a2c1634e8f6aad680a5c2b31d1a3b8ca 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -21,16 +21,20 @@ find_package(Python        REQUIRED COMPONENTS Development)
 find_package(nlohmann_json REQUIRED)
 
 # Library setup
+set(ALE_BUILD_INCLUDE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/include/")
+set(ALE_INSTALL_INCLUDE_DIR "include/ale")
 add_library(ale SHARED
             ${CMAKE_CURRENT_SOURCE_DIR}/src/ale.cpp
-            ${CMAKE_CURRENT_SOURCE_DIR}/src/Rotation.cpp)
-set(ALE_BUILD_INCLUDE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/include/")
+            ${CMAKE_CURRENT_SOURCE_DIR}/src/Rotation.cpp
+            ${CMAKE_CURRENT_SOURCE_DIR}/src/Isd.cpp
+            ${CMAKE_CURRENT_SOURCE_DIR}/src/Util.cpp)
 set(ALE_HEADERS "${ALE_BUILD_INCLUDE_DIR}/ale.h"
-                "${ALE_BUILD_INCLUDE_DIR}/Rotation.h")
-set(ALE_INSTALL_INCLUDE_DIR "include/ale")
+                "${ALE_BUILD_INCLUDE_DIR}/Rotation.h"
+                "${ALE_BUILD_INCLUDE_DIR}/Isd.h"
+                "${ALE_BUILD_INCLUDE_DIR}/Util.h")
 set_target_properties(ale PROPERTIES
-                      VERSION             ${PROJECT_VERSION}
-                      SOVERSION           0)
+                      VERSION   ${PROJECT_VERSION}
+                      SOVERSION 0)
 # Use generator expressions so that downstream projects can use this target
 target_include_directories(ale
                            PUBLIC
diff --git a/include/Distortion.h b/include/Distortion.h
new file mode 100644
index 0000000000000000000000000000000000000000..dd51f8646313d01595707a0a991860aed5f98bb3
--- /dev/null
+++ b/include/Distortion.h
@@ -0,0 +1,14 @@
+#ifndef ALE_DISTORTION_H
+#define ALE_DISTORTION_H
+
+namespace ale {
+  enum DistortionType {
+    TRANSVERSE,
+    RADIAL,
+    KAGUYALISM,
+    DAWNFC,
+    LROLROCNAC
+  };
+}
+
+#endif
diff --git a/include/Isd.h b/include/Isd.h
new file mode 100644
index 0000000000000000000000000000000000000000..c1a544cb15e16e7be0f7bf7b406743298ee45eab
--- /dev/null
+++ b/include/Isd.h
@@ -0,0 +1,78 @@
+#ifndef ALE_ISD_H
+#define ALE_ISD_H
+
+#include <string>
+#include <vector>
+#include <map>
+
+#include "Util.h"
+#include "Distortion.h"
+
+//#include "Rotation.h"
+//#include "State.h"
+
+namespace ale {
+
+  using json = nlohmann::json;
+
+  class Isd {
+    public:
+
+    Isd(std::string);
+
+    std::string usgscsm_name_model;
+    std::string name_platform;
+    std::string image_id;
+    std::string name_sensor;
+
+    double semi_major;
+    double semi_minor;
+
+    double detector_sample_summing;
+    double detector_line_summing;
+
+    double focal_length;
+    double focal_uncertainty;
+
+    double detector_center_line;
+    double detector_center_sample;
+
+    // should probably be ints
+    double starting_detector_line;
+    double starting_detector_sample;
+
+    std::vector<double> focal2pixel_line;
+    std::vector<double> focal2pixel_sample;
+
+    // maybe change
+    DistortionType distortion_model;
+    std::vector<double> distortion_coefficients;
+
+    unsigned int image_lines;
+    unsigned int image_samples;
+
+    double max_reference_height;
+    double min_reference_height;
+
+    std::vector<std::vector<double>> line_scan_rate;
+
+    double starting_ephemeris_time;
+    double center_ephemeris_time;
+
+    double t0_ephemeris_time;
+    double dt_ephemeris_time;
+
+    double t0_quaternions;
+    double dt_quaternions;
+
+    json naif_keywords;
+
+    //Positions sensor_pos;
+    //Positions sun_pos;
+
+    //Rotation sensor_orientation;
+    //Rotation body_orientaion;
+  };
+}
+
+#endif
diff --git a/include/Util.h b/include/Util.h
new file mode 100644
index 0000000000000000000000000000000000000000..c7cd3d0093ebaa617196fad956a6332952e1b6f3
--- /dev/null
+++ b/include/Util.h
@@ -0,0 +1,46 @@
+#ifndef ALE_UTIL_H
+#define ALE_UTIL_H
+
+#include <string>
+#include <nlohmann/json.hpp>
+
+#include "Isd.h"
+#include "Distortion.h"
+
+namespace ale {
+  using json = nlohmann::json;
+
+  double getMinHeight(nlohmann::json isd);
+  std::string getSensorModelName(json isd);
+  std::string getImageId(json isd);
+  std::string getSensorName(json isd);
+  std::string getPlatformName(json isd);
+  std::string getLogFile(nlohmann::json isd);
+  int getTotalLines(json isd);
+  int getTotalSamples(json isd);
+  double getStartingTime(json isd);
+  double getCenterTime(json isd);
+  std::vector<std::vector<double>> getLineScanRate(json isd);
+  int getSampleSumming(json isd);
+  int getLineSumming(json isd);
+  double getFocalLength(json isd);
+  double getFocalLengthUncertainty(json isd);
+  std::vector<double> getFocal2PixelLines(json isd);
+  std::vector<double> getFocal2PixelSamples(json isd);
+  double getDetectorCenterLine(json isd);
+  double getDetectorCenterSample(json isd);
+  double getDetectorStartingLine(json isd);
+  double getDetectorStartingSample(json isd);
+  double getMinHeight(json isd);
+  double getMaxHeight(json isd);
+  double getSemiMajorRadius(json isd);
+  double getSemiMinorRadius(json isd);
+  DistortionType getDistortionModel(json isd);
+  std::vector<double> getDistortionCoeffs(json isd);
+  std::vector<double> getSunPositions(json isd);
+  std::vector<double> getSensorPositions(json isd);
+  std::vector<double> getSensorVelocities(json isd);
+  std::vector<double> getSensorOrientations(json isd);
+}
+
+#endif
diff --git a/include/ale.h b/include/ale.h
index fb7841ba50cbb6831dfab782f1e50232453b3c8a..ee268126c0aac3a4a20ef3e9cb995a3cfb006649 100644
--- a/include/ale.h
+++ b/include/ale.h
@@ -1,7 +1,6 @@
 #ifndef ALE_INCLUDE_ALE_H
 #define ALE_INCLUDE_ALE_H
 
-#include <nlohmann/json.hpp>
 #include <string>
 #include <vector>
 
@@ -15,9 +14,9 @@ namespace ale {
   /// Interpolation enum for defining different methods of interpolation
   enum interpolation {
     /// Interpolate using linear interpolation
-    linear,
+    LINEAR,
     /// Interpolate using spline interpolation
-    spline
+    SPLINE
   };
 
   /**
diff --git a/src/Isd.cpp b/src/Isd.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..cd0e1456484c3d443fad31c38683460967dba718
--- /dev/null
+++ b/src/Isd.cpp
@@ -0,0 +1,44 @@
+#include "Isd.h"
+#include "Util.h"
+
+
+ale::Isd::Isd(std::string isd_file) {
+  json isd = json::parse(isd_file);
+
+  usgscsm_name_model = getSensorModelName(isd);
+  image_id = getImageId(isd);
+  name_platform = getPlatformName(isd);
+  name_sensor = getSensorName(isd);
+
+  image_lines = getTotalLines(isd);
+  image_samples = getTotalSamples(isd);
+
+  starting_ephemeris_time = getStartingTime(isd);
+  center_ephemeris_time = getCenterTime(isd);
+
+  line_scan_rate = getLineScanRate(isd);
+
+  detector_sample_summing = getSampleSumming(isd);
+  detector_line_summing = getLineSumming(isd);
+
+  focal_length = getFocalLength(isd);
+  focal_uncertainty = getFocalLengthUncertainty(isd);
+
+  focal2pixel_line = getFocal2PixelLines(isd);
+  focal2pixel_sample = getFocal2PixelSamples(isd);
+
+  detector_center_line = getDetectorCenterLine(isd);
+  detector_center_sample = getDetectorCenterSample(isd);
+
+  starting_detector_line = getDetectorStartingLine(isd);
+  starting_detector_sample = getDetectorStartingSample(isd);
+
+  min_reference_height = getMinHeight(isd);
+  max_reference_height = getMaxHeight(isd);
+
+  semi_major = getSemiMajorRadius(isd);
+  semi_minor = getSemiMinorRadius(isd);
+
+  distortion_model = getDistortionModel(isd);
+  distortion_coefficients = getDistortionCoeffs(isd);
+}
diff --git a/src/Util.cpp b/src/Util.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..f93d76561d7ce0763c2f2b4a1871935b9efa5470
--- /dev/null
+++ b/src/Util.cpp
@@ -0,0 +1,472 @@
+#include <stdexcept>
+
+#include "Util.h"
+
+std::string ale::getSensorModelName(json isd) {
+  std::string name = "";
+  try {
+    name = isd.at("name_model");
+  } catch (...) {
+    throw std::runtime_error("Could not parse the sensor model name.");
+  }
+  return name;
+}
+
+std::string ale::getImageId(json isd) {
+  std::string id = "";
+  try {
+    id = isd.at("image_identifier");
+  } catch (...) {
+    throw std::runtime_error("Could not parse the image identifier.");
+  }
+  return id;
+}
+
+std::string ale::getSensorName(json isd) {
+  std::string name = "";
+  try {
+    name = isd.at("name_sensor");
+  } catch (...) {
+    throw std::runtime_error("Could not parse the sensor name.");
+  }
+  return name;
+}
+
+std::string ale::getPlatformName(json isd) {
+  std::string name = "";
+  try {
+    name = isd.at("name_platform");
+  } catch (...) {
+    throw std::runtime_error("Could not parse the platform name.");
+  }
+  return name;
+}
+
+std::string ale::getLogFile(nlohmann::json isd) {
+  std::string file = "";
+  try {
+    file = isd.at("log_file");
+  } catch (...) {
+    throw std::runtime_error("Could not parse the log filename.");
+  }
+  return file;
+}
+
+int ale::getTotalLines(json isd) {
+  int lines = 0;
+  try {
+    lines = isd.at("image_lines");
+  } catch (...) {
+    throw std::runtime_error(
+        "Could not parse the number of lines in the image.");
+  }
+  return lines;
+}
+
+int ale::getTotalSamples(json isd) {
+  int samples = 0;
+  try {
+    samples = isd.at("image_samples");
+  } catch (...) {
+    throw std::runtime_error(
+        "Could not parse the number of samples in the image.");
+  }
+  return samples;
+}
+
+double ale::getStartingTime(json isd) {
+  double time = 0.0;
+  try {
+    time = isd.at("starting_ephemeris_time");
+  } catch (...) {
+    throw std::runtime_error("Could not parse the image start time.");
+  }
+  return time;
+}
+
+double ale::getCenterTime(json isd) {
+  double time = 0.0;
+  try {
+    time = isd.at("center_ephemeris_time");
+  } catch (...) {
+    throw std::runtime_error("Could not parse the center image time.");
+  }
+  return time;
+}
+
+std::vector<std::vector<double>> ale::getLineScanRate(json isd) {
+  std::vector<std::vector<double>> lines;
+  try {
+    for (auto &scanRate : isd.at("line_scan_rate")) {
+      lines.push_back(scanRate.get<std::vector<double>>());
+    }
+  } catch (...) {
+    throw std::runtime_error("Could not parse the integration start lines in "
+                             "the integration time table.");
+  }
+  return lines;
+}
+
+
+int ale::getSampleSumming(json isd) {
+  int summing = 0;
+  try {
+    summing = isd.at("detector_sample_summing");
+  } catch (...) {
+    throw std::runtime_error(
+        "Could not parse the sample direction detector pixel summing.");
+  }
+  return summing;
+}
+
+int ale::getLineSumming(json isd) {
+  int summing = 0;
+  try {
+    summing = isd.at("detector_line_summing");
+  } catch (...) {
+    throw std::runtime_error(
+        "Could not parse the line direction detector pixel summing.");
+  }
+  return summing;
+}
+
+double ale::getFocalLength(json isd) {
+  double length = 0.0;
+  try {
+    length = isd.at("focal_length_model").at("focal_length");
+  } catch (...) {
+    throw std::runtime_error("Could not parse the focal length.");
+  }
+  return length;
+}
+
+double ale::getFocalLengthUncertainty(json isd) {
+  double uncertainty = 1.0;
+  try {
+    uncertainty = isd.at("focal_length_model").value("focal_uncertainty", uncertainty);
+  } catch (...) {
+    throw std::runtime_error("Could not parse the focal length uncertainty.");
+  }
+  return uncertainty;
+}
+
+std::vector<double> ale::getFocal2PixelLines(json isd) {
+  std::vector<double> transformation;
+  try {
+    transformation = isd.at("focal2pixel_lines").get<std::vector<double>>();
+  } catch (...) {
+    throw std::runtime_error("Could not parse the focal plane coordinate to "
+                             "detector lines transformation.");
+  }
+  return transformation;
+}
+
+std::vector<double> ale::getFocal2PixelSamples(json isd) {
+  std::vector<double> transformation;
+  try {
+    transformation = isd.at("focal2pixel_samples").get<std::vector<double>>();
+  } catch (...) {
+    throw std::runtime_error("Could not parse the focal plane coordinate to "
+                             "detector samples transformation.");
+  }
+  return transformation;
+}
+
+double ale::getDetectorCenterLine(json isd) {
+  double line;
+  try {
+    line = isd.at("detector_center").at("line");
+  } catch (...) {
+    throw std::runtime_error("Could not parse the detector center line.");
+  }
+  return line;
+}
+
+double ale::getDetectorCenterSample(json isd) {
+  double sample;
+  try {
+    sample = isd.at("detector_center").at("sample");
+  } catch (...) {
+    throw std::runtime_error("Could not parse the detector center sample.");
+  }
+  return sample;
+}
+
+double ale::getDetectorStartingLine(json isd) {
+  double line;
+  try {
+    line = isd.at("starting_detector_line");
+  } catch (...) {
+    throw std::runtime_error("Could not parse the detector starting line.");
+  }
+  return line;
+}
+
+double ale::getDetectorStartingSample(json isd) {
+  double sample;
+  try {
+    sample = isd.at("starting_detector_sample");
+  } catch (...) {
+    throw std::runtime_error("Could not parse the detector starting sample.");
+  }
+  return sample;
+}
+
+double ale::getMinHeight(json isd) {
+  double height = 0.0;
+  try {
+    json referenceHeight = isd.at("reference_height");
+    json minHeight = referenceHeight.at("minheight");
+    height = minHeight.get<double>();
+  } catch (...) {
+    throw std::runtime_error(
+        "Could not parse the minimum height above the reference ellipsoid.");
+  }
+  return height;
+}
+
+double ale::getMaxHeight(json isd) {
+  double height = 0.0;
+  try {
+    json referenceHeight = isd.at("reference_height");
+    json maxHeight = referenceHeight.at("maxheight");
+
+    height = maxHeight.get<double>();
+  } catch (...) {
+    throw std::runtime_error(
+        "Could not parse the maximum height above the reference ellipsoid.");
+  }
+  return height;
+}
+
+double ale::getSemiMajorRadius(json isd) {
+  double radius = 0.0;
+  try {
+    json radii = isd.at("radii");
+    json semiMajor = radii.at("semimajor");
+    radius = semiMajor.get<double>();
+
+  } catch (...) {
+    throw std::runtime_error(
+        "Could not parse the reference ellipsoid semimajor radius.");
+  }
+  return radius;
+}
+
+double ale::getSemiMinorRadius(json isd) {
+  double radius = 0.0;
+  try {
+    json radii = isd.at("radii");
+    json semiMinor = radii.at("semiminor");
+    radius = semiMinor.get<double>();
+
+  } catch (...) {
+    throw std::runtime_error(
+        "Could not parse the reference ellipsoid semiminor radius.");
+  }
+  return radius;
+}
+
+// Converts the distortion model name from the ISD (string) to the enumeration
+// type. Defaults to transverse
+ale::DistortionType ale::getDistortionModel(json isd) {
+  try {
+    json distoriton_subset = isd.at("optical_distortion");
+
+    json::iterator it = distoriton_subset.begin();
+
+    std::string distortion = (std::string)it.key();
+
+    if (distortion.compare("transverse") == 0) {
+      return DistortionType::TRANSVERSE;
+    } else if (distortion.compare("radial") == 0) {
+      return DistortionType::RADIAL;
+    } else if (distortion.compare("kaguyalism") == 0) {
+      return DistortionType::KAGUYALISM;
+    } else if (distortion.compare("dawnfc") == 0) {
+      return DistortionType::DAWNFC;
+    } else if (distortion.compare("lrolrocnac") == 0) {
+      return DistortionType::LROLROCNAC;
+    }
+  } catch (...) {
+    throw std::runtime_error("Could not parse the distortion model.");
+  }
+  return DistortionType::TRANSVERSE;
+}
+
+std::vector<double> ale::getDistortionCoeffs(json isd) {
+  std::vector<double> coefficients;
+
+  ale::DistortionType distortion = getDistortionModel(isd);
+
+  switch (distortion) {
+  case DistortionType::TRANSVERSE: {
+    try {
+      std::vector<double> coefficientsX, coefficientsY;
+
+      coefficientsX = isd.at("optical_distortion")
+                          .at("transverse")
+                          .at("x")
+                          .get<std::vector<double>>();
+      coefficientsX.resize(10, 0.0);
+
+      coefficientsY = isd.at("optical_distortion")
+                          .at("transverse")
+                          .at("y")
+                          .get<std::vector<double>>();
+      coefficientsY.resize(10, 0.0);
+
+      coefficients = coefficientsX;
+
+      coefficients.insert(coefficients.end(), coefficientsY.begin(),
+                          coefficientsY.end());
+      return coefficients;
+    } catch (...) {
+      throw std::runtime_error(
+          "Could not parse a set of transverse distortion model coefficients.");
+      coefficients = std::vector<double>(20, 0.0);
+      coefficients[1] = 1.0;
+      coefficients[12] = 1.0;
+    }
+  } break;
+  case DistortionType::RADIAL: {
+    try {
+      coefficients = isd.at("optical_distortion")
+                         .at("radial")
+                         .at("coefficients")
+                         .get<std::vector<double>>();
+
+      return coefficients;
+    } catch (...) {
+      throw std::runtime_error(
+          "Could not parse the radial distortion model coefficients.");
+      coefficients = std::vector<double>(3, 0.0);
+    }
+  } break;
+  case DistortionType::KAGUYALISM: {
+    try {
+
+      std::vector<double> coefficientsX = isd.at("optical_distortion")
+                                              .at("kaguyalism")
+                                              .at("x")
+                                              .get<std::vector<double>>();
+      std::vector<double> coefficientsY = isd.at("optical_distortion")
+                                              .at("kaguyalism")
+                                              .at("y")
+                                              .get<std::vector<double>>();
+      double boresightX = isd.at("optical_distortion")
+                              .at("kaguyalism")
+                              .at("boresight_x")
+                              .get<double>();
+      double boresightY = isd.at("optical_distortion")
+                              .at("kaguyalism")
+                              .at("boresight_y")
+                              .get<double>();
+
+      coefficientsX.resize(4, 0.0);
+      coefficientsY.resize(4, 0.0);
+      coefficientsX.insert(coefficientsX.begin(), boresightX);
+      coefficientsY.insert(coefficientsY.begin(), boresightY);
+      coefficientsX.insert(coefficientsX.end(), coefficientsY.begin(),
+                           coefficientsY.end());
+
+      return coefficientsX;
+    } catch (...) {
+      throw std::runtime_error("Could not parse a set of Kaguya LISM "
+                               "distortion model coefficients.");
+      coefficients = std::vector<double>(8, 0.0);
+    }
+  } break;
+  case DistortionType::DAWNFC: {
+    try {
+      coefficients = isd.at("optical_distortion")
+                         .at("dawnfc")
+                         .at("coefficients")
+                         .get<std::vector<double>>();
+
+      return coefficients;
+    } catch (...) {
+      throw std::runtime_error(
+          "Could not parse the dawn radial distortion model coefficients.");
+      coefficients = std::vector<double>(1, 0.0);
+    }
+  } break;
+  case DistortionType::LROLROCNAC: {
+    try {
+      coefficients = isd.at("optical_distortion")
+                         .at("lrolrocnac")
+                         .at("coefficients")
+                         .get<std::vector<double>>();
+      return coefficients;
+    } catch (...) {
+      throw std::runtime_error(
+          "Could not parse the lrolrocnac distortion model coefficients.");
+      coefficients = std::vector<double>(1, 0.0);
+    }
+  } break;
+  }
+  throw std::runtime_error(
+      "Could not parse the distortion model coefficients.");
+
+  return coefficients;
+}
+
+std::vector<double> ale::getSunPositions(json isd) {
+  std::vector<double> positions;
+  try {
+    json jayson = isd.at("sun_position");
+    for (auto &location : jayson.at("positions")) {
+      positions.push_back(location[0].get<double>());
+      positions.push_back(location[1].get<double>());
+      positions.push_back(location[2].get<double>());
+    }
+  } catch (...) {
+    throw std::runtime_error("Could not parse the sun positions.");
+  }
+  return positions;
+}
+
+std::vector<double> ale::getSensorPositions(json isd) {
+  std::vector<double> positions;
+  try {
+    json jayson = isd.at("sensor_position");
+    for (auto &location : jayson.at("positions")) {
+      positions.push_back(location[0].get<double>());
+      positions.push_back(location[1].get<double>());
+      positions.push_back(location[2].get<double>());
+    }
+  } catch (...) {
+    throw std::runtime_error("Could not parse the sensor positions.");
+  }
+  return positions;
+}
+
+std::vector<double> ale::getSensorVelocities(json isd) {
+  std::vector<double> velocities;
+  try {
+    json jayson = isd.at("sensor_position");
+    for (auto &velocity : jayson.at("velocities")) {
+      velocities.push_back(velocity[0].get<double>());
+      velocities.push_back(velocity[1].get<double>());
+      velocities.push_back(velocity[2].get<double>());
+    }
+  } catch (...) {
+    throw std::runtime_error("Could not parse the sensor velocities.");
+  }
+  return velocities;
+}
+
+std::vector<double> ale::getSensorOrientations(json isd) {
+  std::vector<double> quaternions;
+  try {
+    for (auto &quaternion : isd.at("sensor_orientation").at("quaternions")) {
+      quaternions.push_back(quaternion[0]);
+      quaternions.push_back(quaternion[1]);
+      quaternions.push_back(quaternion[2]);
+      quaternions.push_back(quaternion[3]);
+    }
+  } catch (...) {
+    throw std::runtime_error("Could not parse the sensor orientations.");
+  }
+  return quaternions;
+}
diff --git a/tests/ctests/AleTest.cpp b/tests/ctests/AleTest.cpp
index 9171974f8dfec98551467b4c7a56ed367ceb8075..4e690a3392e8edd8eeaf5ac13ce7bf909663ae3b 100644
--- a/tests/ctests/AleTest.cpp
+++ b/tests/ctests/AleTest.cpp
@@ -1,6 +1,7 @@
 #include "gtest/gtest.h"
 
 #include "ale.h"
+#include "Isd.h"
 
 #include <stdexcept>
 #include <cmath>
@@ -12,13 +13,14 @@
 
 using namespace std;
 
+
 TEST(PositionInterpTest, LinearInterp) {
   vector<double> times = { -3, -2, -1,  0,  1,  2};
   vector<vector<double>> data = {{ -3, -2, -1,  0,  1,  2},
                                  {  9,  4,  1,  0,  1,  4},
                                  {-27, -8, -1,  0,  1,  8}};
 
-  vector<double> coordinate = ale::getPosition(data, times, -1.5, ale::linear);
+  vector<double> coordinate = ale::getPosition(data, times, -1.5, ale::LINEAR);
 
   ASSERT_EQ(3, coordinate.size());
   EXPECT_DOUBLE_EQ(-1.5, coordinate[0]);
@@ -34,7 +36,7 @@ TEST(PositionInterpTest, FourCoordinates) {
                                  {-27, -8, -1,  0,  1,  8},
                                  { 25,  0, -5, 25,  3,  6}};
 
-  EXPECT_THROW(ale::getPosition(data, times, 0.0, ale::linear),
+  EXPECT_THROW(ale::getPosition(data, times, 0.0, ale::LINEAR),
                invalid_argument);
 }
 
@@ -43,13 +45,13 @@ TEST(LinearInterpTest, ExampleInterpolation) {
   vector<double> times = {0,  1,  2, 3};
   vector<double> data = {0, 2, 1, 0};
 
-  EXPECT_DOUBLE_EQ(0.0, ale::interpolate(data, times, 0.0, ale::linear, 0));
-  EXPECT_DOUBLE_EQ(1.0, ale::interpolate(data, times, 0.5, ale::linear, 0));
-  EXPECT_DOUBLE_EQ(2.0, ale::interpolate(data, times, 1.0, ale::linear, 0));
-  EXPECT_DOUBLE_EQ(1.5, ale::interpolate(data, times, 1.5, ale::linear, 0));
-  EXPECT_DOUBLE_EQ(1.0, ale::interpolate(data, times, 2.0, ale::linear, 0));
-  EXPECT_DOUBLE_EQ(0.5, ale::interpolate(data, times, 2.5, ale::linear, 0));
-  EXPECT_DOUBLE_EQ(0.0, ale::interpolate(data, times, 3.0, ale::linear, 0));
+  EXPECT_DOUBLE_EQ(0.0, ale::interpolate(data, times, 0.0, ale::LINEAR, 0));
+  EXPECT_DOUBLE_EQ(1.0, ale::interpolate(data, times, 0.5, ale::LINEAR, 0));
+  EXPECT_DOUBLE_EQ(2.0, ale::interpolate(data, times, 1.0, ale::LINEAR, 0));
+  EXPECT_DOUBLE_EQ(1.5, ale::interpolate(data, times, 1.5, ale::LINEAR, 0));
+  EXPECT_DOUBLE_EQ(1.0, ale::interpolate(data, times, 2.0, ale::LINEAR, 0));
+  EXPECT_DOUBLE_EQ(0.5, ale::interpolate(data, times, 2.5, ale::LINEAR, 0));
+  EXPECT_DOUBLE_EQ(0.0, ale::interpolate(data, times, 3.0, ale::LINEAR, 0));
 }
 
 
@@ -57,7 +59,7 @@ TEST(LinearInterpTest, NoPoints) {
   vector<double> times = {};
   vector<double> data = {};
 
-  EXPECT_THROW(ale::interpolate(data, times, 0.0, ale::linear, 0),
+  EXPECT_THROW(ale::interpolate(data, times, 0.0, ale::LINEAR, 0),
                invalid_argument);
 }
 
@@ -66,7 +68,7 @@ TEST(LinearInterpTest, DifferentCounts) {
   vector<double> times = { -3, -2, -1,  0,  2};
   vector<double> data = { -3, -2, 1,  2};
 
-  EXPECT_THROW(ale::interpolate(data, times, 0.0, ale::linear, 0),
+  EXPECT_THROW(ale::interpolate(data, times, 0.0, ale::LINEAR, 0),
                invalid_argument);
 }
 
@@ -75,9 +77,9 @@ TEST(LinearInterpTest, Extrapolate) {
   vector<double> times = {0,  1,  2, 3};
   vector<double> data = {0, 2, 1, 0};
 
-  EXPECT_THROW(ale::interpolate(data, times, -1.0, ale::linear, 0),
+  EXPECT_THROW(ale::interpolate(data, times, -1.0, ale::LINEAR, 0),
                invalid_argument);
-  EXPECT_THROW(ale::interpolate(data, times, 4.0, ale::linear, 0),
+  EXPECT_THROW(ale::interpolate(data, times, 4.0, ale::LINEAR, 0),
                invalid_argument);
 }
 
@@ -93,16 +95,16 @@ TEST(SplineInterpTest, ExampleInterpolation) {
 
   // The spline interpolation is only ~1e-10 so we have to define a tolerance
   double tolerance = 1e-10;
-  EXPECT_NEAR(0.0, ale::interpolate(data, times, 0.0, ale::spline, 0), tolerance);
+  EXPECT_NEAR(0.0, ale::interpolate(data, times, 0.0, ale::SPLINE, 0), tolerance);
   EXPECT_NEAR(2.8 * 0.5 - 0.8 * 0.125,
-              ale::interpolate(data, times, 0.5, ale::spline, 0), tolerance);
-  EXPECT_NEAR(2.0, ale::interpolate(data, times, 1.0, ale::spline, 0), tolerance);
+              ale::interpolate(data, times, 0.5, ale::SPLINE, 0), tolerance);
+  EXPECT_NEAR(2.0, ale::interpolate(data, times, 1.0, ale::SPLINE, 0), tolerance);
   EXPECT_NEAR(3.375 - 5.4 * 2.25 + 8.2 * 1.5 - 1.8,
-              ale::interpolate(data, times, 1.5, ale::spline, 0), tolerance);
-  EXPECT_NEAR(1.0, ale::interpolate(data, times, 2.0, ale::spline, 0), tolerance);
+              ale::interpolate(data, times, 1.5, ale::SPLINE, 0), tolerance);
+  EXPECT_NEAR(1.0, ale::interpolate(data, times, 2.0, ale::SPLINE, 0), tolerance);
   EXPECT_NEAR(-0.2 * 15.625 + 1.8 * 6.25 - 6.2 * 2.5 + 7.8,
-              ale::interpolate(data, times, 2.5, ale::spline, 0), tolerance);
-  EXPECT_NEAR(0.0, ale::interpolate(data, times, 3.0, ale::spline, 0), tolerance);
+              ale::interpolate(data, times, 2.5, ale::SPLINE, 0), tolerance);
+  EXPECT_NEAR(0.0, ale::interpolate(data, times, 3.0, ale::SPLINE, 0), tolerance);
 }
 
 
@@ -110,7 +112,7 @@ TEST(SplineInterpTest, NoPoints) {
   vector<double> times = {};
   vector<double> data = {};
 
-  EXPECT_THROW(ale::interpolate(data, times, 0.0, ale::spline, 0),
+  EXPECT_THROW(ale::interpolate(data, times, 0.0, ale::SPLINE, 0),
                invalid_argument);
 }
 
@@ -119,7 +121,7 @@ TEST(SplineInterpTest, DifferentCounts) {
   vector<double> times = { -3, -2, -1,  0,  2};
   vector<double> data = { -3, -2, 1,  2};
 
-  EXPECT_THROW(ale::interpolate(data, times, 0.0, ale::spline, 0),
+  EXPECT_THROW(ale::interpolate(data, times, 0.0, ale::SPLINE, 0),
                invalid_argument);
 }
 
@@ -128,9 +130,9 @@ TEST(SplineInterpTest, Extrapolate) {
   vector<double> times = {0,  1,  2, 3};
   vector<double> data = {0, 2, 1, 0};
 
-  EXPECT_THROW(ale::interpolate(data, times, -1.0, ale::spline, 0),
+  EXPECT_THROW(ale::interpolate(data, times, -1.0, ale::SPLINE, 0),
                invalid_argument);
-  EXPECT_THROW(ale::interpolate(data, times, 4.0, ale::spline, 0),
+  EXPECT_THROW(ale::interpolate(data, times, 4.0, ale::SPLINE, 0),
                invalid_argument);
 }
 
@@ -283,18 +285,23 @@ TEST(RotationInterpTest, ExampleGetRotation) {
   // simple test, only checks if API hit correctly and output is normalized
   vector<double> times = {0,  1,  2, 3};
   vector<vector<double>> rots({{1,1,1,1}, {0,0,0,0}, {1,1,1,1}, {0,0,0,0}});
-  vector<double> r = ale::getRotation(rots, times, 2, ale::linear);
+  vector<double> r = ale::getRotation(rots, times, 2, ale::LINEAR);
   Eigen::Quaterniond quat(r[0], r[1], r[2], r[3]);
 
   EXPECT_DOUBLE_EQ(1, quat.norm());
 }
 
-
 TEST(RotationInterpTest, GetRotationDifferentCounts) {
   // incorrect params
   vector<double> times = {0, 1, 2};
   vector<vector<double>> rots({{1,1,1,1}, {0,0,0,0}, {1,1,1,1}, {0,0,0,0}});
-  EXPECT_THROW(ale::getRotation(rots, times, 2, ale::linear), invalid_argument);
+  EXPECT_THROW(ale::getRotation(rots, times, 2, ale::LINEAR), invalid_argument);
+}
+
+TEST(RotationInterpTest, InvalidTime) {
+  vector<double> times = {0, 1, 2};
+  vector<vector<double>> rots({{1,1,1,1}, {0,0,0,0}, {1,1,1,1}});
+  EXPECT_THROW(ale::getRotation(rots, times, 3, ale::LINEAR), invalid_argument);
 }
 
 TEST(PyInterfaceTest, LoadInvalidLabel) {
@@ -310,9 +317,48 @@ TEST(PyInterfaceTest, LoadValidLabel) {
 TEST(AngularVelocityInterpTest, ExampleGetRotation) {
   vector<double> times = {0,  1};
   vector<vector<double>> rots({{0,0}, {1,0}, {0,1}, {0,0}});
-  vector<double> av = ale::getAngularVelocity(rots, times, 0.5, ale::linear);
+  vector<double> av = ale::getAngularVelocity(rots, times, 0.5, ale::LINEAR);
 
   EXPECT_DOUBLE_EQ(0, av[0]);
   EXPECT_DOUBLE_EQ(0, av[1]);
   EXPECT_DOUBLE_EQ(2 * sqrt(2), av[2]);
 }
+
+TEST(AngularVelocityInterpTest, InvalidTime) {
+  vector<double> times = {0, 1, 2};
+  vector<vector<double>> rots({{0,0}, {1,0}, {0,1}, {0,0}});
+  EXPECT_THROW(ale::getAngularVelocity(rots, times, 3, ale::LINEAR), invalid_argument);
+}
+
+TEST(VelocityInterpTest, ExampleGetVelocity) {
+  vector<double> times = {0, 1, 2};
+  vector<vector<double>> coords({{1, 1, 1}, {2, 2, 2}, {3, 3, 3}});
+  vector<double> v = ale::getVelocity(coords, times, 1, ale::LINEAR);
+
+  //not sure what the values are expected to look like
+  EXPECT_DOUBLE_EQ(v[0], 0);
+  EXPECT_DOUBLE_EQ(v[1], 0);
+  EXPECT_DOUBLE_EQ(v[2], 0);
+}
+
+TEST(VelocityInterpTest, InvalidTime) {
+  vector<double> times = {0, 1, 2};
+  vector<vector<double>> coords({{1, 1, 1}, {2, 2, 2}, {3, 3, 3}});
+  EXPECT_THROW(ale::getVelocity(coords, times, 3, ale::LINEAR), invalid_argument);
+}
+
+TEST(Interpolation, Derivative2)
+{
+  //only checks that case 2 of the switch block is hit
+  vector<double> points = {0, 0, 0};
+  vector<double> times = {0, 1, 2};
+
+  EXPECT_DOUBLE_EQ(ale::interpolate(points, times, 1, ale::LINEAR, 2), 0);
+}
+
+TEST(Interpolation, InvalidDerivative) {
+  vector<double> points = {0, 0, 0};
+  vector<double> times = {0, 1, 2};
+
+  EXPECT_THROW(ale::interpolate(points, times, 1, ale::LINEAR, 3), invalid_argument);
+}
diff --git a/tests/ctests/IsdTests.cpp b/tests/ctests/IsdTests.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..112b552142270226c651af1a1c36802dbc72cc79
--- /dev/null
+++ b/tests/ctests/IsdTests.cpp
@@ -0,0 +1,678 @@
+#include <string>
+#include <fstream>
+#include <streambuf>
+
+#include "gtest/gtest.h"
+
+#include "ale.h"
+#include "Isd.h"
+#include "Util.h"
+
+void ASSERT_DOUBLE_VECTOR_EQ(std::vector<double> v1, std::vector<double> v2) {
+  ASSERT_EQ(v1.size(), v2.size()) << "The two input vectors are different in size";
+
+  for(size_t i = 0; i < v1.size(); i++) {
+    EXPECT_DOUBLE_EQ(v1[i], v2[i]) << "Arrays at " << i << " are not equal.";
+  }
+}
+
+
+void ASSERT_DOUBLE_2D_VECTOR_EQ(std::vector<std::vector<double>> v1, std::vector<std::vector<double>> v2) {
+  ASSERT_EQ(v1.size(), v2.size()) << "The two input vectors are different in size";
+
+  for(size_t i = 0; i < v1.size(); i++) {
+    ASSERT_EQ(v1[i].size(), v2[i].size()) << "The two input vectors at " << i << "are different in size";
+
+    for(size_t j = 0; j < v1[i].size(); j++) {
+      EXPECT_DOUBLE_EQ(v1[i][j], v2[i][j]) << "Arrays at " << i << ", " << j <<  " are not equal.";
+    }
+  }
+}
+
+TEST(Isd, Constructor) {
+  std::string json_str = "{\"image_identifier\":\"TEST_IMAGE\",\"sensor_position\":{\"ephemeris_times\":[297088762.24158406,297088762.3917441,297088762.5419041,297088762.69206405,297088762.84222406,297088762.9923841],\"positions\":[[-1885.29806756,913.1652236,-2961.966828],[-1885.59280128,912.7436266,-2961.91056824],[-1885.88749707,912.32201117,-2961.85424884],[-1886.18215477,911.90037749,-2961.79786985],[-1886.47677475,911.47872522,-2961.7414312],[-1886.77135665,911.05705456,-2961.68493293]],\"velocities\":[[-1.9629237646703683,-2.80759072221274,0.37446657801485306],[-1.9626712192798401,-2.807713482051373,0.3748636774173111],[-1.9624186346660286,-2.807836185534424,0.3752607691067297],[-1.9621660109346446,-2.8079588326107823,0.37565785291714804],[-1.9619133478903363,-2.8080814233753033,0.37605492915558875],[-1.961660645638678,-2.8082039577768683,0.37645199765665144]],\"position_units\":\"KM\",\"time_units\":\"S\",\"reference_frame\":1},\"sun_position\":{\"ephemeris_times\":[297088762.24158406,297088762.3917441,297088762.5419041,297088762.69206405,297088762.84222406,297088762.9923841],\"positions\":[[-1885.29806756,913.1652236,-2961.966828]],\"velocities\":[[-1.9629237646703683,-2.80759072221274,0.37446657801485306]],\"position_units\":\"KM\",\"time_units\":\"S\",\"reference_frame\":1},\"sensor_orientation\":{\"time_dependent_framess\":[-74000,-74900,1],\"constant_frames\":[-74021,-74020,-74699,-74690,-74000],\"reference_frame\":1,\"constant_rotation\":[0.9999995608798441,-1.51960241928035e-05,0.0009370214510594064,1.5276552075356694e-05,0.9999999961910578,-8.593317911879532e-05,-0.000937020141647677,8.594745584079714e-05,0.9999995573030465],\"ephemeris_times\":[297088762.24158406,297088762.3917441,297088762.5419041,297088762.69206405,297088762.84222406,297088762.9923841],\"quaternions\":[[0.42061125,0.18606223,-0.23980124,0.85496338],[0.42062261,0.18612356,-0.23976951,0.85495335],[0.42063547,0.18618438,-0.23973759,0.85494273],[0.42064763,0.18624551,-0.2397057,0.85493237],[0.42065923,0.18630667,-0.23967382,0.85492228],[0.42067144,0.18636687,-0.23964185,0.85491211]],\"angular_velocities\":[[-0.0006409728984903079,0.0005054077299115119,0.0004718267948468069],[-0.0006410700774431097,0.0005044862657976017,0.0004731836236807216],[-0.0006408186407087456,0.0004992170698116158,0.0004802237192760833],[-0.0006363961683672021,0.0004989647975959612,0.00047654664046286975],[-0.0006376443791903504,0.0004996117504290811,0.00047678850931380653],[-0.0006404093657132724,0.0005028749658176146,0.0004805228583087444]]},\"body_rotation\":{\"time_dependent_frames\":[10014,1],\"reference_frame\":1,\"ephemeris_times\":[297088762.24158406,297088762.3917441,297088762.5419041,297088762.69206405,297088762.84222406,297088762.9923841],\"quaternions\":[[-0.8371209459443085,0.2996928944391797,0.10720760458181891,0.4448811306448063],[-0.8371185783490869,0.2996934649760026,0.1072060096645597,0.4448855856569007],[-0.8371162107293473,0.2996940355045328,0.10720441474371896,0.44489004065791765],[-0.8371138430875174,0.2996946060241849,0.1072028198209324,0.44489449564328926],[-0.8371114754203602,0.2996951765357392,0.10720122489401934,0.44489895061910595],[-0.8371091077303039,0.29969574703861046,0.10719962996461516,0.4449034055807993]],\"angular_velocities\":[[3.16238646979841e-05,-2.880432898124293e-05,5.6520131658726165e-05],[3.1623864697983686e-05,-2.880432898124763e-05,5.652013165872402e-05],[3.162386469798325e-05,-2.880432898125237e-05,5.652013165872185e-05],[3.162386469798283e-05,-2.880432898125708e-05,5.6520131658719694e-05],[3.1623864697982405e-05,-2.8804328981261782e-05,5.6520131658717505e-05],[3.162386469798195e-05,-2.88043289812665e-05,5.652013165871536e-05]]},\"radii\":{\"semimajor\":3396.19,\"semiminor\":3376.2,\"unit\":\"km\"},\"detector_sample_summing\":1,\"detector_line_summing\":1,\"focal_length_model\":{\"focal_length\":352.9271664},\"detector_center\":{\"line\":0.430442527,\"sample\":2542.96099},\"starting_detector_line\":0,\"starting_detector_sample\":0,\"focal2pixel_lines\":[0.0,142.85714285714,0.0],\"focal2pixel_samples\":[0.0,0.0,142.85714285714],\"optical_distortion\":{\"radial\":{\"coefficients\":[-0.0073433925920054505,2.8375878636241697e-05,1.2841989124027099e-08]}},\"image_lines\":400,\"image_samples\":5056,\"name_platform\":\"MARS_RECONNAISSANCE_ORBITER\",\"name_sensor\":\"CONTEXT CAMERA\",\"reference_height\":{\"maxheight\":1000,\"minheight\":-1000,\"unit\":\"m\"},\"name_model\":\"USGS_ASTRO_LINE_SCANNER_SENSOR_MODEL\",\"interpolation_method\":\"lagrange\",\"line_scan_rate\":[[0.5,-0.37540000677108765,0.001877]],\"starting_ephemeris_time\":297088762.24158406,\"center_ephemeris_time\":297088762.61698407,\"t0_ephemeris\":-0.37540000677108765,\"dt_ephemeris\":0.15016000270843505,\"t0_quaternion\":-0.37540000677108765,\"dt_quaternion\":0.15016000270843505,\"naif_keywords\":{}}";
+
+  ale::Isd isd(json_str);
+
+  ASSERT_EQ(isd.usgscsm_name_model, "USGS_ASTRO_LINE_SCANNER_SENSOR_MODEL");
+  ASSERT_EQ(isd.name_platform, "MARS_RECONNAISSANCE_ORBITER");
+  ASSERT_EQ(isd.image_id, "TEST_IMAGE");
+  ASSERT_EQ(isd.name_sensor, "CONTEXT CAMERA");
+  ASSERT_DOUBLE_EQ(isd.semi_major, 3396.19);
+  ASSERT_DOUBLE_EQ(isd.semi_minor, 3376.2);
+
+  ASSERT_EQ(isd.detector_sample_summing, 1);
+  ASSERT_EQ(isd.detector_line_summing, 1);
+
+  ASSERT_DOUBLE_EQ(isd.focal_length, 352.9271664);
+  ASSERT_DOUBLE_EQ(isd.focal_uncertainty, 1);
+  ASSERT_DOUBLE_EQ(isd.detector_center_line, 0.430442527);
+  ASSERT_DOUBLE_EQ(isd.detector_center_sample,  2542.96099);
+
+  ASSERT_DOUBLE_EQ(isd.starting_detector_line, 0);
+  ASSERT_DOUBLE_EQ(isd.starting_detector_sample, 0);
+
+  ASSERT_DOUBLE_VECTOR_EQ(isd.focal2pixel_line, std::vector<double>({0.0, 142.85714285714, 0.0}));
+  ASSERT_DOUBLE_VECTOR_EQ(isd.focal2pixel_sample, std::vector<double>({0.0, 0.0, 142.85714285714}));
+
+  ASSERT_EQ(isd.distortion_model, ale::DistortionType::RADIAL);
+  ASSERT_DOUBLE_VECTOR_EQ(isd.distortion_coefficients, std::vector<double>({-0.0073433925920054505, 2.8375878636241697e-05, 1.2841989124027099e-08}));
+
+  ASSERT_EQ(isd.image_lines, 400);
+  ASSERT_EQ(isd.image_samples, 5056);
+
+  ASSERT_DOUBLE_EQ(isd.max_reference_height, 1000);
+  ASSERT_DOUBLE_EQ(isd.min_reference_height, -1000);
+
+  ASSERT_DOUBLE_2D_VECTOR_EQ(isd.line_scan_rate, std::vector<std::vector<double>>({std::vector<double>({0.5, -0.37540000677108765, 0.001877})}));
+
+  ASSERT_DOUBLE_EQ(isd.starting_ephemeris_time, 297088762.24158406);
+  ASSERT_DOUBLE_EQ(isd.center_ephemeris_time, 297088762.61698407);
+}
+
+TEST(Isd, LogFile) {
+  ale::json j;
+  j["log_file"] = "fake/path";
+  EXPECT_STREQ(ale::getLogFile(j).c_str(), "fake/path");
+}
+
+TEST(Isd, TransverseDistortion) {
+  ale::json trans;
+  trans["optical_distortion"]["transverse"]["x"] = {1};
+  trans["optical_distortion"]["transverse"]["y"] = {2};
+
+  std::vector<double> coeffs = ale::getDistortionCoeffs(trans);
+  EXPECT_EQ(ale::getDistortionModel(trans), ale::DistortionType::TRANSVERSE);
+  ASSERT_EQ(coeffs.size(), 20);
+  EXPECT_DOUBLE_EQ(coeffs[0], 1);
+  EXPECT_DOUBLE_EQ(coeffs[10], 2);
+}
+
+TEST(Isd, RadialDistortion) {
+  ale::json radial;
+  radial["optical_distortion"]["radial"]["coefficients"] = {1, 2};
+
+  std::vector<double> coeffs = ale::getDistortionCoeffs(radial);
+  EXPECT_EQ(ale::getDistortionModel(radial), ale::DistortionType::RADIAL);
+  ASSERT_EQ(coeffs.size(), 2);
+  EXPECT_DOUBLE_EQ(coeffs[0], 1);
+  EXPECT_DOUBLE_EQ(coeffs[1], 2);
+}
+
+TEST(Isd, KaguyaLISMDistortion) {
+  ale::json kaguya;
+  kaguya["optical_distortion"]["kaguyalism"]["x"] = {1};
+  kaguya["optical_distortion"]["kaguyalism"]["y"] = {2};
+  kaguya["optical_distortion"]["kaguyalism"]["boresight_x"] = 3;
+  kaguya["optical_distortion"]["kaguyalism"]["boresight_y"] = 4;
+
+  std::vector<double> coeffs = ale::getDistortionCoeffs(kaguya);
+  EXPECT_EQ(ale::getDistortionModel(kaguya), ale::DistortionType::KAGUYALISM);
+  ASSERT_EQ(coeffs.size(), 10);
+  EXPECT_DOUBLE_EQ(coeffs[0], 3);
+  EXPECT_DOUBLE_EQ(coeffs[1], 1);
+  EXPECT_DOUBLE_EQ(coeffs[5], 4);
+  EXPECT_DOUBLE_EQ(coeffs[6], 2);
+}
+
+TEST(Isd, DawnFCDistortion) {
+  ale::json dawn;
+  dawn["optical_distortion"]["dawnfc"]["coefficients"] = {1, 2};
+  std::vector<double> coeffs = ale::getDistortionCoeffs(dawn);
+  EXPECT_EQ(ale::getDistortionModel(dawn), ale::DistortionType::DAWNFC);
+  ASSERT_EQ(coeffs.size(), 2);
+  EXPECT_DOUBLE_EQ(coeffs[0], 1);
+  EXPECT_DOUBLE_EQ(coeffs[1], 2);
+}
+
+TEST(Isd, LroLrocNACDistortion) {
+  ale::json lro;
+  lro["optical_distortion"]["lrolrocnac"]["coefficients"] = {1, 2};
+  std::vector<double> coeffs = ale::getDistortionCoeffs(lro);
+  EXPECT_EQ(ale::getDistortionModel(lro), ale::DistortionType::LROLROCNAC);
+  ASSERT_EQ(coeffs.size(), 2);
+  EXPECT_DOUBLE_EQ(coeffs[0], 1);
+  EXPECT_DOUBLE_EQ(coeffs[1], 2);
+}
+
+TEST(Isd, UnrecognizedDistortion) {
+  ale::json j;
+  j["optical_distortion"]["foo"]["x"] = {1};
+
+  EXPECT_EQ(ale::getDistortionModel(j), ale::DistortionType::TRANSVERSE);
+}
+
+TEST(Isd, BadLogFile) {
+  ale::json j;
+  EXPECT_THROW(ale::getLogFile(j), std::runtime_error);
+}
+
+TEST(Isd, GetSunPositions) {
+  ale::json j;
+  j["sun_position"]["positions"] = {{1, 2, 3}, {4, 5, 6}};
+  std::vector<double> positions = ale::getSunPositions(j);
+  ASSERT_EQ(positions.size(), 6);
+  EXPECT_DOUBLE_EQ(positions[0], 1);
+  EXPECT_DOUBLE_EQ(positions[1], 2);
+  EXPECT_DOUBLE_EQ(positions[2], 3);
+  EXPECT_DOUBLE_EQ(positions[3], 4);
+  EXPECT_DOUBLE_EQ(positions[4], 5);
+  EXPECT_DOUBLE_EQ(positions[5], 6);
+}
+
+TEST(Isd, NoSunPositions) {
+  ale::json j;
+  try {
+    ale::getSunPositions(j);
+    FAIL() << "Expected an exception to be thrown";
+  }
+  catch(std::exception &e) {
+    EXPECT_EQ(std::string(e.what()), "Could not parse the sun positions.");
+  }
+  catch(...) {
+    FAIL() << "Expected an Excpetion with message: \"Could not parse the sun positions.\"";
+  }
+}
+
+TEST(Isd, GetSensorPositions) {
+  ale::json j;
+  j["sensor_position"]["positions"] = {{1, 2, 3}, {4, 5, 6}};
+  std::vector<double> positions = ale::getSensorPositions(j);
+  ASSERT_EQ(positions.size(), 6);
+  EXPECT_DOUBLE_EQ(positions[0], 1);
+  EXPECT_DOUBLE_EQ(positions[1], 2);
+  EXPECT_DOUBLE_EQ(positions[2], 3);
+  EXPECT_DOUBLE_EQ(positions[3], 4);
+  EXPECT_DOUBLE_EQ(positions[4], 5);
+  EXPECT_DOUBLE_EQ(positions[5], 6);
+}
+
+TEST(Isd, NoSensorPositions) {
+  ale::json j;
+  try {
+    ale::getSensorPositions(j);
+    FAIL() << "Expected an exception to be thrown";
+  }
+  catch(std::exception &e) {
+    EXPECT_EQ(std::string(e.what()), "Could not parse the sensor positions.");
+  }
+  catch(...) {
+    FAIL() << "Expected an Excpetion with message: \"Could not parse the sensor positions.\"";
+  }
+}
+
+TEST(Isd, GetSensorVelocities)
+{
+  ale::json j;
+  j["sensor_position"]["velocities"] = {{1, 2, 3}, {4, 5, 6}};
+  std::vector<double> velocities = ale::getSensorVelocities(j);
+  ASSERT_EQ(velocities.size(), 6);
+  EXPECT_DOUBLE_EQ(velocities[0], 1);
+  EXPECT_DOUBLE_EQ(velocities[1], 2);
+  EXPECT_DOUBLE_EQ(velocities[2], 3);
+  EXPECT_DOUBLE_EQ(velocities[3], 4);
+  EXPECT_DOUBLE_EQ(velocities[4], 5);
+  EXPECT_DOUBLE_EQ(velocities[5], 6);
+}
+
+TEST(Isd, NoSensorVelocities) {
+  ale::json j;
+  try {
+    ale::getSensorVelocities(j);
+    FAIL() << "Expected an exception to be thrown";
+  }
+  catch(std::exception &e) {
+    EXPECT_EQ(std::string(e.what()), "Could not parse the sensor velocities.");
+  }
+  catch(...) {
+    FAIL() << "Expected an Excpetion with message: \"Could not parse the sensor velocities.\"";
+  }
+}
+
+TEST(Isd, GetSensorOrientations)
+{
+  ale::json j;
+  j["sensor_orientation"]["quaternions"] = {{1, 2, 3, 4}};
+  std::vector<double> quats = ale::getSensorOrientations(j);
+  ASSERT_EQ(quats.size(), 4);
+  EXPECT_DOUBLE_EQ(quats[0], 1);
+  EXPECT_DOUBLE_EQ(quats[1], 2);
+  EXPECT_DOUBLE_EQ(quats[2], 3);
+  EXPECT_DOUBLE_EQ(quats[3], 4);
+}
+
+TEST(Isd, NoSensorOrientations) {
+  ale::json j;
+  try {
+    ale::getSensorOrientations(j);
+    FAIL() << "Expected an exception to be thrown";
+  }
+  catch(std::exception &e) {
+    EXPECT_EQ(std::string(e.what()), "Could not parse the sensor orientations.");
+  }
+  catch(...) {
+    FAIL() << "Expected an Excpetion with message: \"Could not parse the sensor orientations.\"";
+  }
+}
+
+TEST(Isd, BadNameModel) {
+  std::string bad_json_str("{}");
+  try {
+    ale::getSensorModelName(bad_json_str);
+    // Code that should throw an IException
+    FAIL() << "Expected an exception to be thrown";
+  }
+  catch(std::exception &e) {
+    EXPECT_EQ(std::string(e.what()), "Could not parse the sensor model name.");
+  }
+  catch(...) {
+    FAIL() << "Expected an Excpetion with message: \"Could not parse the sensor model name.\"";
+  }
+}
+
+TEST(Isd, BadNamePlatform) {
+  std::string bad_json_str("{}");
+  try {
+    ale::getPlatformName(bad_json_str);
+    // Code that should throw an IException
+    FAIL() << "Expected an exception to be thrown";
+  }
+  catch(std::exception &e) {
+    ASSERT_EQ(std::string(e.what()), "Could not parse the platform name.");
+  }
+  catch(...) {
+    FAIL() << "Expected an Excpetion with message: \"Could not parse the platform name.\"";
+  }
+}
+
+TEST(Isd, BadImageID) {
+  std::string bad_json_str("{}");
+  try {
+    ale::getImageId(bad_json_str);
+    // Code that should throw an IException
+    FAIL() << "Expected an exception to be thrown";
+  }
+  catch(std::exception &e) {
+    ASSERT_EQ(std::string(e.what()), "Could not parse the image identifier.");
+  }
+  catch(...) {
+    FAIL() << "Expected an Excpetion with message: \"Could not parse the image identifier.\"";
+  }
+}
+
+TEST(Isd, BadSensorName) {
+  std::string bad_json_str("{}");
+  try {
+    ale::getSensorName(bad_json_str);
+    // Code that should throw an IException
+    FAIL() << "Expected an exception to be thrown";
+  }
+  catch(std::exception &e) {
+    ASSERT_EQ(std::string(e.what()), "Could not parse the sensor name.");
+  }
+  catch(...) {
+    FAIL() << "Expected an Excpetion with message: \"Could not parse the sensor name.\"";
+  }
+}
+
+TEST(Isd, BadRadii) {
+  std::string bad_json_str("{}");
+  try {
+    ale::getSemiMajorRadius(bad_json_str);
+    // Code that should throw an IException
+    FAIL() << "Expected an exception to be thrown";
+  }
+  catch(std::exception &e) {
+    EXPECT_EQ(std::string(e.what()), "Could not parse the reference ellipsoid semimajor radius.");
+  }
+  catch(...) {
+    FAIL() << "Expected an Excpetion with message: \"Could not parse the reference ellipsoid semimajor radius.\"";
+  }
+
+  try {
+    ale::getSemiMinorRadius(bad_json_str);
+    // Code that should throw an IException
+    FAIL() << "Expected an exception to be thrown";
+  }
+  catch(std::exception &e) {
+    EXPECT_EQ(std::string(e.what()), "Could not parse the reference ellipsoid semiminor radius.");
+  }
+  catch(...) {
+    FAIL() << "Expected an Excpetion with message: \"Could not parse the reference ellipsoid semiminor radius.\"";
+  }
+}
+
+TEST(Isd, BadSumming) {
+  std::string bad_json_str("{}");
+  try {
+    ale::getSampleSumming(bad_json_str);
+    // Code that should throw an IException
+    FAIL() << "Expected an exception to be thrown";
+  }
+  catch(std::exception &e) {
+    EXPECT_EQ(std::string(e.what()), "Could not parse the sample direction detector pixel summing.");
+  }
+  catch(...) {
+    FAIL() << "Expected an Excpetion with message: \"Could not parse the sample direction detector pixel summing.\"";
+  }
+
+  try {
+    ale::getLineSumming(bad_json_str);
+    // Code that should throw an IException
+    FAIL() << "Expected an exception to be thrown";
+  }
+  catch(std::exception &e) {
+    EXPECT_EQ(std::string(e.what()), "Could not parse the line direction detector pixel summing.");
+  }
+  catch(...) {
+    FAIL() << "Expected an Excpetion with message: \"Could not parse the line direction detector pixel summing.\"";
+  }
+}
+
+TEST(Isd, BadFocalLength) {
+  std::string bad_json_str("{}");
+  try {
+    ale::getFocalLength(bad_json_str);
+    // Code that should throw an IException
+    FAIL() << "Expected an exception to be thrown";
+  }
+  catch(std::exception &e) {
+    EXPECT_EQ(std::string(e.what()), "Could not parse the focal length.");
+  }
+  catch(...) {
+    FAIL() << "Expected an Excpetion with message: \"Could not parse the focal length.\"";
+  }
+
+  try {
+    ale::getFocalLengthUncertainty(bad_json_str);
+    // Code that should throw an IException
+    FAIL() << "Expected an exception to be thrown";
+  }
+  catch(std::exception &e) {
+    EXPECT_EQ(std::string(e.what()), "Could not parse the focal length uncertainty.");
+  }
+  catch(...) {
+    FAIL() << "Expected an Excpetion with message: \"Could not parse the focal length uncertainty.\"";
+  }
+}
+
+TEST(Isd, BadDetectorCenter) {
+  std::string bad_json_str("{}");
+  try {
+    ale::getDetectorCenterSample(bad_json_str);
+    // Code that should throw an IException
+    FAIL() << "Expected an exception to be thrown";
+  }
+  catch(std::exception &e) {
+    EXPECT_EQ(std::string(e.what()), "Could not parse the detector center sample.");
+  }
+  catch(...) {
+    FAIL() << "Expected an Excpetion with message: \"Could not parse the detector center sample.\"";
+  }
+
+  try {
+    ale::getDetectorCenterLine(bad_json_str);
+    // Code that should throw an IException
+    FAIL() << "Expected an exception to be thrown";
+  }
+  catch(std::exception &e) {
+    EXPECT_EQ(std::string(e.what()), "Could not parse the detector center line.");
+  }
+  catch(...) {
+    FAIL() << "Expected an Excpetion with message: \"Could not parse the detector center line.\"";
+  }
+}
+
+TEST(Isd, BadStartingDetector) {
+  std::string bad_json_str("{}");
+  try {
+    ale::getDetectorStartingSample(bad_json_str);
+    // Code that should throw an IException
+    FAIL() << "Expected an exception to be thrown";
+  }
+  catch(std::exception &e) {
+    EXPECT_EQ(std::string(e.what()), "Could not parse the detector starting sample.");
+  }
+  catch(...) {
+    FAIL() << "Expected an Excpetion with message: \"Could not parse the detector starting sample.\"";
+  }
+
+  try {
+    ale::getDetectorStartingLine(bad_json_str);
+    // Code that should throw an IException
+    FAIL() << "Expected an exception to be thrown";
+  }
+  catch(std::exception &e) {
+    EXPECT_EQ(std::string(e.what()), "Could not parse the detector starting line.");
+  }
+  catch(...) {
+    FAIL() << "Expected an Excpetion with message: \"Could not parse the detector starting line.\"";
+  }
+}
+
+TEST(Isd, BadFocal2Pixel) {
+  std::string bad_json_str("{}");
+  try {
+    ale::getFocal2PixelSamples(bad_json_str);
+    // Code that should throw an IException
+    FAIL() << "Expected an exception to be thrown";
+  }
+  catch(std::exception &e) {
+    EXPECT_EQ(std::string(e.what()), "Could not parse the focal plane coordinate to "
+                                     "detector samples transformation.");
+  }
+  catch(...) {
+    FAIL() << "Expected an Excpetion with message: \"Could not parse the focal plane coordinate to "
+                                                    "detector samples transformation.\"";
+  }
+
+  try {
+    ale::getFocal2PixelLines(bad_json_str);
+    // Code that should throw an IException
+    FAIL() << "Expected an exception to be thrown";
+  }
+  catch(std::exception &e) {
+    EXPECT_EQ(std::string(e.what()), "Could not parse the focal plane coordinate to "
+                                     "detector lines transformation.");
+  }
+  catch(...) {
+    FAIL() << "Expected an Excpetion with message: \"Could not parse the focal plane coordinate to "
+                                                     "detector lines transformation.\"";
+  }
+}
+
+TEST(Isd, BadDistortionModel) {
+  std::string bad_json_str("{}");
+  try {
+    ale::getDistortionModel(bad_json_str);
+    // Code that should throw an IException
+    FAIL() << "Expected an exception to be thrown";
+  }
+  catch(std::exception &e) {
+    EXPECT_EQ(std::string(e.what()), "Could not parse the distortion model.");
+  }
+  catch(...) {
+    FAIL() << "Expected an Excpetion with message: \"Could not parse the distortion model.\"";
+  }
+}
+
+TEST(Isd, BadDistortionTransverse) {
+  ale::json bad_json;
+  bad_json["optical_distortion"]["transverse"]["x"] = {"NaN"};
+  bad_json["optical_distortion"]["transverse"]["y"] = {"NaN"};
+
+  try {
+    ale::getDistortionCoeffs(bad_json);
+    // Code that should throw an IException
+    FAIL() << "Expected an exception to be thrown";
+  }
+  catch(std::exception &e) {
+    EXPECT_EQ(std::string(e.what()), "Could not parse a set of transverse distortion model coefficients.");
+  }
+  catch(...) {
+    FAIL() << "Expected an Excpetion with message: \"Could not parse a set of transverse distortion model coefficients.\"";
+  }
+}
+
+TEST(Isd, BadDistortionRadial) {
+  ale::json bad_json;
+  bad_json["optical_distortion"]["radial"]["coefficients"] = {"NaN"};
+
+  try {
+    ale::getDistortionCoeffs(bad_json);
+    // Code that should throw an IException
+    FAIL() << "Expected an exception to be thrown";
+  }
+  catch(std::exception &e) {
+    EXPECT_EQ(std::string(e.what()), "Could not parse the radial distortion model coefficients.");
+  }
+  catch(...) {
+    FAIL() << "Expected an Excpetion with message: \"Could not parse the radial distortion model coefficients.\"";
+  }
+}
+
+TEST(Isd, BadDistortionDawnFC) {
+  ale::json bad_json;
+  bad_json["optical_distortion"]["dawnfc"]["coefficients"] = {"NaN"};
+
+  try {
+    ale::getDistortionCoeffs(bad_json);
+    // Code that should throw an IException
+    FAIL() << "Expected an exception to be thrown";
+  }
+  catch(std::exception &e) {
+    EXPECT_EQ(std::string(e.what()), "Could not parse the dawn radial distortion model coefficients.");
+  }
+  catch(...) {
+    FAIL() << "Expected an Excpetion with message: \"Could not parse the dawn radial distortion model coefficients.\"";
+  }
+}
+
+TEST(Isd, BadDistortionKaguyaLISM) {
+  ale::json bad_json;
+  bad_json["optical_distortion"]["kaguyalism"]["x"] = {"NaN"};
+  bad_json["optical_distortion"]["kaguyalism"]["y"] = {"NaN"};
+  try {
+    ale::getDistortionCoeffs(bad_json);
+    // Code that should throw an IException
+    FAIL() << "Expected an exception to be thrown";
+  }
+  catch(std::exception &e) {
+    EXPECT_EQ(std::string(e.what()), "Could not parse a set of Kaguya LISM distortion model coefficients.");
+  }
+  catch(...) {
+    FAIL() << "Expected an Excpetion with message: \"Could not parse a set of Kaguya LISM distortion model coefficients.\"";
+  }
+}
+
+TEST(Isd, BadDistortionLroLrocNac) {
+  ale::json bad_json;
+  bad_json["optical_distortion"]["lrolrocnac"]["coefficients"] = {"NaN"};
+  try {
+    ale::getDistortionCoeffs(bad_json);
+    // Code that should throw an IException
+    FAIL() << "Expected an exception to be thrown";
+  }
+  catch(std::exception &e) {
+    EXPECT_EQ(std::string(e.what()), "Could not parse the lrolrocnac distortion model coefficients.");
+  }
+  catch(...) {
+    FAIL() << "Expected an Excpetion with message: \"Could not parse the lrolrocnac distortion model coefficients.\"";
+  }
+}
+
+TEST(Isd, BadImageSize) {
+  std::string bad_json_str("{}");
+  try {
+    ale::getTotalSamples(bad_json_str);
+    // Code that should throw an IException
+    FAIL() << "Expected an exception to be thrown";
+  }
+  catch(std::exception &e) {
+    EXPECT_EQ(std::string(e.what()), "Could not parse the number of samples in the image.");
+  }
+  catch(...) {
+    FAIL() << "Expected an Excpetion with message: \"Could not parse the number of samples in the image.\"";
+  }
+
+  try {
+    ale::getTotalLines(bad_json_str);
+    // Code that should throw an IException
+    FAIL() << "Expected an exception to be thrown";
+  }
+  catch(std::exception &e) {
+    EXPECT_EQ(std::string(e.what()), "Could not parse the number of lines in the image.");
+  }
+  catch(...) {
+    FAIL() << "Expected an Excpetion with message: \"Could not parse the number of lines in the image.\"";
+  }
+}
+
+TEST(Isd, BadReferenceHeight) {
+  std::string bad_json_str("{}");
+  try {
+    ale::getMinHeight(bad_json_str);
+    // Code that should throw an IException
+    FAIL() << "Expected an exception to be thrown";
+  }
+  catch(std::exception &e) {
+    EXPECT_EQ(std::string(e.what()), "Could not parse the minimum height above the reference ellipsoid.");
+  }
+  catch(...) {
+    FAIL() << "Expected an Excpetion with message: \"Could not parse the minimum height above the reference ellipsoid.\"";
+  }
+
+  try {
+    ale::getMaxHeight(bad_json_str);
+    // Code that should throw an IException
+    FAIL() << "Expected an exception to be thrown";
+  }
+  catch(std::exception &e) {
+    EXPECT_EQ(std::string(e.what()), "Could not parse the maximum height above the reference ellipsoid.");
+  }
+  catch(...) {
+    FAIL() << "Expected an Excpetion with message: \"Could not parse the maximum height above the reference ellipsoid.\"";
+  }
+}
+
+TEST(Isd, BadLineScanRate) {
+  std::string bad_json_str("{}");
+  try {
+    ale::getLineScanRate(bad_json_str);
+    // Code that should throw an IException
+    FAIL() << "Expected an exception to be thrown";
+  }
+  catch(std::exception &e) {
+    ASSERT_EQ(std::string(e.what()), "Could not parse the integration start lines in the integration time table.");
+  }
+  catch(...) {
+    FAIL() << "Expected an Excpetion with message: \"Could not parse the integration start lines in the integration time table.\"";
+  }
+}
+
+TEST(Isd, BadEphemerisTimes) {
+  std::string bad_json_str("{}");
+  try {
+    ale::getStartingTime(bad_json_str);
+    // Code that should throw an IException
+    FAIL() << "Expected an exception to be thrown";
+  }
+  catch(std::exception &e) {
+    EXPECT_EQ(std::string(e.what()), "Could not parse the image start time.");
+  }
+  catch(...) {
+    FAIL() << "Expected an Excpetion with message: \"Could not parse the image start time.\"";
+  }
+
+  try {
+    ale::getCenterTime(bad_json_str);
+    // Code that should throw an IException
+    FAIL() << "Expected an exception to be thrown";
+  }
+  catch(std::exception &e) {
+    EXPECT_EQ(std::string(e.what()), "Could not parse the center image time.");
+  }
+  catch(...) {
+    FAIL() << "Expected an Excpetion with message: \"Could not parse the center image time.\"";
+  }
+}