Usage

To use the MATLAB Interface, you first need to include the directories containing the code into your MATLAB’s path for the current MATLAB’s session. You can do this by writing at the beginning of your script:

tudat.load();

After this line, you can create objects such as Integrator(), Body(), etc, and access the functionality of the packages included in the MATLAB Interface, such as +convert, +json, etc.

Before detailing how to set up simulations, run them and obtain the results, it is convenient to make a distinction between two possible usage modes of the MATLAB Interface:

  • Seamless mode. You use the MATLAB Interface to set up simulations that will be run directly from your MATLAB script. Temporary input and output files will be generated and deleted by the MATLAB Interface in the background. When the simulation completes, you will be able to access the results directly in the struct results of your Simulation object. You can use this data to generate plots and eventually consolidate (parts of) it in a text file.
  • Input-output mode. You use the MATLAB Interface to set up simulations and to generate JSON input files that will then be provided to the json_interface manually, e.g. from the command line or in Qt Creator. You must specify the output files to be generated by using the addResultsToExport method of your Simulation object. Then, the generated output files can be opened with your favourite text editor and/or loaded into MATLAB for post-processing and plotting.

These modes are not mutually exclusive, i.e. you can run your simulations from MATLAB and still get to keep the input and output files in your directory.

In the following sections, the steps to be followed for each of these modes are described. The first part, setting up the simulation, is generally identical for the two usage modes.

Setting up the simulation

The settings for a Tudat simulation are defined by creating a Simulation object in MATLAB. Then, this object is used to create a JSON input file that will be provided to the json_interface application, either manually by the user or internally by MATLAB.

The properties that can be defined for this Simulation object are very similar to those that can be defined for a JSON Interface. You can read the List of Keys for an exhaustive list of possible keys. Here, only the most frequently-used features, and those that in the MATLAB Interface are set up in a different way, will be discussed.

The Simulation object can be created by writing:

simulation = Simulation();

It is also possible to provide up to four parameters to the constructor:

simulation = Simulation(0,86400,'SSB','J2000');

which is equivalent to:

simulation = Simulation();
simulation.initialEpoch = 0;
simulation.finalEpoch = 86400;
simulation.globalFrameOrigin = 'SSB';
simulation.globalFrameOrientation = 'J2000';

The documentation for the properties initialEpoch, finalEpoch, globalFrameOrigin and globalFrameOrientation can be found in List of Keys.

Providing Spice settings

Spice is used to determine properties of celestial bodies, such as gravitational parameter, radius, ephemeris, etc. When creating a Simulation, the property spice.useStandardKernels will be true by default, so if you want to use the standard Spice kernels you do not need to write any additional code in your MATLAB script.

If you do not need to use Spice, you can disable it by writing:

simulation.spice = [];

By default, Tudat will try to preload the ephemeris of the bodies with the property useDefaultSettings set to true from initialEpoch to finalEpoch. To prevent this from happening, one can write:

simulation.spice.preloadEphemeris = false;

This is necessary when no time-termination condition is provided (i.e. when finalEpoch is not specified, and the propagation will terminate based on other conditions such as altitude). For propagations in which there is a time-termination condition but the probability of meeting another termination condition before reaching the maximum final epoch is large, disabling preload of ephemeris may result in faster propagations. For all other cases, preloading the ephemeris is usually faster than retrieving them from Spice at every integration step.

Providing body settings

A body is created by writing:

body = Body('Asterix');

which creates a Body named Asterix. A second argument can be provided to the constructor to specify whether the default settings for that body should be loaded. This is only valid when the name of the body is recognised by Tudat/Spice (e.g. Sun, Earth, Moon, Mars, etc.). If this second argument is omitted, all the settings for that body are provided manually through the MATLAB Interface. When useDefaultSettings is set to true, but also some settings are provided manually, first the default settings will be loaded when running Tudat, and then the other settings specified manually will be used, potentially overriding some of the default settings that were loaded.

For the Sun, the Moon and the planets, the MATLAB Interface provides predefined bodies (basically, Body objects with the property useDefaultSettings set to true and the corresponding name already set). These bodies are readily accessible by writing e.g.:

earth = Earth();

Then, we can provide additional settings by writing:

earth.atmosphere.type = AtmosphereModels.nrlmsise00;

Finally, when all the bodies needed for the simulation have been created, they have to be added to the Simulation object. We do this by writing e.g.:

simulation.addBodies(Sun,earth,Moon,body);

Note that we can directly add celestial bodies for which we have not defined additional properties (in this case, Sun and Moon).

We can also modify the bodies after having added them to simulation by writing:

simulation.bodies.Asterix.mass = 5000;
simulation.bodies.Earth.ephemeris = ConstantEphemeris(zeros(6,1));

Note that here we refer to the bodies by their names (Asterix, Earth) and not by the name of the MATLAB variables in which they are stored (body, earth). We can also modify the body objects directly, and simulation will be updated automatically. For instance, these two lines are equivalent to the previous code block and can be written safely after the call to addBodies:

body.mass = 5000;
earth.ephemeris = ConstantEphemeris(zeros(6,1));

Providing propagator settings

We can create a propagator by writing:

propagator = Propagator();

However, this creates a non-functional object, as the propagators supported by Tudat have to be either translational, rotational or mass propagators. The most used propagator is the TranslationalPropagator, which is used to propagate the Cartesian state of a body. Thus, we can write:

propagator = TranslationalPropagator();
propagator.bodiesToPropagate = {body};
propagator.centralBodies = {earth};

Since we can propagate the states of several bodies, we have to provide a list (i.e. a cell array of objects) to the property bodiesToPropagate. We also must specify a central body for each of the bodies to be propagated through centralBodies. We can provide either a list of Body objects or a list of body names. The following two lines are equivalent to the last two lines of the previous code block:

propagator.bodiesToPropagate = {'Asterix'};
propagator.centralBodies = {'Earth'};

When converting the Simulation object (which will contain the propagator) to JSON, the name of the body will be used if a Body objects has been provided.

For the propagator object to be valid, we need to specify the accelerations acting on the different bodies. For instance, for an unperturbed motion:

propagator.accelerations.Asterix.Earth = {PointMassGravity()};

In this case, the only acceleration is the Earth’s point-mass gravity acting on the body Asterix. Note that here we also use the names of the bodies. The key accelerations.Asterix.Earth is read as “accelerations on Asterix caused by Earth”. We can create additional acceleration objects, such as:

propagator.accelerations.Asterix.Earth = {SphericalHarmonicGravity(5,5), AerodynamicAcceleration()};
propagator.accelerations.Asterix.Sun = {PointMassGravity(), RadiationPressureAcceleration()};
propagator.accelerations.Asterix.Moon = {PointMassGravity()};

The constructor of SphericalHarmonicGravity takes the maximum degree and order of the spherical harmonic expansion as arguments. For the rest of accelerations, no input argument are needed. In more complex examples, we can provide additional accelerations such as Thrust, MutualSphericalHarmonicGravity, RelativisticCorrectionAcceleration or EmpiricalAcceleration, which do require additional information in general to result in valid propagations.

Finally, when the propagator has been created, we can assign it to the Simulation object:

simulation.propagators = {propagator};

Here we provide a list of propagators, since we may want to propagate several states simultaneously. For instance, we may want to propagate both the translational state of the body Asterix and its mass, so we would need to use two different propagators:

translationalPropagator = TranslationalPropagator();
...
massPropagator = MassPropagator();
...
simulation.propagators = {translationalPropagator, massPropagator};

Providing integrator settings

We can create a fixed step-size Integrator by writing:

integrator = Integrator();
integrator.type = Integrators.rungeKutta4;
integrator.stepSize = 10;

If we want to use a variable step-size integrator, we can write:

integrator = VariableStepSizeIntegrator(RungeKuttaCoefficientSets.rungeKuttaFehlberg78);
integrator.initialStepSize = 20;
integrator.minimumStepSize = 5;
integrator.maximumStepSize = 1e4;
integrator.errorTolerance = 1e-11;

Setting the property errorTolerance sets both the relativeErrorTolerance and absoluteErrorTolerance keys.

Then, we add the integrator to the Simulation object:

simulation.integrator = integrator;

Since a fixed step-size RK4 integrator is used by default, we can simply write this line to provide all the necessary integrator settings:

simulation.integrator.stepSize = 10;

The other required key, integrator.initialEpoch, is retrieved from the property initialEpoch of the Simulation object, if defined.

Requesting results

In order to define the variables whose values have to be either exported to output files and/or loaded into MATLAB after running the propagation, we need to define Tudat variables in MATLAB. There exist four fundamental variable types: independent, state, cpuTime and dependent. The independent variable is typically the epoch (in seconds since J2000), the state is a vector containing all the states of all the bodies being propagated and the CPU time variable represents the cumulative computation time up to each integration step. There exist many dependent variables whose value can be saved, such as altitude, Mach number, relative position, etc.

Creating a Tudat variable in MATLAB can be done in several ways:

epoch = Variable('independent');
state = Variables.state;

For dependent variables, additional information is required. For instance, to create a variable representing the relative velocity of the body named Asterix w.r.t. Earth we write:

v = Variable();
v.body = 'Asterix';
v.dependentVariableType = DependentVariables.relativeVelocity;
v.relativeToBody = 'Earth';

Note that, when we create a variable with an empty constructor, it is assumed to be of dependent type.

However, in the MATLAB Interface, there is a shortcut for defining Tudat variables. The previous block of code is equivalent to:

v = Variable('Asterix.relativeVelocity-Earth');

A few dependent variables, such as acceleration or accelerationNorm, require additional information, namely the type of acceleration. For instance, if we want to define variables representing the aerodynamic and radiation pressure accelerations caused by Earth and the Sun on Asterix, we can write:

drag = Variable('Asterix.acceleration@aerodynamic-Earth');
srp = Variable('Asterix.acceleration@cannonBallRadiationPressure-Sun');

For vectorial variables, if we are only interested in one of the components, we can add the index of the component we want at the end. For instance, for the x-component of aerodynamic drag:

drag_x = Variable('Asterix.acceleration@aerodynamic-Earth[0]');
drag_x = Variable('Asterix.acceleration@aerodynamic-Earth(1)');

Note that, when using the C++ syntax, i.e. [index], the indices start from 0, while when using the MATLAB syntax, i.e. (index), the indices start from 1. Thus, the two previous lines are equivalent.

Once that we have created the variables that we want to compute, we can configure the Simulation object to export their values to an output file (for each integration step). We do this by writing:

simulation.addResultsToExport('dragX.txt',{'independent',drag_x});

Here, the first argument is the output file path (relative to the directory where the input JSON file is located, or to the current working directory if run directly from MATLAB) and the second argument is a list of Result objects. If a Variable or char is provided, it will be converted to a Result object with default settings. A Result object can be used to specify whether the values at all the integration steps are wanted (or only the first and/or last), the number of digits, whether the independent variable should be automatically included (which is false by default in the MATLAB interface, hence the need to include the variable independent in the list).

For instance, we can write:

result = Result();
result.variables = {Variable('Asterix.acceleration@aerodynamic-Earth(1)')};
result.epochsInFirstColumn = true;
result.numericalPrecision = 10;
result.onlyFinalStep = true;
simulation.addResultsToExport('finalDragX.txt',result);

to generate an output file containing the X component of the aerodynamic acceleration only at the last step, together with the final epoch.

If we do not want to export the results to an output file, we can write:

simulation.addResultsToSave('dragX',drag_x);

where the first argument is the name of the MATLAB variable containing the results (after running the simulation) and the second argument is (a list of) result/variable(s). Behind the scenes, this will ask Tudat to create a temporary output file containing the results, which will then be loaded by the MATLAB interface into the struct simulation.results and deleted. Thus, after the call to simulation.run(), the drag can be obtained at simulation.results.dragX.

Defining termination conditions

For the propagation to terminate, we have to define termination conditions; otherwise, it will go on forever or terminate with an error when undefined behaviour is reached (e.g. the satellite reaches infinite velocity, its altitude goes below zero, etc.).

It is possible to define termination conditions for any Variable object. The time-termination condition is based on a limit for the value of the independent variable. However, we need not specify this condition manually; it will be created automatically by Tudat if the property of finalEpoch of the Simulation object has been defined.

In some case, we want to provide additional conditions, such as terminating the propagation when the satellite’s altitude goes below 100 km. We can do this by writing:

simulation.termination = Variable('Asterix.altitude-Earth') < 100000;

Note that the operators <=, >= and == are not defined for Variable objects, so we always have to use either < or >. We can also provide multiple termination conditions by using the operators & and | (note that && and || won’t work):

condition1 = Variable('Asterix.altitude-Earth') < 50000;
condition2 = Variable('Asterix.altitude-Earth') < 80000;
condition3 = Variable('Asterix.machNumber') > 20;
simulation.termination = condition1 | ( condition2 & condition3 );

In any case, if simulation.finalEpoch has been defined, a time-based condition will be added to the provided conditions, so these three blocks of code are equivalent:

simulation.finalEpoch = 1e5;
simulation.termination = Variable('Asterix.machNumber') < 1;
simulation.termination = Variable('Asterix.machNumber') < 1 | Variable('independent') > 1e5;
simulation.finalEpoch = 1e5;
simulation.termination = Variable('Asterix.machNumber') < 1 | Variable('independent') > 1e5;

Defining application options

It is possible to specify options for the json_interface application called by MATLAB or called directly by the user with the JSON files generated by MATLAB as input files by changing the properties of simulation.options:

simulation.options.fullSettingsFile = 'fullSettings.json';

For instance, this is asking the application to generate a JSON file containing all the settings (those provided in the input file(s) and the default values and settings loaded from Tudat). This file will be generated when running the json_interface application, right before integrating the equations of motion.

Running the simulation

If you want to call the json_interface from MATLAB, you can simply write:

simulation.run();

This will generate a temporary JSON input file, use it to run json_interface, load the generated output files, and delete all the temporary input and output files that have been generated.

If you want to run the simulation manually, you will have to export the JSON file containing the settings for the simulation. You do this by using the json package of the MATLAB Interface:

json.export(simulation,'main.json');

which exports the object simulation to the file main.json. Then, you can call the json_interface application:

json_interface main.json

which will generate the output files specified in the key export.

For generating modular files, one can write e.g.:

integrator = ...
json.export(integrator,'rk4.json');

simulation = ...
simulation.integrator = '$(rk4.json)';
json.export(simulation,'main.json');

Which will generate the file rk4.json containing only the integrator settings, and the file main.json containing the remainder of the settings, and a reference to the integrator file by using the special string $(rk4.json). The previous block of code is equivalent to:

integrator = ...
simulation = ...
simulation.integrator = json.modular(integrator,'rk4.json');
json.export(simulation,'main.json');

The function json.modular exports the object integrator to the file rk4.json and returns the string $(rk4.json), which is assigned to simulation.integrator.

For multi-case propagations, one can write e.g.:

simulation = ...
body = ...
simulation.addBodies(body,...);
json.export(simulation,'shared.json');

for i = 1:10
  m = i*1000;
  json.export(json.merge('$(shared.json)','bodies.Asterix.mass',m),sprintf('mass%i.json',m));
end

which generates the file shared.json containing the shared settings, and 10 files such as:

mass1000.json
[
  "$(shared.json)",
  {
    "bodies.Asterix.mass": 1000
  }
]

If no output files are to be generated (seamless mode), the same behaviour can be achieved just by writing:

simulation = ...
body = ...
simulation.addBodies(body,...);

for i = 1:10
  body.mass = i*1000;
  simulation.run();
  results{i} = simulation.results;
end

Accessing the results

After running the propagation, the results can be retrieved from the generated output files, or directly from the struct results of the Simulation object.

In addition to the results requested to be saved in simulation.results by using the method addResultsToSave, it is always possible to access simulation.results.numericalSolution, which is a matrix containing, for each step, the epoch in the first column and the states (of all the bodies) in subsequent columns.

The function called internally by the MATLAB Interface to load the results from the temporary output files that have been generated is import.results. This is the same function that should be used when importing the results from output files generated by running the json_interface manually:

[results,failure] = import.results('output.txt');

This function reads the contents of the output file into the matrix results and can also return a second argument, failure, which will be true if the propagation terminated before reaching the termination condition. If an error occurred during propagation, the results will only be available until the integration step previous to that error. In that case, the JSON Interface adds the header FAILURE to the generated output files, which is detected by the import.results function.

When calling simulation.run() and loading the results automatically into MATLAB, the information on whether the propagation failed is not available, but it is not needed because a message will be printed to the command window in case that a propagation error is encountered. However, when running many propagations e.g. on a server using the json_interface application, it is possible that the only remaining consolidated information be the output file and not the command-line warnings/errors. Thus, in that case, the presence of the FAILURE header in the output files is useful.

To turn off this feature (i.e. to prevent output files from including a line containing the text FAILURE when a propagation error is encountered), one can write:

simulation.options.tagOutputFilesIfPropagationFails = false;

In that case, it is safe to import the results by calling built-in MATLAB functions such as load or importdata.

In addition to the output files, after calling simulatin.run(), it is possible to retrieve the actual settings that have been used for the propagation (including all the default values) by writing simulation.fullSettings.