1281 lines
47 KiB
Python
1281 lines
47 KiB
Python
# -*- coding: utf-8 -*-
|
||
import datetime as dt_module
|
||
import re
|
||
import sys
|
||
from collections import namedtuple
|
||
from datetime import date, datetime, timedelta
|
||
from numpy import (
|
||
array, concatenate, cos, float64, int64, isnan, isinf, linspace,
|
||
nan, ndarray, nonzero, pi, rollaxis, searchsorted, sin, where, zeros_like,
|
||
|
||
)
|
||
from time import strftime, struct_time
|
||
from ._compatibility import interp
|
||
from .constants import ASEC2RAD, B1950, DAY_S, T0, tau
|
||
from .curvelib import Splines, build_spline_given_ends
|
||
from .descriptorlib import reify
|
||
from .earthlib import sidereal_time, earth_rotation_angle
|
||
from .framelib import ICRS_to_J2000 as B
|
||
from .functions import (A, mxm, mxmxm, load_bundled_npy, rot_x, rot_y, rot_z,
|
||
_to_array, _reconcile)
|
||
from .nutationlib import (
|
||
build_nutation_matrix, equation_of_the_equinoxes_complimentary_terms,
|
||
iau2000a_radians, mean_obliquity,
|
||
)
|
||
from .precessionlib import compute_precession
|
||
|
||
_EMPTY_TUPLE = () # since we use the value as a sentinel
|
||
DAY_US = 86400000000.0
|
||
GREGORIAN_START = 2299161
|
||
GREGORIAN_START_ENGLAND = 2361222
|
||
_OLD_PYTHON = sys.version_info < (2, 7)
|
||
def _cat(*args): return concatenate(args, axis=1)
|
||
|
||
CalendarTuple = namedtuple('CalendarTuple', 'year month day hour minute second')
|
||
|
||
class CalendarArray(ndarray):
|
||
@property
|
||
def year(self): return self[0]
|
||
@property
|
||
def month(self): return self[1]
|
||
@property
|
||
def day(self): return self[2]
|
||
@property
|
||
def hour(self): return self[3]
|
||
@property
|
||
def minute(self): return self[4]
|
||
@property
|
||
def second(self): return self[5]
|
||
|
||
if hasattr(dt_module, 'timezone'):
|
||
utc = dt_module.timezone.utc
|
||
@staticmethod
|
||
def _utcnow():
|
||
return datetime.now(utc) # datetime.UTC was not added until 3.11
|
||
else:
|
||
class UTC(dt_module.tzinfo):
|
||
'UTC'
|
||
zero = timedelta(0)
|
||
def utcoffset(self, dt):
|
||
return self.zero
|
||
def tzname(self, dt):
|
||
return 'UTC'
|
||
def dst(self, dt):
|
||
return self.zero
|
||
|
||
utc = UTC()
|
||
_utcnow = datetime.utcnow
|
||
|
||
# Much of the following code is adapted from the USNO's "novas.c".
|
||
|
||
_time_zero = dt_module.time(tzinfo=utc)
|
||
MONTH_NAMES = A['0', 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
|
||
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
|
||
|
||
tt_minus_tai = array(32.184 / DAY_S)
|
||
|
||
class Timescale(object):
|
||
"""The data necessary to express dates in different timescales.
|
||
|
||
A `Timescale` provides time objects with the data tables they need
|
||
to translate between different time scales: the schedule of UTC leap
|
||
seconds, and the value of ∆T over time. Most programs create a
|
||
single `Timescale` which they use to build their `Time` objects:
|
||
|
||
>>> from skyfield.api import load
|
||
>>> ts = load.timescale()
|
||
>>> t = ts.utc(1980, 3, 1, 9, 30)
|
||
>>> t
|
||
<Time tt=2444299.896425741>
|
||
|
||
See :ref:`downloading-timescale-files` if you are interested in
|
||
checking how recent the data is in the files loaded by the
|
||
timescale.
|
||
|
||
"""
|
||
_utcnow = _utcnow
|
||
polar_motion_table = None
|
||
|
||
def __init__(self, delta_t_recent, leap_dates, leap_offsets):
|
||
if callable(delta_t_recent):
|
||
# Let a caller completely override our approach to ∆T by
|
||
# passing a function of their own.
|
||
self.delta_t_function = delta_t_recent
|
||
else:
|
||
self.delta_t_table = delta_t_recent # so users can see it
|
||
self.delta_t_function = build_delta_t(delta_t_recent)
|
||
|
||
self.leap_dates, self.leap_offsets = leap_dates, leap_offsets
|
||
self.J2000 = Time(self, float64(T0))
|
||
self.B1950 = Time(self, float64(B1950))
|
||
self.julian_calendar_cutoff = None
|
||
|
||
# Our internal leap-second table has three columns:
|
||
#
|
||
# _leap_utc Integer UTC seconds since JD 0 w/leap seconds missing.
|
||
# _leap_offsets Integer offset between UTC and TAI.
|
||
# _leap_tai Integer TAI seconds since JD 0.
|
||
|
||
is_legacy_table = isinf(leap_dates[-1])
|
||
if is_legacy_table:
|
||
leap_dates = leap_dates[2:-1]
|
||
leap_offsets = leap_offsets[3:]
|
||
|
||
one_zero = [[-1,0]]
|
||
self._leap_utc = (leap_dates[:,None] * DAY_S + one_zero).flatten()
|
||
self._leap_offsets = (leap_offsets[:,None] + one_zero).flatten()
|
||
self._leap_tai = self._leap_utc + self._leap_offsets
|
||
|
||
def now(self):
|
||
"""Return the current date and time as a `Time` object."""
|
||
return self.from_datetime(self._utcnow().replace(tzinfo=utc))
|
||
|
||
def from_datetime(self, datetime):
|
||
"""Return a `Time` for a Python ``datetime``.
|
||
|
||
The ``datetime`` must be “timezone-aware”: it must have a time
|
||
zone object as its ``tzinfo`` attribute instead of ``None``.
|
||
|
||
.. versionadded:: 1.24
|
||
|
||
"""
|
||
return self._utc(_datetime_to_utc_tuple(datetime))
|
||
|
||
def from_datetimes(self, datetime_list):
|
||
"""Return a `Time` for a list of Python ``datetime`` objects.
|
||
|
||
The ``datetime`` objects must each be “timezone-aware”: they
|
||
must each have a time zone object as their ``tzinfo`` attribute
|
||
instead of ``None``.
|
||
|
||
.. versionadded:: 1.24
|
||
|
||
"""
|
||
tuples = (_datetime_to_utc_tuple(d) for d in datetime_list)
|
||
return self._utc(array(value) for value in zip(*tuples))
|
||
|
||
def utc(self, year, month=1, day=1, hour=0, minute=0, second=0.0):
|
||
"""Build a `Time` from a UTC `calendar date`.
|
||
|
||
.. versionadded:: 1.24
|
||
Passing a Python ``datetime`` or a list of datetimes as the
|
||
first argument has been deprecated (and was never supported
|
||
for the other time scale methods). Instead, use the methods
|
||
:meth:`~skyfield.timelib.Timescale.from_datetime()` and
|
||
:meth:`~skyfield.timelib.Timescale.from_datetimes()`.
|
||
|
||
"""
|
||
# TODO: someday deprecate passing datetime objects here, as
|
||
# there are now separate constructors for them.
|
||
if isinstance(year, datetime):
|
||
return self.from_datetime(year)
|
||
if isinstance(year, date):
|
||
return self.from_datetime(datetime.combine(year, _time_zero))
|
||
if hasattr(year, '__len__') and isinstance(year[0], datetime):
|
||
return self.from_datetimes(year)
|
||
|
||
a = _to_array
|
||
tup = a(year), a(month), a(day), a(hour), a(minute), a(second)
|
||
return self._utc(tup)
|
||
|
||
def _utc(self, tup):
|
||
# Build a Time from a UTC tuple, carefully preserving its exact
|
||
# second number in the Time's hidden TAI seconds field.
|
||
year, month, day, hour, minute, second = tup
|
||
cutoff = self.julian_calendar_cutoff
|
||
|
||
# Figure out exactly the TAI second number.
|
||
seconds = (julian_day(year, month, day, cutoff) - 0.5) * DAY_S
|
||
seconds, sfr = divmod(seconds, 1.0) # in case there were any fractions
|
||
seconds += interp(seconds, self._leap_utc, self._leap_offsets)
|
||
more = hour * 3600.0 + minute * 60.0 + second
|
||
seconds2, sfr = divmod(sfr + more, 1.0)
|
||
seconds += seconds2
|
||
|
||
# For the other timescales, use the usual Julian date + fraction.
|
||
whole, fraction = divmod(seconds, DAY_S)
|
||
fraction += sfr
|
||
fraction /= DAY_S
|
||
|
||
t = Time(self, whole, fraction + tt_minus_tai)
|
||
t.tai_fraction = fraction
|
||
t._tai_seconds = seconds, sfr
|
||
return t
|
||
|
||
def _jd(self, year, month, day, hour, minute, second):
|
||
a = _to_array
|
||
cutoff = self.julian_calendar_cutoff
|
||
whole = julian_day(a(year), a(month), a(day), cutoff) - 0.5
|
||
fraction = (a(second) + a(minute) * 60.0 + a(hour) * 3600.0) / DAY_S
|
||
return _reconcile(whole, fraction)
|
||
|
||
def _cal(self, whole, fraction):
|
||
return calendar_tuple(whole, fraction, self.julian_calendar_cutoff)
|
||
|
||
def _strftime(self, format, jd, fraction, seconds_bump=None):
|
||
# Python forces an unhappy choice upon us: either use the faster
|
||
# time.strftime() and lose support for '%f', or use the slower
|
||
# datetime.strftime() and crash if years are negative. We take the
|
||
# first option, but then patch '%f' support back in by secretly
|
||
# passing the microseconds string as the time zone name. After all,
|
||
# the routines supported by this function never use time zones.
|
||
# What could go wrong?
|
||
|
||
offset, ms = _strftime_offset_seconds(format)
|
||
fraction = fraction + offset / DAY_S
|
||
year, month, day, hour, minute, second = self._cal(jd, fraction)
|
||
z = year * 0
|
||
|
||
# TODO: will this always produce the same whole number that
|
||
# calendar_tuple() produces internally? Or should we make a private
|
||
# version of calendar_tuple() that returns it to us for use here?
|
||
weekday = (fraction + 0.5 + _to_array(jd)).astype(int) % 7
|
||
|
||
if _format_uses_day_of_year(format):
|
||
start_of_year = julian_day(year, 1, 1, self.julian_calendar_cutoff)
|
||
yday = (jd + fraction + 1.5 - start_of_year).astype(int)
|
||
else:
|
||
yday = z
|
||
|
||
if ms:
|
||
format = format[:ms.start()] + '%Z' + format[ms.end():]
|
||
second = (second * 1e6).astype(int)
|
||
second, usec = divmod(second, 1000000)
|
||
if seconds_bump is not None:
|
||
second += seconds_bump
|
||
if getattr(jd, 'ndim', 0):
|
||
u = ['%06d' % u for u in usec]
|
||
tup = (year, month, day, hour, minute, second,
|
||
weekday, yday, z, u)
|
||
return [strftime(format, struct_time(t)) for t in zip(*tup)]
|
||
u = '%06d' % usec
|
||
tup = year, month, day, hour, minute, second, weekday, yday, z, u
|
||
return strftime(format, struct_time(tup))
|
||
else:
|
||
second = second.astype(int)
|
||
if seconds_bump is not None:
|
||
second += seconds_bump
|
||
tup = year, month, day, hour, minute, second, weekday, yday, z
|
||
if getattr(jd, 'ndim', 0):
|
||
return [strftime(format, item) for item in zip(*tup)]
|
||
return strftime(format, tup)
|
||
|
||
def _utc_jd(self, whole, fraction):
|
||
# Switch from days to seconds.
|
||
seconds = whole * DAY_S
|
||
seconds2, fraction_s = divmod(fraction * DAY_S, 1.0)
|
||
seconds += seconds2
|
||
|
||
# Add an integer number of leap seconds.
|
||
seconds += interp(seconds, self._leap_utc, self._leap_offsets)
|
||
|
||
# Switch back to days.
|
||
whole, fraction = divmod(seconds, DAY_S)
|
||
fraction += fraction_s
|
||
fraction /= DAY_S
|
||
return self.tai_jd(whole, fraction)
|
||
|
||
def tai(self, year=None, month=1, day=1, hour=0, minute=0, second=0.0,
|
||
jd=None):
|
||
"""Build a `Time` from an International Atomic Time `calendar date`.
|
||
|
||
.. versionadded:: 1.6
|
||
Passing a Julian date with ``jd=`` has been deprecated;
|
||
instead, use :meth:`~skyfield.timelib.Timescale.tai_jd()`.
|
||
|
||
"""
|
||
if jd is not None:
|
||
return self.tai_jd(jd) # deprecate someday
|
||
whole, fraction = self._jd(year, month, day, hour, minute, second)
|
||
t = Time(self, whole, fraction + tt_minus_tai)
|
||
t.tai_fraction = fraction
|
||
return t
|
||
|
||
def tai_jd(self, jd, fraction=None):
|
||
"""Build a `Time` from an International Atomic Time Julian date."""
|
||
jd, fraction = _normalize_jd_and_fraction(jd, fraction)
|
||
t = Time(self, jd, fraction + tt_minus_tai)
|
||
t.tai_fraction = fraction
|
||
return t
|
||
|
||
def tt(self, year=None, month=1, day=1, hour=0, minute=0, second=0.0,
|
||
jd=None):
|
||
"""Build a `Time` from a Terrestrial Time `calendar date`.
|
||
|
||
.. versionadded:: 1.6
|
||
Passing a Julian date with ``jd=`` has been deprecated;
|
||
instead, use :meth:`~skyfield.timelib.Timescale.tt_jd()`.
|
||
|
||
"""
|
||
if jd is not None:
|
||
return self.tt_jd(jd) # deprecate someday
|
||
whole, fraction = self._jd(year, month, day, hour, minute, second)
|
||
return Time(self, whole, fraction)
|
||
|
||
def tt_jd(self, jd, fraction=None):
|
||
"""Build a `Time` from a Terrestrial Time Julian date."""
|
||
jd, fraction = _normalize_jd_and_fraction(jd, fraction)
|
||
return Time(self, jd, fraction)
|
||
|
||
def J(self, year):
|
||
"""Build a `Time` from a Terrestrial Time Julian year or array.
|
||
|
||
Julian years are convenient uniform periods of exactly 365.25
|
||
days of Terrestrial Time, centered on 2000 January 1 12h TT =
|
||
Julian year 2000.0.
|
||
|
||
"""
|
||
tt = _to_array(year) * 365.25 + 1721045.0
|
||
return Time(self, tt, 0.0)
|
||
|
||
def tdb(self, year=None, month=1, day=1, hour=0, minute=0, second=0.0,
|
||
jd=None):
|
||
"""Build a `Time` from a Barycentric Dynamical Time `calendar date`.
|
||
|
||
.. versionadded:: 1.6
|
||
Passing a Julian date with ``jd=`` has been deprecated;
|
||
instead, use :meth:`~skyfield.timelib.Timescale.tdb_jd()`.
|
||
|
||
"""
|
||
if jd is not None:
|
||
return self.tdb_jd(jd) # deprecate someday
|
||
whole, fraction = self._jd(year, month, day, hour, minute, second)
|
||
jd = whole + fraction # TODO: why do tests break if we pass separately
|
||
return Time(self, jd, - tdb_minus_tt(jd) / DAY_S)
|
||
|
||
def tdb_jd(self, jd, fraction=None):
|
||
"""Build a `Time` from a Barycentric Dynamical Time Julian date."""
|
||
jd, fraction = _normalize_jd_and_fraction(jd, fraction)
|
||
t = Time(self, jd, fraction - tdb_minus_tt(jd, fraction) / DAY_S)
|
||
t.tdb_fraction = fraction
|
||
return t
|
||
|
||
def ut1(self, year=None, month=1, day=1, hour=0, minute=0, second=0.0,
|
||
jd=None):
|
||
"""Build a `Time` from a UT1 Universal Time `calendar date`.
|
||
|
||
.. versionadded:: 1.6
|
||
Passing a Julian date with ``jd=`` has been deprecated;
|
||
instead, use :meth:`~skyfield.timelib.Timescale.ut1_jd()`.
|
||
|
||
"""
|
||
if jd is None: # TODO: deprecate the jd parameter to this method
|
||
whole, fraction = self._jd(year, month, day, hour, minute, second)
|
||
jd = whole + fraction # TODO: can we pass high precision on?
|
||
return self.ut1_jd(jd)
|
||
|
||
def ut1_jd(self, jd):
|
||
"""Build a `Time` from a UT1 Universal Time Julian date."""
|
||
ut1 = _to_array(jd)
|
||
|
||
# Estimate TT = UT1, to get a rough Delta T estimate.
|
||
tt_approx = ut1
|
||
delta_t_approx = self.delta_t_function(tt_approx)
|
||
|
||
# Use the rough Delta T to make a much better estimate of TT,
|
||
# then generate an even better Delta T.
|
||
tt_approx = ut1 + delta_t_approx / DAY_S
|
||
delta_t_approx = self.delta_t_function(tt_approx)
|
||
|
||
# We can now estimate TT with an error of < 1e-9 seconds within
|
||
# 10 centuries of either side of the present; for details, see:
|
||
# https://github.com/skyfielders/astronomy-notebooks
|
||
# and look for the notebook "error-in-timescale-ut1.ipynb".
|
||
delta_t_approx /= DAY_S
|
||
t = Time(self, ut1, delta_t_approx)
|
||
t.ut1_fraction = 0.0 * ut1
|
||
return t
|
||
|
||
def from_astropy(self, t):
|
||
"""Build a Skyfield `Time` from an AstroPy time object."""
|
||
return self.tt(jd=t.tt.jd)
|
||
|
||
def linspace(self, t0, t1, num=50):
|
||
"""Return ``num`` times spaced uniformly between ``t0`` to ``t1``.
|
||
|
||
This routine is named after, and powered by, the NumPy routine
|
||
`linspace()`_.
|
||
|
||
.. _linspace(): https://numpy.org/doc/stable/reference/generated/numpy.linspace.html
|
||
|
||
"""
|
||
whole0 = t0.whole
|
||
frac0 = t0.tt_fraction
|
||
whole1 = t1.whole
|
||
frac1 = t1.tt_fraction
|
||
return Time(
|
||
self,
|
||
linspace(whole0, whole1, num),
|
||
linspace(frac0, frac1, num),
|
||
)
|
||
|
||
class Time(object):
|
||
"""A single moment in history, or an array of several moments.
|
||
|
||
Skyfield programs don’t usually instantiate this class directly, but
|
||
instead build time objects using one of the timescale methods listed
|
||
at `timescale-summary`. If you do attempt the low-level operation
|
||
of building a time object yourself, either leave ``tt_fraction`` at
|
||
its default value of ``None`` — in which case Skyfield will assume
|
||
the fraction is zero — or provide a ``tt_fraction`` array that has
|
||
exactly the same dimensions as your ``tt`` array.
|
||
|
||
"""
|
||
def __init__(self, ts, tt, tt_fraction=None):
|
||
if tt_fraction is None:
|
||
tt_fraction = zeros_like(tt)
|
||
self.ts = ts
|
||
self.whole = tt
|
||
self.tt_fraction = tt_fraction
|
||
self.shape = getattr(tt, 'shape', _EMPTY_TUPLE)
|
||
|
||
def __len__(self):
|
||
shape = self.shape
|
||
if shape is _EMPTY_TUPLE:
|
||
raise TypeError('this is a single Time, not an array')
|
||
return shape[0]
|
||
|
||
def __repr__(self):
|
||
size = getattr(self.tt, 'size', -1)
|
||
if size > 3:
|
||
rstr = '<Time tt=[{0} ... {1}] len={2}>'
|
||
return rstr.format(self.tt[0], self.tt[-1], size)
|
||
else:
|
||
return ('<Time tt={0}>'.format(self.tt)
|
||
.replace('[ ', '[').replace(' ', ' '))
|
||
|
||
def __getitem__(self, index):
|
||
if self.shape is _EMPTY_TUPLE:
|
||
raise TypeError('this is a single Time, not an array')
|
||
# TODO: also copy cached matrices?
|
||
t = Time(self.ts, self.whole[index], self.tt_fraction[index])
|
||
d = self.__dict__
|
||
for name in 'tai_fraction', 'tdb_fraction', 'ut1_fraction':
|
||
value = d.get(name)
|
||
if value is not None:
|
||
setattr(t, name, value[index])
|
||
return t
|
||
|
||
def astimezone(self, tz):
|
||
"""Convert to a Python ``datetime`` in a particular timezone ``tz``.
|
||
|
||
If this time is an array, then an array of datetimes is returned
|
||
instead of a single value.
|
||
|
||
"""
|
||
dt, leap_second = self.astimezone_and_leap_second(tz)
|
||
return dt
|
||
|
||
def astimezone_and_leap_second(self, tz):
|
||
"""Convert to a Python ``datetime`` and leap second in a timezone.
|
||
|
||
Convert this time to a Python ``datetime`` and a leap second::
|
||
|
||
dt, leap_second = t.astimezone_and_leap_second(tz)
|
||
|
||
The argument ``tz`` should be a ``datetime`` compatible
|
||
timezone.
|
||
|
||
The leap second value is provided because a Python ``datetime``
|
||
can only number seconds ``0`` through ``59``, but leap seconds
|
||
have a designation of at least ``60``. The leap second return
|
||
value will normally be ``0``, but will instead be ``1`` if the
|
||
date and time are a UTC leap second. Add the leap second value
|
||
to the ``second`` field of the ``datetime`` to learn the real
|
||
name of the second.
|
||
|
||
If this time is an array, then an array of ``datetime`` objects
|
||
and an array of leap second integers is returned, instead of a
|
||
single value each.
|
||
|
||
"""
|
||
dt, leap_second = self.utc_datetime_and_leap_second()
|
||
normalize = getattr(tz, 'normalize', None)
|
||
if self.shape and normalize is not None:
|
||
dt = array([normalize(d.astimezone(tz)) for d in dt])
|
||
elif self.shape:
|
||
dt = array([d.astimezone(tz) for d in dt])
|
||
elif normalize is not None:
|
||
dt = normalize(dt.astimezone(tz))
|
||
else:
|
||
dt = dt.astimezone(tz)
|
||
return dt, leap_second
|
||
|
||
def toordinal(self):
|
||
"""Return the proleptic Gregorian ordinal of the UTC date.
|
||
|
||
This method makes Skyfield `Time` objects compatible with Python
|
||
`datetime`_ objects, which also provide a ``toordinal()``
|
||
method. Thanks to this method, a `Time` can often be used
|
||
directly as a coordinate for a plot.
|
||
|
||
"""
|
||
whole, fraction, is_leap_second = self._utc_seconds(0.0)
|
||
return (whole - 148731076800.0 + fraction) / DAY_S
|
||
|
||
def utc_datetime(self):
|
||
"""Convert to a Python ``datetime`` in UTC.
|
||
|
||
If this time is an array, then a list of datetimes is returned
|
||
instead of a single value.
|
||
|
||
"""
|
||
dt, leap_second = self.utc_datetime_and_leap_second()
|
||
return dt
|
||
|
||
def utc_datetime_and_leap_second(self):
|
||
"""Convert to a Python ``datetime`` in UTC, plus a leap second value.
|
||
|
||
Convert this time to a `datetime`_ object and a leap second::
|
||
|
||
dt, leap_second = t.utc_datetime_and_leap_second()
|
||
|
||
The leap second value is provided because a Python ``datetime``
|
||
can only number seconds ``0`` through ``59``, but leap seconds
|
||
have a designation of at least ``60``. The leap second return
|
||
value will normally be ``0``, but will instead be ``1`` if the
|
||
date and time are a UTC leap second. Add the leap second value
|
||
to the ``second`` field of the ``datetime`` to learn the real
|
||
name of the second.
|
||
|
||
If this time is an array, then an array of ``datetime`` objects
|
||
and an array of leap second integers is returned, instead of a
|
||
single value each.
|
||
|
||
"""
|
||
year, month, day, hour, minute, second = self._utc_tuple(0.5e-6)
|
||
micro = (second * 1e6).astype(int)
|
||
second, micro = divmod(micro, 1000000)
|
||
leap_second = second // 60
|
||
second -= leap_second # fit within limited bounds of Python datetime
|
||
if self.shape:
|
||
zone = [utc] * self.shape[0]
|
||
argsets = zip(year, month, day, hour, minute, second, micro, zone)
|
||
d = []
|
||
a = d.append
|
||
try: # placed outside the loop for efficiency
|
||
for args in argsets:
|
||
a(datetime(*args))
|
||
except ValueError as e:
|
||
_upgrade_datetime_exception(args, e)
|
||
raise
|
||
d = array(d)
|
||
else:
|
||
args = year, month, day, hour, minute, second, micro, utc
|
||
try:
|
||
d = datetime(*args)
|
||
except ValueError as e:
|
||
_upgrade_datetime_exception(args, e)
|
||
raise
|
||
return d, leap_second
|
||
|
||
def utc_iso(self, delimiter='T', places=0):
|
||
"""Convert to an ISO 8601 string like ``2014-01-18T01:35:38Z`` in UTC.
|
||
|
||
If this time is an array of dates, then a sequence of strings is
|
||
returned instead of a single string.
|
||
|
||
"""
|
||
# "places" used to be the 1st argument, so continue to allow an
|
||
# integer in that spot. TODO: deprecate this in Skyfield 2.0
|
||
# and remove it in 3.0.
|
||
if isinstance(delimiter, int):
|
||
places = delimiter
|
||
delimiter = 'T'
|
||
|
||
if places:
|
||
power_of_ten = 10 ** places
|
||
offset = 0.5 / power_of_ten
|
||
year, month, day, hour, minute, second = self._utc_tuple(offset)
|
||
second, fraction = divmod(second, 1.0)
|
||
fraction *= power_of_ten
|
||
format = '%04d-%02d-%02d{0}%02d:%02d:%02d.%0{1}dZ'.format(
|
||
delimiter, places)
|
||
args = (year, month, day, hour, minute, second, fraction)
|
||
else:
|
||
format = '%04d-%02d-%02d{0}%02d:%02d:%02dZ'.format(delimiter)
|
||
args = self._utc_tuple(0.5)
|
||
|
||
if self.shape:
|
||
return [format % tup for tup in zip(*args)]
|
||
else:
|
||
return format % args
|
||
|
||
def utc_jpl(self):
|
||
"""Convert to a string like ``A.D. 2014-Jan-18 01:35:37.5000 UTC``.
|
||
|
||
Returns a string for this date and time in UTC, in the format
|
||
used by the JPL HORIZONS system. If this time is an array of
|
||
dates, then a sequence of strings is returned instead of a
|
||
single string.
|
||
|
||
"""
|
||
year, month, day, hour, minute, second = self._utc_tuple(0.00005)
|
||
second, fraction = divmod(second, 1.0)
|
||
fraction *= 1e4
|
||
bc = year < 1
|
||
year = abs(year - bc)
|
||
era = where(bc, 'B.C.', 'A.D.')
|
||
format = '%s %04d-%s-%02d %02d:%02d:%02d.%04d UTC'
|
||
args = (era, year, MONTH_NAMES[month], day,
|
||
hour, minute, second, fraction)
|
||
|
||
if self.shape:
|
||
return [format % tup for tup in zip(*args)]
|
||
else:
|
||
return format % args
|
||
|
||
def utc_strftime(self, format='%Y-%m-%d %H:%M:%S UTC'):
|
||
"""Format the UTC time using a Python datetime formatting string.
|
||
|
||
This calls Python’s ``time.strftime()`` to format the date and
|
||
time. A single string is returned or else a whole array of
|
||
strings, depending on whether this time object is an array.
|
||
The most commonly used formats are:
|
||
|
||
* ``%Y`` four-digit year, ``%y`` two-digit year
|
||
* ``%m`` month number, ``%B`` name, ``%b`` abbreviation
|
||
* ``%d`` day of month
|
||
* ``%H`` hour
|
||
* ``%M`` minute
|
||
* ``%S`` second
|
||
* ``%A`` day of week, ``%a`` its abbreviation
|
||
|
||
The ``%Z`` and ``%z`` formats are not supported; instead, simply
|
||
use the literal characters ``'UTC'`` in your format string.
|
||
|
||
If the smallest time unit in your format is minutes or seconds,
|
||
then the time is rounded to the nearest minute or second.
|
||
Otherwise the value is truncated rather than rounded.
|
||
|
||
"""
|
||
offset, uses_ms = _strftime_offset_seconds(format)
|
||
year, month, day, hour, minute, second, jd = self._utc_tuple(offset, 1)
|
||
start_of_year = julian_day(year, 1, 1, self.ts.julian_calendar_cutoff)
|
||
weekday = jd % 7
|
||
yday = jd + 1 - start_of_year
|
||
return _strftime(format, year, month, day, hour, minute, second,
|
||
weekday, yday, uses_ms)
|
||
|
||
def _utc_tuple(self, offset, return_jd=False):
|
||
"""Return UTC as (year, month, day, hour, minute, second.fraction).
|
||
|
||
The `offset` in seconds is added to the UTC time before it is
|
||
split into its components. This is useful if the user is going
|
||
to round the result before displaying it. If the result is
|
||
going to be displayed as seconds, for example, set `offset` to
|
||
0.5 and then throw away the fraction; if the result is going to
|
||
be displayed as minutes, set `offset` to 30.0 and then throw
|
||
away the seconds; and so forth.
|
||
|
||
"""
|
||
second, sfr, is_leap_second = self._utc_seconds(offset)
|
||
second = second.astype(int64)
|
||
second -= is_leap_second
|
||
jd, second = divmod(second + 43200, 86400)
|
||
cutoff = self.ts.julian_calendar_cutoff
|
||
year, month, day = compute_calendar_date(jd, cutoff)
|
||
minute, second = divmod(second, 60)
|
||
hour, minute = divmod(minute, 60)
|
||
second += is_leap_second
|
||
if not return_jd:
|
||
return year, month, day, hour, minute, second + sfr
|
||
return year, month, day, hour, minute, second + sfr, jd
|
||
|
||
def _utc_seconds(self, offset):
|
||
"""Return integer seconds since JD 0.0, plus a 0 ≤ fraction < 1."""
|
||
seconds, fr = self._tai_seconds
|
||
seconds2, fr = divmod(fr + offset, 1.0)
|
||
seconds = seconds + seconds2 # not +=, which would modify cached array
|
||
ts = self.ts
|
||
tai_minus_utc = interp(seconds, ts._leap_tai, ts._leap_offsets)
|
||
tai_minus_utc, is_leap_second = divmod(tai_minus_utc, 1.0)
|
||
is_leap_second = is_leap_second > 0.0
|
||
return seconds - tai_minus_utc, fr, is_leap_second
|
||
|
||
@reify
|
||
def _tai_seconds(self):
|
||
# If UTC was supplied, this will already hold an exact value; otherwise:
|
||
seconds, fr = divmod(self.whole * DAY_S, 1.0)
|
||
seconds2, fr = divmod(fr + self.tai_fraction * DAY_S, 1.0)
|
||
seconds += seconds2
|
||
return seconds, fr
|
||
|
||
def _leap_seconds(self):
|
||
# TODO: should this be reified?
|
||
ts = self.ts
|
||
seconds, fr = self._tai_seconds
|
||
return interp(seconds, ts._leap_tai, ts._leap_offsets)
|
||
|
||
# Calendar tuples.
|
||
|
||
def tai_calendar(self):
|
||
"""TAI as a (year, month, day, hour, minute, second) `calendar date`."""
|
||
return self.ts._cal(self.whole, self.tai_fraction)
|
||
|
||
def tt_calendar(self):
|
||
"""TT as a (year, month, day, hour, minute, second) `calendar date`."""
|
||
return self.ts._cal(self.whole, self.tt_fraction)
|
||
|
||
def tdb_calendar(self):
|
||
"""TDB as a (year, month, day, hour, minute, second) `calendar date`."""
|
||
return self.ts._cal(self.whole, self.tdb_fraction)
|
||
|
||
def ut1_calendar(self):
|
||
"""UT1 as a (year, month, day, hour, minute, second) `calendar date`."""
|
||
return self.ts._cal(self.whole, self.ut1_fraction)
|
||
|
||
# Date formatting.
|
||
|
||
def tai_strftime(self, format='%Y-%m-%d %H:%M:%S TAI'):
|
||
"""Format TAI with a datetime strftime() format string."""
|
||
return self.ts._strftime(format, self.whole, self.tai_fraction)
|
||
|
||
def tt_strftime(self, format='%Y-%m-%d %H:%M:%S TT'):
|
||
"""Format TT with a datetime strftime() format string."""
|
||
return self.ts._strftime(format, self.whole, self.tt_fraction)
|
||
|
||
def tdb_strftime(self, format='%Y-%m-%d %H:%M:%S TDB'):
|
||
"""Format TDB with a datetime strftime() format string."""
|
||
return self.ts._strftime(format, self.whole, self.tdb_fraction)
|
||
|
||
def ut1_strftime(self, format='%Y-%m-%d %H:%M:%S UT1'):
|
||
"""Format UT1 with a datetime strftime() format string."""
|
||
return self.ts._strftime(format, self.whole, self.ut1_fraction)
|
||
|
||
# Convenient caching of several expensive functions of time.
|
||
|
||
@reify
|
||
def M(self):
|
||
"""3×3 rotation matrix: ICRS → equinox of this date."""
|
||
|
||
# Compute N and P instead of asking for self.N and self.P to
|
||
# avoid keeping copies of them, since Skyfield itself never uses
|
||
# them again once M has been computed. But we do check in case
|
||
# a user has forced them to be built already.
|
||
|
||
d = self.__dict__
|
||
|
||
P = d.get('P')
|
||
if P is None:
|
||
P = self.precession_matrix()
|
||
|
||
N = d.get('N')
|
||
if N is None:
|
||
N = self.nutation_matrix()
|
||
|
||
return mxmxm(N, P, B)
|
||
|
||
@reify
|
||
def MT(self):
|
||
"""3×3 rotation matrix: equinox of this date → ICRS."""
|
||
return rollaxis(self.M, 1)
|
||
|
||
@reify
|
||
def C(self):
|
||
# Calculate the Equation of Origins in cycles
|
||
eq_origins = (earth_rotation_angle(self.ut1) - self.gast / 24.0)
|
||
R = rot_z(2 * pi * eq_origins)
|
||
return mxm(R, self.M)
|
||
|
||
@reify
|
||
def CT(self):
|
||
return rollaxis(self.C, 1)
|
||
|
||
@reify
|
||
def _nutation_angles_radians(self):
|
||
# TODO: add psi and eps corrections support back in here, rather
|
||
# than at points of use.
|
||
return iau2000a_radians(self)
|
||
|
||
def _nutation_angles(self, angles):
|
||
# Sample code shared with early adopters suggested that setting
|
||
# this attribute manually could avoid the expense of IAU 2000A,
|
||
# so this setter continues to support the pattern.
|
||
|
||
d_psi, d_eps = angles
|
||
self._nutation_angles_radians = (
|
||
d_psi / 1e7 * ASEC2RAD,
|
||
d_eps / 1e7 * ASEC2RAD,
|
||
)
|
||
|
||
_nutation_angles = property(None, _nutation_angles)
|
||
|
||
@reify
|
||
def _mean_obliquity_radians(self):
|
||
# Cached because it is used to compute both gast and N.
|
||
return mean_obliquity(self.tdb) * ASEC2RAD
|
||
|
||
# Conversion between timescales.
|
||
|
||
@reify
|
||
def J(self):
|
||
"""Return a floating point Julian year or array of years for this date.
|
||
|
||
Julian years are convenient uniform periods of exactly 365.25
|
||
days of Terrestrial Time, centered on 2000 January 1 12h TT =
|
||
Julian year 2000.0.
|
||
|
||
"""
|
||
return (self.whole - 1721045.0 + self.tt_fraction) / 365.25
|
||
|
||
@reify
|
||
def utc(self):
|
||
"""A tuple ``(year, month, day, hour, minute, second)`` in UTC."""
|
||
utc = self._utc_tuple(0.0)
|
||
return (array(utc).view(CalendarArray) if self.shape
|
||
else CalendarTuple(*utc))
|
||
|
||
@reify
|
||
def tai_fraction(self):
|
||
return self.tt_fraction - tt_minus_tai
|
||
|
||
@reify
|
||
def tdb_fraction(self):
|
||
fr = self.tt_fraction
|
||
return fr + tdb_minus_tt(self.whole, fr) / DAY_S
|
||
|
||
@reify
|
||
def ut1_fraction(self):
|
||
return self.tt_fraction - self.delta_t / DAY_S
|
||
|
||
@reify
|
||
def delta_t(self):
|
||
return self.ts.delta_t_function(self.tt)
|
||
|
||
@reify
|
||
def dut1(self):
|
||
return 32.184 + self._leap_seconds() - self.delta_t
|
||
|
||
@reify
|
||
def gmst(self):
|
||
"""Greenwich Mean Sidereal Time (GMST) in hours."""
|
||
return sidereal_time(self)
|
||
|
||
@reify
|
||
def gast(self):
|
||
"""Greenwich Apparent Sidereal Time (GAST) in hours."""
|
||
d_psi, _ = self._nutation_angles_radians
|
||
tt = self.tt
|
||
# TODO: move this into an eqeq function?
|
||
c_terms = equation_of_the_equinoxes_complimentary_terms(tt)
|
||
eq_eq = d_psi * cos(self._mean_obliquity_radians) + c_terms
|
||
return (self.gmst + eq_eq / tau * 24.0) % 24.0
|
||
|
||
# Low-precision floats generated from internal float pairs.
|
||
|
||
@property
|
||
def tai(self):
|
||
return self.whole + self.tai_fraction
|
||
|
||
@property
|
||
def tt(self):
|
||
return self.whole + self.tt_fraction
|
||
|
||
@property
|
||
def tdb(self):
|
||
return self.whole + self.tdb_fraction
|
||
|
||
@property
|
||
def ut1(self):
|
||
return self.whole + self.ut1_fraction
|
||
|
||
# Earth position as a function of time.
|
||
|
||
def polar_motion_angles(self):
|
||
table = self.ts.polar_motion_table
|
||
if table is None:
|
||
return 0.0, 0.0, 0.0
|
||
sprime = -47.0e-6 * (self.tdb - T0) / 36525.0
|
||
tt, x, y = table
|
||
return sprime, interp(self.tt, tt, x), interp(self.tt, tt, y)
|
||
|
||
def polar_motion_matrix(self):
|
||
sprime, x, y = self.polar_motion_angles()
|
||
return mxmxm(
|
||
rot_x(y * ASEC2RAD),
|
||
rot_y(x * ASEC2RAD),
|
||
rot_z(-sprime * ASEC2RAD),
|
||
)
|
||
|
||
def nutation_matrix(self):
|
||
"""Compute the 3×3 nutation matrix N for this date."""
|
||
d_psi, d_eps = self._nutation_angles_radians
|
||
mean_obliquity = self._mean_obliquity_radians
|
||
true_obliquity = mean_obliquity + d_eps
|
||
return build_nutation_matrix(mean_obliquity, true_obliquity, d_psi)
|
||
|
||
def precession_matrix(self):
|
||
"""Compute the 3×3 precession matrix P for this date."""
|
||
return compute_precession(self.tdb)
|
||
|
||
# Various dunders.
|
||
|
||
def __eq__(self, other_time):
|
||
if isinstance(other_time, Time):
|
||
return self.__sub__(other_time) == 0.0
|
||
return False
|
||
|
||
def __lt__(self, other):
|
||
return ((self.tt_fraction - other.tt_fraction)
|
||
+ (self.whole - other.whole)) < 0
|
||
|
||
def __add__(self, other_time):
|
||
if isinstance(other_time, timedelta):
|
||
w = other_time.days
|
||
f = other_time.seconds / DAY_S + other_time.microseconds / DAY_US
|
||
elif isinstance(other_time, (int, float, ndarray)):
|
||
w, f = divmod(other_time, 1.0)
|
||
else:
|
||
return NotImplemented
|
||
|
||
return self.ts.tt_jd(self.whole + w, self.tt_fraction + f)
|
||
|
||
def __sub__(self, other_time):
|
||
if isinstance(other_time, Time):
|
||
return self.whole - other_time.whole + (
|
||
self.tt_fraction - other_time.tt_fraction
|
||
)
|
||
elif isinstance(other_time, timedelta):
|
||
w = other_time.days
|
||
f = other_time.seconds / DAY_S + other_time.microseconds / DAY_US
|
||
elif isinstance(other_time, (int, float)):
|
||
w, f = divmod(other_time, 1.0)
|
||
else:
|
||
return NotImplemented
|
||
|
||
return self.ts.tt_jd(self.whole - w, self.tt_fraction - f)
|
||
|
||
def __hash__(self):
|
||
# Someone wanted to use Time objects with functools.lru_cache so
|
||
# we make this attempt to support hashability; beware that it
|
||
# will return the same hash for very closely spaced times that
|
||
# all round to the same floating point TT.
|
||
return hash(self.tt)
|
||
|
||
# Interoperability.
|
||
|
||
def to_astropy(self):
|
||
"""Return an AstroPy object representing this time."""
|
||
from astropy.time import Time
|
||
return Time(self.whole, self.tt_fraction, format='jd', scale='tt')
|
||
|
||
# Deprecated attributes that were once used internally, consuming
|
||
# memory with matrices that are never used again by Skyfield once
|
||
# t.M has been computed.
|
||
|
||
P = reify(precession_matrix)
|
||
N = reify(nutation_matrix)
|
||
P.__doc__ = N.__doc__ = None # omit from Sphinx documentation
|
||
|
||
@reify
|
||
def PT(self): return rollaxis(self.P, 1)
|
||
@reify
|
||
def NT(self): return rollaxis(self.N, 1)
|
||
|
||
def julian_day(year, month=1, day=1, julian_before=None):
|
||
"""Given a calendar date, return a Julian day integer.
|
||
|
||
Uses the proleptic Gregorian calendar unless ``julian_before`` is
|
||
set to a specific Julian day, in which case the Julian calendar is
|
||
used for dates older than that.
|
||
|
||
"""
|
||
# Support months <1 and >12 by overflowing cleanly into adjacent years.
|
||
y, month = divmod(month - 1, 12)
|
||
year = year + y
|
||
month += 1
|
||
|
||
# See the Explanatory Supplement to the Astronomical Almanac 15.11.
|
||
janfeb = month <= 2
|
||
g = year + 4716 - janfeb
|
||
f = (month + 9) % 12
|
||
e = 1461 * g // 4 + day - 1402
|
||
J = e + (153 * f + 2) // 5
|
||
|
||
mask = 1 if (julian_before is None) else (J >= julian_before)
|
||
J += (38 - (g + 184) // 100 * 3 // 4) * mask
|
||
return J
|
||
|
||
def julian_date(year, month=1, day=1, hour=0, minute=0, second=0.0):
|
||
"""Given a proleptic Gregorian calendar date and time, build a Julian date.
|
||
|
||
The difference between a “Julian day” and a “Julian date” is that
|
||
the “day” is the integer part, while the “date” includes a fraction
|
||
indicating the time.
|
||
|
||
"""
|
||
return julian_day(year, month, day) - 0.5 + (
|
||
second + minute * 60.0 + hour * 3600.0) / DAY_S
|
||
|
||
def julian_date_of_besselian_epoch(b):
|
||
return 2415020.31352 + (b - 1900.0) * 365.242198781
|
||
|
||
def compute_calendar_date(jd_integer, julian_before=None):
|
||
"""Convert Julian day ``jd_integer`` into a calendar (year, month, day).
|
||
|
||
Uses the proleptic Gregorian calendar unless ``julian_before`` is
|
||
set to a specific Julian day, in which case the Julian calendar is
|
||
used for dates older than that.
|
||
|
||
"""
|
||
use_gregorian = (julian_before is None) or (jd_integer >= julian_before)
|
||
|
||
# See the Explanatory Supplement to the Astronomical Almanac 15.11.
|
||
f = jd_integer + 1401
|
||
f += use_gregorian * ((4 * jd_integer + 274277) // 146097 * 3 // 4 - 38)
|
||
e = 4 * f + 3
|
||
g = e % 1461 // 4
|
||
h = 5 * g + 2
|
||
day = h % 153 // 5 + 1
|
||
month = (h // 153 + 2) % 12 + 1
|
||
year = e // 1461 - 4716 + (12 + 2 - month) // 12
|
||
return year, month, day
|
||
|
||
calendar_date = compute_calendar_date # old name, in case anyone used it
|
||
|
||
def calendar_tuple(jd_float, fraction=0.0, julian_before=None):
|
||
"""Return a (year, month, day, hour, minute, second.fraction) tuple."""
|
||
jd_float = _to_array(jd_float)
|
||
whole1, fraction1 = divmod(jd_float, 1.0)
|
||
whole2, fraction = divmod(fraction1 + fraction + 0.5, 1.0)
|
||
whole = (whole1 + whole2).astype(int)
|
||
year, month, day = compute_calendar_date(whole, julian_before)
|
||
second = fraction * 86400.0
|
||
minute, second = divmod(second, 60.0)
|
||
minute = minute.astype(int)
|
||
hour, minute = divmod(minute, 60)
|
||
return year, month, day, hour, minute, second
|
||
|
||
def tdb_minus_tt(jd_tdb, fraction_tdb=0.0):
|
||
"""Computes how far TDB is in advance of TT, given TDB.
|
||
|
||
Given that the two time scales never diverge by more than 2ms, TT
|
||
can also be given as the argument to perform the conversion in the
|
||
other direction.
|
||
|
||
"""
|
||
t = (jd_tdb - T0 + fraction_tdb) / 36525.0
|
||
|
||
# USNO Circular 179, eq. 2.6.
|
||
return (0.001657 * sin ( 628.3076 * t + 6.2401)
|
||
+ 0.000022 * sin ( 575.3385 * t + 4.2970)
|
||
+ 0.000014 * sin (1256.6152 * t + 6.1969)
|
||
+ 0.000005 * sin ( 606.9777 * t + 4.0212)
|
||
+ 0.000005 * sin ( 52.9691 * t + 0.4444)
|
||
+ 0.000002 * sin ( 21.3299 * t + 5.5431)
|
||
+ 0.000010 * t * sin ( 628.3076 * t + 4.2490))
|
||
|
||
class DeltaT(object):
|
||
def __init__(self, table_tt, table_delta_t, long_term_function):
|
||
self.table_tt = table_tt
|
||
self.table_delta_t = table_delta_t
|
||
self.long_term_function = long_term_function
|
||
|
||
def __call__(self, tt):
|
||
delta_t = interp(tt, self.table_tt, self.table_delta_t, nan, nan)
|
||
if getattr(delta_t, 'shape', None):
|
||
nan_indexes = nonzero(isnan(delta_t))
|
||
if nan_indexes:
|
||
J = (tt[nan_indexes] - 1721045.0) / 365.25
|
||
delta_t[nan_indexes] = self.long_term_function(J)
|
||
else:
|
||
if isnan(delta_t):
|
||
J = (tt - 1721045.0) / 365.25
|
||
delta_t = self.long_term_function(J)
|
||
return delta_t
|
||
|
||
delta_t_parabola_stephenson_morrison_hohenkerk_2016 = Splines(
|
||
[1825.0, 1925.0, 0.0, 32.5, 0.0, -320.0])
|
||
|
||
delta_t_parabola_morrison_stephenson_2004 = Splines(
|
||
[1820.0, 1920.0, 0.0, 32.0, 0.0, -20.0])
|
||
|
||
def build_delta_t(delta_t_recent):
|
||
"""Return a function t→∆T, given recent real-world observations of ∆T.
|
||
|
||
When asked for ∆T outside the range of the `delta_t_recent` table,
|
||
Skyfield uses the splines for 720 BC – AD 2015 computed by Morrison,
|
||
Stephenson, Hohenkerk, and Zawilski. For dates that fall outside of
|
||
the spines, we use the long-term parabola of Stephenson, Morrison,
|
||
and Hohenkerk.
|
||
|
||
"""
|
||
parabola = delta_t_parabola_stephenson_morrison_hohenkerk_2016
|
||
s15_table = load_bundled_npy('delta_t.npz')['Table-S15.2020.txt']
|
||
table_tt, table_delta_t = delta_t_recent
|
||
|
||
p = parabola
|
||
pd = parabola.derivative
|
||
s = Splines(s15_table)
|
||
sd = s.derivative
|
||
|
||
long_term_parabola_width = p.upper[0] - p.lower[0]
|
||
|
||
# How many years wide we make the splines that connect the tables to
|
||
# the long-term parabola; tuned by hand until the derivative graphed
|
||
# by `work_on_delta_t_discontinuities()` in `design/delta_t.py`
|
||
# doesn't look too terrible.
|
||
patch_width = 800.0
|
||
|
||
# To the left of the Table-S15 splines, design a spline connecting
|
||
# them to the long-term parabola.
|
||
|
||
x1 = s.lower[0] # For the current table, this = -720.0.
|
||
x0 = x1 - patch_width
|
||
left = build_spline_given_ends(x0, p(x0), pd(x0), x1, s(x1), sd(x1))
|
||
|
||
# And to the left of that, put the pure long-term parabola.
|
||
|
||
x1 = x0
|
||
x0 = x1 - long_term_parabola_width
|
||
far_left = build_spline_given_ends(x0, p(x0), pd(x0), x1, p(x1), pd(x1))
|
||
|
||
# Truncate the splines table where the daily table starts, and
|
||
# adjust the final spline to remove any discontinuity.
|
||
|
||
x = (table_tt[0] - 1721045.0) / 365.25 # TT to J centuries
|
||
|
||
i = searchsorted(s15_table[0], x)
|
||
s15_table = s15_table[:,:i]
|
||
|
||
desired_y = table_delta_t[0]
|
||
current_y = s(x)
|
||
x0, x1, a3, a2, a1, a0 = s15_table[:,-1]
|
||
t = (x - x0) / (x1 - x0)
|
||
a1 = a1 + (desired_y - current_y) / t # adjust linear term
|
||
s15_table[:,-1] = x0, x1, a3, a2, a1, a0
|
||
|
||
# To the right of the recent ∆T table, design a spline connecting
|
||
# smoothly to the long-term parabola.
|
||
|
||
x0 = (table_tt[-1] - 1721045.0) / 365.25 # TT to J centuries
|
||
x1 = (x0 + patch_width) // 100.0 * 100.0 # Choose multiple of 100 years
|
||
y0 = table_delta_t[-1]
|
||
|
||
lookback = min(366, len(table_delta_t)) # Slope of last year of ∆T.
|
||
slope = (table_delta_t[-1] - table_delta_t[-lookback]) * lookback / 365.0
|
||
right = build_spline_given_ends(x0, y0, slope, x1, p(x1), pd(x1))
|
||
|
||
# At the far right, finish with the pure long-term parabola.
|
||
|
||
x0 = x1
|
||
x1 = x0 + long_term_parabola_width
|
||
far_right = build_spline_given_ends(x0, p(x0), pd(x0), x1, p(x1), pd(x1))
|
||
|
||
curve = Splines(_cat(
|
||
array([far_left]).T,
|
||
array([left]).T,
|
||
s15_table,
|
||
array([right]).T,
|
||
array([far_right]).T,
|
||
))
|
||
|
||
return DeltaT(table_tt, table_delta_t, curve)
|
||
|
||
def build_delta_t_table(delta_t_recent):
|
||
"""Deprecated: the Delta T interpolation table used in Skyfield <= 1.37."""
|
||
bundled = _cat(
|
||
load_bundled_npy('morrison_stephenson_deltat.npy')[:,:22],
|
||
load_bundled_npy('historic_deltat.npy'),
|
||
)
|
||
recent_start_time = delta_t_recent[0,0]
|
||
i = searchsorted(bundled[0], recent_start_time)
|
||
century = 36524.0
|
||
start_tt = bundled[0,0] - century
|
||
start_J = Time(None, start_tt).J
|
||
end_tt = delta_t_recent[0,-1] + century
|
||
end_J = Time(None, end_tt).J
|
||
return _cat(
|
||
[[start_tt], [delta_t_parabola_morrison_stephenson_2004(start_J)]],
|
||
bundled[:,:i],
|
||
delta_t_recent,
|
||
[[end_tt], [delta_t_parabola_morrison_stephenson_2004(end_J)]],
|
||
)
|
||
|
||
_format_uses_milliseconds = re.compile(r'%[-_0^#EO]*f').search
|
||
_format_uses_seconds = re.compile(r'%[-_0^#EO]*[STXc]').search
|
||
_format_uses_minutes = re.compile(r'%[-_0^#EO]*[MR]').search
|
||
_format_uses_day_of_year = re.compile(r'%[-_0^#EO]*j').search
|
||
|
||
def _datetime_to_utc_tuple(dt):
|
||
z = dt.tzinfo
|
||
if z is None:
|
||
raise ValueError(_naive_complaint)
|
||
if z is not utc:
|
||
dt = dt.astimezone(utc)
|
||
return (dt.year, dt.month, dt.day,
|
||
dt.hour, dt.minute, dt.second + dt.microsecond / 1e6)
|
||
|
||
def _normalize_jd_and_fraction(jd, fraction):
|
||
jd = _to_array(jd)
|
||
if fraction is None:
|
||
jd, fraction = divmod(jd, 1.0)
|
||
else:
|
||
jd, fraction = _reconcile(jd, _to_array(fraction))
|
||
return jd, fraction
|
||
|
||
def _strftime_offset_seconds(format):
|
||
uses_ms = _format_uses_milliseconds(format)
|
||
if uses_ms:
|
||
if _OLD_PYTHON:
|
||
raise ValueError('strftime() "%f" not supported under Python 2')
|
||
offset = 1e-16 # encourage .0 to not turn into .999999
|
||
elif _format_uses_seconds(format):
|
||
offset = 0.5
|
||
elif _format_uses_minutes(format):
|
||
offset = 30.0
|
||
else:
|
||
offset = 0.0
|
||
return offset, uses_ms
|
||
|
||
def _strftime(format, year, month, day, hour, minute, second,
|
||
weekday, yday, uses_ms):
|
||
zero = year * 0
|
||
|
||
# TODO: avoid computing yday if this is false:
|
||
#_format_uses_day_of_year(format)
|
||
|
||
if uses_ms:
|
||
format = format[:uses_ms.start()] + '%Z' + format[uses_ms.end():]
|
||
second = (second * 1e6).astype(int)
|
||
second, usec = divmod(second, 1000000)
|
||
if getattr(year, 'ndim', 0):
|
||
u = ['%06d' % u for u in usec]
|
||
tup = (year, month, day, hour, minute, second,
|
||
weekday, yday, zero, u)
|
||
return [strftime(format, struct_time(t)) for t in zip(*tup)]
|
||
u = '%06d' % usec
|
||
tup = year, month, day, hour, minute, second, weekday, yday, zero, u
|
||
return strftime(format, struct_time(tup))
|
||
else:
|
||
second = second.astype(int)
|
||
tup = year, month, day, hour, minute, second, weekday, yday, zero
|
||
if getattr(year, 'ndim', 0):
|
||
return [strftime(format, item) for item in zip(*tup)]
|
||
return strftime(format, tup)
|
||
|
||
def _upgrade_datetime_exception(args, e):
|
||
year, month, day, hour, minute, second, micro, zone = args
|
||
if year < 0:
|
||
e.args = ("Python's datetime does not support negative"
|
||
' years like the year {}'.format(year),)
|
||
elif month == 2 and day == 29:
|
||
e.args = ("Python's datetime does not support Julian leap"
|
||
' days like {} February 29 that are missing from'
|
||
' the Gregorian calendar'.format(year),)
|
||
|
||
_naive_complaint = """cannot interpret a datetime that lacks a timezone
|
||
|
||
You must either specify that your datetime is in UTC:
|
||
|
||
from skyfield.api import utc
|
||
d = datetime(..., tzinfo=utc) # to build a new datetime
|
||
d = d.replace(tzinfo=utc) # to fix an existing datetime
|
||
|
||
Or use a timezone object like those provided by the third-party `pytz` library:
|
||
|
||
from pytz import timezone
|
||
eastern = timezone('US/Eastern')
|
||
d = eastern.localize(datetime(2014, 1, 16, 1, 32, 9))"""
|