Skyfield: HomeTable of ContentsAPI Reference

Dates and Time

Astronomers use several numerical scales to measure time. Skyfield often has to use more than one of them within a single computation! The Time class is used to represent a single moment in time, or an array of such moments. It keeps track of all the different ways the same moment might be designated in different time scales. It performs this conversion using data tables that are stored in the Timescale object that was used to create it:

# Building a time object

from skyfield.api import load
ts = load.timescale()     # Timescale object
t = ts.utc(2014, 1, 18)   # Time object

print(t.tai)
print(t.tt)
print(t.tdb)
2456675.5004050927
2456675.5007775924
2456675.5007775975

Most applications create only one Timescale object, which is conventionally named ts, and use it to build all of their times.

Each Time object only computes a given timescale the first time it’s asked. On subsequent requests for the same timescale, it returns the value that it cached the first time, without recomputing it. This has an important consequence: if your program will need to do several computations for the same time, be sure to use the same Time object for all of them. Otherwise your separate time objects will have to compute the same time scales all over again. See the Values cached on the Time object section below for more details.

Each time scale supported by Time is described in detail in one of the sections below. The supported time scales are:

You can build a Time from several different source timescales. Here’s a summary.

# All the ways you can create a Time object:

t = ts.utc(year, month, day, hour, minute, second)
t = ts.from_datetime(dt)
t = ts.from_datetimes([dt1, dt2, ...])
t = ts.now()

t = ts.tai(year, month, day, hour, minute, second)
t = ts.tai_jd(floating_point_Julian_date)

t = ts.tt(year, month, day, hour, minute, second)
t = ts.tt_jd(floating_point_Julian_date)

t = ts.tdb(year, month, day, hour, minute, second)
t = ts.tdb_jd(floating_point_Julian_date)

t = ts.ut1(year, month, day, hour, minute, second)
t = ts.ut1_jd(floating_point_Julian_date)

The meaning of each timescale is discussed in the sections below.

Building and printing UTC

The utc parameter in the examples above specifies Coordinated Universal Time (UTC), the world clock known affectionately as “Greenwich Mean Time” which is the basis for all of the world’s timezones.

If you are comfortable dealing directly with UTC, you can simply set and retrieve it manually. You can provide its constructor with just the year, month, and day, or be more specific and give an hour, minute, and second. And not only can you attach a fraction to the seconds, but you can also freely use fractional days, hours, and minutes. For example:

# Four ways to specify 2014 January 18 01:35:37.5

t1 = ts.utc(2014, 1, 18.06640625)
t2 = ts.utc(2014, 1, 18, 1.59375)
t3 = ts.utc(2014, 1, 18, 1, 35.625)
t4 = ts.utc(2014, 1, 18, 1, 35, 37.5)

assert t1 == t2 == t3 == t4    # True!

# Several ways to print a time as UTC.

print(tuple(t1.utc))
print(t1.utc_iso(' '))
print(t1.utc_jpl())
print(t1.utc_strftime('Date %Y-%m-%d and time %H:%M:%S'))
(2014, 1, 18, 1, 35, 37.5)
2014-01-18 01:35:38Z
A.D. 2014-Jan-18 01:35:37.5000 UT
Date 2014-01-18 and time 01:35:38

The 6 values returned by utc() can also be accessed as the attributes year, month, day, hour, minute, and second.

print(t1.utc.year, '/', t2.utc.month, '/', t2.utc.day)
print(t2.utc.hour, ':', t2.utc.minute, ':', t2.utc.second)
2014 / 1 / 18
1 : 35 : 37.5

And by scraping together the minimal support for UTC that exists in the Python Standard Library, Skyfield is able to offer a now() function that reads your system clock and returns the current time as a Julian date object (assuming that your operating system clock is correct and configured with the correct time zone):

from skyfield.api import load

# Asking the current date and time

ts = load.timescale()
t = ts.now()
print(t.utc_jpl())
A.D. 2015-Oct-11 10:00:00.0000 UT

UTC and your timezone

To move beyond UTC and work with other world timezones, you will need to install a time zone database for your version of Python.

This documentation will focus on the approach which works universally across all Python versions. You can install the third-party pytz library by listing it in the dependencies of your package, or adding it to your project’s requirements.txt file, or simply installing it manually:

pip install pytz

Once it is installed, building Julian dates from local times is simple. Instantiate a normal Python datetime, pass it to the localize() method of your time zone, and pass the result to Skyfield:

from datetime import datetime
from pytz import timezone

eastern = timezone('US/Eastern')

# Converting US Eastern Time to a Julian date.

d = datetime(2014, 1, 16, 1, 32, 9)
e = eastern.localize(d)
t = ts.from_datetime(e)

And if Skyfield returns a Julian date at the end of a calculation, you can ask the Julian date object to build a datetime object for either UTC or for your own timezone:

# UTC datetime

dt = t.utc_datetime()
print('UTC: ' + str(dt))

# Converting back to an Eastern Time datetime.

dt = t.astimezone(eastern)
print('EST: ' + str(dt))
UTC: 2014-01-16 06:32:09+00:00
EST: 2014-01-16 01:32:09-05:00

As we would expect, 1:32 AM in the Eastern time zone in January is 6:32 AM local time in Greenwich, England, five hours to the east across the Atlantic.

Note that Skyfield’s astimezone() method will detect that you are using a pytz timezone and automatically call its normalize() method for you — which makes sure that daylight savings time is handled correctly — to spare you from having to make the call yourself.

If you want a Time to hold an entire array of dates, as discussed below in Date arrays, then you can provide a list of datetime objects when building a Julian date. The UTC methods will then return whole lists of values.

UTC and leap seconds

The rate of Earth’s rotation is gradually slowing down. Since the UTC standard specifies a fixed length for the second, promises a day of 24 hours, and limits an hour to 60 minutes, the only way to stay within the rules while keeping UTC synchronized with the Earth is to occasionally add an extra leap second to one of the year’s minutes.

The International Earth Rotation Service currently restricts itself to appending a leap second to the last minute of June or the last minute of December. When a leap second is inserted, its minute counts 61 seconds numbered 00–60 instead of staying within the usual range 00–59. One recent leap second was in June 2012:

# Display 5 seconds around a leap second

five_seconds = [58, 59, 60, 61, 62]
t = ts.utc(2012, 6, 30, 23, 59, five_seconds)

for string in t.utc_jpl():
    print(string)
A.D. 2012-Jun-30 23:59:58.0000 UT
A.D. 2012-Jun-30 23:59:59.0000 UT
A.D. 2012-Jun-30 23:59:60.0000 UT
A.D. 2012-Jul-01 00:00:00.0000 UT
A.D. 2012-Jul-01 00:00:01.0000 UT

Note that Skyfield has no problem with a calendar tuple that has hours, minutes, or — as in this case — seconds that are out of range. When we provided a range of numbers 58 through 62 as seconds, Skyfield added exactly the number of seconds we specified to the end of June and let the value overflow cleanly into the beginning of July.

Keep two consequences in mind when using UTC in your calculations.

First, expect an occasional jump or discrepancy if you are striding forward through time using the UTC minute, hour, or day. A graph will show a planet moving slightly farther during an hour that was lengthened by a leap second. An Earth satellite’s velocity will seem higher when you reach the minute that includes 61 seconds. And so forth. Problems like these are the reason that the Time class only uses UTC for input and output, and insists on keeping time internally using the uniform time scales discussed below in Uniform time scales: TAI, TT, and TDB.

Second, leap seconds disqualify the Python datetime from use as a general way to represent time because it refuses to accept seconds greater than 59:

datetime(2012, 6, 30, 19, 59, 60)
Traceback (most recent call last):
  ...
ValueError: second must be in 0..59

That is why Skyfield offers a second version of each method that returns a datetime:

t.utc_datetime_and_leap_second()
t.astimezone_and_leap_second(tz)

These more accurate alternatives also return a leap_second, which usually has the value 0 but jumps to 1 when Skyfield is forced to represent a leap second as a datetime with the incorrect time 23:59:59.

# Asking for the leap_second flag to learn the whole story

dt, leap_second = t.astimezone_and_leap_second(eastern)

for dt_i, leap_second_i in zip(dt, leap_second):
    print('{0}  leap_second = {1}'.format(dt_i, leap_second_i))
2012-06-30 19:59:58-04:00  leap_second = 0
2012-06-30 19:59:59-04:00  leap_second = 0
2012-06-30 19:59:59-04:00  leap_second = 1
2012-06-30 20:00:00-04:00  leap_second = 0
2012-06-30 20:00:01-04:00  leap_second = 0

Using calendar tuples to represent UTC times is more elegant than using Python datetime objects because leap seconds can be represented accurately. If your application cannot avoid using datetime objects, then you will have to decide whether to simply ignore the leap_second value or to somehow output the leap second information.

Date arrays

If you want to ask where a planet or satellite was at a whole list of different times and dates, then Skyfield will work most efficiently if you build a single Time object that holds an entire array of dates, instead of building many separate Time objects. There are three techniques for building arrays.

The last possibility is generally the one that is the most fun, because its lets you vary whichever time unit you want while holding the others steady. And you are free to provide out-of-range values and leave it to Skyfield to work out the correct result. Here are some examples:

ts.utc(range(1900, 1950))     # Fifty years 1900–1949
ts.utc(1980, range(1, 25))    # Twenty-four months
ts.utc(2005, 5, [1, 10, 20])  # 1st, 10th, and 20th of May

# The ten seconds crossing the 1974 leap second
ts.utc(1975, 1, 1, 0, 0, range(-5, 5))

The resulting Time object will hold an array of times instead of just a single scalar value. As illustrated in the previous section (on leap seconds), you can use a Python for to print each time separately:

t = ts.utc(2020, 6, 16, 7, range(4))

for ti in t:
    print(ti.utc_strftime('%Y-%m-%d %H:%M'))
2020-06-16 07:00
2020-06-16 07:01
2020-06-16 07:02
2020-06-16 07:03

When you provide a time array as input to a Skyfield calculation, the output array will have an extra dimension that expands what would normally be a single result into as many results as you provided dates. We can compute the position of the Earth as an example:

# Single Earth position

planets = load('de421.bsp')
earth = planets['earth']

t = ts.utc(2014, 1, 1)
pos = earth.at(t).position.au
print(pos)
[-0.17461758  0.88567056  0.38384886]
# Whole array of Earth positions

days = [1, 2, 3, 4]
t = ts.utc(2014, 1, days)
pos = earth.at(t).position.au
print(pos)
[[-0.17461758 -0.19179872 -0.20891924 -0.22597338]
 [ 0.88567056  0.88265548  0.87936337  0.87579547]
 [ 0.38384886  0.38254134  0.38111391  0.37956709]]

Note the shape of the resulting NumPy array. If you unpack this array into three names, then you get three four-element arrays corresponding to the four dates. These four-element arrays are ready to be submitted to matplotlib and other scientific Python tools:

x, y, z = pos    # four values each
plot(x, y)       # example matplotlib call

If you instead slice along the second axis, then you can retrieve an individual position for a particular date — and the first position is exactly what was returned above when we computed the January 1st position by itself:

print(pos[:,0])
[-0.17461758  0.88567056  0.38384886]

You can combine a Python for loop with Python’s zip() builtin to print each time alongside the corresponding coordinates. There are two techniques, one of which is less efficient and the other more efficient.

# Less efficient: loop over `t`, forcing the creation of
# a separate `Time` object for each iteration of the loop.

for ti, xi, yi, zi in zip(t, x, y, z):
    print('{}  x = {:.2f} y = {:.2f} z = {:.2f}'.format(
        ti.utc_strftime('%Y-%m-%d'), xi, yi, zi,
    ))
2014-01-01  x = -0.17 y = 0.89 z = 0.38
2014-01-02  x = -0.19 y = 0.88 z = 0.38
2014-01-03  x = -0.21 y = 0.88 z = 0.38
2014-01-04  x = -0.23 y = 0.88 z = 0.38
# More efficient: loop over the output of a `Time` method,
# which returns an array of the same length as `t`.

t_strings = t.utc_strftime('%Y-%m-%d')

for tstr, xi, yi, zi in zip(t_strings, x, y, z):
    print('{}  x = {:.2f} y = {:.2f} z = {:.2f}'.format(
        tstr, xi, yi, zi,
    ))
2014-01-01  x = -0.17 y = 0.89 z = 0.38
2014-01-02  x = -0.19 y = 0.88 z = 0.38
2014-01-03  x = -0.21 y = 0.88 z = 0.38
2014-01-04  x = -0.23 y = 0.88 z = 0.38

Finally, converting an array Julian date back into a calendar tuple results in the year, month, and all of the other values being as deep as the array itself:

print(t.utc)
[[2014. 2014. 2014. 2014.]
 [   1.    1.    1.    1.]
 [   1.    2.    3.    4.]
 [   0.    0.    0.    0.]
 [   0.    0.    0.    0.]
 [   0.    0.    0.    0.]]

Again, simply slice across the second dimension of the array to pull a particular calendar tuple out of the larger result:

print(t.utc[:,2])
[2014.    1.    3.    0.    0.    0.]

The rows can be fetched not only by index but also through the attribute names year, month, day, hour, minute, and second.

print(t.utc.year)
print(t.utc.month)
print(t.utc.day)
print(t.utc.hour)
[2014. 2014. 2014. 2014.]
[1. 1. 1. 1.]
[1. 2. 3. 4.]
[0. 0. 0. 0.]

Uniform time scales: TAI, TT, and TDB

Date arithmetic becomes very simple as we leave UTC behind and consider completely uniform time scales. Days are always 24 hours, hours always 60 minutes, and minutes always 60 seconds without any variation or exceptions. Such time scales are not appropriate for your morning alarm clock because they are never delayed or adjusted to stay in sync with the slowing rotation of the earth. But that is what makes them useful for astronomical calculation — because physics keeps up its dance, and the stars and planets move in their courses, whether humanity pauses to observe a UTC leap second or not.

Because they make every day the same length, uniform time scales can express dates as a simple floating-point count of days elapsed. To make all historical dates come out as positive numbers, astronomers traditionally assign each date a “Julian day” number that starts counting at 4713 BC January 1 in the old Julian calendar — the same date as 4714 BC November 24 in our Gregorian calendar. Following a tradition going back to the Greeks and Ptolemy, the count starts at noon, since the sun’s transit is an observable event but the moment of midnight is not.

So twelve noon was the moment of Julian date zero:

# When was Julian date zero?

bc_4714 = -4713
t = ts.tt(bc_4714, 11, 24, 12)
print(t.tt)
0.0

Did you notice how negative years work? People still counted by starting at one, not zero, when the scholar Dionysius Exiguus created the eras BC and AD in around the year AD 500. So his scheme has 1 BC followed immediately by AD 1 without a break. To avoid an off-by-one error, astronomers usually ignore BC and count backwards through a year zero and on into negative years. So negative year −n is what might otherwise be called either “n+1 BC” or “n+1 BCE” in a history textbook.

More than two million days have passed since 4714 BC, so modern dates tend to be rather large numbers:

# 2014 January 1 as a Julian Date

t = ts.utc(2014, 1, 1)
print('TAI = %r' % t.tai)
print('TT  = %r' % t.tt)
print('TDB = %r' % t.tdb)
TAI = 2456658.5004050927
TT  = 2456658.5007775924
TDB = 2456658.500777592

What are these three different uniform time scales?

International Atomic Time (TAI) is maintained by the worldwide network of atomic clocks referenced by researchers with a need for very accurate time. The official leap second table is actually a table of offsets between TAI and UTC. At the end of June 2012, for example, the TAI−UTC offset was changed from 34.0 to 35.0 which is what generated the leap second in UTC.

Terrestrial Time (TT) differs from TAI only because astronomers were already maintaining a uniform time scale of their own before TAI was established, using a slightly different starting point for the day. For practical purposes, TT is simply TAI plus exactly 32.184 seconds. So it is now more than a minute ahead of UTC.

Barycentric Dynamical Time (TDB) runs at approximately the rate that an atomic clock would run if it were at rest with respect to the Solar System barycenter, and therefore unaffected by the Earth’s motion. The acceleration that Earth experiences in its orbit — sometimes speeding up, sometimes slowing down — varies the rate at which our atomic clocks seem to run to an outside observer, as predicted by Einstein’s theory of General Relativity. So physical simulations of the Solar System tend to use TDB, which is continuous with the Teph time scale traditionally used for Solar System and spacecraft simulations at the Jet Propulsion Laboratory.

UT1 and ∆T

Finally, UT1 is the least uniform time scale of all because its clock cannot be housed in a laboratory, nor is its rate established by any human convention. It is, rather, the clock whose “hand” is the rotation of the Earth itself!

The UT1 time is derived from the direction that the Earth happens to be pointing at any given moment. And the Earth is a young world with a still-molten iron core, a viscous mantle, and continents that rise and fall as each passing ice age weighs them down with ice and then melts away. We think that we can predict, with high accuracy, where the planets will be in their orbits thousands of years from now. But to predict the fluid dynamics of an elastic rotating ellipsoid is, at the moment, beyond us. We cannot, for example, run a simulation or formula to predict leap seconds more than a few months ahead of time! Instead, we simply have to watch with sensitive instruments to see what the Earth will do next.

If you are interested in the Earth as a dynamic body, visit the Long-term Delta T page provided by the United States Naval Observatory. You will find graphs and tables showing how the length of Earth’s day expands and contracts by milliseconds over the decades. The accumulated error at any given moment is provided as ∆T, the evolving difference between TT and UT1 that dropped below zero in 1871 but then rose past it in 1902 and now stands at more than +67.2 seconds.

The task of governing leap seconds can be stated, then, as the task of keeping the difference between TT and UTC close to the natural value ∆T out in the wild. The standards bodies promise, in fact, that the difference between these two artificial time scales will always be within 0.9 seconds of the observed ∆T value.

In calculations that do not involve Earth’s rotation, ∆T never arises. The positions of planets, the distance to the Moon, and the movement of a comet or asteroid all ignore ∆T completely. When, then, does ∆T come into play?

When you create your ts timescale object at the beginning of your program, Skyfield downloads up-to-date deltat.data and deltat.preds files (if they are not already downloaded) from the United States Naval Observatory. These provide sub-millisecond level measurements of the direction that the Earth is pointing, allowing Skyfield to make

When you ask about dates in the far future or past, Skyfield will run off the end of its tables and will instead use the formula of Morrison and Stephenson (2004) to estimate when day and night might have occurred in that era.

Setting a Custom Value For ∆T

If you ever want to specify your own value for ∆T, then provide a delta_t keyword argument when creating your timescale:

load.timescale(delta_t=67.2810).utc((2014, 1, 1))

Downloading new timescale files

The timescale object uses three files, from NASA and the International Earth Rotation Service, that provide Earth rotation data and UTC leap seconds.

Each Skyfield release includes recent copies of these three timescale data files. You might be interested in checking their age and downloading new copies, especially if your application will run for several years on the same version of Skyfield. Earlier versions of Skyfield tried downloading new files automatically, but the result was a disaster: scripts that had been running fine for a year would, for example, unexpectedly die if their timescale files went out of date when the network happened to be down.

The two effects of several-year-old timescale files are:

  1. Positions affected by the Earth’s rotation, like altitude and azimuth angles, will gradually grow less precise over the years without updated Earth positions from new ∆T files.
  2. Dates in UTC will be off by one or more whole seconds, once leap seconds occur that were not included in Skyfield’s copy of the leap second file.

The two ∆T files include no explicit expiration date, but you can check the date of the last measured value of ∆T as well as the date of the final speculative prediction:

from datetime import date
from skyfield.timelib import calendar_tuple

julian_dates, values = load('deltat.data', builtin=True)
y, m, d, _, _, _ = calendar_tuple(julian_dates[-1])
last_observation = date(y, m, d)

julian_dates, values = load('deltat.preds', builtin=True)
y, m, d, _, _, _ = calendar_tuple(julian_dates[-1])
last_prediction = date(y, m, d)

print('Date of last ∆T observation:', last_observation)
print('Date of final ∆T prediction:', last_prediction)
Date of last ∆T observation: 2020-02-01
Date of final ∆T prediction: 2027-10-01

By contrast, the leap second file offers an explicit expiration date right in the file itself, which Skyfield parses and returns as a standard Python date:

expiration_date, leap_second_dat = load('Leap_Second.dat', builtin=True)
print('Leap second data expires on:', expiration_date)
Leap second data expires on: 2021-07-28

To instead check the dates of timescale files that you have downloaded yourself and have sitting on disk, simply remove builtin=True from the above calls to load().

Using Python’s today() function and basic date arithmetic, you could determine whether these dates are far enough in the past that your application wants to attempt to download more recent files. For example:

if (date.today() - last_observation).days > 365:

    # Force the download of new files.
    load('deltat.data', reload=True)
    load('deltat.preds', reload=True)
    load('Leap_Second.dat', reload=True)

    # Use them to build a new timescale.
    ts = load.timescale(builtin=False)

If you do download new files, remember to always build your timescales with builtin=False or Skyfield will ignore your files and use its internal ones instead.

Keep in mind that downloads can be dangerous if your application needs to run unattended: what if the network is down at the moment the files get too old? What if the files go out of date on the original server and the download does not change their apparent age? In the interests of safety and simplicity, it is probably more likely that you will want an automatic update to run as a separate periodic batch job than as part of your main program.

Values cached on the Time object

When you create a Time it goes ahead and computes its tt Terrestrial Time attribute starting from whatever time argument you provide. If you provide the utc parameter, for example, then the date first computes and sets tai and then computes and sets tt. Each of the other time attributes only gets computed once, the first time you access it.

The general rule is that attributes are only computed once, and can be accessed again and again for free, while methods never cache their results — think of the () parentheses after a method name as your reminder that “this will do a fresh computation every time.”

In addition to time scales, each Time object caches several other quantities that are often needed in astronomy. Skyfield only computes these attributes on-demand, the first time the user tries to access them or invokes a computation that needs their value:

gmst
Greenwich Mean Sidereal Time in hours, in the range 0.0 ≤ gmst < 24.0.
gast
Greenwich Apparent Sidereal Time in hours, in the range 0.0 ≤ gast < 24.0.
M, MT
This 3×3 matrix and its inverse perform the complete rotation between a vector in the ICRF and a vector in the dynamical reference system of this Julian date.
C, CT
This 3×3 matrix and its inverse perform the complete rotation between a vector in the ICRF and a vector in the celestial intermediate reference system (CIRS) of this Julian date.

You will typically never need to access these matrices yourself, as they are used automatically by the radec() method when you use its epoch= parameter to ask for a right ascension and declination in the dynamical reference system, and when you ask a Topos object for its position.