OPI is an interface with the goal to facilitate the implementation of orbital propagators into different applications.
To calculate orbital motion, many different software programs exist emphasizing on different aspects such as execution speed or accuracy. They often require different input parameters and are written in different languages. This makes comparing or exchanging them a challenging task. OPI aims at simplifying this by providing a common way of handling propagation. Propagators using OPI are designed as plugins/shared libraries that can be loaded by a host program via the interface.
Features at a glance:
- Implement orbital propagators or force models as independent plugins
- Automatically find and load propagator plugins available on your platform
- Create, manage, copy and modify populations of orbital objects
- Multi-language support (C, C++, Fortran, Python, C#)
- Platform-independent (although hosts and propagators may not be)
- Extensible GPU computing support
- Automatic reading of configuration and resource files for propagators
OPI currently supports C, C++, Fortran, CUDA and OpenCL for propagators, and C, C++, Fortran and Python for hosts. The C API can also be used for integration into other languages like C#. Hosts and plugins don't have to be written in the same language in order to collaborate. OPI itself is written in C++, with auto-generated bindings for C, Fortran and Python. For GPU support, it supplies a plugin that scans for capable devices and helps to initialize CUDA or OpenCL-enabled propagators.
Please note that this software is still under development and the interface functions are subject to change. Your feedback is appreciated.
To implement a basic OPI propagator in C++, create a class that inherits from
OPI::Propagator. Apart from setting some basic constants you need to implement,
at the very least, its runPropagation()
function:
#include "OPI/opi_cpp.h"
// Set the name the plugin will appear as to the host
#define OPI_PLUGIN_NAME "MyPropagator"
// Set optional author and description
#define OPI_PLUGIN_AUTHOR "My Organization"
#define OPI_PLUGIN_DESC "Example Propagator"
// Set the version number for the plugin here.
#define OPI_PLUGIN_VERSION_MAJOR 0
#define OPI_PLUGIN_VERSION_MINOR 1
#define OPI_PLUGIN_VERSION_PATCH 0
class MyPropagator: public OPI::Propagator
{
public:
OPI::ErrorCode MyPropagator::runPropagation(OPI::Population& population, double julian_day, double dt,
OPI::PropagationMode mode = OPI::MODE_SINGLE_EPOCH, OPI::IndexList* indices = nullptr);
// Implement additional functions for returning information about the propagator,
// loading proprietary file formats and defining init and de-init behaviour
};
// Include the macro that makes the propagator available as a plugin
#define OPI_IMPLEMENT_CPP_PROPAGATOR MyPropagator
#include "OPI/opi_implement_plugin.h"
The runPropagation()
function is executed from the host application (via calls to propagate()
), in a loop over all
propagation time steps. Its implementation inside the plugin manipulates the given population based on time information
as well as the state of the population itself. The following example also shows a way to handle index lists which can
optionally be provided by the host to specify which objects to consider:
OPI::ErrorCode MyPropagator::runPropagation(OPI::Population& population, double julian_day, double dt,
OPI::PropagationMode mode, OPI::IndexList* indices)
{
// If an index list is given, loop over the size of the index list.
// Otherwise, loop over the entire population.
int loopSize = (indices ? indices->getSize() : population.getSize());
for (int i=0; i<loopSize; i++)
{
// Get pointer to the Population's orbital data
OPI::Orbit orbits = population.getOrbit();
// Get object index from index list if given, or use loop counter otherwise
int objectIndex = (indices ? indices->getData(OPI::DEVICE_HOST)[i] : i);
// Calculate mean motion for the length of the given time step
double meanMotion = sqrt(EARTH_GRAVITATIONAL_CONSTANT
/ pow(orbits[objectIndex].semi_major_axis, 3.0));
// Add mean motion to the object's mean anomaly and normalize to radian range
orbits[objectIndex].mean_anomaly =
fmod(orbits[objectIndex].mean_anomaly + meanMotion * dt, 2*M_PI);
}
// Tell OPI that the population has been updated on the host device. This
// ensures correct synchronization with CUDA and OpenCL devices.
population.update(OPI::DATA_ORBIT, OPI::DEVICE_HOST);
return OPI::SUCCESS;
}
If your propagator needs to read object data in a proprietary format, implement
the loadPopulation()
function which is accessible from the host and allows you
to define the conversion to an OPI population. By default, it should read data in
OPI's own binary format.
OPI::ErrorCode loadPopulation(OPI::Population& population, const char* filename)
{
// Load given file or directory and fill population accordingly
// The default is to simply load OPI's own binary format:
population.read(filename);
return OPI::SUCCESS;
}
Propagators can define configuration variables called PropagatorProperties
that
can be set by the host program at runtime, or via a config file. Supported variable
types are floats/doubles, integers and strings. To declare a PropagatorProperty inside the
Propagator, use one of the functions registerProperty()
or createProperty()
. The
former will let you bind an existing variable to a Property while the latter will
let OPI manage the variable internally:
class PropertiesCPP: public OPI::Propagator
{
private:
int testproperty_int;
float testproperty_float;
std::string testproperty_string;
public:
PropertiesCPP(OPI::Host& host)
{
testproperty_int = 0;
testproperty_float = 0.0f;
testproperty_string = "test";
// Expose the above member variables as properties
registerProperty("ThisIsAnInteger", &testproperty_int);
registerProperty("ThisIsAFloat", &testproperty_float);
registerProperty("ThisIsAString", &testproperty_string);
// This creates a string property that's managed by OPI.
createProperty("ThisIsAnotherString", "defaultString");
}
In order to access Properties from either the host or the propagator, use the appropriate setter and getter functions:
setProperty("ThisIsAnInteger", 23);
int value = getPropertyInt("ThisIsAnInteger");
Another way to set properties and assign default values is via a config file. Simply
place a file with the same base name and the .cfg
suffix into the plugin folder and OPI
will read the properties from there (properties not defined by the propagator will be
automatically added using the createProperty()
function). For example, if your plugin is
plugins/sgp4.dll
, create a file named plugins/sgp4.cfg
with the following contents:
# "Operations Mode" - "a" for advanced (default), "i" for improved.
opsmode = "a"
# Gravity constants - "wgs72" (default), "wgs72old" or "wgs84".
whichconst = "wgs72"
This will create the string properties opsmode
and whichconst
and set them to the
given default values. Data types in config files are denoted by quotation marks and decimal
dots (i.e. 1.0 would be parsed as a float, 1 as an integer and "1.0" as a string).
Most propagators rely on additional data in order to function. Similar to config files, OPI
Propagators can read data from resource archives. If you place a zip archive containing your
resource files into the plugin folder with the plugin's base name and the suffix .dat
, you
can access the files inside that archive from the propagator. For example, if your plugin
is plugins/sgp4.dll
, create a zip archive containing the file "test.txt" and place it as
plugins/sgp4.dat
. Then, inside the propagator, simply access its contents as follows:
char* buffer;
size_t size = loadResource("test.txt", &buffer);
std::cout << buffer << std::endl;
delete buffer; // Don't forget to free the buffer after you're done with it.
To implement a basic host in C++, create a class that derives from OPI::Host, or create an instance of it directly. Use it to load a plugin directory, select the desired plugin and use it to propagate a population.
#define OPI_DISABLE_OPENCL //OpenCL not required for host
#include "OPI/opi_cpp.h"
int main(int argc, char* argv[])
{
// Initialize host
OPI::Host host;
// Load plugin directory. Optionally, specify a GPU computing platform with
// platform number and device number.
host.loadPlugins("plugins",OPI::Host::PLATFORM_OPENCL, 0, 0);
// Create a population with a single object on the given host
OPI::Population population(host, 1);
// Fill the population with orbit data
population.getOrbit()[0] = { 6800.0, 0.0001, 23.5, 0.0, 0.0, 0.0 };
// Get the desired propagator
OPI::Propagator* myPropagator = host.getPropagator("MyPropagator");
if (myPropagator)
{
// Initialize the propagator
myPropagator->enable();
// Set a start date and time step size for the propagation
const double startDate = 2458201.5;
const double stepSize = 60.0;
// Propagate population for one day (1440 time steps at 60 seconds each)
for (int i=0; i<1440; i++)
{
double currentTime = startDate + i * stepSize / 86400.0;
// Propagate with the default settings (single epoch, no index list)
OPI::ErrorCode status = myPropagator.propagate(population, currentTime, stepSize);
}
// Print the first object's mean anomaly
std::cout << population.getOrbit()[0].mean_anomaly << endl;
// Deinitialize the propagator
myPropagator.disable();
}
return 0;
}
Over the last few years a few changes have been made to the interface that have now been merged into the master branch. Those changes, dubbed the 2019 interface, deprecate the previous version and will require you to update propagators and hosts. Updated documentation and examples will follow shortly, until then the easiest way to get help at the moment is to contact me directly (mmoeckel on GitHub). A quick overview of the most significant changes:
- New license: Moved from LGPL to the simpler and more permissive MIT license.
- Indexed propagation and multi-time propagation have moved from individual functions to the main "propagate"/"calculate" functions. A mode setting has been introduced to switch between single epoch (default) and individual epoch mode. If indexed propagation is not required the IndexList pointer should be set to nullptr (default). Propagators that don't support individual epoch mode or indexed propagation shall return NOT_IMPLEMENTED when IndexList is not null or individual epoch mode is selected, respectively. To update from the 2015 interface, simply implement your runPropagation function like this:
OPI::ErrorCode MyPropagator::runPropagation(OPI::Population& population, double julian_day, double dt, OPI::PropagationMode mode, OPI::IndexList* indices)
{
if (mode == OPI::MODE_INDIVIDUAL_EPOCHS)
{
// move code from runMultiTimePropagation() here, or:
return OPI::NOT_IMPLEMENTED;
}
if (indices != nullptr)
{
// move code from runIndexedPropagation() here, or:
return OPI::NOT_IMPLEMENTED;
}
// implement propagation function
return OPI::SUCCESS;
}
- Beginning of life and end of life fields have moved to an "Epoch" struct inside the population. The Epoch also has a "current_epoch" field that is used for individual epoch propagation.
- Perturbation modules now return a Perturbation instance containing delta values instead of a modified population:
OPI::ErrorCode MyForceModel::runCalculation(OPI::Population& data, OPI::Perturbations& delta, double julian_day, double dt, OPI::PropagationMode mode, OPI::IndexList* indices)
{
// Population is read-only
// Calculate changes and store them in the appropriate field in "delta"
}
- Covariance matrix fields have been introduced.
- Propagators can implement a "loadPopulation" function that loads propagator-specific files and creates OPI populations from it. The default behaviour of this function is to load a population in the binary .opi format.
- Propagators now have an "align" function that brings all objects to the same epoch (that of its "latest" object). It should automatically work with all propagators that support both individual epoch mode and indexed propagation.
- New functions to copy and append populations.
- Saved populations (.opi files) are now stored gzipped.
- All modules can now read config files individually.
- Support for reading files from resource archives.
Issues:
- Example code is outdated
- Fortran interface is untested
Q: Does OPI's open source license affect my host and propagator applications?
A: No. OPI is explicitly designed to be independent from hosts and propagators so using OPI does not require you to adopt open source licenses for your applications or plugins. While we always welcome all kinds of source code contributions we only ask you to submit fixes and improvements that you make to the OPI library itself. One major goal of OPI is to be able to exchange, compare and collaborate on orbital propagators with other researchers. If you are unable to share your propagator or force model implementation in source code form then OPI may provide an easy way for you to distribute it as a pre-compiled plugin instead.
Building OPI has been tested on Linux (recent versions of Debian, Ubuntu and OpenSuSE), Windows (Visual Studio 2017/2019) and OSX (deprecated). OPI uses CMake as a build system, so simply follow the usual instructions below or use the GUI tool (in-place builds are not allowed):
mkdir build
cd build
cmake .. #or 'cmake-gui ..' - whichever you prefer
make
make install
make doc #optional, to build the API documentation - requires Doxygen
You can set the CMAKE_INSTALL_PREFIX variable to a custom directory of you don't want a system-wide install. In that case, you must make sure that the lib directory is in your library path at runtime by setting the LD_LIBRARY_PATH variable accordingly. If you require support for CUDA propagators, make sure the CUDA SDK is installed and can be found by CMake.
To start using OPI, take a look at the documentation provided with the library. If you have any questions, please contact me (mmoeckel on GitHub).
For a graphical OPI host and population editor, check out OPI Explorer.