Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DM-26620: Catch astropy.time warnings about dubious years #367

Merged
merged 3 commits into from
Sep 5, 2020
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
18 changes: 13 additions & 5 deletions python/lsst/daf/butler/core/time_utils.py
Expand Up @@ -23,9 +23,10 @@
__all__ = ("astropy_to_nsec", "nsec_to_astropy", "times_equal")

import logging
import warnings

import astropy.time

import astropy.utils.exceptions

# These constants can be used by client code.
# EPOCH is used to construct times as read from database, its precision is
Expand Down Expand Up @@ -73,8 +74,11 @@ def astropy_to_nsec(astropy_time: astropy.time.Time) -> int:
after the max. time then it returns number corresponding to max. time.
"""
# sometimes comparison produces warnings if input value is in UTC
# scale, transform it to TAI before doing anyhting
value = astropy_time.tai
# scale, transform it to TAI before doing anything but also trap
# warnings in case we are dealing with simulated data from the future
with warnings.catch_warnings():
warnings.simplefilter("ignore", category=astropy.utils.exceptions.AstropyWarning)
value = astropy_time.tai
# anything before epoch or after MAX_TIME is truncated
if value < EPOCH:
_LOG.warning("'%s' is earlier than epoch time '%s', epoch time will be used instead",
Expand Down Expand Up @@ -135,8 +139,12 @@ def times_equal(time1: astropy.time.Time,
"""
# To compare we need them in common scale, for simplicity just
# bring them both to TAI scale
time1 = time1.tai
time2 = time2.tai
# Hide any warnings from this conversion since they are not relevant
# to the equality
with warnings.catch_warnings():
warnings.simplefilter("ignore", category=astropy.utils.exceptions.AstropyWarning)
time1 = time1.tai
time2 = time2.tai
delta = (time2.jd1 - time1.jd1) + (time2.jd2 - time1.jd2)
delta *= _NSEC_PER_DAY
return abs(delta) < precision_nsec
46 changes: 30 additions & 16 deletions python/lsst/daf/butler/core/timespan.py
Expand Up @@ -29,7 +29,9 @@
from typing import Any, ClassVar, Dict, Iterator, Mapping, NamedTuple, Optional, Tuple, Type, TypeVar, Union

import astropy.time
import astropy.utils.exceptions
import sqlalchemy
import warnings

from . import ddl
from .time_utils import astropy_to_nsec, EPOCH, MAX_TIME, times_equal
Expand Down Expand Up @@ -64,14 +66,18 @@ class Timespan(NamedTuple):
"""

def __str__(self) -> str:
if self.begin is None:
head = "(-∞, "
else:
head = f"[{self.begin.tai.isot}, "
if self.end is None:
tail = "∞)"
else:
tail = f"{self.end.tai.isot})"
# Trap dubious year warnings in case we have timespans from
# simulated data in the future
with warnings.catch_warnings():
warnings.simplefilter("ignore", category=astropy.utils.exceptions.AstropyWarning)
if self.begin is None:
head = "(-∞, "
else:
head = f"[{self.begin.tai.isot}, "
if self.end is None:
tail = "∞)"
else:
tail = f"{self.end.tai.isot})"
return head + tail

def __repr__(self) -> str:
Expand Down Expand Up @@ -430,14 +436,22 @@ def update(cls, timespan: Optional[Timespan], *,
begin = None
end = None
else:
if timespan.begin is None or timespan.begin < EPOCH:
begin = EPOCH
else:
begin = timespan.begin
if timespan.end is None or timespan.end > MAX_TIME:
end = MAX_TIME
else:
end = timespan.end
# These comparisons can trigger UTC -> TAI conversions that
# can result in warnings for simulated data from the future
with warnings.catch_warnings():
warnings.simplefilter("ignore", category=astropy.utils.exceptions.AstropyWarning)
if timespan.begin is None or timespan.begin < EPOCH:
begin = EPOCH
else:
begin = timespan.begin
# MAX_TIME is first in comparison to force a conversion
# from the supplied time scale to TAI rather than
# forcing MAX_TIME to be continually converted to
# the target time scale (which triggers warnings to UTC)
if timespan.end is None or MAX_TIME <= timespan.end:
end = MAX_TIME
else:
end = timespan.end
result[f"{cls.NAME}_begin"] = begin
result[f"{cls.NAME}_end"] = end
return result
Expand Down
12 changes: 12 additions & 0 deletions tests/test_time_utils.py
Expand Up @@ -23,6 +23,7 @@
"""

import unittest
import warnings

from astropy.time import Time, TimeDelta
from lsst.daf.butler import time_utils
Expand Down Expand Up @@ -53,6 +54,17 @@ def test_max_time(self):
value_max = time_utils.astropy_to_nsec(time_utils.MAX_TIME)
self.assertEqual(value, value_max)

# Check that we do not warn inside our code for UTC in the future
with self.assertWarns(Warning):
time = Time("2101-01-01T00:00:00", format="isot", scale="utc")
timj marked this conversation as resolved.
Show resolved Hide resolved

# unittest can't test for no warnings so we run the test and
# trigger our own warning and count all the warnings
with self.assertWarns(Warning) as cm:
time_utils.astropy_to_nsec(time)
warnings.warn("deliberate")
self.assertEqual(str(cm.warning), "deliberate")

def test_round_trip(self):
"""Test precision of round-trip conversion.
"""
Expand Down
19 changes: 19 additions & 0 deletions tests/test_timespan.py
Expand Up @@ -21,6 +21,7 @@

import unittest
import itertools
import warnings

import astropy.time

Expand Down Expand Up @@ -125,6 +126,24 @@ def testTimescales(self):
end=astropy.time.Time('2013-06-17T13:34:42.947', scale='utc', format='isot'))
self.assertEqual(ts1, ts2, f"Compare {ts1} with {ts2}")

def testFuture(self):
"""Check that we do not get warnings from future dates."""

# Astropy will give "dubious year" for UTC five years in the future
# so hide these expected warnings from the test output
with self.assertWarns(Warning):
ts1 = Timespan(begin=astropy.time.Time('2213-06-17 13:34:45.775000', scale='utc', format='iso'),
end=astropy.time.Time('2213-06-17 13:35:17.947000', scale='utc', format='iso'))
ts2 = Timespan(begin=astropy.time.Time('2213-06-17 13:34:45.775000', scale='utc', format='iso'),
end=astropy.time.Time('2213-06-17 13:35:17.947000', scale='utc', format='iso'))

# unittest can't test for no warnings so we run the test and
# trigger our own warning and count all the warnings
with self.assertWarns(Warning) as cm:
self.assertEqual(ts1, ts2)
warnings.warn("deliberate")
self.assertEqual(str(cm.warning), "deliberate")


if __name__ == "__main__":
unittest.main()
4 changes: 2 additions & 2 deletions tests/test_utils.py
Expand Up @@ -217,13 +217,13 @@ def testGlobList(self):
"""
# test an absolute string
patterns = globToRegex(["bar"])
self.assertEquals(len(patterns), 1)
self.assertEqual(len(patterns), 1)
self.assertTrue(bool(re.fullmatch(patterns[0], "bar")))
self.assertIsNone(re.fullmatch(patterns[0], "boz"))

# test leading & trailing wildcard in multiple patterns
patterns = globToRegex(["ba*", "*.fits"])
self.assertEquals(len(patterns), 2)
self.assertEqual(len(patterns), 2)
# check the "ba*" pattern:
self.assertTrue(bool(re.fullmatch(patterns[0], "bar")))
self.assertTrue(bool(re.fullmatch(patterns[0], "baz")))
Expand Down