Settings classes¶
Almost all classes containing settings for Tudat simulations are used in the form of shared pointers. Currently, boost::shared_ptr
is being used. Support for converting shared pointers to and from nlohmann::json
could be added just by writing:
namespace boost
{
template< typename T >
void to_json( nlohmann::json& jsonObject, const shared_ptr< T >& sharedPointer )
{
if ( sharedPointer != NULL )
{
jsonObject = *sharedPointer; // T's to_json called
}
}
template< typename T >
void from_json( const nlohmann::json& jsonObject, shared_ptr< T >& sharedPointer )
{
if ( ! jsonObject.is_null( ) )
{
T object = jsonObject; // T's from_json called
sharedPointer = make_shared< T >( object );
}
}
}
Then, if class T
is convertible to/from nlohmann::json
, boost::shared_ptr< T >
would also be. However, this approach has some drawbacks, namely:
T
must be default-constructible for the code to compile.- Separate
to_json
andfrom_json
functions have to be written for each derived class ofT
, potentially leading to the duplication of code.
Thus, given that the settings classes used throughout Tudat are always used as shared pointers, rather than providing to_json
and from_json
functions for these classes, these functions have been written for shared pointers of these classes. Before introducing the best-practices to be followed when writing these functions, the way in which the keys to be used in these functions are defined in the JSON Interface library is described.
Definition of keys¶
In all the example to_json
and from_json
functions presented so far, the keys were hard-coded, i.e. literal strings were used when using the []
operator of a nlohmann::json
object, calling the getValue
function or constructing a key path (by concatenating several strings). However, this approach makes code-updating very complex. Image that, in the future, we want to update a key called initialTime
to initialEpoch
. Although a global search could do the trick, this may result in modifying parts of the code that should not be modified. If we want to update the name of the key type to modelType
, but only for rotation model settings, the only option is doing it manually to avoid changing also the type
keys of other objects.
Thus, in the JSON Interface library, literal strings are never used inside to_json
and from_json
functions. Instead, all the keys that are recognised by the JSON Interface are declared in Tudat/JsonInterface/Support/keys.h
, and their string-value is defined in Tudat/JsonInterface/Support/keys.cpp
. This is done using a struct called Keys
containing several nested structs for each level. For instance:
namespace tudat
{
namespace json_interface
{
struct Keys
{
static const std::string initialEpoch;
static const std::string finalEpoch;
...
static const std::string bodies;
struct Body
{
...
static const std::string rotationModel;
struct RotationModel
{
static const std::string type;
static const std::string originalFrame;
static const std::string targetFrame;
static const std::string initialOrientation;
static const std::string initialTime;
static const std::string rotationRate;
};
...
};
...
};
} // namespace json_interface
} // namespace tudat
namespace tudat
{
namespace json_interface
{
const std::string Keys::initialEpoch = "initialEpoch";
const std::string Keys::finalEpoch = "finalEpoch";
...
// Body
const std::string Keys::bodies = "bodies";
...
// // Body::RotationModel
const std::string Keys::Body::rotationModel = "rotationModel";
const std::string Keys::Body::RotationModel::type = "type";
const std::string Keys::Body::RotationModel::originalFrame = "originalFrame";
const std::string Keys::Body::RotationModel::targetFrame = "targetFrame";
const std::string Keys::Body::RotationModel::initialOrientation = "initialOrientation";
const std::string Keys::Body::RotationModel::initialTime = "initialTime";
const std::string Keys::Body::RotationModel::rotationRate = "rotationRate";
...
} // namespace json_interface
} // namespace tudat
Note that the keys for the different derived classes of RotationModelSettings
are all defined at the same level (i.e. a different struct is not created for each derived class). When going through a settings class and defining its keys, it is good practice to define also the keys for the derived classes that will not supported by the JSON Interface (initially), and commenting them out.
When one wants to modify a key, changing its string value in keys.cpp
should suffice. However, it is good practice to keep the name of the keys and the values of the keys consistent, so “Rename Symbol Under Cursor” should be used as well to replace all the occurrences of the key.
Caution
When debugging an UndefinedKeyError
, the following situation can arise when parsing, for instance, the following JSON file (only relevant section shown):
"bodies": {
"Earth": {
"rotationModel": {
"type": "simple",
"originalFrame": "ECLIPJ2000",
"targetFrame": "IAU_Earth",
"initialTime": 0,
"rotationRate": 7e-05
}
}
}
libc++abi.dylib: terminating with uncaught exception of type tudat::json_interface::UndefinedKeyError:
Undefined key: bodies.Earth.rotationModel.initialOrientation
The key initialOrientation stores a defaultable-property, i.e. if not provided the value can be inferred from the keys originalFrame
, targetFrame
and initialTime
. Thus, the error should not be generated if the key is not defined. One would probably tend to start by looking at the from_json
function of EphemerisSettings
when debugging this issue. However, the source of the problem can be in the keys.cpp
. Even if the from_json
function is completely correct, the previous error would be printed if, in the keys.cpp
, we had:
const std::string Keys::Body::RotationModel::initialOrientation = "initialOrientation";
const std::string Keys::Body::RotationModel::initialTime = "initialOrientation";
which can happen easily when copy-pasting. Thus, what is actually happening is that, when retrieving the value for initialTime
(non-defaultable property), the key initialOrientation is mistakenly accessed (and not found). To prevent these issues, a search for any given key inside the keys.cpp
file should always result in an even number of occurrences. In this way, we also make sure that e.g. the value stored at the key rotationRate does not end up being used for the property initialTime
of our RotationModelSettings
, in which case no error or warning would be generated during conversion to nlohmann::json
as both as non-defaultable properties and store values of the same type (double
).
Writing from_json
functions¶
Generally, the settings classes used in Tudat are not default-constructible. By providing from_json
functions for their shared pointers, rather than for the class itself, we can get the code to compile without the need to provide default constructors because a shared pointer is default-constructible (it is NULL
by default).
To illustrate the structure of a from_json
function for a shared pointer to a settings class, the RotationModelSettings
example is described here:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | #include <Tudat/SimulationSetup/EnvironmentSetup/createRotationModel.h>
#include "Tudat/JsonInterface/Support/valueAccess.h"
#include "Tudat/JsonInterface/Support/valueConversions.h"
namespace tudat
{
namespace simulation_setup
{
//! Map of `RotationModelType`s string representations.
static std::map< RotationModelType, std::string > rotationModelTypes =
{
{ simple_rotation_model, "simple" },
{ spice_rotation_model, "spice" }
};
//! `RotationModelType`s not supported by `json_interface`.
static std::vector< RotationModelType > unsupportedRotationModelTypes = { };
//! Convert `RotationModelType` to `nlohmann::json`.
inline void to_json( nlohmann::json& jsonObject, const RotationModelType& rotationModelType )
{
jsonObject = json_interface::stringFromEnum( rotationModelType, rotationModelTypes );
}
//! Convert `nlohmann::json` to `RotationModelType`.
inline void from_json( const nlohmann::json& jsonObject, RotationModelType& rotationModelType )
{
rotationModelType = json_interface::enumFromString( jsonObject, rotationModelTypes );
}
//! Create a `nlohmann::json` object from a shared pointer to a `RotationModelSettings` object.
void to_json( nlohmann::json& jsonObject, const boost::shared_ptr< RotationModelSettings >& rotationModelSettings );
//! Create a shared pointer to a `RotationModelSettings` object from a `nlohmann::json` object.
void from_json( const nlohmann::json& jsonObject, boost::shared_ptr< RotationModelSettings >& rotationModelSettings );
} // namespace simulation_setup
} // namespace tudat
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 | namespace tudat
{
namespace simulation_setup
{
...
//! Create a shared pointer to a `RotationModelSettings` object from a `nlohmann::json` object.
void from_json( const nlohmann::json& jsonObject, boost::shared_ptr< RotationModelSettings >& rotationModelSettings )
{
using namespace json_interface;
using K = Keys::Body::RotationModel;
// Base class settings
const RotationModelType rotationModelType = getValue< RotationModelType >( jsonObject, K::type );
const std::string originalFrame = getValue< std::string >( jsonObject, K::originalFrame );
const std::string targetFrame = getValue< std::string >( jsonObject, K::targetFrame );
switch ( rotationModelType ) {
case simple_rotation_model:
{
const double initialTime = getValue< double >( jsonObject, K::initialTime );
// Get JSON object for initialOrientation (or create it if not defined)
nlohmann::json jsonInitialOrientation;
if ( isDefined( jsonObject, K::initialOrientation ) )
{
jsonInitialOrientation = getValue< nlohmann::json >( jsonObject, K::initialOrientation );
}
else
{
jsonInitialOrientation[ K::originalFrame ] = originalFrame;
jsonInitialOrientation[ K::targetFrame ] = targetFrame;
jsonInitialOrientation[ K::initialTime ] = initialTime;
}
rotationModelSettings = boost::make_shared< SimpleRotationModelSettings >(
originalFrame,
targetFrame,
getAs< Eigen::Quaterniond >( jsonInitialOrientation ),
initialTime,
getValue< double >( jsonObject, K::rotationRate ) );
return;
}
case spice_rotation_model:
{
rotationModelSettings = boost::make_shared< RotationModelSettings >(
rotationModelType, originalFrame, targetFrame );
return;
}
default:
handleUnimplementedEnumValue( rotationModelType, rotationModelTypes, unsupportedRotationModelTypes );
}
}
} // namespace simulation_setup
} // namespace tudat
|
Note that the files Tudat/JsonInterface/Support/valueAccess.h
and Tudat/JsonInterface/Support/valueConversions.h
are always included. The former includes enhaced value access functions (getValue
) and the latter overrides (and defines) to_json
and from_json
functions for frequently-used types, such as std::vector
or Eigen::Matrix
. The file Tudat/JsonInterface/Support/valueAccess.h
includes Tudat/JsonInterface/Support/keys.h
, so all the keys available in the JSON Interface are readily accessible.
Typically, the first lines of a from_json
function are (for our example):
using namespace json_interface;
using K = Keys::Body::RotationModel;
Generally, when creating a shared pointer to a settings class, only the keys for that class are needed (unless some keys of the root mainJson
object have to be accessed). Thus we can use a shorter-name such as K
. The other keys can still be accessed using the full name Keys::...
. Do not write using Keys = Keys::...
, as this would result in all the other keys being unaccessible.
Although it may be convenient to make the check on whether the provided jsonObject
object is null
, and return immediately a NULL
shared pointer if it is, this situation will generally not happen in practice. When the user does not want to provide a rotation model, rather than writing "rotationModel": null
, they leave the key rotationModel undefined. The from_json
function of BodySettings
is responsible for only calling the from_json
function of RotationModelSettings
if the key rotationModel is defined. If the user does provide null
manually in their input file, this will result in an UndefinedKeyError
for key bodies.Earth.rotationModel.type (the first key to be accessed in the from_json
function) will be thrown.
The settings classes used in Tudat typically have a type property that can be used to determine which derived class should be used when creating the shared pointer object. This is retrieved in line 15. Then, the settings for the base class (shared by all the derived classes) are retrieved in lines 16 and 17. The next step is to write a switch
that modifies (re-constructs) the rotationModelSettings
(passed by reference) depending on the rotationModelType
. Generally, a return
is added at the end of each switch case.
Finally, the default case of the switch always calls the handleUnimplementedEnumValue
function, with the first argument the type converted from JSON, the second argument the map of string representations for the enumeration, and the third argument the list of enumeration values not supported by the JSON interface. This will throw an UnsupportedEnumError
, suggesting the user to write their own JSON-based C++ application, or an UnimplementedEnumError
, if the provided enum value is not marked as unsupported, but we (the coders) forgot to write its implementation, printing a warning in which the user is kindly asked to open an issue on GitHub.
In most cases, the defaultable properties use the default value defined in the setting class constructors. For instance, consider the following constructor:
BasicSolidBodyGravityFieldVariationSettings(
const std::vector< std::string > deformingBodies,
const std::vector< std::vector< std::complex< double > > > loveNumbers,
const double bodyReferenceRadius,
const boost::shared_ptr< InterpolatorSettings > interpolatorSettings = NULL ):
If the user does not provide the key interpolator, the same default value defined in the constructor (NULL
) should be used to create the settings object from the nlohmann::json
object. To keep the behaviour of C++ Tudat applications and JSON-based Tudat applications consistent, if in the future the default interpolator settings are changed from NULL
to e.g. boost::make_shared< LagrangeInterpolatorSettings >( 6 )
in the constructor, this change should also be reflected in the JSON Interface. To make this happen automatically, the default values are not hard-coded in the from_json
functions. Instead, an instance constructed only with the mandatory properties is used to create the actual shared pointer:
BasicSolidBodyGravityFieldVariationSettings defaults( { }, { }, TUDAT_NAN );
gravityFieldVariationSettings = boost::make_shared< BasicSolidBodyGravityFieldVariationSettings >(
getValue< std::vector< std::string > >( jsonObject, K::deformingBodies ),
getValue< std::vector< std::vector< std::complex< double > > > >( jsonObject, K::loveNumbers ),
getValue< double >( jsonObject, K::referenceRadius ),
getValue( jsonObject, K::interpolator, defaults.getInterpolatorSettings( ) ) );
Since the settings classes are only used to store information, their constructors are generally empty, so in most cases the temporary object from which the default properties is retrieved can be constructed with values such as { }
, ""
or TUDAT_NAN
for the mandatory properties.
Writing to_json
functions¶
An example of a to_json
function is provided below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | namespace tudat
{
namespace simulation_setup
{
//! Create a `nlohmann::json` object from a shared pointer to a `RotationModelSettings` object.
void to_json( nlohmann::json& jsonObject, const boost::shared_ptr< RotationModelSettings >& rotationModelSettings )
{
if ( ! rotationModelSettings )
{
return;
}
using namespace json_interface;
using K = Keys::Body::RotationModel;
const RotationModelType rotationModelType = rotationModelSettings->getRotationType( );
jsonObject[ K::type ] = rotationModelType;
jsonObject[ K::originalFrame ] = rotationModelSettings->getOriginalFrame( );
jsonObject[ K::targetFrame ] = rotationModelSettings->getTargetFrame( );
switch ( rotationModelType )
{
case simple_rotation_model:
{
boost::shared_ptr< SimpleRotationModelSettings > simpleRotationModelSettings =
boost::dynamic_pointer_cast< SimpleRotationModelSettings >( rotationModelSettings );
assertNonNullPointer( simpleRotationModelSettings );
jsonObject[ K::initialOrientation ] = simpleRotationModelSettings->getInitialOrientation( );
jsonObject[ K::initialTime ] = simpleRotationModelSettings->getInitialTime( );
jsonObject[ K::rotationRate ] = simpleRotationModelSettings->getRotationRate( );
return;
}
case spice_rotation_model:
return;
default:
handleUnimplementedEnumValue( rotationModelType, rotationModelTypes, unsupportedRotationModelTypes );
}
}
...
} // namespace simulation_setup
} // namespace tudat
|
First, a check on the nullity of the shared pointer is done. If it is NULL
, the null nlohmann::json
object won’t be modified. Otherwise, the settings object will be used to define the keys of the nlohmann::json
object.
The structure is similar to the one for from_json
functions. The main difference is that the []
mutator operator is used to modify the nlohmann::json
object, instead of using the getValue
function to access it. Additionally, in every switch case the original shared pointer has to be dynamically casted to the corresponding derived class. Then, the function assertNonNullPointer
is called. This throws an NullPointerError
when the settings derived class and the value of its type property do not match.
Some switch cases, such as spice_rotation_model
, are empty because they do not contain additional information other than that of the original base class. Thus, the only needed statement is return;
.