Source code for astra.jdutil
"""Julian date ↔ UTC datetime conversions (stdlib only; avoids import cycles).
Public API
----------
jd_utc_to_datetime / datetime_utc_to_jd — canonical UTC-aware implementations.
jd_to_datetime / datetime_to_jd — short-form aliases (used by ocm.py etc.).
Timezone note
-------------
``np.datetime64`` has no concept of time-zones (it stores a UTC integer count).
Constructing it from a *timezone-aware* ``datetime`` object emits:
UserWarning: no explicit representation of timezones available for np.datetime64
This is silenced by anchoring the J2000 reference on an **ISO string** (timezone-
naive, implicitly UTC) so numpy never needs to round-trip through a tzinfo object.
Timezone-awareness is re-attached explicitly on the Python side before returning.
"""
from __future__ import annotations
import numpy as np
from datetime import datetime, timezone
# J2000 reference epoch: 2000-01-01T12:00:00 UTC.
_J2000_JD: float = 2451545.0
_J2000_EPOCH: datetime = datetime(2000, 1, 1, 12, 0, 0, tzinfo=timezone.utc)
# numpy anchor without tzinfo to suppress UserWarning about timezone representation.
# The string "2000-01-01T12:00:00" is unambiguously UTC in Julian-Date arithmetic.
_J2000_NP: np.datetime64 = np.datetime64("2000-01-01T12:00:00", "us")
[docs]
def jd_utc_to_datetime(jd: float | np.ndarray) -> datetime | np.ndarray:
"""Convert UTC Julian Date(s) to timezone-aware UTC datetime(s).
Args:
jd: Julian Date scalar or numpy array.
Returns:
Scalar: timezone-aware ``datetime`` (UTC).
Array: object-dtype numpy array of timezone-aware ``datetime`` values.
Example::
from astra.jdutil import jd_utc_to_datetime
dt = jd_utc_to_datetime(2451545.0)
# -> datetime(2000, 1, 1, 12, 0, tzinfo=timezone.utc)
"""
jd_arr = np.asarray(jd, dtype=float)
days = jd_arr - _J2000_JD
# Represent offset as integer microseconds to avoid float precision loss.
us_offsets = np.round(days * 86400.0 * 1_000_000.0).astype("int64")
offsets_td64 = us_offsets.astype("timedelta64[us]")
dt64 = _J2000_NP + offsets_td64
from typing import cast
if jd_arr.ndim == 0:
# Scalar: extract Python datetime and attach UTC
raw = dt64.item() # returns datetime without tzinfo (numpy UTC epoch)
return cast(datetime, raw).replace(tzinfo=timezone.utc)
# Vectorised array case
out = np.array(
[cast(datetime, d).replace(tzinfo=timezone.utc) for d in dt64.astype(object)],
dtype=object,
)
return out.reshape(jd_arr.shape)
[docs]
def datetime_utc_to_jd(dt: datetime | np.ndarray) -> float | np.ndarray:
"""Convert UTC-aware datetime(s) to Julian Date.
Args:
dt: Single timezone-aware ``datetime``, or numpy array of datetimes.
Naive datetimes are assumed to be UTC.
Returns:
Julian Date as ``float`` (scalar) or ``np.ndarray`` (array input).
Example::
from astra.jdutil import datetime_utc_to_jd
from datetime import datetime, timezone
jd = datetime_utc_to_jd(datetime(2000, 1, 1, 12, tzinfo=timezone.utc))
# -> 2451545.0
"""
if isinstance(dt, np.ndarray):
dt64 = np.asarray(dt, dtype="datetime64[us]")
delta = dt64 - _J2000_NP
return _J2000_JD + delta.astype(float) / 86_400_000_000.0
# Scalar datetime branch — delta is a stdlib timedelta, total_seconds() is valid.
if dt.tzinfo is None:
dt = dt.replace(tzinfo=timezone.utc)
dt_utc = dt.astimezone(timezone.utc)
delta_s: float = (dt_utc - _J2000_EPOCH).total_seconds()
return _J2000_JD + delta_s / 86400.0
# ---------------------------------------------------------------------------
# Short-form public aliases
# ---------------------------------------------------------------------------
# ``jd_to_datetime`` and ``datetime_to_jd`` are the names that ocm.py and other
# callers import. They delegate to the canonical implementations above so
# there is exactly one code path.
[docs]
def jd_to_datetime(jd: float | np.ndarray) -> datetime | np.ndarray:
"""Alias for ``jd_utc_to_datetime`` — convert UTC Julian Date(s) to datetime(s).
Args:
jd: Julian Date scalar or array.
Returns:
Timezone-aware UTC datetime, or object array of datetimes.
Example::
from astra.jdutil import jd_to_datetime
dt = jd_to_datetime(2451545.0) # 2000-01-01 12:00:00+00:00
"""
return jd_utc_to_datetime(jd)
[docs]
def datetime_to_jd(dt: datetime | np.ndarray) -> float | np.ndarray:
"""Alias for ``datetime_utc_to_jd`` — convert UTC datetime(s) to Julian Date.
Args:
dt: Timezone-aware datetime, or numpy array of datetimes.
Naive datetimes are assumed UTC.
Returns:
Julian Date float, or numpy array of floats.
Example::
from astra.jdutil import datetime_to_jd
from datetime import datetime, timezone
jd = datetime_to_jd(datetime(2000, 1, 1, 12, tzinfo=timezone.utc))
# -> 2451545.0
"""
return datetime_utc_to_jd(dt)