Artificial satellite code in C/C++
Last revised 24 Sep 2009
Reason for this code
Click here to download the source code and
SAT_CODE.DLL (128728 bytes)
Copyright (not much of one)
OSes/compilers supported thus far
Change log for this version
Acknowledgments
Other source code implementations of SGP4/SDP4
After some hunting for C/C++ code to implement the assorted
satellite theories SGP, SGP4, SGP8, and the "deep-space" variants
SDP4 and SDP4, I found
the source code posted by Neoklis Kyriazis. As he mentions
in the
readme.txt file, the C source code is derived from the NORAD
FORTRAN routines in the Spacetrack #3 report, and from T S Kelso's
Pascal routines (which also came from Spacetrack #3.) In my case,
all I've done is to take Neoklis Kyriazis' source and revise it
somewhat, in several directions.
Copyright: The code is derived from Neoklis Kyriazis' code,
and you may want to read the COPYRIGH.TXT file from the
source code on his site. But essentially, his code is in turn
derived from, and therefore is itself, public domain materials, so
that his code (and the code posted here) is in the public domain,
usable for all purposes commercial and otherwise (I'm using it in
my own commercial software; that's part of what is driving my
interest in it.)
OSes/compilers supported thus far:
Four 'make' files are provided: linmake for gcc on Linux,
watcom.mak for OpenWATCOM
, sat_code.mak for a statically-linked version in MS
Visual C/C++, and sat_dll.mak for a version that creates a
Windoze DLL. This last may prove useful for cross-language support; there
has been at least one inquiry about using the library with Visual Basic,
which can make use of DLLs.
Changes: Here are the differences between my code
"du jour" and the original code provided by Neoklis Kyriazis:
(24 Sep 2009) To my surprise, I found out that the
write_elements_in_tle_format() function wrote the international
designator right-justified, instead of left-justified. This is flat-out
wrong, but not something most programs (or human viewers of TLEs) would
have noticed. It's fixed.
Also, some error checking has been added to certain functions.
For example, the select_ephemeris function has error checking
now to ensure that 0 <= eccentricity < 1, and that the mean motion is
greater than zero. Some other functions now return error codes (instead
of just generating math exceptions). I added some explanatory comments to
deep.cpp and gave some variables more human-readable names.
Also, I updated the linmake makefile for Linux, and
found that it can also be used with MinGW,
a version of gcc for Windows.
(2 Jan 2009) I realized that tle_out.cpp had a subtle
sort-of-bug: a TLE with an epoch of '07365.58333' (partway through
31 December 2007) was written out with an epoch of '08000.58333' (partway
through 0 January 2008). Dates for New Year's Eve of any year would
have been similarly afflicted. This actually would have been okay with
most satellite code, but I suppose some error-checking code out there
might have objected, and it could confuse humans reading the TLEs.
It's fixed.
(26 & 13 Jun 2007) Aleks ???, author of the
Heavensat satellite tracking software, located some divide-by-zero
problems with satellites with zero inclination, in the Deep_dpinit
function. Changing this line:
deep_arg->ssh = sh / deep_arg->sinio;
to:
deep_arg->ssh = (deep_arg->sinio ? sh / deep_arg->sinio : 0.);
fixes the first problem. (The problem was already fixed in the Dundee and
Center for Space versions described below, using slightly different
logic.) The same problem occured about thirty lines further down:
numbers are divided by deep_arg->sinio without checking to see if that
quantity might be zero. I made a similar fix.
To check all this out, I added some test cases to test.tle
with inclinations of 0 and 90 degrees. All appears to work.
Also, cubic polynomials are evaluated frequently in deep.cpp.
I added a trivial function that takes the coefficients of the polynomial,
mostly just to make the code a little neater.
Also, I made a small improvement so that when the code is in
Dundee-compliant mode, it can save
integration states from one call to the next. Previously, when in
Dundee-compliant mode, this code always had to do the full integration
from t=0 to t=tsince.
Also, I figured out what some of the 'mystery constants' in
deep.cpp were, and did some documenting.
(20 Oct 2006) I've been swapping an e-mail or two with Paul
Crawford, one of the authors of a paper entitled
"Revisiting Spacetrack Report #3." I can highly recommend
this paper. These researchers caught several problems I'd overlooked,
and made some good improvements. They fixed issues that can occur
for eccentricities near zero and one; smoothed out the handling of
the Lyddane problem; and corrected a few constants and coefficients.
Furthermore, they described the fixes in a manner that allowed me to
duplicate them in my code. On the test suite of TLEs mentioned in the
above paper, we now match to within a few millimeters, worst case.
In the process of doing this, I made many changes simply to make it
easier to follow what's going on. (For example, the code for
Deep_dpper() is much more "logical" now.)
Note that to get agreement with the
Dundee code,
one must set a flag in the code for "Dundee compliance". An example of
this is given in test2.cpp, with the line
sxpx_set_implementation_param( SXPX_DUNDEE_COMPLIANCE, 1);
In the default, non-Dundee-compliant mode, the results from the
SDP4() function may depend on what calls have been made
to the function previously. For example, if you asked to know where
a satellite was at times 1000 and 900 minutes after epoch, the code
would integrate forward 1000 minutes, then integrate backward 100
minutes. If you asked for a position at tsince=900 minutes, the code
would simply integrate forward, and the result would be slightly
different.
Similarly, in the default mode, the effects of lunar/solar
perturbations are only recomputed if tsince changes by more than 30
minutes. This is a generally safe thing to do, since those perturbations
are slowly varying. But it does mean that if you asked for positions
at tsince=1000 and tsince=1020, the perturbations as of 1000 minutes
would be applied on the second call. So again, results are not
perfectly replicable; they depend on previous calls to SDP4().
In Dundee-compliant mode, however, the integration always runs
from epoch to tsince, and the perturbations are always recomputed.
Those are the only differences from the default mode. The default
mode is good for all purposes except benchmarking against the Dundee
implementation; for that, I wanted to have exact agreement in results.
(20 Oct 2006) The integration scheme in Deep_dpsec() has
been changed extensively (again). Instead
of splitting up the integration range into equal steps of 720 minutes
or less, steps of 720 minutes are taken until the final step. I did
that in order to match the results of the Dundee code. Separately, I
realized that the second-order Taylor series integration method used in
this part of the code could almost trivially be extended to n-order
integration; the higher derivatives fall out with very little math
required, once the first two are computed.
By default, this code still uses only the second-order terms.
But I've found that if one goes to higher orders, you can increase the
integration step size without a penalty in precision, and it runs
faster. (Not usually an issue, unless you're using elderly TLEs.
Unfortunately, due to restrictions in the distribution of TLEs,
some of us are having to do just that.)
(28 Aug 2006) From the beginning, there was code in
get_el.cpp to take the two input lines of a TLE, and parse
them out into a tle_t structure (the function
parse_elements() does this.) Now, there's a function in
the new file tle_out.cpp that does the reverse of this:
write_elements_in_tle_format() will put your tle_t
structure back out into the two-line format.
As a side effect of this, the tle_t structure had to
be expanded a bit. It now contains the NORAD number and international
designation, bulletin number, revolution number, classification
(almost always "U", or "Unclassified"), and ephemeris type... in
other words, all the bits and pieces of data given in a TLE.
(14 May 2005) Caught a small bug in the Deep_dpinit()
function in deep.cpp. The code checked to see if the TLE
epoch matched that of the previous TLE; if it did, some computations
could be skipped, because the variables wouldn't change. That was
true back when the variables in question were stored statically
(and would still be true in certain conditions now), but the
variables are now stored in the deep_arg_t structure;
I was running into uninitialized variable problems when feeding the
code a series of TLEs with identical epochs.
(15 April 2005) I'm now looking at generating TLEs from my
Find_Orb orbit determination software.
(Actually, it already generates them, but they're not very accurate.
I just convert a state vector to classical orbital elements, without
taking into account the fact that SxPx elements don't quite match up
to classical ones.) Anyway, doing this has caused me to add some
comments to norad.h concerning units, and to clean up
get_el.h a little. No actual changes in functionality
are involved; I just needed to make sure that I (and others) could
see what was going on.
(16 February 2005) After almost two years of no changes, I
regarded this code as rather stable. But today, I received a set of
TLEs wherein the decade character was a space, rather than a zero.
A small fix to get_el.cpp allowed the code to function
despite this oddity.
(22 April 2003) The dpsec function in deep.cpp
has been a long-time source of confusion. It contained a
very bizarre numerical integration routine which turns out to have
numerous Bad Things in it, resulting in unnecessary complexity
and performance degredation. (But not, as best I can tell,
actual bugs.) I've fixed these; the resulting code is a
lot easier to follow. Explaining how the code used to
work and what I changed is a long story;
click here for details.
(22 April 2003) Earlier, I recognized that the SDP4 and SGP4 code
had large chunks of common code, and moved them
to common.cpp. It's not so obvious, but their initialization
routines, SGP4_init and SDP4_init, also had
a lot of redundant code. I've extracted that into a new
sxpx_init function, eliminating a lot of waste.
(29 March 2003) Peter Birtwhistle ran into a problem with
sat_id that was traceable to a
very high-drag satellite. Projected a few days into the
future, this satellite ended up with an "orbit" a few kilometers
above the center of the earth, and generated a math error (square
root of a negative number). I evaded this by adding some error
checks in common.cpp. If certain parameters exceed their
proper limits, the state vector is now set to all zeroes and the
function returns. This is at least more "graceful" than having it
crash and/or return utter garbage.
(8 March 2003) There is a new example program,
sat_id.cpp. This is similar to obs_tes2.cpp (in
fact, large chunks of the code are identical). The only real
change is that it reads through a file containing astrometric
observations in the MPC format. As it comes across them, it
looks for satellites within a given radius of those coordinates
as seen from that location at that time. This is both an "example"
program for programmers and a tool for non-programming astronomers,
who routinely get images of satellites, but don't know if they
might actually be Earth-passing asteroids. There is a writeup
describing the program from their viewpoint
here.
(12 December 2002) There is a new example program,
obs_tes2.cpp, showing how to use the routines to get a list
of satellites within a given search radius of a given RA/dec, as
seen from a given lat/lon/altitude.
(20 October 2002) Mostly, a bunch of cosmetic changes. I
realized that large chunks of the SGP4 and SDP4 code are in common.
With some more work, I can probably eliminate a lot of redundant
code (and, thereby, opportunities for bugs.) So far, all I've
done is to combine a lot of code in the new common.cpp
code. This still leaves a lot of redundancy in the init
routines. All of this did require me to rearrange some of the
items in the params array and in the deep_arg_t
structures. (Nothing added or changed; just swapped around so
that the order in SGP4 would match that in SDP4.)
(20 October 2002) There's a new dynamic.cpp file for
Windows use only, for C/C++ programmers looking to bind to the
sat_code.dll routines at run-time. (In other words,
probably nobody will have much interest in it.) I intend
to use this in my existing planetarium/star charting
software. This program currently supports only SGP4; using
run-time linking, it'll be able to use SDP4 and SxP8 functions if
people download the sat_code.dll, without bulking up the
(already bloated) main executable. People who don't care about
artificial satellites (i.e., most users of the program) can simply
not download the DLL.
Also included is a test3.cpp program that makes use
of the new 'dynamic' functions, showing how they are used in comparison
to the 'usual', statically-linked functions.
(25 September 2002) I realized that in the DLL version, the
functions exported were not declared as extern "C".
They would therefore not be callable from a Visual Basic (or, probably,
a Pascal or FORTRAN) app. I've fixed this. (Only the DLL version
is affected by this; the changes compile to "no change" in the other
versions.)
(25 September 2002) I also realized that the example output files
were out of date. I've fixed this. You should be able to run
test_sat and test2 and get the results given in
result and test2.out, respectively.
(23 September 2002) I've added a few functions to support topocentric
ephemerides. The method for computing the satellite position hasn't
changed, but there are now some functions for computing an "observer"
position, and for converting the difference between the two into an
RA/dec and distance. Click here for details.
(17 August 2002) Jim Lewis pointed out a
page on the STK site discussing some of the changes AGI had
to make to the SxP4 code. One of those changes was to avoid problems
with zero-eccentricity satellites. We did a little hunting and
found two cases where this would lead to a division by zero, and
made two small changes (both in sgp4.cpp and marked
with '17 Aug 2002') to repair them.
(16 August 2002) Jim Lewis pointed out a slight blunder in the
dpper code in norad.cpp: two variables, 'pgh'
and 'ph', were being modified between calls in ways that ought not
to have been happening. In most test cases, this didn't matter; in
others, it could lead to weird results. A couple of lines have been
changed to kill this bug.
(2 August 2002) Jim Lewis, of Ten
Sigma Research, pointed out that the sources in the
sat_code.zip file were a bit confused; one of the older sources
had been left in, while several new ones were omitted! This was due
to a mistake I made in the batch file used to create the ZIP file.
If you downloaded sat_code.zip before 2 August, you should
definitely click here to download the corrected
version.
I renamed *.c to *.cpp, just to get extra compiler checking, and
tried it under MS Visual C/C++ 5.0, WATCOM C 10, MS Visual C/C++ 1.0
(to get a 16-bit DOS app), and Linux gcc. No problems under any of
these, no alterations required. Renaming back to .c should also be
okay.
There are now two different test programs. test_sat.cpp
the "test data" values from Spacetrack 3 stored in it, and if you run
test_sat with a command line argument, the differences
between the "test data" values and the ones actually being computed is
shown. Any errors stand out as non-zero values. At the suggestion of
Thierry Marais, I added six other TLEs to exercise assorted possible
execution paths for SDPx.
However, the Spacetrack examples and the six TLEs still don't
exercise all of the various paths through which the code can go. The
behavior can depend on previous calls to the functions and so forth.
test2.cpp, therefore, reads in test.tle and tries
out a long list of test cases. The output of test2 in
32-bit Windows, 16-bit DOS, 32-bit DOS extender, and Linux forms
agrees down to the last decimal place (about 10 microns).
I added get_el.c, which contains the function needed to
parse TLE data into the tle_t structures.
After hearing about assorted errors in the original Spacetrack Fortran
code, I contacted Rob Matson after finding these posts:
http://www.satobs.org/seesat/Oct-1997/0440.html
http://www.satobs.org/seesat/Oct-1997/0497.html
The "Lyddane modifications" had already been made by Neoklis Kyriazis,
but the lunisolar perturbation modifications (a matter of retaining
some offsets as to what the perturbations were at the epoch) were not
made. I made those modifications, and promptly got values a few
km different from the original Spacetrack values. At present, if you
compile norad.c with RETAIN_PERTURBATION_VALUES_AT_EPOCH
#defined, you'll get that modification. But by default, it will (as
apparently was done for the Spacetrack examples) leave those values at
zero (by omitting a few lines of code.)
If you do include this modification, be aware that you
will get mismatches between the code and the Spacetrack 3 examples of
order 20 km. Omitting this modification (as is done by default) gets
a match within about 6 m for SDP4, and about 100 meters for SDP8.
The AcTan() function is essentially the C atan2( )
function rearranged so that the output runs from 0 to 2π
(instead of -π to +π). So it can be reduced to a couple of
lines of code.
Similarly, the Modulus() and FMod2p() functions
are slight variants of the fmod() library functions. I wiped
Modulus() out completely.
In a few places, items were not explicitly cast from double to
integer types. That caused my compiler to belch some warnings. Just
for cosmetics, I added "(int)" and "(long)" in those places.
Calling pow() to square, cube, or raise a number to the
fourth power is a Bad Idea in terms of speed, and occurred several times
in the code. pow() has to bring in the heavy machinery of taking a
logarithm and then an exp()-type call. In such cases, I revised the
code slightly to just multiply the number by itself. (Not that this
optimization is immensely helpful. Neoklis Kyriazis, who did the
original port of this code from Fortran, already did the major thing I
planned to do, namely, computing certain "constants" that are
independent of 'tsince' during an initialization stage, so they don't
get recomputed at each call. That helps a lot.)
You may sometimes want to get a position and not care about
the velocity. I revised the code so that, if the 'vel'
argument to any of the five satellite functions is NULL, the
velocity goes uncomputed. (Saves peanuts in computation time, I
admit, but spares you having to declare a velocity vector,
compute it, and then ignore it.)
I moved certain #include statements from norad.h
to the C files. norad.c really only needed math.h,
test_sat.c only needed stdio.h and stdlib.h.
I broke up norad.c into components, for several reasons.
It looks to me as if the SxP8 methods get almost no use, and I may
not actually include them in my commercial software. Modularizing
code is a Good Idea in any case. With the breakup, a person could
include only SGP, or only SGP4, etc., thereby drastically reducing
the amount of code brought into play.
I separated the 'initialization' step from the rest of the process
for all five models. Thus, you now call (for example) SDP4_init()
once after loading a TLE. After that, you can call SDP4
as desired. (This is essentially a cosmetic change. Previously,
the SDP4 function would use a flag to determine if it should
initialize.)
In the interests of further "encapsulating" the code and letting
people avoid looking at the internals of the code, I arranged it so
that most structures were kept internally, with the "public interface"
declared in norad.h and "private routines" declared in
norad_in.h. I also realized that in many cases, variables
were declared with much greater scope than was absolutely necessary.
Most of that sort of thing has been cleaned up now.
I eliminated the 'vector_t' type. The position and
velocity vectors are handled simply as arrays of three doubles. This
just reflects a personal preference; I like being able to write code
such as
for( i = 0; i < 3; i++)
do something with vector element i;
as opposed to
do something with vector element x;
do it again, but this time to vector element y;
do it again, but this time to vector element z;
Also, I changed the units from Earth radii to kilometers.
I've never been able to come up with a case where you would
actually want the former.
Deep() had a slew of static variables that, it
appeared to me, more logically belonged as part of the
deep_arg_t structure. So I moved them there.
All five SxPx functions make no changes to tsince or
the input TLE data, so I made those arguments 'const'.
Deep() logically ought to be three separate functions:
one to initialize (Deep_dpinit), one for secular effects (Deep_dpsec),
and one for periodic effects (Deep_dpper). I've made that division.
At one point, I got slightly differing results from test_sat,
down at the millimeter level, but I couldn't see why there should be
any difference, and suspected an uninitialized variable that
might, at a future date, cause kilometer or teraparsec-level errors.
I eventually traced it to π being given to a
mere thirteen places in norad.c. That was overkill in the
Bad Old Days of single-precision, but double-precision values are good
to sixteen or seventeen decimal places. I added a few more digits and
made sure that constants such as π/180 would
be accurately rendered.
"...The primary purpose of the Data statement is to give names
to constants; instead of referring to π as 3.141592653589793
at every appearance, the variable Pi can be given that value
with a Data statement and used instead of the longer form of
the constant. This also simplifies modifying the program,
should the value of π change."
-- Fortran manual for Xerox Computers
When compiled with full optimizations with Microsoft Visual C,
the select_ephemeris() function misbehaved to the extent of
setting the DEEP_SPACE_EPHEM_FLAG incorrectly on occasion.
I traced it to this line (in basics.cpp):
temp = ck2 * 1.5 * (r1*r1*3.0-1.0) / pow( 1.0-tle->eo*tle->eo, 1.5);
which sets 'temp' to INF, despite the fact that ck2, r1,
and tle->eo had reasonable values. Changing it to read as follows:
temp = ck2 * 1.5 * (r1*r1*3.0-1.0) * pow( 1.0-tle->eo*tle->eo, -1.5);
should, in theory, have changed nothing. In practice,
it fixes the bug, so I assume an optimization error occured. (I mention
it because in the past, I've sometimes seen "optimization errors" turn
out to be "errors that were there all along, but optimizing made it
more likely that they'd pop up at you.")
The code set the satellite "epoch" to be one read directly from a
TLE. Day 41 of year 2002 would be 02041, day 333 of year 1997
would be 97333. This is very TLE-specific; almost all my code
(and most astronomical code) works in terms of JD (and internally,
norad.c has to convert this sort of "epoch" to a JD before it
can do anything with it.) Fixing this involved some modest changes to
ThetaG() and allowed the complete removal of
Julian_Date_of_Year().
(25 Jun 2002) After a brief bug scare (I thought the code might
be producing wrong values), I went through deep.cpp and
did a lot of housekeeping. The actual functionality of the code is
unchanged, but the logic is much easier to follow, constant variables
are set to be 'const', and most variables have been moved to be within
their proper scope. The 'flags' turn out to be almost totally
unnecessary, except for those indicating if the object is synchronous
or in a resonant (12-hour, high-eccentricity) orbit.
(27 Jun 2002) Careful checking against Thierry Marais' code revealed
some small differences. Some of these were due to the constants
'ck4', 's', 'qoms2t', and 'xke' being given to insufficient precision
(see norad_in.h for details). Also, the ThetaG()
function in deep.cpp had miscellaneous very strange problems,
including a very subtle loss-of-precision one causing differences at
the 2-centimeter level. (After assorted fixes, my code and Thierry's
quite different code agree to the 10-micron level.)
"Observer" functions: An example program showing how to
compute topocentric ephemerides is given in obs_test.cpp.
The topocentric observer's viewpoint is defined in terms of a
longitude, latitude, and height in meters above the ellipsoid.
The lat_alt_to_parallax() function converts these last
two to "parallax constant" form, rho_sin_phi and rho_cos_phi.
Next, given a particular time, the observer_cartesian_coords()
function converts all this to a Cartesian, earth-centered
vector, just like that returned by the satellite functions.
Now that you have the observer's coordinates and the satellite's
coordinates, you can hand both off to the
get_satellite_ra_dec_delta() to get an RA/dec and distance (in
kilometers) to the satellite. Please note that, so far, everything
has been done in the mean ecliptic of date; we'll usually want to
use the epoch_of_date_to_j2000() function to convert the
RA/dec to J2000 coordinates.
Acknowledgments: Thanks go first to Neoklis Kyriazis for
supplying the source on which these efforts are based. I got some
helpful data on test cases for SD* routines from Thierry Marais (the
last six TLEs computed by test_sat are examples given by
him that should exercise all the paths of those functions.) I was
quite baffled by the assorted "versions" of SD* out there, and
Rob Matson gave me a wonderful rundown of the tweaks I would have
to make to the code to get the solar-lunar perturbations to work in
a reasonable manner. Paul Gabriel and Mike McCants gave me some
pushes in the right directions when I initially began looking into
the problem of getting this sort of library of routines together.
Jim Lewis tested the code on a Sun box and found several small
errors in the process.
Other source code implementations of SGP4/SDP4:
The code on this page was derived from Neoklis Kyriazis' source code,
which in turn was derived from
Dr. TS Kelso's
Pascal implementation and the
FORTRAN routines in NORAD's Spacetrack report #3. (The preceding
link is in PDF, but you can cut-and-paste FORTRAN source from it.)
Another C/C++ adaptation of Kelso's code is provided with
GPS 2.4, a
program intended primarily for tracking GPS satellites, but which
is not limited to them.
Dr Kelso's code implements only SGP4 and SDP4. Dominik Brodowski
has posted
Pascal source for SGP and SxP8.
The implementation of SGP4/SDP4 at
"Revisiting Spacetrack Report #3", by David A. Vallado,
Paul Crawford, Richard Hujsak, and T. S. Kelso, is well-thought of by
many, including me. (I swapped a series of e-mails with the authors,
resulting in some bug fixes, almost all of them in my code and not
theirs.) Source in C++, FORTRAN, Java, MATLAB, and Pascal is
provided.
An advantage of this implementation is that the authors were at
pains to make the reasoning behind their changes clear. That removes
much of the mystery behind what can be a very mysterious chunk of code.
(Of course, I'd like to think that I've achieved similar clarity!)
Paul Crawford has posted a C implementation of SDP4 and SGP4
here. It
provided several bug fixes; in fact, as of 15 October 2006, my code
and Crawford's code produce identical results on a variety of test cases.
You can also
download C source code for the PREDICT software. This program
runs on Linux, DOS, and Sharp Zaurus PDA. Looks as if SGP4 and
SDP4 are implemented.
There is also an
implementation of SGP4 in Java.
Claudio Pizzillo has announced an
implementation of
SGP, SGP4, SDP4, SGP8, and SDP8 in VB.NET.
Note that most of the bug fixes described for my code will also apply
to these sources (except the Dundee code). In particular, almost all of
them lack the change in dpper to get certain variables
initialized at the right time; search through deep.cpp for
SPACETRACK_3 for details. If you do decide to make use of a
different implementation, I suggest you keep the URL for this page
around; the bug comments will still be useful to you.