Adding features

For each settings-containing class, a directory is added to tudatBundle/matlabInterface/MatlabInterface. The name of this directory is irrelevant for the code to work, but it is preferable to the same name for the directory as for the base class. The name of the files do have to coincide with the name of the classes/enumerations. For instance, for the Integrator class, we will create the file MatlabInterface/Integrator/Integrator.m. In that same directory, we will add all the files for the derived classes (in this case only VariableStepSizeIntegrator.m) as well as the files for the enumerations needed by these classes (in this case, Integrators.m and RungeKuttaCoefficientSets.m). Note that the name of the enumerations is in plural, and does not include the word Type at the end. In the same way, the settings-containing classes do not add the word Settings at the name: this is done to avoid repeating Settings and Type everywhere in the code unnecessarily, as in the MATLAB interface all the custom classes are used to define settings and all the enumerations to defined types, so it is not necessary to explicitly add this information to their names.

In some cases, when a class can only be contained by another class, it is recommended to put that class (and its derived classes and associated enumerations) in a directory inside the parent’s class directory. For instance, the file MatlabInterface/Body/GravityField/GravityField.m is in a directory inside the MatlabInterface/Body directory because only Body objects can contain GravityField objects when setting up a valid Tudat simulation.

Enumerations

An enumeration is defined by writing its possible values names. These names must match the string representations defined in the C++ code. Note that the unsupported enum values are also included, but commented out:

MatlabInterface/Acceleration/Accelerations.m
classdef Accelerations
    enumeration
        undefined
        pointMassGravity
        aerodynamic
        cannonBallRadiationPressure
        sphericalHarmonicGravity
        mutualSphericalHarmonicGravity
        % thirdBodyPointMassGravity
        % thirdBodySphericalHarmonicGravity
        % thirdBodyMutualSphericalHarmonicGravity
        thrust
        relativisticCorrection
        empirical
    end
end

Settings classes

Properties and methods

A settings-containing class defines a set of properties, whose names must match exactly those of the JSON keys. The properties that must not be used during conversion to JSON are marked as transient:

MatlabInterface/Body/Body.m
classdef Body < jsonable
    properties
        useDefaultSettings
        initialState
        mass
        rotationalState
        referenceArea
        aerodynamics
        atmosphere
        ephemeris
        gravityField
        gravityFieldVariation
        radiationPressure
        rotationModel
        shapeModel
    end
    properties (Transient)
        name
    end
    properties (Transient, Dependent)
        dragCoefficient
        radiationPressureCoefficient
    end
    ...
end

Dependent properties can be used as shortcuts to other properties. For instance, Body.dragCoefficient sets/gets the value of Body.aerodynamics.dragCoefficient, and Body.radiationPressureCoefficient sets/gets the value of Body.radiationPressure.Sun.radiationPressureCoefficient. Thus, we need to provide set/get methods. For instance:

MatlabInterface/Body/Body.m
classdef Body < jsonable
...
methods
    ...
    function value = get.dragCoefficient(obj)
        value = obj.aerodynamics.dragCoefficient;
    end

    function set.dragCoefficient(obj,value)
        obj.aerodynamics.dragCoefficient = value;
    end
    ...
end

The classes provided in the MATLAB Interface do not provide functionality, they are just classes containing settings that can be converted to JSON. Thus, they do not contain many methods (normally, just those overriding the methods of jsonable to determine which properties are mandatory, paths, etc.). One exception is the Simulation class, which does include some methods that add functionality, such as those for running the simulation, adding bodies, defining results to be exported to JSON files or to be imported from a Tudat propagation into MATLAB, etc.

For other classes, in addition to getters and setters for dependent properties and overriding the function needed to determine their JSON representation, we need to write the following methods:

  • Setters for properties storing enumeration values, in order to enable using a char (in addition to an enumeration value) when defining it. For instance:

    MatlabInterface/Integrator/VariableStepSizeIntegrator.m
    classdef VariableStepSizeIntegrator < Integrator
    properties
        ...
        rungeKuttaCoefficientSet
        ...
    end
    ...
    methods
        ...
        function set.rungeKuttaCoefficientSet(obj,value)
            if ~isa(value,'RungeKuttaCoefficientSets')
                value = RungeKuttaCoefficientSets(value);
            end
            obj.rungeKuttaCoefficientSet = value;
        end
    ...
    end
    

    In this way, these two assignments are equivalent and both result in a valid integrator:

    integrator.rungeKuttaCoefficientSet = RungeKuttaCoefficientSets.rungeKuttaFehlberg78;
    integrator.rungeKuttaCoefficientSet = 'rungeKuttaFehlberg78';
    

    Without writing the method set.rungeKuttaCoefficientSet, if the user writes a value that is not in the enumeration list, the second assignment does not generate any warnings or error, an invalid integrator will be generated and an error will be thrown when running Tudat. By writing the set.rungeKuttaCoefficientSet method, the second assignment will throw a MATLAB error if the provided enum value is not in the enumeration list, preventing the generation of invalid JSON files.

  • Getters for properties storing body names, in order to be able to provide either body objects or body names. For instance:

    MatlabInterface/Acceleration/Thrust.m
    classdef Thrust < Acceleration
        properties
            ...
            centralBody
        end
    
        methods
            ...
            function bodyName = get.centralBody(obj)
                if isa(obj.centralBody,'Body')
                    bodyName = obj.centralBody.name;
                else
                    bodyName = obj.centralBody;
                end
            end
            ...
        end
    end
    

    When getting the centralBody property of a Thrust object, a char will be returned even if the user assigned a Body object to this property; in that case, the transient property name of the body will be used. Thus, the following two assignments are equivalent and valid:

    thrust.centralBody = 'Earth';
    thrust.centralBody = Earth;
    

    Note that Earth is just a pre-defined Body with the property name set to 'Earth' and the property useDefaultSettings set to true (so that its properties are obtained automatically from Spice if not specified manually). Pre-defined objects for the other celestial bodies also exist.

Constructors

We can provide constructors for the settings-containing classes. Usually, in the classes provided in the MATLAB interface, a type containing an enumeration value is provided as first (and only) argument and assigned to a type property:

MatlabInterface/Acceleration.m
classdef Acceleration < jsonable
    properties
        type
    end

    methods
        function obj = Acceleration(type)
            if nargin >= 1
                obj.type = type;
            end
        end
        ...
    end
end

Providing constructors with additional arguments is discouraged, as this usually results in code that is not readily understandable. For instance, the following must be avoided:

integrator = VariableStepSizeIntegrator(10,'rungeKuttaFehlberg78',1,1000,1E-9,1E-9);

If the user writes this in their MATLAB script, it is difficult to know what each parameter represents. If one checks the constructor definition (in MATLAB, only one constructor can be provided per class), it will look like VariableStepSizeIntegrator(varargin) in most cases, which does not provide useful information. Moreover, if more properties are added in the future, these could only be added at the end of the list of input arguments; otherwise existing code would be broken (or worse, would still run but generating in wrong results). Thus, it is recommended to create instances of settings-containing objects using an empty constructor whenever possible:

integrator = VariableStepSizeIntegrator();
integrator.initialStepSize = 10;
integrator.rungeKuttaCoefficientSet = 'rungeKuttaFehlberg78';
integrator.minimumStepSize = 1;
integrator.maximumStepSize = 1000;
integrator.relativeErrorTolerance = 1E-9;
integrator.absoluteErrorTolerance = 1E-9;

In any case, an empty constructor must always be provided (i.e. the length of varargin must be allowed to be zero). In this way, we can write:

simulation = Simulation();
...
simulation.integrator.stepSize = 20;
json.export(simulation,'main.json');

because in the constructor of Simulation, the property integrator is set to Integrator(). If we skip this step, the code above would still be valid, but the property simulation.integrator would be a struct instead of an Integrator object, so all the functionality defined in the Integrator class for converting to JSON would be lost. This means that, all the properties storing settings-containing objects must be initialised in the constructor with their corresponding empty constructors to prevent this problems. For instance:

MatlabInterface/Acceleration/Thrust.m
classdef Thrust < Acceleration
    properties
        direction
        magnitude
        dataInterpolation
        specificImpulse
        frame
        centralBody
    end

    methods
        function obj = Thrust()
            obj@Acceleration(Accelerations.thrust);
            obj.direction = ThrustDirection();
            obj.magnitude = ThrustMagnitude();
            obj.dataInterpolation = DataInterpolation();
        end
        ...
    end
end

Note that before we call the base class’ constructor first by writing obj@BaseClass(...).

For optional properties, we can also set them during construction, but in that case the used empty constructor must return an empty object. For instance, consider the constructor of the Body class:

function obj = Body(name,useDefaultSettings)
    obj.initialState = State();
    obj.aerodynamics = ConstantAerodynamics();
    obj.atmosphere = Atmosphere();
    obj.ephemeris = Ephemeris();
    obj.gravityField = GravityField();
    obj.gravityFieldVariation = GravityFieldVariation();
    obj.rotationModel = RotationModel();
    obj.shapeModel = ShapeModel();
    if nargin >= 1
        obj.name = name;
        if nargin >= 2
            obj.useDefaultSettings = useDefaultSettings;
        end
    end
end

Thanks to this constructor, one can write:

body = Body('Vehicle');
% body.aerdoynamics = ConstantAerodynamics();
body.aerodynamics.forceCoefficients = [2 0 0];
body.aerodynamics.referenceArea = 10;

without the need to write the line that has been commented out. However, ConstantAerodynamics() must provide an empty object, otherwise the property body.aerodynamics will be defined in the JSON file even when the user does not provide any aerodynamics settings. For Atmosphere, Ephemeris, GravityField, etc. this is trivial, but for ConstantAerodynamics this is more difficult because it derives from Aerodynamics and its property type should be set to 'constant', resulting in an object that is not empty. Thus, this is only possible because the JSON Interface uses the default value "constant" for the type of the aerodynamics settings when no provided in the input file.