libpredict is an ANSI C library for predicting satellite orbits based on TLEs, developed by ARK. This was primarily developed for use in flyby, but can also be useful on its own. If you just want to track a satellite, flyby is usually a better choice, but if you want to go down to a deeper level and be able to apply satellite prediction to more advanced and complex usecases in a more flexible way, libpredict might be suitable.

The goal of libpredict was mainly to separate the satellite calculations from predict for use in its fork, flyby, and enable reuse of the API in other satellite applications. C implementation became a requirement due to the well-defined binary compatibility for C libraries and the use of C in both predict and flyby. While the core routines are in C, we will also at some point be providing high-level bindings for other languages like python. See also: Development of flyby and libpredict.

This post outlines in detail how libpredict can be used to track satellites in a programming language, and is long and technical and probably mostly for those with special interest in the topic. If life gets too frustrating and boring, you can scroll down to the plots and rest your eyes on colorful satellite tracks:-). An earlier post, Satellite tracking using flyby, gives a top-down motivation for why we are doing this at all and a more user-friendly approach to satellite tracking.

In order to use libpredict, it first has to be compiled. This is outlined in Satellite tracking using flyby. After compilation and installation, libpredict can be included in your program.

Assuming you are using C or C++, you have to include

#include <predict/predict.h>

in your source code. When compiling your code, it is also necessary to link against libpredict. If you are using gcc or g++ to compile, this is done using

gcc -lpredict [your source code] -o [your executable]

In addition, -L [libpredict installation path]/lib -I [libpredict installation path]/include can be added if your libpredict installation path is very non-standard.

Library settings and source files would typically be defined through a Makefile or CMakeLists.txt-file. The basic libpredict CMakeLists.txt-example defines how this can be done using CMake.

Below follows a simple example. We will go through and explain in more detail each line of this code.

#include <predict/predict.h>

int main() {
	char *tle_string[] = {"1 07530U 74089B   17058.02491442 -.00000048  00000-0 -22049-4 0  9995",
	"2 07530 101.6163  28.9438 0011984 174.4353 227.0960 12.53625643935054"};

	//satellite representation
	predict_orbital_elements_t *orbital_elements;
	orbital_elements = predict_parse_tle(tle_string);

	//containers for time-dependent properties
	struct predict_orbit orbit; //independent of an observer
	struct predict_observation observation; //relative to an observer

	//prediction time
	predict_julian_date_t pred_time;
	pred_time = predict_to_julian(time(NULL));

	//calculate point along satellite orbit
	predict_orbit(orbital_elements, &orbit, pred_time);
	printf("%f %f\n", orbit.latitude, orbit.longitude);

	//observe the orbit
	double latitude = 63.42;
	double longitude = 10.39;
	predict_observer_t *observer;
	observer = predict_create_observer("LA1K", latitude/180.0*M_PI, longitude/180.0*M_PI, 0);
	predict_observe_orbit(observer, &orbit, &observation);
	printf("%f %f\n", observation.azimuth, observation.elevation);

	//calculate time for the next time satellite passes over the horizon
	predict_julian_date_t aos_time = predict_next_aos(observer, orbital_elements, pred_time);
	double time_to_aos = (aos_time - pred_time)*24*60;
	printf("Time to next AOS: %d minutes\n", time_to_aos);

	//calculate doppler shift
	double doppler_shift = predict_doppler_shift(observer, &orbit, downlink_frequency);
}

libpredict needs to be fed satellite properties. Properties for a specific satellite are represented by

predict_orbital_elements_t *orbital_elements;

This structure remains constant throughout the program and represents your satellite. The defined properties here represent the state of the satellite at a specific time (the epoch time), and all later predicted properties are extrapolated from the measured properties using perturbation models. The properties are transformed from a TLE using

orbital_elements = predict_parse_tle(tle_string);

The tle_string is currently defined as a char array containg the two text lines associated with each satellite in e.g. https://www.celestrak.com/NORAD/elements/amateur.txt. If we were to hard-code the OSCAR-7 satellite, we would have to do

char *tle_string[] = {"1 07530U 74089B   17058.02491442 -.00000048  00000-0 -22049-4 0  9995",
"2 07530 101.6163  28.9438 0011984 174.4353 227.0960 12.53625643935054"};

In a production situation, this could typically be read from a file or a database. Flyby, for example, assumes flat file text files like amateur.txt that are read into a TLE database and then converted to orbital elements.

libpredict separates properties that are true regardless of an observer, and properties that are taken relative to a observer. The former is represented by

struct predict_orbit orbit;

and defines the latitude, longitude and altitude of a satellite, along with some other useful properties. These are accessed by orbit.latitude, orbit.longitude, and so on, but won’t contain useful values until we try to predict them. The properties that are relative to an observer are represented by

struct predict_observation observation;

This includes properties like the azimuth, elevation, whether the satellite is visible on the sky. We use struct, and have not typedefed them in order to indicate that these are “light-weight” structures containing only numeric fields that can be freely fiddled with and discarded at will without any special concern over memory management, initialization or deinitialization. This can be compared to e.g. predict_orbital_elements_t, which has special initialization and deinitialization functions. Instances of *_t structures also have special meaning (specific location, specific satellite) and typically stay the same, while e.g. struct predict_orbit has no special meaning, can be reused across different satellites and times or discarded and is only a container for fleeting numerical values. While you would always have to call both predict_orbit() and then predict_observe_orbit() in order to get what you usually would want, we have still chosen to separate observer-independent and observer-dependent properties and functions for a more “clean” API and better separation between objects, and cohesiveness within each object.

libpredict uses its own time format in order to be able to predict satellite positions at a finer time granularity than seconds, platform-independently, and without dragging in extra time libraries:

predict_julian_date_t pred_time;

You don’t have to care about how this is represented internally, except that it is the number of days since a specific date. Conversion functions to and from UNIX timestamps are available, so that you easily can obtain a current timestamp using e.g.

pred_time = predict_to_julian(time(NULL));

Using this time, we can now do predictions. The observation-independent properties at time pred_time are calculated using

predict_orbit(orbital_elements, &orbit, pred_time);

The fields in orbit will now contain actual values that can be shown or used for further calculations.

In order to predict properties relative to a position, we first have to define our position. This is represented by

predict_observer_t *observer;

Given that we are located at longitude 63.42 degrees north and latitude 10.39 degrees east, we can create this using

observer = predict_create_observer("LA1K", 63.42/180.0*M_PI, 10.39/180.0*M_PI, 0);

Properties relative to this observer are then predicted using

predict_observe_orbit(observer, &orbit, &observation);

The structure observation now contains the properties of the satellite at the time pred_time. Note that we didn’t have to use the orbital elements or the time to observe the orbit, as this is calculated directly from the position of the satellite, not by extrapolating from the model.

We can also predict the next time the satellite will pass over our horizon (the so-called AOS, arrival of satellite or arrival of signal):

predict_julian_date_t aos_time = predict_next_aos(observer, orbital_elements, pred_time);

The aos_time can be converted back to a UNIX timestamp or compared against pred_time to calculate the next time the satellite will pass over our horizon. A corresponding function exists for LOS, loss of satellite or signal, when the satellite passes down the horizon.

To calculate the doppler shift of the downlink frequency of a satellite signal as received by our fictional station, we can use

double doppler_shift = predict_doppler_shift(observer, &orbit, downlink_frequency);

The doppler shift will have to be added to the downlink frequency in order to correct it. The unit of the doppler shift will be given by the unit of the input downlink frequency.

We have generated some figures using this example (currently in a separate feature branch in the git repository, to be merged eventually), by finding the time of a pass using predict_next_aos() and predicting various properties some time before the pass until some time after the pass. Each figure appears twice, once for each of two passes of OSCAR-7 above Trondheim, Norway:  good pass after 2017-02-28 11:30 UTC (pass 1), and then a slightly worse pass after 2017-03-02 09:30 UTC (pass 2).

A map showing the satellite pass relative to the real world:

Pass 1.


Pass 2.

Various properties as a function of time during the pass:


Pass 1.


Pass 2.

(Update, 2017-03-16: Updated doppler shift plot after a bug was found in the calculations.)

For pass 1, the first derivative of the elevation is almost discontinuous at the elevation peak, which is to be expected around 90 degrees elevation. For satellites that would not pass directly overhead, like pass 2, the elevation will have more continuous first derivatives. The change in the azimuth for pass 1 seems extreme, but since the antenna is pointing with an elevation close to 90 degrees, the required change in antenna position would not be very significant. This is better seen when plotting the properties in a polar diagram.

Azimuth and elevation in a polar diagram, showing approximately how the antenna at the location would have to be steered:


Pass 1.


Pass 2.

Figures for pass 1 can be directly reproduced from the example, while the hard-coded time would have to be changed for pass 2.

libpredict contains other API functions for obtaining various properties relevant for a satellite, and can predict the relative positions of the sun and moon. All of these functions can be found in predict.h, which also defines documentation for the function arguments, what each function does and a description for the various fields of all the structs.

Flyby defines a lot of the behavior of libpredict. Its source code can be used as a general guideline for how libpredict can be used. Smaller applications using libpredict also exist, like pass_trigger or the various examples present in the libpredict repository.