Writing custom JSON-based applications

A simple JSON-based application contains just a few lines of code:

#include <Tudat/JsonInterface/jsonInterface.h>

int main( )
{
  tudat::json_interface::JsonSimulationManager< > jsonSimulationManager( "main.json" );
  jsonSimulationManager.updateSettings( );
  jsonSimulationManager.runPropagation( );
  jsonSimulationManager.exportResults( );
  return EXIT_SUCCESS;
}

which will always use the file main.json in the current working directory as input file. If one wants to be able to run the application using different input files, then the code becomes:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#include <Tudat/JsonInterface/jsonInterface.h>

int main( int argc, char* argv[ ] )
{
  const std::string inputPath = argv[ 1 ];
  tudat::json_interface::JsonSimulationManager< > jsonSimulationManager( inputPath );
  jsonSimulationManager.updateSettings( );
  jsonSimulationManager.runPropagation( );
  jsonSimulationManager.exportResults( );
  return EXIT_SUCCESS;
}

which is basically the implementation of the json_interface application in Tudat/JsonInterface/jsonInterface.cpp.

The four basic steps in a JSON-based applications are:

  1. Create a JsonSimulationManager object (line 6). This class takes two template arguments, the first being the type to be used for the independent variable (the epoch) and the second the scalar type for the state variable (position, velocity, mass, etc.). When these types are not specified, the default values (double) are used, i.e. JsonSimulationManager< > is equivalent to JsonSimulationManager< double, double >. The input argument is the absolute or relative path to a JSON file.

Note

When creating a JsonSimulationManager using this constructor, the current working directory is set to the directory in which the specified file is located.

  1. Set up the simulation (line 7). This uses the information contained in the JSON objects parsed from the specified input file to create all the settings objects necessary for the simulation (integrator, bodies, propagator, etc.).
  2. Run the propagation (line 8). In addition to integrating the equations of motion, calling the method run this will also check whether there are unused keys, print messages and/or generate a file with all the settings that are going be used depending on the settings specified in the key options of the JSON object.
  3. Export the results (line 9). Calling the method exportResults will generate the output files with the requested results specified in the key export of the JSON object.

Since not all Tudat features are supported by the JSON interface, in some cases it will be necessary to perform some additional steps between step 2 and 3, i.e. the simulation will be first set up from a JSON file, then additional settings will be provided manually, and the the simulation will be run. Then, optionally, the results can be exported using the exportResults method. However, defining the additional settings between lines 7 and 8 does not always lead to the desired results, since step 2 is a complex process in which some variables depend on each another.

Imagine that the aerodynamic coefficients of the body to propagate are not specified in the JSON file because they will be provided manually in the C++ file. This may result in an error, since trying to set up a simulation in which one wants to save e.g. the aerodynamic drag on a body with no aerodynamic coefficients interface will result in an error, which is generated while setting up the simulation and not when actually running it. Using a placeholder aerodynamic coefficients interface in the JSON file (e.g. zero force coefficients), this will result in a successful simulation set up, but then, when one wants to reset the aerodynamic coefficients interface in the C++ application manually, it is necessary to reset also all objects that had been set up based on the placeholder coefficients (e.g. the vehicle’s flight conditions). This can be complex and is prone to leading to errors (sometimes run-time errors, sometimes successful simulations in which the results are wrong), so a different approach is followed when writing custom C++ applications.

When setting up the simulation (step 2), the following virtual method is called:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
virtual void updateSettings( )
{
    ...
    resetIntegratorSettings( );
    resetSpice( );
    resetBodies( );              // must be called after resetIntegratorSettings and resetSpice
    resetExportSettings( );
    resetPropagatorSettings( );  // must be called after resetBodies and resetExportSettings
    resetApplicationOptions( );
    resetDynamicsSimulator( );
}

Note that each of the methods called by this method is also virtual. This means that a derived class of JsonSimulationManager can be created if a custom implementation of any of these methods is needed. The method updateSettingsFromJSONObject is generally not overridden, as it is dangerous to modify the order in which each of the virtual methods is called. In one does want to modify this method, the following has to be taken into account:

  • The default implementation of resetBodies uses the integrator settings’ initial time to interpolate the ephemeris of celestial bodies if Spice is enabled and the key spice.preloadEpehemeris is set to true.
  • The default implementation of resetPropagatorSettings uses the integrator settings’ initial time to infer the initial state of the celestial bodies from their ephemeris, as well as some of the properties of other bodies (such as initial translational state or mass). Additionally, the variable to be computed are determined from the export settings.

In practice, this means that resetBodies must be called after resetIntegratorSettings and resetSpice, and resetPropagatorSettings must be called after resetBodies and resetExportSettings.

Thus, to avoid undefined behaviour, rather than overriding the updateSettings method, one would override just some of the virtual methods called therein. It is recommended to call the original implementation inside the custom implementations of these methods, and then provide additional steps. For instance, before the main function:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
class CustomJsonSimulationManager : public tudat::json_interface::JsonSimulationManager< >
{
public:
    // Inherit constructor.
    using JsonSimulationManager< >::JsonSimulationManager;

protected:
    // Override resetBodies method.
    virtual void resetBodies( )
    {
        // First, call the original resetBodies, which uses the information in the JSON file.
        JsonSimulationManager::resetBodies( );

        // Then, provide additional steps.
        ...
    }
};

Then, in the main function, we only need to change line 6 to:

CustomJsonSimulationManager jsonSimulationManager( inputPath );

If one wants to perform some operations on the results of the integration before exporting them, or does not want to export them to an output file, the call to the exportResults methods can be omitted, and the results can be retrieved from:

std::map< double, Eigen::VectorXd > stateHistory =
    jsonSimulationManager.getDynamicsSimulator( )->getEquationsOfMotionNumericalSolution( );

std::map< double, Eigen::VectorXd > dependentVariablesHistory =
    jsonSimulationManager.getDynamicsSimulator( )->getDependentVariableHistory( );

A tutorial on how to write a custom JSON-based application can be found in Apollo Capsule Entry.