Loading...
Searching...
No Matches
C99 Usage

This guide is specifically for using SuperNOVAS as a C99 library. There is a separate guide for using the C++11 API. The links below let you jump to the relevant sections:

- Building your application with SuperNOVAS (C99)
- Example C99 usage
- Tips & Tricks (C99)

SuperNOVAS is a C99 library at its core. If you are looking for maximum speed, or want to use SuperNOVAS on older platforms, the C99 API is there for you.

The following links provide further useful documentation resources for you:

- C99 API documentation
- SuperNOVAS pages on github.io

This document has been updated for the v1.7 and later releases.

# Building your application with SuperNOVAS (C99)

There are a number of ways you can build your application with SuperNOVAS. See which of the options suits your needs best:

- Using a GNU `Makefile`
- Using CMake
- Deprecated API
- Legacy linking `solarsystem()` and `readeph()` modules
- Legacy modules: a better way

## Using a GNU Makefile

Details

Provided you have installed the SuperNOVAS headers and (static or shared) libraries into a standard location, you can build your application against it easily. For example, to build myastroapp.c against SuperNOVAS, you might have a Makefile with contents like:

myastroapp: myastroapp.c
$(CC) -o $@ $(CFLAGS) $^ -lsupernovas -lm -lcurl

If you have a legacy NOVAS C 3.1 application, it is possible that the compilation will give you errors due to missing includes for stdio.h, stdlib.h, ctype.h or string.h, because these headers were implicitly included with novas.h in NOVAS C 3.1, but not in SuperNOVAS (at least not by default), as a matter of best practice. If this is a problem for you can 'fix' it in one of two ways: (1) Add the missing #include directives to your application source explicitly, or if that's not an option for you, then (2) set the -DCOMPAT compiler flag when compiling your application:

myastroapp: myastroapp.c
$(CC) -o $@ $(CFLAGS) -DCOMPAT $^ -lsupernovas -lm -lcurl

If your application uses optional planet or ephemeris calculator modules, you may need to specify the additional shared libraries also:

myastroapp: myastroapp.c
$(CC) -o $@ $(CFLAGS) $^ -lsupernovas -lsolsys-calceph -lcalceph -lm -lcurl

## Using CMake

Details

Add the appropriate bits from below to the CMakeLists.txt file of your application (my-application):

find_package(supernovas REQUIRED)
target_include_directories(my-application PRIVATE ${supernovas_INCLUDE_DIRS})
target_link_libraries(my-application PRIVATE ${supernovas_LIBRARIES})

## Deprecated API

SuperNOVAS began deprecating some NOVAS C functions, either because they are no longer needed; or are not easy to use and have better alternatives around; or are internals that should never have been exposed to end-users. The deprecations are marked in the inline and HTML API documentations, suggesting also alternatives.

That said, the deprecated parts of the API are NOT removed, nor we plan on removing them for the foreseeable future. Instead, they serve as a gentle reminder to users that perhaps they should stay away from these features for their own good.

However, you have the option to force yourself to avoid using the deprecated API if you choose to do so, by compiling your application with -D_EXCLUDE_DEPRECATED, or equivalently, by defining _EXCLUDE_DEPRECATED in your source code before including novas.h. E.g.:

// Include novas.h without the deprecated definitions
#define _EXCLUDE_DEPRECATED
#include <novas.h>
novas.h
SuperNOVAS types, definitions, and function prototypes.

After that, your compiler will complain if your source code references any of the deprecated entities, so you may change that part of your code to use the recommended alternatives instead.

## Legacy linking solarsystem() / solarsystem_hp() and readeph() modules

The NOVAS C way to handle planet or other ephemeris functions was to link particular modules to provide the solarsystem() / solarsystem_hp() and readeph() functions. This approach is discouraged in SuperNOVAS, with preference for selecting implementations at runtime. The old, deprecated way, of incorporating Solar-system data is supported, nevertheless, for legacy applications with some caveats.

Details

To use your own existing default solarsystem() implementation in the NOVAS C way, you will have to build SuperNOVAS with SOLSYS_SOURCE set to the source file(s) of the implementation (config.mk or the environment).

The same principle applies to using your specific legacy readeph() implementation, except that you must set READEPH_SOURCE to the source file(s) of the chosen implementation when building SuperNOVAS).

(You might have to also add additional include directories to CPPFLAGS, e.g. -I/my-path/include for you custom sources for their associated headers).

## Legacy modules: a better way

A better way to recycle your old planet and ephemeris calculator modules may be to rename solarsystem() / solarsystem_hp() functions therein to e.g. my_planet_calculator() / my_planet_calculator_hp() and then in your application can specify these functions as the provider at runtime.

Details

E.g.:

set_planet_calculator(my_planet_calculator);
set_planet_calculator(my_planet_calculator_hp);

(You might also change the short type parameters to the SuperNOVAS enum types while at it, to conform to the SuperNOVAS novas_planet_provider / novas_planet_provider_hp types.)

For readeph() implementations, it is recommended that you change both the name and the footprint to e.g.:

int my_ephem_provider(const char *name, long id, double jd_tdb_high, double jd_tdb_low,
enum novas_origin *origin, double *pos, double *vel);
novas_origin
novas_origin
The origin of the ICRS system for referencing positions and velocities for solar-system bodies.
Definition novas.h:1100

and then then apply it in your application as:

set_ephem_provider(my_ephem_provider);
set_ephem_provider
int set_ephem_provider(novas_ephem_provider func)
Sets the function to use for obtaining position / velocity information for minor planets,...
Definition ephemeris.c:118

While all of that requires some minimal changes to your old code, the advantage of this preferred approach is that you do not need to re-build the library with USER_SOLSYS and/or USER_READEPH defined.

# Example C99 usage

- Calculating positions for a sidereal source
- Calculating positions for a Solar-system source
- Going in reverse...
- Calculate rise, set, and transit times
- Coordinate and velocity transforms (change of coordinate system)

SuperNOVAS v1.1 has introduced a new, more intuitive, more elegant, and more efficient approach for calculating astrometric positions of celestial objects. The guide below is geared towards this new method. However, the original NOVAS C approach remains viable also (albeit often less efficient).

Note
  You may find an equivalent legacy example showcasing the original NOVAS method in NOVAS-legacy.md.

## Calculating positions for a sidereal source

A sidereal source may be anything beyond the Solar system with 'fixed' catalog coordinates. It may be a star, or a galactic molecular cloud, or a distant quasar.

- Specify the object of interest
- Specify the observer location
- Specify the time of observation
- Set up the observing frame
- Calculate an apparent place on sky
- Calculate azimuth and elevation angles at the observing location

### Specify the object of interest

First, you must provide the astrometric parameters (coordinates, and optionally radial velocity or redshift, proper motion, and/or parallax or distance also). Let's assume we pick a star for which we have B1950 (i.e. FK4) coordinates. We begin with the assigned name and the R.A. / Dec coordinates.

cat_entry star; // Structure to contain information on sidereal source
// Let's assume we have B1950 (FK4) coordinates of
// 16h26m20.1918s, -26d19m23.138s
novas_init_cat_entry(&star, "Antares", 16.43894213, -26.323094);
cat_entry
struct novas_cat_entry cat_entry
Basic astrometric data for any sidereal object located outside the solar system.
novas_init_cat_entry
int novas_init_cat_entry(cat_entry *restrict source, const char *restrict name, double ra, double dec)
Initializes a catalog entry, such as a star or a distant galaxy, with a name and coordinates.
Definition target.c:109

If you have coordinates as strings in decimal or HMS / DMS format, you might use novas_str_hours() and/or novas_str_degrees() to convert them to hours/degrees for novas_init_cat_entry(), with a fair bit of flexibility on the particulars of the representation, e.g.:

novas_init_entry(&star, "Antares",
novas_str_hours("16h 26m 20.1918s"), // e.g. using h,m,s and spaces as separators
novas_str_degrees("-26:19:23.138")); // e.g. using colons to separate components
novas_str_hours
double novas_str_hours(const char *restrict hms)
Returns a time or time-like angleparsed from a string that contains either a decimal hours or else a ...
Definition parse.c:769
novas_str_degrees
double novas_str_degrees(const char *restrict dms)
Returns an angle parsed from a string that contains either a decimal degrees or else a broken-down DM...
Definition parse.c:730

Next, if it's a star or some other source within our own Galaxy, you'll want to specify its proper motion (in the same reference system as the above coordinates), so we can calculate its position for the epoch of observation.

// Now, let's add proper motion of 12.11, -23.30 mas/year.
novas_set_proper_motion(&star, -12.11, -23.30);
novas_set_proper_motion
int novas_set_proper_motion(cat_entry *source, double pm_ra, double pm_dec)
Sets the proper-motion for a (Galactic) catalog source.
Definition target.c:257

For Galactic sources you will also want to set the parallax using novas_set_parallax() or equivalently the distance (in parsecs) using novas_set_distance(), e.g.:

// Add parallax of 5.89 mas
novas_set_parallax(&star, 5.89);
novas_set_parallax
int novas_set_parallax(cat_entry *source, double mas)
Sets the parallax (a measure of distance) for a catalog source.
Definition target.c:278

Finally, for spectroscopic applications you will also want to set the radial velocity. You can use novas_set_ssb_vel() if you have standard radial velocities defined with respect to the Solar System Barycenter; or novas_set_lsr_vel() if the velocity is relative to the Local Standard of Rest (LSR); or else novas_set_redshift() if you have a redshift measure (as is typical for distant galaxies and quasars). E.g.:

// Add a radial velocity of -3.4 km/s (relative to SSB)
novas_set_ssb_vel(&star, -3.4);
novas_set_ssb_vel
int novas_set_ssb_vel(cat_entry *source, double v_kms)
Sets a radial velocity for a catalog source, defined w.r.t.
Definition target.c:181

Alternatively, if you prefer, you may use the original NOVAS C make_cat_entry() to set the astrometric parameters above all at once.

Details

E.g.:

make_cat_entry("Antares", "FK4", 1,
novas_str_hours("16h 26m 20.1918s"),
novas_str_degrees("-26:19:23.138"),
-12.11, -23.30, 5.89, -3.4, &star);
make_cat_entry
short make_cat_entry(const char *restrict name, const char *restrict catalog, long cat_num, double ra, double dec, double pm_ra, double pm_dec, double parallax, double rv, cat_entry *source)
Fully populates the data structure for a catalog source, such as a star, with all parameters provided...
Definition target.c:336

Next, we wrap that catalog source into a generic celestial object structure. (An object handles various Solar-system sources also, as you'll see further below). Whereas the catalog source may have been defined in any epoch / catalog system, the object structure shall define ICRS coordinates always (no exceptions):

object source; // Encapsulates a sidereal or a Solar-system source
// Use the B1950 astrometric parameters to set up the observables in ICRS...
make_cat_object_sys(&star, "B1950", &source);
make_cat_object_sys
int make_cat_object_sys(const cat_entry *star, const char *restrict system, object *source)
Populates and object data structure with the data for a catalog source for a given system of catalog ...
Definition target.c:547

Alternatively, for high-_z_ sources you might simply use the 1-step make_redshifted_object_sys() e.g.:

object quasar;
// 12h29m6.6997s +2d3m8.598s (ICRS) z=0.158339
make_redshifted_object_sys("3c273", 12.4851944, 2.0523883, "ICRS", 0.158339, &quasar);
make_redshifted_object_sys
int make_redshifted_object_sys(const char *name, double ra, double dec, const char *restrict system, double z, object *source)
Populates a celestial object data structure with the parameters for a redhifted catalog source,...
Definition target.c:664

### Specify the observer location

Next, we define the location where we observe from. Let's assume we have a GPS location:

observer obs; // Structure to contain observer location
// Specify the location we are observing from, e.g. a GPS / WGS84 location
// 50.7374 deg N, 7.0982 deg E, 60m elevation
make_gps_observer(50.7374, 7.0982, 60.0, &obs);
make_gps_observer
int make_gps_observer(double latitude, double longitude, double height, observer *obs)
Initializes an observer data structure for a ground-based observer with the specified GPS / WGS84 loc...
Definition observer.c:277
observer
struct novas_observer observer
Observer location.

Remarks
  You might use make_itrf_observer() instead if the location is defined on the GRS80 reference ellipsoid, used e.g. for ITRF locations. Or, if you have geocentric Cartesian coordinates, you could use make_xyz_site() and then make_observer_at_site().

Again you might use novas_str_degrees() for typical string representations of the longitude and latitude coordinates here, such as:

make_gps_observer(
novas_str_degrees("50.7374N"),
novas_str_degrees("7.0982 deg E"),
60.0,
&obs);

Alternatively, you can also specify airborne observers, or observers in Earth orbit, in heliocentric orbit, at the geocenter, or at the Solar-system barycenter. The above also sets default, mean annual weather parameters based on the location and a global model based on Feulner et al. (2013).

You can, of course, set actual weather values after, as appropriate, if you need them for the refraction models, e.g.:

obs.on_surf.temperature = 12.4; // [C] Ambient temperature
obs.on_surf.pressure = 973.47; // [mbar] Atmospheric pressure
obs.on_surf.humidity = 48.3; // [%] relative humidity to use for refraction.
novas_observer::on_surf
struct novas_on_surface on_surf
structure containing data for an observer's location on the surface of the Earth (if where = NOVAS_OB...
Definition novas.h:1714
novas_on_surface::temperature
double temperature
[C] temperature (degrees Celsius); for optical refraction
Definition novas.h:1642
novas_on_surface::humidity
double humidity
[%] Relative humidity.
Definition novas.h:1644
novas_on_surface::pressure
double pressure
[mbar] atmospheric pressure for optical refraction
Definition novas.h:1643

### Specify the time of observation

Next, we set the time of observation. For a ground-based observer, you will need to provide SuperNOVAS with the UT1 - UTC time difference (a.k.a. DUT1), and the current leap seconds. You can obtain suitable values for DUT1 from IERS, and for the highest precision, interpolate for the time of observations. For the example, let's assume 37 leap seconds, and DUT1 = 0.042,

int leap_seconds = 37; // [s] UTC - TAI time difference
double dut1 = 0.042; // [s] UT1 - UTC time difference

Then we can set the time of observation, for example, using the current UNIX time:

novas_timespec obs_time; // Structure that will define astrometric time
// Set the time of observation to the precise UTC-based UNIX time
novas_set_current_time(leap_seconds, dut1, &obs_time);
novas_set_current_time
int novas_set_current_time(int leap, double dut1, novas_timespec *restrict time)
Sets the time with the UNIX time obtained from the system clock.
Definition timescale.c:1021
novas_timespec
A structure, which defines a precise instant of time that can be expressed in any of the astronomical...
Definition novas.h:1822

Alternatively, you may set the time as a Julian date in the time measure of choice (UTC, UT1, TT, TDB, GPS, TAI, TCG, or TCB):

double jd_tai = ... // TAI-based Julian Date
novas_set_time(NOVAS_TAI, jd_tai, leap_seconds, dut1, &obs_time);
NOVAS_TAI
@ NOVAS_TAI
International Atomic Time (TAI).
Definition novas.h:1796
novas_set_time
int novas_set_time(enum novas_timescale timescale, double jd, int leap, double dut1, novas_timespec *restrict time)
Sets an astronomical time to the fractional Julian Date value, defined in the specified timescale.
Definition timescale.c:473

or, for the best precision we may do the same with an integer / fractional split:

long ijd_tai = ... // Integer part of the TAI-based Julian Date
double fjd_tai = ... // Fractional part of the TAI-based Julian Date
novas_set_split_time(NOVAS_TAI, ijd_tai, fjd_tai, leap_seconds, dut1, &obs_time);
novas_set_split_time
int novas_set_split_time(enum novas_timescale timescale, long ijd, double fjd, int leap, double dut1, novas_timespec *restrict time)
Sets an astronomical time to the split Julian Date value, defined in the specified timescale.
Definition timescale.c:605

Or, you might use string dates, such as an ISO timestamp:

novas_set_str_time(NOVAS_UTC, "2025-01-26T22:05:14.234+0200", leap_seconds, dut1, &obs_time);
NOVAS_UTC
@ NOVAS_UTC
Universal Coordinated Time (UTC).
Definition novas.h:1798
novas_set_str_time
int novas_set_str_time(enum novas_timescale timescale, const char *restrict str, int leap, double dut1, novas_timespec *restrict time)
Sets astronomical time in a specific timescale using a string specification.
Definition timescale.c:515

Note, that the likes of novas_set_time() will automatically apply diurnal corrections to the supplied UT1-UTC time difference for libration and ocean tides. Thus, the supplied values should not include these. Rather you should pass dut1 directly (or interpolated) from the IERS Bulletin values for the time of observation.

As of v1.7, you can also let SuperNOVAS fetch leap seconds and the UT1 - UTC time difference automatically from IERS, provided you are online and you don't mind the slight delay associated with the HTTP query. Simply pass NAN in place of the dut1 parameter, and the value of leap_seconds becomes irrelevant (e.g. you can use 0 or -1, if you will). However, you should be prepared that the lookup may fail, and so you should always check the return status, e.g.:

// Use NAN for dut1 to fetch leap seconds and the UT1 - UTC difference from IERS
if(novas_set_current_time(-1, NAN, &obs_time) != 0) {
// Oops, failed to get leap seconds and dut1 from IERS...
return -1;
}

You can also disable the automatic fetch and replacement of NAN dut1 values by calling novas_set_auto_fetch_eop(0). See Earth orientation for the set of functions that can be used for fetching appropriate values for the leap seconds and the UT1-UTC time difference from local or external sources.

### Set up the observing frame

Next, we set up an observing frame, which is defined for a unique combination of the observer location and the time of observation:

novas_frame obs_frame; // Structure that will define the observing frame
double xp = ... // [mas] Earth polar offset x, e.g. from IERS Bulletin A.
double yp = ... // [mas] Earth polar offset y, from same source as above.
// Initialize the observing frame with the given observing parameters
novas_make_frame(NOVAS_REDUCED_ACCURACY, &obs, &obs_time, xp, yp, &obs_frame);
NOVAS_REDUCED_ACCURACY
@ NOVAS_REDUCED_ACCURACY
Calculate with truncated terms.
Definition novas.h:989
novas_make_frame
int novas_make_frame(enum novas_accuracy accuracy, const observer *obs, const novas_timespec *time, double xp, double yp, novas_frame *frame)
Sets up a observing frame for a specific observer location, time of observation, and accuracy require...
Definition frames.c:525
novas_frame
A set of parameters that uniquely define the place and time of observation.
Definition novas.h:1916

Here xp and yp are small (sub-arcsec level) corrections to Earth orientation. Values for these are are published in the IERS Bulletins. These values should be interpolated for the time of observation, but should NOT be corrected for libration and ocean tides (novas_make_frame() will apply such corrections as appropriate for full accuracy frames). The Earth orientation parameters (EOP) are needed only when converting positions from the celestial CIRS (or TOD) frame to the Earth-fixed ITRS (or PEF) frames. You may ignore these and set zeroes if not interested in Earth-fixed calculations or if sub-arcsecond precision is not required.

As of v1.7, you may leave xp and yp undefined by setting either or both values to NAN. In that case, novas_make_frame() will automatically fetch appropriately interpolated polar offsets from IERS if possible (provided you did not call novas_auto_fetch_eop(0) to disable it). It of course will add arbitrary latencies and a source of indeterminacy into your application. You can use your own URLs (such as on a local server) instead of the default ones with novas_set_eop_url() for faster / more reliable EOP lookup. Because the lookup mail fail, you should definitely check the return status after, e.g.:

// Use NAN for xp / yp to fetch polar offsets automatically from IERS, if possible
if(novas_make_frame(NOVAS_REDUCED_ACCURACY, &obs, &obs_time, NAN, NAN, &obs_frame) != 0) {
// Oops, fetching polar offsets from IERS failed...
return -1;
}

See Earth orientation for the set of functions that can be used for fetching appropriate EOP values from local or external sources.

The advantage of using the observing frame, is that it enables very fast position calculations for multiple objects in that frame (see the benchmarks), since all sources in a frame have well-defined, fixed, topological positions on the celestial sphere. It is only a matter of expressing these positions as coordinates (and velocities) in a particular coordinate system. So, if you need to calculate positions for thousands of sources for the same observer and time, it will be significantly faster than using the low-level NOVAS C routines instead. You can create derivative frames for different observer locations, if need be, via novas_change_observer().

Important
  Without a proper ephemeris provider for the major planets, you are invariably restricted to working with NOVAS_REDUCED_ACCURACY frames, providing milliarcsecond precision at most. Attempting to construct high-accuracy frames without an appropriate high-precision ephemeris provider will result in an error from the requisite ephemeris() calls.

Remarks
  NOVAS_FULL_ACCURACY frames require a high-precision ephemeris provider for the major planets, e.g. to account for the gravitational deflections. Without it, μas accuracy cannot be ensured, in general. See section on Incorporating Solar-system ephemeris data or services further below.

### Calculate an apparent place on sky

Now we can calculate the apparent R.A. and declination for our source, which includes proper motion (for sidereal sources) or light-time correction (for Solar-system bodies), and also aberration corrections for the moving observer and gravitational deflection around the major Solar System bodies (in full accuracy mode). You can calculate an apparent location in the coordinate system of choice (ICRS/GCRS, CIRS, J2000, MOD, TOD, TIRS, or ITRS) using novas_sky_pos(). E.g.:

sky_pos apparent; // Structure containing the precise observed position
novas_sky_pos(&source, &obs_frame, NOVAS_CIRS, &apparent);
NOVAS_CIRS
@ NOVAS_CIRS
Celestial Intermediate Reference System: dynamical system of the true equator, with its origin at the...
Definition novas.h:888
sky_pos
struct novas_sky_pos sky_pos
Celestial object's place on the sky; contains the output from place().
novas_sky_pos
int novas_sky_pos(const object *restrict object, const novas_frame *restrict frame, enum novas_reference_system sys, sky_pos *restrict out)
Calculates an apparent location on sky for the source.
Definition frames.c:854

Apart from providing precise apparent R.A. and declination coordinates, the sky_pos structure also provides the x,y,z unit vector pointing in the observed direction of the source (in the designated coordinate system). We also get radial velocity (for spectroscopy), and apparent distance for Solar-system bodies (e.g. for apparent-to-physical size conversion).

Note
  If you want geometric positions (and/or velocities) instead, without aberration and gravitational deflection, you might use novas_geom_posvel().

Remarks
  Regardless, which reference system you have used in the calculations above, you can always easily and efficiently change the coordinate reference system in which your results are expressed, by creating an appropriate transform via novas_make_transform() and then using novas_transform_vector() or novas_transform_skypos(). More on coordinate transforms further below.

### Calculate azimuth and elevation angles at the observing location

If your ultimate goal is to calculate the azimuth and elevation angles of the source at the specified observing location, you can proceed from the sky_pos data you obtained above (in whichever coordinate system!):

double az, el; // [deg] local azimuth and elevation angles to populate
// Convert the apparent position in CIRS on sky to horizontal coordinates
novas_app_to_hor(&obs_frame, NOVAS_CIRS, apparent.ra, apparent.dec, novas_standard_refraction,
&az, &el);
novas_standard_refraction
double novas_standard_refraction(double jd_tt, const on_surface *loc, enum novas_refraction_type type, double el)
Returns an optical refraction correction for a standard atmosphere.
Definition refract.c:252
novas_app_to_hor
int novas_app_to_hor(const novas_frame *restrict frame, enum novas_reference_system sys, double ra, double dec, RefractionModel ref_model, double *restrict az, double *restrict el)
Converts an observed apparent position vector in the specified coordinate system to local horizontal ...
Definition frames.c:1023
novas_sky_pos::dec
double dec
[deg] apparent, topocentric, or astrometric declination (degrees)
Definition novas.h:1743
novas_sky_pos::ra
double ra
[h] apparent, topocentric, or astrometric right ascension (hours)
Definition novas.h:1742

Above we converted the apparent coordinates, that were calculated in CIRS, to refracted azimuth and elevation coordinates at the observing location, using the novas_standard_refraction() function to provide a suitable refraction correction. We could have used novas_optical_refraction() instead to use the weather data embedded in the frame's observer structure, or some user-defined refraction model, or else NULL to calculate unrefracted elevation angles.

## Calculating positions for a Solar-system source

- Planets and/or ephemeris type objects
- Solar-system objects with Keplerian orbital parameters
- Approximate planet orbitals
- Moon's position and phase

Solar-system sources work similarly to the above with a few important differences at the start.

### Planets and/or ephemeris type objects

Historically, NOVAS divided Solar-system objects into two categories: (1) major planets (including also the Sun, the Moon, and the Solar-system Barycenter); and (2) 'ephemeris' type objects, which are all other Solar-system objects. The main difference is the numbering convention. NOVAS major planets have definitive ID numbers (see enum novas_planet), whereas 'ephemeris' objects have user-defined IDs. They are also handled by two separate adapter functions (although SuperNOVAS has the option of using the same ephemeris provider for both types of objects also).

Thus, instead of make_cat_object() you define your source as a planet or ephemeris type object with a name or ID number that is used by the ephemeris service you provided. For major planets you might want to use make_planet(), if they use a novas_planet_provider function to access ephemeris data with their NOVAS IDs, or else make_ephem_object() for more generic ephemeris handling via a user-provided novas_ephem_provider. E.g.:

object mars, ceres; // Hold data on solar-system bodies.
// Mars will be handled by the planet provider function
make_planet(NOVAS_MARS, &mars);
without background checks
// Ceres will be handled by the generic ephemeris provider function, which let's say
// uses the NAIF ID of 2000001 _or_ the name 'Ceres' (depending on the implementation)
make_ephem_object("Ceres", 2000001, &ceres);
NOVAS_MARS
@ NOVAS_MARS
Major planet number for Mars in NOVAS.
Definition novas.h:625
make_planet
int make_planet(enum novas_planet num, object *restrict planet)
Sets a celestial object to be a major planet, or the Sun, Moon, Solar-system Barycenter,...
Definition target.c:465
make_ephem_object
int make_ephem_object(const char *name, long num, object *body)
Sets a celestial object to be a Solar-system ephemeris body.
Definition target.c:708

Important
  Before you can handle all major planets and other ephemeris objects this way, you will have to provide one or more functions to obtain the barycentric ICRS positions for your Solar-system source(s) of interest for the specific Barycentric Dynamical Time (TDB) of observation. See section on Incorporating Solar-system ephemeris data or services.

And then, it's the same spiel as before, e.g.:

int status = novas_sky_pos(&mars, &obs_frame, NOVAS_TOD, &apparent);
if(status) {
// Oops, something went wrong...
...
}
NOVAS_TOD
@ NOVAS_TOD
True equinox Of Date: dynamical system of the 'true' equator, with its origin at the 'true' equinox (...
Definition novas.h:884

### Solar-system objects with Keplerian orbital parameters

As of version 1.2 you can also define solar system sources with Keplerian orbital elements (such as the most up-to-date ones provided by the Minor Planet Center for asteroids, comets, etc.):

object NEA; // e.g. a Near-Earth Asteroid
// Fill in the orbital parameters (pay attention to units!)
novas_orbital orbit = NOVAS_ORBIT_INIT;
orbit.a = ...; // Major axis in AU...
... // ... and the rest of the orbital elements
// Create an object for that orbit
make_orbital_object("NEAxxx", -1, &orbit, &NEA);
make_orbital_object
int make_orbital_object(const char *name, long num, const novas_orbital *orbit, object *body)
Sets a celestial object to be a Solar-system orbital body.
Definition target.c:731
NOVAS_ORBIT_INIT
#define NOVAS_ORBIT_INIT
Initializer for novas_orbital for heliocentric orbits using GCRS ecliptic parametrization.
Definition novas.h:1447
novas_orbital
Keplerian orbital elements for NOVAS_ORBITAL_OBJECT type.
Definition novas.h:1416
novas_orbital::a
double a
[AU] semi-major axis
Definition novas.h:1419

Note
  Even with orbital elements, you will, in general, still require an ephemeris provider also, to obtain precise positions for the Sun, an Earth-based observer, or the planet, around which the orbit is defined.

### Approximate planet orbitals

Finally, as of version 1.4, you might generate approximate (arcmin-level) orbitals for the major planets (but not Earth!), the Moon, and the Earth-Moon Barycenter (EMB) also. E.g.:

double jd_tdb = ... // Time (epoch) for which to calculate orbital parameters
// Planet orbitals, e.g. for Mars
novas_orbital mars_orbit = NOVAS_ORBIT_INIT;
novas_make_planet_orbit(NOVAS_MARS, jd_tdb, &mars_orbit);
// Moon's (very approcimate) orbital around Earth
novas_orbital moon_orbit = NOVAS_ORBIT_INIT;
novas_make_moon_orbit(jd_tdb, &moon_orbit);
novas_make_planet_orbit
int novas_make_planet_orbit(enum novas_planet id, double jd_tdb, novas_orbital *restrict orbit)
Get approximate current heliocentric orbital elements for the major planets.
Definition planets.c:103
novas_make_moon_orbit
int novas_make_moon_orbit(double jd_tdb, novas_orbital *restrict orbit)
Gets an approximation of the current Keplerian orbital elements for the Moon relative to the geocente...
Definition moon.c:834

While the planet and Moon orbitals are not suitable for precision applications, they can be useful for determining approximate positions (e.g. via the novas_approx_heliocentric() and novas_approx_sky_pos() functions), and for rise/set time calculations.

### Moon's position and phase

SuperNOVAS can calculate positions and velocities for the Moon to arcsecond (or km) level, or better, accuracy using the ELP2000 / MPP02 semi-analytical model by Chapront & Francou (2003). This means that you can calculate astrometric quantities for the Moon with reasonable accuracy even without an ephemeris provider configured.

For example, you can calculate the apparent place of the Moon in an observing frame as:

novas_frame frame = ...; // observer location and time of observation
sky_pos apparent = {}; // apparent data structure to populate...
// Calculate apparent position, say in true-of-date (TOD) system
if(novas_moon_elp_sky_pos(&frame, NOVAS_TOD, &apparent) != 0) {
// Oops something went wrong...
return -1;
}
novas_moon_elp_sky_pos
int novas_moon_elp_sky_pos(const novas_frame *restrict frame, enum novas_reference_system sys, sky_pos *restrict pos)
Returns the Moon's apparent place, relative to an Earth-based observer (or the geocenter),...
Definition moon.c:692

Alternatively, you can obtain geometric positions and velocities of the Moon, relative to the observer using novas_moon_elp_posvel() instead.

You can also obtain the current phase of the Moon, for the time of observation:

double jd_tdb = ...; // TDB-based Julian Date of observation
// [deg] Moon's phase at the specified time (0 is new moon).
double phase = novas_moon_phase(jd_tdb);
novas_moon_phase
double novas_moon_phase(double jd_tdb)
Calculates the Moon's phase at a given time.
Definition moon.c:1081

or, calculate when the Moon will reach a particular phase next:

// TDB-based Julian Date of the next full moon (phase = 180 deg), after TDB-based date
double jd_tdb_full = novas_next_moon_phase(180.0, jd_tdb);
novas_next_moon_phase
double novas_next_moon_phase(double phase, double jd_tdb)
Calculates the date / time at which the Moon will reach the specified phase next, after the specified...
Definition moon.c:1151

## Going in reverse...

Of course, SuperNOVAS allows you to go in reverse, for example from an observed Az/El position all the way to proper ICRS R.A./Dec coordinates.

E.g.:

double az = ..., el = ...; // [deg] measured azimuth and elevation angles
double pos[3]; // [arb. u.] xyz position vector
double ra, dec; // [h, deg] R.A. and declination to populate
// Calculate the observer's apparent coordinates from the observed Az/El values,
// lets say in CIRS (but it could also be ICRS, for all that matters).
novas_hor_to_app(&obs_frame, az, el, novas_standard_refraction, NOVAS_CIRS, &ra, &dec);
// Convert apparent to ICRS geometric positions (no parallax)
novas_app_to_geom(&obs_frame, NOVAS_CIRS, ra, dec, 0.0, pos);
// Convert ICRS rectangular equatorial to R.A. and Dec
vector2radec(pos, &ra, &dec);
vector2radec
short vector2radec(const double *restrict pos, double *restrict ra, double *restrict dec)
Converts a vector in equatorial rectangular coordinates to equatorial spherical coordinates.
Definition util.c:662
novas_hor_to_app
int novas_hor_to_app(const novas_frame *restrict frame, double az, double el, RefractionModel ref_model, enum novas_reference_system sys, double *restrict ra, double *restrict dec)
Converts an observed azimuth and elevation coordinate to right ascension (R.A.) and declination coord...
Definition frames.c:1118
novas_app_to_geom
int novas_app_to_geom(const novas_frame *restrict frame, enum novas_reference_system sys, double ra, double dec, double dist, double *restrict geom_icrs)
Converts an observed apparent sky position of a source to an ICRS geometric position,...
Definition frames.c:1207

Voila! And, of course you might want the coordinates in some other reference systems, such as B1950. For that you can simply add a transformation before vector2radec() above, e.g. as:

...
// Transform position from ICRS to B1950
gcrs_to_mod(NOVAS_JD_B1950, pos, pos);
// Convert B1950 xyz position to R.A. and Dec
vector2radec(pos, &ra, &dec);
NOVAS_JD_B1950
#define NOVAS_JD_B1950
[day] Julian date at B1950 (NASA / NAIF SPICE definition) precession(), transform_cat()
Definition novas.h:273

## Calculate rise, set, and transit times

You may be interested to know when sources rise above or set below some specific elevation angle, or at what time they appear to transit at the observer location. SuperNOVAS has routines to help you with that too.

Given that rise, set, or transit times are dependent on the day of observation, and observer location, they are effectively tied to an observer frame.

novas_frame frame = ...; // Earth-based observer location and lower-bound time of interest.
object source = ...; // Source of interest
// UTC-based Julian day *after* observer frame, when source rises above 30 degrees of elevation
// next, given a standard optical refraction model.
double jd_rise = novas_rises_above(30.0, &source, &frame, novas_standard_refraction);
// UTC-based Julian day *after* observer frame, of next source transit
double jd_transit = novas_transit_time(&source, &frame);
// UTC-based Julian day *after* observer frame, when source sets below 30 degrees of elevation
// next, not accounting for refraction.
double jd_set = novas_sets_below(30.0, &source, &frame, NULL);
novas_sets_below
double novas_sets_below(double el, const object *restrict source, const novas_frame *restrict frame, RefractionModel ref_model)
Returns the UTC date at which a distant source appears to set below the specified elevation angle.
Definition frames.c:1736
novas_rises_above
double novas_rises_above(double el, const object *restrict source, const novas_frame *restrict frame, RefractionModel ref_model)
Returns the UTC date at which a distant source appears to rise above the specified elevation angle.
Definition frames.c:1697
novas_transit_time
double novas_transit_time(const object *restrict source, const novas_frame *restrict frame)
Returns the UTC date at which a source transits the local meridian.
Definition frames.c:1663

Note, that in the current implementation these calls are not well suited sources that are at or within the geostationary orbit, such as such as Low Earth Orbit satellites (LEOs), geostationary satellites (which never really rise, set, or transit), or some Near Earth Objects (NEOs), which will rise set multiple times per day. For the latter, the above calls may still return a valid time, only without the guarantee that it is the time of the first such event after the specified frame instant. A future implementation may address near-Earth orbits better, so stay tuned for updates.

## Coordinate and velocity transforms (change of coordinate system)

SuperNOVAS introduces matrix transforms (correctly since version 1.4), which can take a position or velocity vector (geometric or apparent), obtained for an observer frame, from one coordinate system to another efficiently. E.g.:

novas_frame frame = ... // The observer frame (time and location)
double j2000_vec[3] = ...; // IN: original position vector, say in J2000.
double tirs_vec[3] = {0.0}; // OUT: equivalent vector in TIRS we want to obtain
novas_transform T; // Coordinate transformation data
// Calculate the transformation matrix from J2000 to TIRS in the given observer frame.
novas_make_transform(&frame, NOVAS_J2000, NOVAS_TIRS, &T);
// Transform the J2000 position or velocity vector to TIRS...
novas_transform_vector(j2000_vec, &T, tirs_vec);
NOVAS_TIRS
@ NOVAS_TIRS
Terrestrial Intermediate Reference System.
Definition novas.h:908
NOVAS_J2000
@ NOVAS_J2000
The J2000 dynamical reference system.
Definition novas.h:896
novas_make_transform
int novas_make_transform(const novas_frame *frame, enum novas_reference_system from_system, enum novas_reference_system to_system, novas_transform *transform)
Calculates a transformation matrix that can be used to convert positions and velocities from one coor...
Definition frames.c:1280
novas_transform_vector
int novas_transform_vector(const double *in, const novas_transform *restrict transform, double *out)
Transforms a position or velocity 3-vector from one coordinate reference system to another.
Definition frames.c:1457
novas_transform
A transformation between two astronomical coordinate systems for the same observer location and time.
Definition novas.h:1976

Transformations support all SupeNOVAS reference systems, that is ICRS/GCRS, J2000, TOD, MOD, CIRS, TIRS, and ITRS. The same transform can also be used to convert apparent positions in a sky_pos structure also, e.g.:

...
sky_pos j2000_pos = ... // IN: in J2000, e.g. via novas_sky_pos()...
sky_pos tirs_pos; // OUT: equivalent TIRS position to calculate...
// Transform the J2000 apparent positions to TIRS....
novas_transform_sky_pos(&j2000_pos, &T, &tirs_pos);
novas_transform_sky_pos
int novas_transform_sky_pos(const sky_pos *in, const novas_transform *restrict transform, sky_pos *out)
Transforms a position or velocity 3-vector from one coordinate reference system to another.
Definition frames.c:1483

# Tips and tricks (C99)

- Multi-threaded calculations
- Physical units
- String times and angles
- String dates

## Multi-threaded calculations

Some of the calculations involved can be expensive from a computational perspective. For the most typical use case however, NOVAS (and SuperNOVAS) has a trick up its sleeve: it caches the last result of intensive calculations so they may be re-used if the call is made with the same environmental parameters again (such as JD time and accuracy).

A direct consequence of the caching of results is that calculations are generally not thread-safe as implemented by the original NOVAS C 3.1 library. One thread may be in the process of returning cached values for one set of input parameters while, at the same time, another thread is saving cached values for a different set of parameters. Thus, when running calculations in more than one thread, the NOVAS C results returned may at times be incorrect, or more precisely they may not correspond to the requested input parameters.

While you should never call the original NOVAS C library from multiple threads simultaneously, SuperNOVAS caches the results in thread local variables (provided your compiler supports it), and is therefore generally safe to use in multi-threaded applications. Just make sure that you:

- use a compiler which supports the C11 language standard;
- or, compile with GCC >= 3.3;
- or else, set the appropriate non-standard keyword to use for declaring thread-local variables for your compiler in config.mk or in your equivalent build setup.

## Physical units

The NOVAS API has been using conventional units (e.g. AU, km, day, deg, h) typically for its parameters and return values alike. Hence, SuperNOVAS follows the same conventions for its added functions and data structures also. However, when interfacing SuperNOVAS with other programs, libraries, or data files, it is often necessary to use quantities that are expressed in different units, such as SI or CGS. To facilitate such conversions, novas.h provides a set of unit constants, which can be used for converting to/from SI units (and radians).

For example, novas.h contains the following definitions:

#define NOVAS_DAY 86400.0
#define NOVAS_DEGREE (M_PI / 180.0)
#define NOVAS_HOURANGLE (M_PI / 12.0)

You can use these, for example, to convert quantities expressed in conventional units for NOVAS to standard (SI) values, by multiplying NOVAS quantities with the corresponding unit definition. E.g.:

// A difference in Julian Dates [day] in seconds.
double delta_t = (tjd - tjd0) * NOVAS_DAY;
// R.A. [h] / declination [deg] converted radians (e.g. for trigonometric functions).
double ra_rad = ra_h * NOVAS_HOURANGLE;
double dec_rad = dec_d * NOVAS_DEGREE;
NOVAS_DAY
#define NOVAS_DAY
[s] The length of a synodic day, that is 24 hours exactly.
Definition novas.h:297
NOVAS_DEGREE
#define NOVAS_DEGREE
[rad] A degree expressed in radians.
Definition novas.h:317
NOVAS_HOURANGLE
#define NOVAS_HOURANGLE
[rad] An hour of angle expressed in radians.
Definition novas.h:332

And vice-versa: to convert values expressed in standard (SI) units, you can divide by the appropriate constant to 'cast' an SI value into the particular physical unit, e.g.:

// Increment a Julian Date [day] with some time differential [s].
double tjd = tjd0 + delta_t / NOVAS_DAY;
// convert R.A. / declination in radians to hours and degrees
double ra_h = ra_rad / NOVAS_HOURANGLE;
double dec_d = dec_rad / NOVAS_DEGREE;

Finally, you can combine them to convert between two different conventional units, e.g.:

// Convert angle from [h] -> [rad] -> [deg]
double lst_d = lst_h * NOVAS_HOURANGLE / NOVAS_DEGREE;
// Convert [AU/day] -> [m/s] (SI) -> [km/s]
double v_kms = v_auday * (NOVAS_AU / NOVAS_DAY) / NOVAS_KMS
NOVAS_KMS
#define NOVAS_KMS
[m] One km/s in m/s.
Definition novas.h:342
NOVAS_AU
#define NOVAS_AU
[m] Astronomical unit (AU).
Definition novas.h:348

## String times and angles

SuperNOVAS functions typically input and output times and angles as decimal values (hours and degrees, but also as days and hour-angles), but that is not how these are represented in many cases. Time and right-ascention are often given as string values indicating hours, minutes, and seconds (e.g. "11:32:31.595", or "11h 32m 31.595s"). Similarly angles, are commonly represented as degrees, arc-minutes, and arc-seconds (e.g. "+53 60 19.9"). For that reason, SuperNOVAS provides a set of functions to convert string values expressed in decimal or broken-down format to floating point representation. E.g.,

// Right ascention from string
double ra_h = novas_str_hours("9 18 49.068");
// Declination from string
double dec_d = novas_str_degrees("-53 10 07.33");

The conversions have a lot of flexibility. Components can be separated by spaces (as above), by colons, commas, or underscores, by the letters 'h'/'d', 'm', and 's', by single (minutes) and double quotes (seconds), or any combination thereof. Decimal values may be followed by 'h' or 'd' unit markers. Additionally, angles can end with a compass direction, such as 'N', 'E', 'S' or 'W'. So the above could also have been:

double ra_h = novas_str_hours("9h_18:49.068\"");
double dec_d = novas_str_degrees("53d 10'_07.33S");

or as decimals:

double ra_h = novas_str_hours("9.31363");
double dec_d = novas_str_degrees("53.16870278d South");

## String dates

Dates are typically represented broken down into year, month, and day (e.g. "2025-02-16", or "16.02.2025", or "2/16/2025"), with or without a time marker, which itself may or may not include a time zone specification. In astronomy, the most commonly used string representation of dates is with ISO 8601 timestamps. The following are all valid ISO date specifications:

2025-02-16 # Date only (0 UTC)
2025-02-16T19:35:21Z # UTC date/time
2025-02-16T19:35:21.832Z # UTC date/time with decimals
2025-02-16T14:35:21+0500 # date in time zone (e.g. EST)
2025-02-16T14:35:21.832+05:00 # alternative time zone specification

SuperNOVAS provides functions to convert between ISO dates/times and their string representation for convenience. E.g.,

novas_timespec time; // Astronomical time specification
char timestamp[40]; // A string to contain an ISO representation
// Parse an ISO timestamp into a Julian day (w/o returning the tail).
double jd = novas_parse_iso_date("2025-02-16T19:35:21Z", NULL);
if(isnan(jd)) {
// Oops could not parse date.
...
}
// Use the parsed JD date (in UTC) with the appropriate leap seconds
// and UT1-UTC time difference
novas_set_time(NOVAS_UTC, jd, leap_seconds, dut1, &time);
// Print an ISO timestamp, with millisecond precision, into the
// designated string buffer.
novas_iso_timestamp(&time, timestamp, sizeof(timestamp));
novas_parse_iso_date
double novas_parse_iso_date(const char *restrict date, char **restrict tail)
Parses an ISO 8601 timestamp, converting it to a Julian day.
Definition parse.c:1101
novas_iso_timestamp
int novas_iso_timestamp(const novas_timespec *restrict time, char *restrict dst, int maxlen)
Prints a UTC-based ISO timestamp to millisecond precision to the specified string buffer.
Definition timescale.c:1211

Note
  ISO 8601 timestamps are always UTC-based and expressed in the Gregorian calendar, as per specification, even for dates that preceded the Gregorian calendar reform of 1582 (i.e. 'proleptic Gregorian' dates).

Other SuperNOVAS string date functions will process dates in the astronomical calendar of date by default, that is in the Gregorian calendar after the Gregorian calendar reform of 1582, or the Julian/Roman calendar for dates prior, and support timescales other than UTC also. E.g.:

// Print a TDB timestamp in the astronomical calendar of date instead
novas_timestamp(&time, NOVAS_TDB, timestamp, sizeof(timestamp));
NOVAS_TDB
@ NOVAS_TDB
Barycentric Dynamical Time (TDB).
Definition novas.h:1793
novas_timestamp
int novas_timestamp(const novas_timespec *restrict time, enum novas_timescale scale, char *restrict dst, int maxlen)
Prints an astronomical timestamp to millisecond precision in the specified timescale to the specified...
Definition timescale.c:1281

Or, parse an astronomical date:

// Parse astronomical dates into a Julian day...
double jd = novas_date("2025-02-16T19:35:21");
if(isnan(jd)) {
// Oops could not parse date.
...
}
novas_date
double novas_date(const char *restrict date)
Returns a Julian date (in non-specific timescale) corresponding the specified input string date / tim...
Definition timescale.c:1100

Or, parse an astronomical date, including the timescale specification:

// Parse a TAI-based timestamp into a Julian day and corresponding timescale
double jd = novas_date_scale("2025-02-16T19:35:21+0200 TAI", &scale);
if(isnan(jd)) {
// Oops could not parse date.
...
}
novas_date_scale
double novas_date_scale(const char *restrict date, enum novas_timescale *restrict scale)
Returns a Julian date and the timescale corresponding the specified input string date/time and timesc...
Definition timescale.c:1128

Sometimes your input dates are represented in various other formats. You can have additional flexibility for parsing dates using the novas_parse_date_format() and novas_timescale_for_string() functions.

Details

E.g.,

char *pos = NULL; // We'll keep track of the string parse position here
enum novas_timescale scale; // We'll parse the timescale here (if we can)
// Parse the M/D/Y date up to the 'TAI' timescale specification...
double jd = novas_parse_date_format(NOVAS_GREGORIAN_CALENDAR, NOVAS_MDY,
"2/16/2025 20:08:49.082 TAI", &pos);
// Then parse the 'TAI' timescale marker, after the date/time specification
scale = novas_timescale_for_string(pos);
if(scale < 0) {
// Oops, not a valid timescale marker. Perhaps assume UTC...
scale = NOVAS_UTC;
}
// Now set the time for the given calendar, date format, and timescale of the
// string representation.
novas_set_time(scale, jd, leap_seconds, dut1, &time);
novas_timescale
novas_timescale
Constants to reference various astronomical timescales used.
Definition novas.h:1791
novas_timescale_for_string
enum novas_timescale novas_timescale_for_string(const char *restrict str)
Returns the timescale constant for a string that denotes the timescale in with a standard abbreviatio...
Definition parse.c:1169
novas_parse_date_format
double novas_parse_date_format(enum novas_calendar_type calendar, enum novas_date_format format, const char *restrict date, char **restrict tail)
Parses a calendar date/time string, expressed in the specified type of calendar, into a Julian day (J...
Definition parse.c:901
NOVAS_MDY
@ NOVAS_MDY
month, then day, then year
Definition novas.h:2073
NOVAS_GREGORIAN_CALENDAR
@ NOVAS_GREGORIAN_CALENDAR
The Gregorian calendar introduced on 15 October 1582, the day after 4 October.
Definition novas.h:2089

Copyright (C) 2026 Attila Kovács