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:
constructing one from a Julian Date (i.e. a float representing the number of days passed from some historical date).
constructing one from a datetime object.
constructing directly from an ISO 8601 string.
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:
It is not corresponding to a 1st of January (as per definition of the Julian Dates)
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.