Epochs and Julian Dates#

The representation of an epoch, that is of a specific point in time, be it in the future or in the past, can be rather confusing. In pykep we opted to offer the dedicated class epoch that takes care to offer a simple interface and, under the hoods, interfaces seamlessly both to the c++ std::chrono library and the python datetime module.

Let us briefly show its interface.

An epoch may be created in one of four ways:

  1. constructing one from a Julian Date (i.e. a float representing the number of days passed from some historical date).

  2. constructing one from a datetime object.

  3. constructing directly from an ISO 8601 string.

  4. requesting the current date from the pykep.epoch.now() function.

Note

MJD2000 is the Default Julian Date. When not specified othewise by the user, in the context of epoch arithmetics a float will always be considered by pykep as a Modified Julian Date 2000, i.e. as the number of days from 2000-01-01T00:00:00.000000, or as days if it represents a duration.

Note

The date in pykep does not account for leap seconds. If the user wishes to use the exact ISO 8601 representation of some epoch, also including leap seconds, he will have to account for the offset himself. As of of 2023 this may account to maximum 28 seconds. More info on leap seconds.

Note

The calendar used in pykep is the proleptic Gregorian calendar. This means that calendar dates are considered in the Gregorian system also before the 1580, year of the introduction of the Gregorain dates. Confusion may arise then if trying to match pykep dates to historical events.

import pykep as pk
import datetime

Julian dates#

ep = pk.epoch(0.)

we can print this on screen:

print(ep)
2000-01-01T00:00:00.000000

.. or instantiate an epoch by explicitly mentioning the Julian Date type:

ep = pk.epoch(0., pk.epoch.julian_type.MJD2000)
print(ep)
2000-01-01T00:00:00.000000

.. or use a different Julian Date than the default MJD2000:

ep = pk.epoch(2460676.5000000, pk.epoch.julian_type.JD)
print(ep)
2025-01-01T00:00:00.000000

Note

pykep supports the following Julian Dates MJD2000 (the default), MJD and JD. (see pykep.epoch.julian_type)

We may also request an epoch corresponding to the current UTC time:

ep = pk.epoch.now()
print(ep)
2025-01-08T15:30:19.165180

or construct it from an iso string:

ep = pk.epoch("2023-10-28T00:01:02.12")
print(ep)
2023-10-28T00:01:02.120000

Datetime interoperability#

If we have a datetime object from python builtin datetime library we can construct an epoch with it:

dt = datetime.datetime(year=2033, month=11, day=12, hour=12, minute=22, second=12, microsecond=14532)
ep = pk.epoch(dt)
print(ep)
2033-11-12T12:22:12.014532

and convert it, when needed, to a julian representation:

print(ep.mjd)
63913.51541683486

The epoch math#

Additions and subtractions are allowed between epochs and floats or datetime.timedelta. When using floats days are always assumed.

ep = pk.epoch(0)
ep = ep + 21.2353525 # This will be interpreted as days
print(ep)
2000-01-22T05:38:54.456000
ep = pk.epoch(0)
ep = ep + datetime.timedelta(hours = 5, seconds=54, days=21, minutes=38, microseconds=456000) # This will be interpreted as days
print(ep)
2000-01-22T05:38:54.456000

Some basic comparison operators are also allowed and may turn handy!

print(ep < ep + 4)
print(ep == ep + datetime.timedelta(days=32) - 32)
True
True

Leap seconds and Gregorian dates.#

An everlasting source of confusion arises whenever leap-seconds (post 1972) and pre-Gregorian dates (before 1580) are considered. These are both variations to the Gregorian calendar that account for the historical attempts to try and establish a calendar that makes some sense. A clearly impossible, yet commendable, effort since the Earth orbital period and its rotation period are not commensurable.

For peace of mind, pykep ignores both and uses the “proleptic Gregorian calendar”. It is up to the user to make corrections when reading or using calendar dates. Lets see a few examples. Say we want to instantiate an epoch corresponding to the calendar date “1999-02-01T10:23:22”. We would obviously like to write:

ep = pk.epoch("1999-02-01T10:23:22")
print(ep)
1999-02-01T10:23:22.000000

The screen output confirms our attempt is successfull … but what is printed on the screen is the proleptic Gregorian calendar representation of the epoch constructed. Thus if we instead wanted to match it to the actual calendar in use by most people in 1999, we should add the leap seconds (see https://en.wikipedia.org/wiki/Leap_second). In particular 22 seconds have been added between 1972 and 1999, hence the epoch must be transformed:

ep_with_leap = ep + datetime.timedelta(seconds=22)
print(ep_with_leap)
1999-02-01T10:23:44.000000

Note the use of datetime objects here, as the alternative floating point representation would actually introduce small errors:

ep_with_leap2 = ep + 22. * pk.SEC2DAY
print(ep_with_leap2)
1999-02-01T10:23:43.999999

Note

The float arithmetics on epochs should only be used when high accuracies are not needed.

A second point of attention is with calendar dates prior to 1580, when the Gregorian calendar was introduced. Since in pykep we use the proleptic Gregorian calendar (i.e. we assume the Gregorian calendar is valid also before the 1580), our calendar dates will be off w.r.t. those we know and that are instead defined by the Julian Calndar which was active before 1580.

For example, lets print the reference epoch for the Julian Date 0.:

print(pk.epoch(0, pk.epoch.julian_type.JD))
-4713-11-24T12:00:00.000000

Now, this is confusing in at least two distinct ways:

  1. It is not corresponding to a 1st of January (as per definition of the Julian Dates)

  2. It indicates the years BC (in the anno domini calendar) with a minus (-4713) and accounts for the existence of a year 0: thus the year -1 will be 2 BC

All in all, the Proleptic Gregorian Calendar Date for the reference epoch of the Julian Date is the 24th of November 4714BC at noon, NOT the 1st of January 4713 at noon which is, instead, its Julian definition.