Datetime toolbox#

The SPICE toolkit provides a very large number of time convertors, but sometimes it is quite complicated to manipulate them. In this section we will look how you can perform simple or complex time conversion with the planetary-coverage SPICE tools.

Datetime parser#

Tip

This function does not require to load any tls kernel to work.

The SPICE toolkit provides a very efficient tool (spiceypy.tparse()) to read a large variety of input string. Unfortunately, the results is provided as the number of seconds past J2000 only. To fix that you can use the datetime() function to convert any temporal string into a numpy.datetime64 object:

from planetary_coverage import datetime

datetime('Tue Aug  6 11:10:57  1996')
numpy.datetime64('1996-08-06T11:10:57')
datetime('2/3/1996 17:18:12.002')
numpy.datetime64('1996-02-03T17:18:12.002')
datetime('18 B.C. Jun 3, 12:29:28.291')
numpy.datetime64('-017-06-03T12:29:28.291')
datetime("'92-271/ 12:28:30.291")
numpy.datetime64('1992-09-27T12:28:30.291')
datetime('2451515.2981 (JD)')
numpy.datetime64('1999-12-02T19:09:15.840')
datetime('31-JAN-1987')
numpy.datetime64('1987-01-31')
datetime('feb/4/1987')
numpy.datetime64('1987-02-04')
datetime('March-7-1987-3:10:39.221')
numpy.datetime64('1987-03-07T03:10:39.221')
datetime('1999-12-02T19:09:15.84Z')
numpy.datetime64('1999-12-02T19:09:15.840')

MAPPS datetime string (with a _ ) also works, even though they are not natively compatible with the SPICE tparse function:

datetime('2001-JAN-01_12:34:56.789')
numpy.datetime64('2001-01-01T12:34:56.789')

You can also provide a numpy.datetime64 object as input:

datetime(np.datetime64('1999-12-02'))
numpy.datetime64('1999-12-02')

Or a native datetime.datetime object:

from datetime import datetime as native_datetime

datetime(native_datetime(2022, 1, 2, 3))
numpy.datetime64('2022-01-02T03','h')

It also works with a list of times even if they are not formatted in the same way:

datetime([
  '31-JAN-1987',
  '@feb/4/1987',
  'March-7-1987-3:10:39.221',
])
[numpy.datetime64('1987-01-31'),
 numpy.datetime64('1987-02-04'),
 numpy.datetime64('1987-03-07T03:10:39.221')]

You can also provide invalid time strings:

datetime('N/A')  # 'NaT' or 'Unable to determine' are accepted
numpy.datetime64('NaT')

The main advantage of the numpy.datetime64 objects over the date string, is its ability to perform time computations:

datetime('Tue Aug  6 11:10:57  1996') - datetime('2/3/1996 17:18:12')
numpy.timedelta64(15961965,'s')

Caution

SPICE parser does not support time system specification, it always assume that the input time is in UTC:

>>> datetime('2022 SEP 04 TDB')
ValueError: TPARSE does not support the specification of a time system in a string.
            The time system TDB was specified.

Danger

The input and output dates are always in the Gregorian Calendar. You need to be careful if you use dates older than October 15th, 1582. This is a known issue with the way SPICE is parsing times by default. The same concern applies to numpy.datetime64 objects. Fortunately, both issues are consistent between the 2 libraries.

# This date should does not exist in the Gregorian Calendar
>>> datetime('14 October, 1582')
numpy.datetime64('1582-10-14')

Sorting datetimes#

Datetimes list can be sorted with the sorted_datetimes() function:

from planetary_coverage import sorted_datetimes

sorted_datetimes([
    '2020-01-01T12:34:56.789',
    '2020-01-01T12:34:56Z',
    '2020-01-01T12',
    'January 1, 2000',
])
['January 1, 2000',
 '2020-01-01T12',
 '2020-01-01T12:34:56Z',
 '2020-01-01T12:34:56.789']

Input times don’t need to be pre-formatted. The output sorted list will not be post-formatted (only the order of the elements is changed).

You can reverse the output:

sorted_datetimes(['2020-01-01T12', 'January 1, 2000'], reverse=True)
['2020-01-01T12', 'January 1, 2000']

And if you have a list of list containing datetime, you can sort them by providing the index of the column with datetimes values:

sorted_datetimes([
    ['A', '2031-01-01'],
    ['B', '2032-01-01T01:23:45'],
    ['C', '2031-01-02'],
    ['D', '2032-01-01T12:34:56.7890'],
    ['E', '2032-01-01T12:34:56Z'],
    ['F', '2031-01-02T12'],
    ['G', '2031-01-01T12'],
    ['H', '2031-01-02'],
], index=1)
[['A', '2031-01-01'],
 ['G', '2031-01-01T12'],
 ['C', '2031-01-02'],
 ['H', '2031-01-02'],
 ['F', '2031-01-02T12'],
 ['B', '2032-01-01T01:23:45'],
 ['E', '2032-01-01T12:34:56Z'],
 ['D', '2032-01-01T12:34:56.7890']]

If multiple datetimes are present, you can provide multiple indexes and they will be sorted by each index (the first indexes will be used in priority):

sorted_datetimes([
    ['A', '2031-01-01', '2031-01-01T12'],
    ['B', '2032-01-01T01:23:45', '2032-01-01T12:34:56.7890'],
    ['C', '2031-01-02', '2031-01-02T12'],
    ['D', '2032-01-01T01:23:45', '2032-01-01T12:34:56Z'],
], index=(1, 2))
[['A', '2031-01-01', '2031-01-01T12'],
 ['C', '2031-01-02', '2031-01-02T12'],
 ['D', '2032-01-01T01:23:45', '2032-01-01T12:34:56Z'],
 ['B', '2032-01-01T01:23:45', '2032-01-01T12:34:56.7890']]

Datetime convertor#

Julian Date#

Caution

This Julian Date calculations don’t take into account leap seconds. Use Ephemeris Time (et) if you need 1 second precision.

You can parse a string to get its Julian day numbers with jdn_hms():

from planetary_coverage.spice import jd, jdn_hms, ymd

jdn_hms('2023 AUG 10, 16:47:09.123')
(2460167, 16, 47, 9, 123)

Or you can get its decimal value with jd() (used for astronomical purposes mainly):

jd('2023-08-10T16:47:09.123')
2460167.199411146

You can convert Julian Date numbers back to Calendar year with ymd():

ymd(2_460_167)
(2023, 8, 10)

ET / UTC / TDB#

One of the most basic function of spiceypy is to convert UTC time to et ephemeris time (epoch in the SPICE documentation) and vice versa. To perform this time computation, we need to load at least one kernel containing the list of leapseconds. We will load it with the SpicePool:

SpicePool + 'naif0012.tls'

Currently, the SPICE routines are not parallelized/vectorized and only a single time can be converted at a time. Here, we provide 3 new functions (et(), utc() and tdb()) to extend the default spiceypy.str2et(), spiceypy.et2utc() and spiceypy.timout() to convert lists and arrays of times:

from planetary_coverage import et, tdb, utc

With a single input:

et('2033-01-01')
1041422469.184
et('2033-01-01 TDB')
1041422400.0

With multiple inputs:

et('2033-01-01T00:00:00', '2033-JAN-01 00:00:01')
[1041422469.184, 1041422470.184]

With a list:

et(['2033-01-01T00:00:00', '2033-JAN-01 00:00:01'])
array([1.04142247e+09, 1.04142247e+09])

With a tuple:

et(('2033-01-01T00:00:00', '2033-JAN-01 00:00:01'))
array([1.04142247e+09, 1.04142247e+09])

With any numpy.ndarray:

et(np.array(['2033-01-01T00:00:00', '2033-JAN-01 00:00:01']))
array([1.04142247e+09, 1.04142247e+09])

The same applies to reverse utc() and tbd() functions (displayed in ISOC and TDB format respectively):

utc(1041422469.184)
numpy.datetime64('2033-01-01T00:00:00.000')
tdb(1041422400.0)
'2033-01-01 00:00:00.000 TDB'
utc(1041422469.184, 1041422470.184)
array(['2033-01-01T00:00:00.000', '2033-01-01T00:00:01.000'],
      dtype='datetime64[ms]')

ET ranges#

To quickly compute trajectories, we needed a way to generate evenly spaced time steps in the ephemeris time frame between a start and a stop time.

from planetary_coverage.spice import et_range

By default, the temporal step is 1 second:

et_range('2033-01-01T00:00:00', '2033-JAN-01 00:00:10')
array([1.04142247e+09, 1.04142247e+09, 1.04142247e+09, 1.04142247e+09,
       1.04142247e+09, 1.04142247e+09, 1.04142248e+09, 1.04142248e+09,
       1.04142248e+09, 1.04142248e+09, 1.04142248e+09])

But you can provide you own steps:

et_range('2033-01-01T00:00:00', '2033-JAN-01 00:00:10', '5s')
array([1.04142247e+09, 1.04142247e+09, 1.04142248e+09])

Caution

Short unit version are accepted, but H and S are considered invalid to avoid the confusion between m = minute and M = month.

Plural units are also valid.

Examples of valid units:

  • ms, msec, millisecond

  • s, sec, second

  • m, min, minute

  • h, hour

  • D, day

  • M, month

  • Y, year

Note

The start time is not parsed so the month step is always 30.5 days.

>>> utc_range(
>>>     '2033-01-01',
>>>     '2033-03-01',
>>>     '1 month'
>>> )
['2033-01-01T00:00:00',
 '2033-01-31T12:00:00',
 '2033-03-01T00:00:00']

You can utc_range() to get the output as UTC times:

from planetary_coverage.spice import utc_range

utc_range('2033-01-01T00:00:00', '2033-JAN-01 00:00:10', '5 sec')
array(['2033-01-01T00:00:00.000', '2033-01-01T00:00:05.000',
       '2033-01-01T00:00:10.000'], dtype='datetime64[ms]')

Hint

If a start time matches the previous stop time, the two sequences will be merged.

If you need to define complex irregular sequences of times, you can use the et_ranges()/utc_ranges() function that accept a list of start, stop and step times:

from planetary_coverage.spice import utc_ranges

utc_ranges(
    ('2000-01-01', '2000-01-02', '12h'),
    ('2000-01-02', '2000-01-02T01:30', '30 min'),
)
array(['2000-01-01T00:00:00.000', '2000-01-01T12:00:00.000',
       '2000-01-02T00:00:00.000', '2000-01-02T00:30:00.000',
       '2000-01-02T01:00:00.000', '2000-01-02T01:30:00.000'],
      dtype='datetime64[ms]')

In some cases, you might be interested to get a temporal sequence around a given date. For example, in a flyby configuration around the closest approach (CA) point. For that, you can use the et_ca_range()/utc_ca_range() function with a temporal sequence:

from planetary_coverage.spice import utc_ca_range

utc_ca_range('2000-01-01', (2, 'D', '1 day'))
array(['1999-12-30T00:00:00.000', '1999-12-31T00:00:00.000',
       '2000-01-01T00:00:00.000', '2000-01-02T00:00:00.000',
       '2000-01-03T00:00:00.000'], dtype='datetime64[ms]')

The value spans between -2 days to +2 days around a CA (here, 2000-01-01) and a time step of 1 day.

If no specific temporal sequence is provided, the default pattern will be:

(10, 'm', '1 sec'), (1, 'h', '10 sec'), (2, 'h', '1 min'), (12, 'h', '10 min')

Which will lead to the following sampling:

  • 1 pt from CA -12 h to CA -2 h every 10 min

  • 1 pt from CA -2 h to CA -1 h every 1 min

  • 1 pt from CA -1 h to CA -10 m every 10 sec

  • 1 pt from CA -10 m to CA +10 m every 1 sec

  • 1 pt from CA +10 m to CA +1 h every 10 sec

  • 1 pt from CA +1 h to CA +2 h every 1 min

  • 1 pt from CA +2 h to CA +12 h every 10 min

At the end you will get 2,041 points evenly spread around the CA point:

utc_ca_range('2000-01-01')
array(['1999-12-31T12:00:00.000', '1999-12-31T12:10:00.000',
       '1999-12-31T12:20:00.000', ..., '2000-01-01T11:40:00.000',
       '2000-01-01T11:50:00.000', '2000-01-01T12:00:00.000'],
      dtype='datetime64[ms]')

Datetime formatter#

When you have a numpy.datetime64 object you can convert it to a regular UTC string with the str() native function:

t = datetime('2033-01-02 12:34:56')

str(t)
'2033-01-02T12:34:56'

You can also format it in ISO format with a trailing Z:

from planetary_coverage.spice import iso

iso(t)
'2033-01-02T12:34:56Z'

Or in MAPPS datetime format (with a _ delimiter):

from planetary_coverage.spice import mapps_datetime

mapps_datetime(t)
'2033-JAN-02_12:34:56'