From 56df810e89b4c2251e5912803d7c0dcfde89b98a Mon Sep 17 00:00:00 2001
From: Oleg Alexandrov <oleg.alexandrov@gmail.com>
Date: Fri, 22 Apr 2022 15:11:10 -0700
Subject: [PATCH] Expansion of the usgscsm_cam_test tool and documentation
 (#377)

* Flesh out the usgscsm_cam_test tool and minor touchups

* Adjust the test command

* Try to create a manual page for the new tool

* Fix link to subpage

* Expand the doc

Update usgscsm_cam_test.rst

Minor wording changes

More minor wording

More wording changes

Consistent style and other minor

Another wording fix

* Minor header file tune-up

* Use true and false instead of 0 and 1

* Add test to read model state

* Minor code touch up, and maybe the submodule error goes away

* Try to fix the json issue

* The json thing just would not let go
---
 README.md                              |  90 +++++---
 bin/usgscsm_cam_test.cc                | 276 ++++++++++++++++++++-----
 docs/source/tools/usgscsm_cam_test.rst |  42 ++++
 src/UsgsAstroFrameSensorModel.cpp      |   1 +
 src/UsgsAstroLsSensorModel.cpp         |   3 +-
 src/UsgsAstroSarSensorModel.cpp        |   5 +-
 tests/CMakeLists.txt                   |  15 +-
 7 files changed, 348 insertions(+), 84 deletions(-)
 create mode 100644 docs/source/tools/usgscsm_cam_test.rst

diff --git a/README.md b/README.md
index e1d5f64..c22e4e4 100644
--- a/README.md
+++ b/README.md
@@ -4,62 +4,90 @@
 
 # USGSCSM
 
-Community Sensor Model (CSM) compliant sensor models created by USGS Astrogeology
-Science Center.
+This library provides *Community Sensor Model (CSM)*-compliant sensor models 
+created by the USGS Astrogeology Science Center.
 
-USGSCSM contains three different sensor models. The first, is a generic
-framing camera model written from scratch. The second is a generic line scan
-camera model based on code from BAE Systems Information and Electronic Systems
-Integration Inc. The third is a generic SAR sensor model.
+USGSCSM contains three different sensor models. The first is a
+generic framing camera model written from scratch. The second is a
+generic line scan camera model based on code from BAE Systems
+Information and Electronic Systems Integration, Inc. The third is a
+generic synthetic-aperture radar (SAR) sensor model.
 
 ## Using USGSCSM
 
 This library is a CSM plugin library that is intended to be dynamically loaded
-at run time along side the
+at run-time alongside the
 [CSM API library](https://github.com/USGS-Astrogeology/csm).
 
-Once, the library is loaded, it can be accessed through the CSM Plugin interface.
-For an example of how to do through the CSM c++ interface see the SensorModelFactory
+Once the library is loaded, it can be accessed through the CSM plugin interface.
+For an example of how to do through the CSM C++ interface see the SensorModelFactory
 class in [SensorUtils](https://github.com/USGS-Astrogeology/SensorUtils).
 For an example of how to do this through the CSM Python bindings see this
 [notebook](http://nbviewer.jupyter.org/gist/thareUSGS/4c0eb72799edc33ff4816b2587027148).
 
-From the CSM Plugin interface, a generic framing camera model
-(USGS_ASTRO_FRAME_SENSOR_MODEL) or generic line scan camera model
-(USGS_ASTRO_LINE_SCANNER_SENSOR_MODEL) can be instantiated from suitable Image
-Support Data (ISD). Under the CSM standard, each plugin library can define its
-own ISD format. This library uses an auxiliary JSON formatted file that must be
-next to the image file passed to the CSM::ISD class. We provide an OpenAPI
-server for generating these,
-[pfeffernusse](https://github.com/USGS-Astrogeology/pfeffernusse). The swagger
-specification is located on
+From the CSM plugin interface, a generic framing camera model
+(USGS_ASTRO_FRAME_SENSOR_MODEL), line scan camera model
+(USGS_ASTRO_LINE_SCANNER_SENSOR_MODEL), or a SAR model
+(USGS_ASTRO_SAR_SENSOR_MODEL) can be instantiated from a suitable *Image
+Support Data (ISD)* file.
+
+## Camera model format and model state
+
+Under the CSM standard, each plugin library can define its own ISD
+camera model format. This library uses an auxiliary JSON formatted file that must
+be next to the image file passed to the CSM::ISD class. We provide an
+OpenAPI server for generating these,
+[pfeffernusse](https://github.com/USGS-Astrogeology/pfeffernusse). The
+swagger specification is located on
 [swaggerhub](https://app.swaggerhub.com/apis/USGS-Astro/pfeffernusse2/0.1.4-oas3).
-You can also use [ALE](https://github.com/USGS-Astrogeology/ale) directly with
-metakernels to generate the auxiliary JSON file.
+You can also use [ALE](https://github.com/USGS-Astrogeology/ale)
+directly with metakernels to generate the auxiliary JSON file.
+
+The camera model read from an ISD file is converted at load time to an
+internal representation which makes camera operations more
+efficient. This optimized *model state* can be saved to disk as a
+JSON-formatted file, be used interchangeably with the
+original ISD model, and also shared among various photogrammetric
+packages.
+
+The camera model state can be modified by an application of a rotation
+and translation, which is necessary in order to refine a camera's
+position and orientation in photogrammetry, while these operations are
+not easy to express in the original ISD format.
+
+This library provides functionality for saving the model state file,
+as discussed in the next section.
+
+## Camera model processsing
+
+USGSCSM ships with a program named ``usgscsm_cam_test``, which is
+able to load a CSM camera model, whether in the original ISD format or its
+model state representation, export the model state, and perform basic
+camera operations, as described in its
+[documentation](docs/source/tools/usgscsm_cam_test.rst).
 
 ## Enabling logging
 
-You can enable logging of the internal operations in the sensor models by setting
-the `USGSCSM_LOG_FILE` environment variable to the file you would like to log to.
-You can also log to standard out by setting it to `stdout` or standard error
-by setting it to `stderr`. Note that these logs can become quite large, multiple
-GBs.
+Logging of the internal operations in the sensor models can be enabled by setting
+the `USGSCSM_LOG_FILE` environment variable to the file the log should be written to.
+To have the logging information printed to the standard output or standard error, set
+this to `stdout` or `stderr`. Note that these logs can become several GB in size.
 
 ---
 
-## Build Requirements
+## Build requirements
 
 * cmake 3.15 or newer
 * GNU-compatible Make
 * a C++11 compliant compiler
 
-This repository has all of its external c++ dependencies included in it. The
+This repository has all of its external C++ dependencies included in it. The
 excellent header-only JSON library
 [JSON for Modern C++](https://github.com/nlohmann/json) is included directly in
 the source code. The other three dependencies, The Abstraction Library for
 Ephemerides, the CSM API library, and googletest are included as git submodules.
 When you clone this library make sure you add the `--recursive` flag to your
-`git clone` command. Alterntively, you can run
+`git clone` command. Alternatively, you can run
 `git submodule update --init --recursive` after cloning.
 
 You can also install the build requirements using Conda with the provided
@@ -88,13 +116,13 @@ You can also disable the tests and the googletest dependency by adding the
 
 ## Testing USGSCSM
 
-All of the tests for USGSCSM are written in the googletests framework
+All of the tests for USGSCSM are written in the googletest framework
 and are run via ctest. To run all of the tests simply run `ctest` in the build.
 
 All of the tests are purposefully written to use generic data that values have
 been hand validated. This data can be found under `tests/data`.
 
-## Code Style
+## Code style
 
 This software package uses a modified form of the
 [Google C++ Style Guide](https://google.github.io/styleguide/cppguide.html).
@@ -110,4 +138,4 @@ To attempt to automatically format any new code to this style, run:
 For more information see: [ClangFormat](https://clang.llvm.org/docs/ClangFormat.html)
 
 To check for compliance, run: `cpplint file.cpp` and ignore errors in the list of exclusions above.
-For more information, see: [cpplint](https://github.com/cpplint/cpplint)
+For more information, see: [cpplint](https://github.com/cpplint/cpplint).
diff --git a/bin/usgscsm_cam_test.cc b/bin/usgscsm_cam_test.cc
index a520630..48e894d 100644
--- a/bin/usgscsm_cam_test.cc
+++ b/bin/usgscsm_cam_test.cc
@@ -1,86 +1,270 @@
 // A tool to perform some basic tests and operations on a CSM camera model.
-//
+// 
 // Functionality:
+//--------------
 //
-// - Load a CSM model in ISD format.
-//
-// Future functionality:
+// - Load a CSM model in ISD format or model state format, via:
+//   --model <model file>
 //
-// - Test projecting rays from the camera to ground and vice-versa.
-// - Load a CSM model state (stored in a .json file, just like
-//   an ISD model).
-// - Ability to export a CSM model in ISD format to a CSM model state file.
+// - Save the model state if invoked with:
+//   --output-model-state <model state .json file>
+// 
+// - Test projecting rays from the camera to ground, then back,
+//   and compare with the original pixel values.  
 
 #include <UsgsAstroPlugin.h>
 #include <RasterGM.h>
 #include <UsgsAstroLsSensorModel.h>
 
 #include <iostream>
-int main(int argc, char **argv) {
+#include <fstream>
+#include <getopt.h> // For parsing command-line options
 
-  if (argc != 2) {
-    std::cerr << "Usage: " << argv[0] << " <model file>" << std::endl;
-    return 1;
+struct Options {
+  std::string model;              // the .json file in isd or model state format
+  std::string output_model_state; // the output model state in .json format
+  int sample_rate;
+  double subpixel_offset, height_above_datum;
+  Options(): sample_rate(0), subpixel_offset(0.0), height_above_datum(0.0) {}
+};
+  
+// Parse the input options with getopt.
+bool parseOptions(int argc, char **argv, Options & opt) {
+
+  // Collect the parsed options in a map
+  std::map<std::string, std::string> parsed_options;
+  
+  int digit_optind = 0;
+  while (1) {
+    int this_option_optind = optind ? optind : 1;
+    std::string short_options = ""; // no short options
+    int option_index = 0;
+    static struct option long_options[] = {
+      {"model",              required_argument, 0,  0},
+      {"output-model-state", required_argument, 0,  0},
+      {"sample-rate",        required_argument, 0,  0},
+      {"subpixel-offset",    required_argument, 0,  0},
+      {"height-above-datum", required_argument, 0,  0},
+      {"help",               no_argument,       0,  0}
+    };
+
+    int c = getopt_long(argc, argv, short_options.c_str(), long_options, &option_index);
+    if (c == -1) // done parsing all options
+      break;
+
+    std::string opt_name = long_options[option_index].name;
+    if (c == 0) {
+      if (optarg) {
+        // The option has a value
+        parsed_options[opt_name] = optarg;
+      } else {
+        // The option has no value
+        parsed_options[opt_name] = "";
+      }
+    } else {
+      // Something went wrong in parsing. The parser will print a message. Just add to it.
+      std::cout << "Cannot continue.\n";
+      return false;
+    }
+  }
+
+  // Print unexpected input arguments
+  if (optind < argc) {
+    printf("Unexpected argument: ");
+    while (optind < argc)
+      printf("%s ", argv[optind++]);
+    printf("\n");
+
+    return false;
+  }
+
+  // See if the user asked for help
+  bool print_help_and_exit = (parsed_options.find("help") != parsed_options.end());
+  
+  // It is safe to access non-existent values from a map, the result
+  // will be an empty string
+  opt.model = parsed_options["model"];
+  if (opt.model == "") 
+    print_help_and_exit = true;
+
+  // Enforce that the sample rate is an integer
+  double sample_rate_double = atof(parsed_options["sample-rate"].c_str());
+  if (sample_rate_double != round(sample_rate_double)) {
+    std::cout << "The value of --sample-rate must be an integer.\n";
+    print_help_and_exit = true;
   }
+  
+  // Print the help and exit
+  if (print_help_and_exit) {
+    std::cout << "Usage: " << argv[0] << " --model <model file> [other options]"
+              << "\nSee the documentation for more information.\n";
+    return false;
+  }
+
+  // Collect all other option values. If not set the values will default to 0.
+  opt.output_model_state = parsed_options["output-model-state"];
+  opt.sample_rate        = sample_rate_double;
+  opt.subpixel_offset    = atof(parsed_options["subpixel-offset"].c_str());
+  opt.height_above_datum = atof(parsed_options["height-above-datum"].c_str());
+
+  return true;
+}
+
+// Read a file's content in a single string
+bool readFileInString(std::string const& filename, std::string & str) {
+
+  str.clear(); // clear the output
+
+  std::ifstream ifs(filename.c_str());
+  if (!ifs.is_open()) {
+    std::cout << "Cannot open file: " << filename << std::endl;
+    return false;
+  }
+  
+  ifs.seekg(0, std::ios::end);   
+  str.reserve(ifs.tellg());
+  ifs.seekg(0, std::ios::beg);
+  str.assign((std::istreambuf_iterator<char>(ifs)),
+             std::istreambuf_iterator<char>());
+  ifs.close();
+
+  return true;
+}
+
+// Sort the errors and print some stats
+void printErrors(std::vector<double> & errors) {
+  std::sort(errors.begin(), errors.end());
+
+  if (errors.empty()) {
+    std::cout << "Empty list of errors.\n";
+    return;
+  }
+  
+  std::cout << "Norm of pixel errors after projecting from camera to ground and back.\n";
+  std::cout << "Min:    " << errors[0] << "\n";
+  std::cout << "Median: " << errors[errors.size()/2] << "\n";
+  std::cout << "Max:    " << errors.back() << "\n";
+  std::cout << "Count:  " << errors.size() << "\n";
+}
+
+double pixDiffNorm(csm::ImageCoord const& a, csm::ImageCoord const& b) {
+  return sqrt((a.line - b.line) * (a.line - b.line) + (a.samp - b.samp) * (a.samp - b.samp));
+}
+
+// Load a CSM camera model from an ISD or state file. Return true on success.
+bool loadCsmCameraModel(std::string const& model_file,
+                       std::shared_ptr<csm::RasterGM> & model) {
 
   // This is needed to trigger loading libusgscsm.so. Otherwise 0
   // plugins are detected.
   UsgsAstroLsSensorModel lsModel;
 
-  // Load the isd
-  std::string model_file = argv[1];
+  // Try to read the model as an ISD
   csm::Isd isd(model_file);
-  std::cout << "Loading model: " << model_file << std::endl;
-
+  
+  // Read the model in a string, for potentially finding parsing the
+  // model state from it.
+  std::string model_state;
+  if (!readFileInString(model_file, model_state))
+    return false;
+  
   // Check if loading the model worked
   bool success = false;
 
-  std::shared_ptr<csm::RasterGM> model;
-
-  // Try all detected plugins and models for each plugin
+  // Try all detected plugins and all models for each plugin.
   csm::PluginList plugins = csm::Plugin::getList();
   for (auto iter = plugins.begin(); iter != plugins.end(); iter++) {
-
+    
     const csm::Plugin* csm_plugin = (*iter);
-
     std::cout << "Detected CSM plugin: " << csm_plugin->getPluginName()  << "\n";
-    // For each plugin, loop through the available models.
+
     size_t num_models = csm_plugin->getNumModels();
     std::cout << "Number of models for this plugin: " << num_models << "\n";
+    
+    // First try to construct the model from isd, and if that fails, from the state
+    csm::Model *csm = NULL;
+    csm::WarningList* warnings = NULL;
     for (size_t i = 0; i < num_models; i++) {
-
+      
       std::string model_name = (*iter)->getModelName(i);
+      if (csm_plugin->canModelBeConstructedFromISD(isd, model_name, warnings)) {
+        // Try to construct the model from the isd
+        csm = csm_plugin->constructModelFromISD(isd, model_name, warnings);
+        std::cout << "Loaded a CSM model of type " << model_name << " from ISD file "
+                  << model_file << ".\n";
+        success = true;
+      } else if (csm_plugin->canModelBeConstructedFromState(model_name, model_state, warnings)) {
+        // Try to construct it from the model state
+        csm = csm_plugin->constructModelFromState(model_state, warnings);
+        std::cout << "Loaded a CSM model of type " << model_name << " from model state file "
+                  << model_file << ".\n";
+        success = true;
+      } else {
+        // No luck so far
+        continue;
+      }
 
-      csm::WarningList* warnings = NULL;
-
-      if (csm_plugin->canModelBeConstructedFromISD(isd, model_name)) {
-
-        csm::Model *csm = csm_plugin->constructModelFromISD(isd, model_name, warnings);
-        csm::RasterGM *modelPtr = dynamic_cast<csm::RasterGM*>(csm);
-
-        if (modelPtr == NULL) {
-          std::cerr << "Could not load correctly a CSM model of type: "
-                    << model_name << "\n";
-          return 1;
-        } else {
-          // Assign it to a smart pointer which will handle its deallocation
-          model = std::shared_ptr<csm::RasterGM>(modelPtr);
-          success = true;
-          std::cout << "Loaded CSM model of type " << model_name
-                    << " from " << model_file << ".\n";
-        }
+      csm::RasterGM *modelPtr = dynamic_cast<csm::RasterGM*>(csm);
+      if (modelPtr == NULL) {
+        // Normally earlier checks should be enough and this should not happen
+        std::cerr << "Could not load correctly a CSM model.\n";
+        return false;
+      } else {
+        // Assign to a smart pointer which will handle deallocation
+        model = std::shared_ptr<csm::RasterGM>(modelPtr);
+        break;
       }
     }
   }
-
+  
   if (!success) {
     std::cerr << "Failed to load a CSM model from: " << model_file << ".\n";
-    return 1;
+    return false;
   }
+  
+  return true;
+}
+
+int main(int argc, char **argv) {
+
+  Options opt;
+  if (!parseOptions(argc, argv, opt))
+    return 1;
 
-  csm::ImageVector image_size = model->getImageSize();
-  std::cout << "Camera image rows and columns: "
-            << image_size.samp << ' ' << image_size.line << "\n";
+  // Keep the model as smart pointer to the class from which the
+  // specific model types inherit.
+  std::shared_ptr<csm::RasterGM> model;
+
+  if (!loadCsmCameraModel(opt.model, model))
+    return 1;
 
+  if (opt.output_model_state != "") {
+    std::cout << "Writing model state: " << opt.output_model_state << "\n";
+    std::ofstream ofs(opt.output_model_state.c_str());
+    ofs << model->getModelState() << "\n";
+    ofs.close();
+  }
+
+  if (opt.sample_rate > 0) {
+    csm::ImageVector image_size = model->getImageSize();
+    std::cout << "\n";
+    std::cout << "Camera image rows and columns: "
+              << image_size.line << ' ' << image_size.samp << "\n";
+    std::cout << "Row and column sample rate: " << opt.sample_rate << "\n";
+    std::cout << "Subpixel offset for each pixel: " << opt.subpixel_offset << "\n";
+    std::cout << "Ground height (relative to datum): " << opt.height_above_datum << "\n";
+    std::vector<double> errors;
+    for (int samp = 0; samp < image_size.samp; samp += opt.sample_rate) {
+      for (int line = 0; line < image_size.line; line += opt.sample_rate) {
+        csm::ImageCoord c(line + opt.subpixel_offset, samp + opt.subpixel_offset);
+        csm::EcefCoord ground = model->imageToGround(c, opt.height_above_datum);
+        csm::ImageCoord d = model->groundToImage(ground);
+        double error = pixDiffNorm(c, d);
+        errors.push_back(error);
+      }   
+    }
+    printErrors(errors);
+  }
+    
   return 0;
 }
diff --git a/docs/source/tools/usgscsm_cam_test.rst b/docs/source/tools/usgscsm_cam_test.rst
new file mode 100644
index 0000000..f630110
--- /dev/null
+++ b/docs/source/tools/usgscsm_cam_test.rst
@@ -0,0 +1,42 @@
+usgscsm_cam_test
+================
+
+This program is shipped with the USGSCSM library in the ``bin`` directory.
+It can be used for performing several operations involving CSM camera
+models, such as loading a camera model, whether in the original ISD format
+or its model state representation, exporting the model state, computing
+projections from pixels in the camera to the ground and back, and
+then verifying that the original pixels are obtained.
+
+Example (load a camera model and save the model state)::
+
+    usgscsm_cam_test --model input.json --output-model-state out_state.json
+
+Example (perform per-pixel operations)::
+
+    usgscsm_cam_test --model camera.json --sample-rate 100 \
+       --height-above-datum 320.3 --subpixel-offset 0.57
+
+Command line options
+~~~~~~~~~~~~~~~~~~~~
+
+--model <string (default: "")>
+    Input CSM model (in ISD or model state format).
+--output-model-state <string (default: "")>
+    If specified, save the model state to this file.
+
+--sample-rate <integer (default: 0)>
+    If positive, select pixels in the camera at the intersection of 
+    every one out of this many rows and columns, and perform projections 
+    to the ground and back.
+    
+--subpixel-offset <double (default: 0.0)> 
+    Add this value to every pixel row and column to 
+    be sampled.
+
+--height-above-datum <double (default: 0.0)>
+    Let the ground be obtained from the datum for this camera by 
+    adding to its radii this value (the units are meters).
+
+--help <no value>
+    Print the usage message.
diff --git a/src/UsgsAstroFrameSensorModel.cpp b/src/UsgsAstroFrameSensorModel.cpp
index 1930432..7577371 100644
--- a/src/UsgsAstroFrameSensorModel.cpp
+++ b/src/UsgsAstroFrameSensorModel.cpp
@@ -752,6 +752,7 @@ std::string UsgsAstroFrameSensorModel::getModelState() const {
        {m_referencePointXyz.x, m_referencePointXyz.y, m_referencePointXyz.z}},
       {"m_currentParameterCovariance", m_currentParameterCovariance}};
 
+  // Use dump(2) to avoid creating the model string as a single long line
   std::string stateString = getModelName() + "\n" + state.dump(2);
   return stateString;
 }
diff --git a/src/UsgsAstroLsSensorModel.cpp b/src/UsgsAstroLsSensorModel.cpp
index 3ff95ab..fd03eed 100644
--- a/src/UsgsAstroLsSensorModel.cpp
+++ b/src/UsgsAstroLsSensorModel.cpp
@@ -435,8 +435,9 @@ std::string UsgsAstroLsSensorModel::getModelState() const {
   MESSAGE_LOG("num sun positions: {} ", m_sunPosition.size())
 
   state["m_sunVelocity"] = m_sunVelocity;
-  MESSAGE_LOG("num sun velocities: {} ", m_sunVelocity.size())
+  MESSAGE_LOG("num sun velocities: {} ", m_sunVelocity.size());
 
+  // Use dump(2) to avoid creating the model string as a single long line
   std::string stateString = getModelName() + "\n" + state.dump(2);
   return stateString;
 }
diff --git a/src/UsgsAstroSarSensorModel.cpp b/src/UsgsAstroSarSensorModel.cpp
index f84b206..95f1822 100644
--- a/src/UsgsAstroSarSensorModel.cpp
+++ b/src/UsgsAstroSarSensorModel.cpp
@@ -330,7 +330,8 @@ string UsgsAstroSarSensorModel::getModelState() const {
   state["m_scaleConversionTimes"] = m_scaleConversionTimes;
   state["m_covariance"] = m_covariance;
 
-  std::string stateString = getModelName() + "\n" + state.dump();
+  // Use dump(2) to avoid creating the model string as a single long line
+  std::string stateString = getModelName() + "\n" + state.dump(2);
   return stateString;
 }
 
@@ -503,7 +504,7 @@ csm::EcefCoord UsgsAstroSarSensorModel::imageToGround(
   MESSAGE_LOG("Calculating imageToGround with: {}, {}, {}, {}", imagePt.samp,
               imagePt.line, height, desiredPrecision);
   double time =
-      m_startingEphemerisTime + (imagePt.line - 0.5) * m_exposureDuration;
+    m_startingEphemerisTime + (imagePt.line - 0.5) * m_exposureDuration;
   double groundRange = (imagePt.samp - 0.5) * m_scaledPixelWidth;
   std::vector<double> coeffs = getRangeCoefficients(time);
   double slantRange = groundRangeToSlantRange(groundRange, coeffs);
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 8db7152..5f671fb 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -18,9 +18,16 @@ else()
     target_link_libraries(runCSMCameraModelTests usgscsm ${GTEST_LIBRARIES} ${GTEST_MAIN_LIBRARIES} pthread)
 endif()
 
-# Test the test_usgscsm_cam_test program
-add_test(NAME test_usgscsm_cam_test_linescan
-    COMMAND usgscsm_cam_test data/orbitalLineScan.json
+# Test the test_usgscsm_cam_test program. These tests will be generated
+# by cmake and then executed in the current order. The second test
+# uses as input the output of the first test.
+# 1. Save the model state for an ISD camera model.
+add_test(NAME test_usgscsm_cam_test_save_state
+    COMMAND usgscsm_cam_test --model data/orbitalLineScan.json --output-model-state model_state.json
     WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}/tests)
-      
+# 2. Load back the state and save it again.
+add_test(NAME test_usgscsm_cam_test_load_state
+    COMMAND usgscsm_cam_test --model model_state.json --output-model-state model_state2.json
+    WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}/tests)
+
 gtest_discover_tests(runCSMCameraModelTests WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}/tests)
-- 
GitLab