1# -*- coding: utf-8 -*- 2# Licensed under a 3-clause BSD style license - see LICENSE.rst 3import fnmatch 4import time 5import re 6import datetime 7import warnings 8from decimal import Decimal 9from collections import OrderedDict, defaultdict 10 11import numpy as np 12import erfa 13 14from astropy.utils.decorators import lazyproperty, classproperty 15from astropy.utils.exceptions import AstropyDeprecationWarning 16import astropy.units as u 17 18from . import _parse_times 19from . import utils 20from .utils import day_frac, quantity_day_frac, two_sum, two_product 21from . import conf 22 23__all__ = ['TimeFormat', 'TimeJD', 'TimeMJD', 'TimeFromEpoch', 'TimeUnix', 24 'TimeUnixTai', 'TimeCxcSec', 'TimeGPS', 'TimeDecimalYear', 25 'TimePlotDate', 'TimeUnique', 'TimeDatetime', 'TimeString', 26 'TimeISO', 'TimeISOT', 'TimeFITS', 'TimeYearDayTime', 27 'TimeEpochDate', 'TimeBesselianEpoch', 'TimeJulianEpoch', 28 'TimeDeltaFormat', 'TimeDeltaSec', 'TimeDeltaJD', 29 'TimeEpochDateString', 'TimeBesselianEpochString', 30 'TimeJulianEpochString', 'TIME_FORMATS', 'TIME_DELTA_FORMATS', 31 'TimezoneInfo', 'TimeDeltaDatetime', 'TimeDatetime64', 'TimeYMDHMS', 32 'TimeNumeric', 'TimeDeltaNumeric'] 33 34__doctest_skip__ = ['TimePlotDate'] 35 36# These both get filled in at end after TimeFormat subclasses defined. 37# Use an OrderedDict to fix the order in which formats are tried. 38# This ensures, e.g., that 'isot' gets tried before 'fits'. 39TIME_FORMATS = OrderedDict() 40TIME_DELTA_FORMATS = OrderedDict() 41 42# Translations between deprecated FITS timescales defined by 43# Rots et al. 2015, A&A 574:A36, and timescales used here. 44FITS_DEPRECATED_SCALES = {'TDT': 'tt', 'ET': 'tt', 45 'GMT': 'utc', 'UT': 'utc', 'IAT': 'tai'} 46 47 48def _regexify_subfmts(subfmts): 49 """ 50 Iterate through each of the sub-formats and try substituting simple 51 regular expressions for the strptime codes for year, month, day-of-month, 52 hour, minute, second. If no % characters remain then turn the final string 53 into a compiled regex. This assumes time formats do not have a % in them. 54 55 This is done both to speed up parsing of strings and to allow mixed formats 56 where strptime does not quite work well enough. 57 """ 58 new_subfmts = [] 59 for subfmt_tuple in subfmts: 60 subfmt_in = subfmt_tuple[1] 61 if isinstance(subfmt_in, str): 62 for strptime_code, regex in (('%Y', r'(?P<year>\d\d\d\d)'), 63 ('%m', r'(?P<mon>\d{1,2})'), 64 ('%d', r'(?P<mday>\d{1,2})'), 65 ('%H', r'(?P<hour>\d{1,2})'), 66 ('%M', r'(?P<min>\d{1,2})'), 67 ('%S', r'(?P<sec>\d{1,2})')): 68 subfmt_in = subfmt_in.replace(strptime_code, regex) 69 70 if '%' not in subfmt_in: 71 subfmt_tuple = (subfmt_tuple[0], 72 re.compile(subfmt_in + '$'), 73 subfmt_tuple[2]) 74 new_subfmts.append(subfmt_tuple) 75 76 return tuple(new_subfmts) 77 78 79class TimeFormat: 80 """ 81 Base class for time representations. 82 83 Parameters 84 ---------- 85 val1 : numpy ndarray, list, number, str, or bytes 86 Values to initialize the time or times. Bytes are decoded as ascii. 87 val2 : numpy ndarray, list, or number; optional 88 Value(s) to initialize the time or times. Only used for numerical 89 input, to help preserve precision. 90 scale : str 91 Time scale of input value(s) 92 precision : int 93 Precision for seconds as floating point 94 in_subfmt : str 95 Select subformat for inputting string times 96 out_subfmt : str 97 Select subformat for outputting string times 98 from_jd : bool 99 If true then val1, val2 are jd1, jd2 100 """ 101 102 _default_scale = 'utc' # As of astropy 0.4 103 subfmts = () 104 _registry = TIME_FORMATS 105 106 def __init__(self, val1, val2, scale, precision, 107 in_subfmt, out_subfmt, from_jd=False): 108 self.scale = scale # validation of scale done later with _check_scale 109 self.precision = precision 110 self.in_subfmt = in_subfmt 111 self.out_subfmt = out_subfmt 112 113 self._jd1, self._jd2 = None, None 114 115 if from_jd: 116 self.jd1 = val1 117 self.jd2 = val2 118 else: 119 val1, val2 = self._check_val_type(val1, val2) 120 self.set_jds(val1, val2) 121 122 def __init_subclass__(cls, **kwargs): 123 # Register time formats that define a name, but leave out astropy_time since 124 # it is not a user-accessible format and is only used for initialization into 125 # a different format. 126 if 'name' in cls.__dict__ and cls.name != 'astropy_time': 127 # FIXME: check here that we're not introducing a collision with 128 # an existing method or attribute; problem is it could be either 129 # astropy.time.Time or astropy.time.TimeDelta, and at the point 130 # where this is run neither of those classes have necessarily been 131 # constructed yet. 132 if 'value' in cls.__dict__ and not hasattr(cls.value, "fget"): 133 raise ValueError("If defined, 'value' must be a property") 134 135 cls._registry[cls.name] = cls 136 137 # If this class defines its own subfmts, preprocess the definitions. 138 if 'subfmts' in cls.__dict__: 139 cls.subfmts = _regexify_subfmts(cls.subfmts) 140 141 return super().__init_subclass__(**kwargs) 142 143 @classmethod 144 def _get_allowed_subfmt(cls, subfmt): 145 """Get an allowed subfmt for this class, either the input ``subfmt`` 146 if this is valid or '*' as a default. This method gets used in situations 147 where the format of an existing Time object is changing and so the 148 out_ or in_subfmt may need to be coerced to the default '*' if that 149 ``subfmt`` is no longer valid. 150 """ 151 try: 152 cls._select_subfmts(subfmt) 153 except ValueError: 154 subfmt = '*' 155 return subfmt 156 157 @property 158 def in_subfmt(self): 159 return self._in_subfmt 160 161 @in_subfmt.setter 162 def in_subfmt(self, subfmt): 163 # Validate subfmt value for this class, raises ValueError if not. 164 self._select_subfmts(subfmt) 165 self._in_subfmt = subfmt 166 167 @property 168 def out_subfmt(self): 169 return self._out_subfmt 170 171 @out_subfmt.setter 172 def out_subfmt(self, subfmt): 173 # Validate subfmt value for this class, raises ValueError if not. 174 self._select_subfmts(subfmt) 175 self._out_subfmt = subfmt 176 177 @property 178 def jd1(self): 179 return self._jd1 180 181 @jd1.setter 182 def jd1(self, jd1): 183 self._jd1 = _validate_jd_for_storage(jd1) 184 if self._jd2 is not None: 185 self._jd1, self._jd2 = _broadcast_writeable(self._jd1, self._jd2) 186 187 @property 188 def jd2(self): 189 return self._jd2 190 191 @jd2.setter 192 def jd2(self, jd2): 193 self._jd2 = _validate_jd_for_storage(jd2) 194 if self._jd1 is not None: 195 self._jd1, self._jd2 = _broadcast_writeable(self._jd1, self._jd2) 196 197 def __len__(self): 198 return len(self.jd1) 199 200 @property 201 def scale(self): 202 """Time scale""" 203 self._scale = self._check_scale(self._scale) 204 return self._scale 205 206 @scale.setter 207 def scale(self, val): 208 self._scale = val 209 210 def mask_if_needed(self, value): 211 if self.masked: 212 value = np.ma.array(value, mask=self.mask, copy=False) 213 return value 214 215 @property 216 def mask(self): 217 if 'mask' not in self.cache: 218 self.cache['mask'] = np.isnan(self.jd2) 219 if self.cache['mask'].shape: 220 self.cache['mask'].flags.writeable = False 221 return self.cache['mask'] 222 223 @property 224 def masked(self): 225 if 'masked' not in self.cache: 226 self.cache['masked'] = bool(np.any(self.mask)) 227 return self.cache['masked'] 228 229 @property 230 def jd2_filled(self): 231 return np.nan_to_num(self.jd2) if self.masked else self.jd2 232 233 @lazyproperty 234 def cache(self): 235 """ 236 Return the cache associated with this instance. 237 """ 238 return defaultdict(dict) 239 240 def _check_val_type(self, val1, val2): 241 """Input value validation, typically overridden by derived classes""" 242 # val1 cannot contain nan, but val2 can contain nan 243 isfinite1 = np.isfinite(val1) 244 if val1.size > 1: # Calling .all() on a scalar is surprisingly slow 245 isfinite1 = isfinite1.all() # Note: arr.all() about 3x faster than np.all(arr) 246 elif val1.size == 0: 247 isfinite1 = False 248 ok1 = (val1.dtype.kind == 'f' and val1.dtype.itemsize >= 8 249 and isfinite1 or val1.size == 0) 250 ok2 = val2 is None or ( 251 val2.dtype.kind == 'f' and val2.dtype.itemsize >= 8 252 and not np.any(np.isinf(val2))) or val2.size == 0 253 if not (ok1 and ok2): 254 raise TypeError('Input values for {} class must be finite doubles' 255 .format(self.name)) 256 257 if getattr(val1, 'unit', None) is not None: 258 # Convert any quantity-likes to days first, attempting to be 259 # careful with the conversion, so that, e.g., large numbers of 260 # seconds get converted without losing precision because 261 # 1/86400 is not exactly representable as a float. 262 val1 = u.Quantity(val1, copy=False) 263 if val2 is not None: 264 val2 = u.Quantity(val2, copy=False) 265 266 try: 267 val1, val2 = quantity_day_frac(val1, val2) 268 except u.UnitsError: 269 raise u.UnitConversionError( 270 "only quantities with time units can be " 271 "used to instantiate Time instances.") 272 # We now have days, but the format may expect another unit. 273 # On purpose, multiply with 1./day_unit because typically it is 274 # 1./erfa.DAYSEC, and inverting it recovers the integer. 275 # (This conversion will get undone in format's set_jds, hence 276 # there may be room for optimizing this.) 277 factor = 1. / getattr(self, 'unit', 1.) 278 if factor != 1.: 279 val1, carry = two_product(val1, factor) 280 carry += val2 * factor 281 val1, val2 = two_sum(val1, carry) 282 283 elif getattr(val2, 'unit', None) is not None: 284 raise TypeError('Cannot mix float and Quantity inputs') 285 286 if val2 is None: 287 val2 = np.array(0, dtype=val1.dtype) 288 289 def asarray_or_scalar(val): 290 """ 291 Remove ndarray subclasses since for jd1/jd2 we want a pure ndarray 292 or a Python or numpy scalar. 293 """ 294 return np.asarray(val) if isinstance(val, np.ndarray) else val 295 296 return asarray_or_scalar(val1), asarray_or_scalar(val2) 297 298 def _check_scale(self, scale): 299 """ 300 Return a validated scale value. 301 302 If there is a class attribute 'scale' then that defines the default / 303 required time scale for this format. In this case if a scale value was 304 provided that needs to match the class default, otherwise return 305 the class default. 306 307 Otherwise just make sure that scale is in the allowed list of 308 scales. Provide a different error message if `None` (no value) was 309 supplied. 310 """ 311 if scale is None: 312 scale = self._default_scale 313 314 if scale not in TIME_SCALES: 315 raise ScaleValueError("Scale value '{}' not in " 316 "allowed values {}" 317 .format(scale, TIME_SCALES)) 318 319 return scale 320 321 def set_jds(self, val1, val2): 322 """ 323 Set internal jd1 and jd2 from val1 and val2. Must be provided 324 by derived classes. 325 """ 326 raise NotImplementedError 327 328 def to_value(self, parent=None, out_subfmt=None): 329 """ 330 Return time representation from internal jd1 and jd2 in specified 331 ``out_subfmt``. 332 333 This is the base method that ignores ``parent`` and uses the ``value`` 334 property to compute the output. This is done by temporarily setting 335 ``self.out_subfmt`` and calling ``self.value``. This is required for 336 legacy Format subclasses prior to astropy 4.0 New code should instead 337 implement the value functionality in ``to_value()`` and then make the 338 ``value`` property be a simple call to ``self.to_value()``. 339 340 Parameters 341 ---------- 342 parent : object 343 Parent `~astropy.time.Time` object associated with this 344 `~astropy.time.TimeFormat` object 345 out_subfmt : str or None 346 Output subformt (use existing self.out_subfmt if `None`) 347 348 Returns 349 ------- 350 value : numpy.array, numpy.ma.array 351 Array or masked array of formatted time representation values 352 """ 353 # Get value via ``value`` property, overriding out_subfmt temporarily if needed. 354 if out_subfmt is not None: 355 out_subfmt_orig = self.out_subfmt 356 try: 357 self.out_subfmt = out_subfmt 358 value = self.value 359 finally: 360 self.out_subfmt = out_subfmt_orig 361 else: 362 value = self.value 363 364 return self.mask_if_needed(value) 365 366 @property 367 def value(self): 368 raise NotImplementedError 369 370 @classmethod 371 def _select_subfmts(cls, pattern): 372 """ 373 Return a list of subformats where name matches ``pattern`` using 374 fnmatch. 375 376 If no subformat matches pattern then a ValueError is raised. A special 377 case is a format with no allowed subformats, i.e. subfmts=(), and 378 pattern='*'. This is OK and happens when this method is used for 379 validation of an out_subfmt. 380 """ 381 if not isinstance(pattern, str): 382 raise ValueError('subfmt attribute must be a string') 383 384 subfmts = [x for x in cls.subfmts if fnmatch.fnmatchcase(x[0], pattern)] 385 if len(subfmts) == 0 and pattern != '*': 386 if len(cls.subfmts) == 0: 387 raise ValueError(f'subformat not allowed for format {cls.name}') 388 else: 389 subfmt_names = [x[0] for x in cls.subfmts] 390 raise ValueError(f'subformat {pattern!r} must match one of ' 391 f'{subfmt_names} for format {cls.name}') 392 393 return subfmts 394 395 396class TimeNumeric(TimeFormat): 397 subfmts = ( 398 ('float', np.float64, None, np.add), 399 ('long', np.longdouble, utils.longdouble_to_twoval, 400 utils.twoval_to_longdouble), 401 ('decimal', np.object_, utils.decimal_to_twoval, 402 utils.twoval_to_decimal), 403 ('str', np.str_, utils.decimal_to_twoval, utils.twoval_to_string), 404 ('bytes', np.bytes_, utils.bytes_to_twoval, utils.twoval_to_bytes), 405 ) 406 407 def _check_val_type(self, val1, val2): 408 """Input value validation, typically overridden by derived classes""" 409 # Save original state of val2 because the super()._check_val_type below 410 # may change val2 from None to np.array(0). The value is saved in order 411 # to prevent a useless and slow call to np.result_type() below in the 412 # most common use-case of providing only val1. 413 orig_val2_is_none = val2 is None 414 415 if val1.dtype.kind == 'f': 416 val1, val2 = super()._check_val_type(val1, val2) 417 elif (not orig_val2_is_none 418 or not (val1.dtype.kind in 'US' 419 or (val1.dtype.kind == 'O' 420 and all(isinstance(v, Decimal) for v in val1.flat)))): 421 raise TypeError( 422 'for {} class, input should be doubles, string, or Decimal, ' 423 'and second values are only allowed for doubles.' 424 .format(self.name)) 425 426 val_dtype = (val1.dtype if orig_val2_is_none else 427 np.result_type(val1.dtype, val2.dtype)) 428 subfmts = self._select_subfmts(self.in_subfmt) 429 for subfmt, dtype, convert, _ in subfmts: 430 if np.issubdtype(val_dtype, dtype): 431 break 432 else: 433 raise ValueError('input type not among selected sub-formats.') 434 435 if convert is not None: 436 try: 437 val1, val2 = convert(val1, val2) 438 except Exception: 439 raise TypeError( 440 'for {} class, input should be (long) doubles, string, ' 441 'or Decimal, and second values are only allowed for ' 442 '(long) doubles.'.format(self.name)) 443 444 return val1, val2 445 446 def to_value(self, jd1=None, jd2=None, parent=None, out_subfmt=None): 447 """ 448 Return time representation from internal jd1 and jd2. 449 Subclasses that require ``parent`` or to adjust the jds should 450 override this method. 451 """ 452 # TODO: do this in __init_subclass__? 453 if self.__class__.value.fget is not self.__class__.to_value: 454 return self.value 455 456 if jd1 is None: 457 jd1 = self.jd1 458 if jd2 is None: 459 jd2 = self.jd2 460 if out_subfmt is None: 461 out_subfmt = self.out_subfmt 462 subfmt = self._select_subfmts(out_subfmt)[0] 463 kwargs = {} 464 if subfmt[0] in ('str', 'bytes'): 465 unit = getattr(self, 'unit', 1) 466 digits = int(np.ceil(np.log10(unit / np.finfo(float).eps))) 467 # TODO: allow a way to override the format. 468 kwargs['fmt'] = f'.{digits}f' 469 value = subfmt[3](jd1, jd2, **kwargs) 470 return self.mask_if_needed(value) 471 472 value = property(to_value) 473 474 475class TimeJD(TimeNumeric): 476 """ 477 Julian Date time format. 478 This represents the number of days since the beginning of 479 the Julian Period. 480 For example, 2451544.5 in JD is midnight on January 1, 2000. 481 """ 482 name = 'jd' 483 484 def set_jds(self, val1, val2): 485 self._check_scale(self._scale) # Validate scale. 486 self.jd1, self.jd2 = day_frac(val1, val2) 487 488 489class TimeMJD(TimeNumeric): 490 """ 491 Modified Julian Date time format. 492 This represents the number of days since midnight on November 17, 1858. 493 For example, 51544.0 in MJD is midnight on January 1, 2000. 494 """ 495 name = 'mjd' 496 497 def set_jds(self, val1, val2): 498 self._check_scale(self._scale) # Validate scale. 499 jd1, jd2 = day_frac(val1, val2) 500 jd1 += erfa.DJM0 # erfa.DJM0=2400000.5 (from erfam.h). 501 self.jd1, self.jd2 = day_frac(jd1, jd2) 502 503 def to_value(self, **kwargs): 504 jd1 = self.jd1 - erfa.DJM0 # This cannot lose precision. 505 jd2 = self.jd2 506 return super().to_value(jd1=jd1, jd2=jd2, **kwargs) 507 508 value = property(to_value) 509 510 511class TimeDecimalYear(TimeNumeric): 512 """ 513 Time as a decimal year, with integer values corresponding to midnight 514 of the first day of each year. For example 2000.5 corresponds to the 515 ISO time '2000-07-02 00:00:00'. 516 """ 517 name = 'decimalyear' 518 519 def set_jds(self, val1, val2): 520 self._check_scale(self._scale) # Validate scale. 521 522 sum12, err12 = two_sum(val1, val2) 523 iy_start = np.trunc(sum12).astype(int) 524 extra, y_frac = two_sum(sum12, -iy_start) 525 y_frac += extra + err12 526 527 val = (val1 + val2).astype(np.double) 528 iy_start = np.trunc(val).astype(int) 529 530 imon = np.ones_like(iy_start) 531 iday = np.ones_like(iy_start) 532 ihr = np.zeros_like(iy_start) 533 imin = np.zeros_like(iy_start) 534 isec = np.zeros_like(y_frac) 535 536 # Possible enhancement: use np.unique to only compute start, stop 537 # for unique values of iy_start. 538 scale = self.scale.upper().encode('ascii') 539 jd1_start, jd2_start = erfa.dtf2d(scale, iy_start, imon, iday, 540 ihr, imin, isec) 541 jd1_end, jd2_end = erfa.dtf2d(scale, iy_start + 1, imon, iday, 542 ihr, imin, isec) 543 544 t_start = Time(jd1_start, jd2_start, scale=self.scale, format='jd') 545 t_end = Time(jd1_end, jd2_end, scale=self.scale, format='jd') 546 t_frac = t_start + (t_end - t_start) * y_frac 547 548 self.jd1, self.jd2 = day_frac(t_frac.jd1, t_frac.jd2) 549 550 def to_value(self, **kwargs): 551 scale = self.scale.upper().encode('ascii') 552 iy_start, ims, ids, ihmsfs = erfa.d2dtf(scale, 0, # precision=0 553 self.jd1, self.jd2_filled) 554 imon = np.ones_like(iy_start) 555 iday = np.ones_like(iy_start) 556 ihr = np.zeros_like(iy_start) 557 imin = np.zeros_like(iy_start) 558 isec = np.zeros_like(self.jd1) 559 560 # Possible enhancement: use np.unique to only compute start, stop 561 # for unique values of iy_start. 562 scale = self.scale.upper().encode('ascii') 563 jd1_start, jd2_start = erfa.dtf2d(scale, iy_start, imon, iday, 564 ihr, imin, isec) 565 jd1_end, jd2_end = erfa.dtf2d(scale, iy_start + 1, imon, iday, 566 ihr, imin, isec) 567 # Trying to be precise, but more than float64 not useful. 568 dt = (self.jd1 - jd1_start) + (self.jd2 - jd2_start) 569 dt_end = (jd1_end - jd1_start) + (jd2_end - jd2_start) 570 decimalyear = iy_start + dt / dt_end 571 572 return super().to_value(jd1=decimalyear, jd2=np.float64(0.0), **kwargs) 573 574 value = property(to_value) 575 576 577class TimeFromEpoch(TimeNumeric): 578 """ 579 Base class for times that represent the interval from a particular 580 epoch as a floating point multiple of a unit time interval (e.g. seconds 581 or days). 582 """ 583 584 @classproperty(lazy=True) 585 def _epoch(cls): 586 # Ideally we would use `def epoch(cls)` here and not have the instance 587 # property below. However, this breaks the sphinx API docs generation 588 # in a way that was not resolved. See #10406 for details. 589 return Time(cls.epoch_val, cls.epoch_val2, scale=cls.epoch_scale, 590 format=cls.epoch_format) 591 592 @property 593 def epoch(self): 594 """Reference epoch time from which the time interval is measured""" 595 return self._epoch 596 597 def set_jds(self, val1, val2): 598 """ 599 Initialize the internal jd1 and jd2 attributes given val1 and val2. 600 For an TimeFromEpoch subclass like TimeUnix these will be floats giving 601 the effective seconds since an epoch time (e.g. 1970-01-01 00:00:00). 602 """ 603 # Form new JDs based on epoch time + time from epoch (converted to JD). 604 # One subtlety that might not be obvious is that 1.000 Julian days in 605 # UTC can be 86400 or 86401 seconds. For the TimeUnix format the 606 # assumption is that every day is exactly 86400 seconds, so this is, in 607 # principle, doing the math incorrectly, *except* that it matches the 608 # definition of Unix time which does not include leap seconds. 609 610 # note: use divisor=1./self.unit, since this is either 1 or 1/86400, 611 # and 1/86400 is not exactly representable as a float64, so multiplying 612 # by that will cause rounding errors. (But inverting it as a float64 613 # recovers the exact number) 614 day, frac = day_frac(val1, val2, divisor=1. / self.unit) 615 616 jd1 = self.epoch.jd1 + day 617 jd2 = self.epoch.jd2 + frac 618 619 # For the usual case that scale is the same as epoch_scale, we only need 620 # to ensure that abs(jd2) <= 0.5. Since abs(self.epoch.jd2) <= 0.5 and 621 # abs(frac) <= 0.5, we can do simple (fast) checks and arithmetic here 622 # without another call to day_frac(). Note also that `round(jd2.item())` 623 # is about 10x faster than `np.round(jd2)`` for a scalar. 624 if self.epoch.scale == self.scale: 625 jd1_extra = np.round(jd2) if jd2.shape else round(jd2.item()) 626 jd1 += jd1_extra 627 jd2 -= jd1_extra 628 629 self.jd1, self.jd2 = jd1, jd2 630 return 631 632 # Create a temporary Time object corresponding to the new (jd1, jd2) in 633 # the epoch scale (e.g. UTC for TimeUnix) then convert that to the 634 # desired time scale for this object. 635 # 636 # A known limitation is that the transform from self.epoch_scale to 637 # self.scale cannot involve any metadata like lat or lon. 638 try: 639 tm = getattr(Time(jd1, jd2, scale=self.epoch_scale, 640 format='jd'), self.scale) 641 except Exception as err: 642 raise ScaleValueError("Cannot convert from '{}' epoch scale '{}'" 643 "to specified scale '{}', got error:\n{}" 644 .format(self.name, self.epoch_scale, 645 self.scale, err)) from err 646 647 self.jd1, self.jd2 = day_frac(tm._time.jd1, tm._time.jd2) 648 649 def to_value(self, parent=None, **kwargs): 650 # Make sure that scale is the same as epoch scale so we can just 651 # subtract the epoch and convert 652 if self.scale != self.epoch_scale: 653 if parent is None: 654 raise ValueError('cannot compute value without parent Time object') 655 try: 656 tm = getattr(parent, self.epoch_scale) 657 except Exception as err: 658 raise ScaleValueError("Cannot convert from '{}' epoch scale '{}'" 659 "to specified scale '{}', got error:\n{}" 660 .format(self.name, self.epoch_scale, 661 self.scale, err)) from err 662 663 jd1, jd2 = tm._time.jd1, tm._time.jd2 664 else: 665 jd1, jd2 = self.jd1, self.jd2 666 667 # This factor is guaranteed to be exactly representable, which 668 # means time_from_epoch1 is calculated exactly. 669 factor = 1. / self.unit 670 time_from_epoch1 = (jd1 - self.epoch.jd1) * factor 671 time_from_epoch2 = (jd2 - self.epoch.jd2) * factor 672 673 return super().to_value(jd1=time_from_epoch1, jd2=time_from_epoch2, **kwargs) 674 675 value = property(to_value) 676 677 @property 678 def _default_scale(self): 679 return self.epoch_scale 680 681 682class TimeUnix(TimeFromEpoch): 683 """ 684 Unix time (UTC): seconds from 1970-01-01 00:00:00 UTC, ignoring leap seconds. 685 686 For example, 946684800.0 in Unix time is midnight on January 1, 2000. 687 688 NOTE: this quantity is not exactly unix time and differs from the strict 689 POSIX definition by up to 1 second on days with a leap second. POSIX 690 unix time actually jumps backward by 1 second at midnight on leap second 691 days while this class value is monotonically increasing at 86400 seconds 692 per UTC day. 693 """ 694 name = 'unix' 695 unit = 1.0 / erfa.DAYSEC # in days (1 day == 86400 seconds) 696 epoch_val = '1970-01-01 00:00:00' 697 epoch_val2 = None 698 epoch_scale = 'utc' 699 epoch_format = 'iso' 700 701 702class TimeUnixTai(TimeUnix): 703 """ 704 Unix time (TAI): SI seconds elapsed since 1970-01-01 00:00:00 TAI (see caveats). 705 706 This will generally differ from standard (UTC) Unix time by the cumulative 707 integral number of leap seconds introduced into UTC since 1972-01-01 UTC 708 plus the initial offset of 10 seconds at that date. 709 710 This convention matches the definition of linux CLOCK_TAI 711 (https://www.cl.cam.ac.uk/~mgk25/posix-clocks.html), 712 and the Precision Time Protocol 713 (https://en.wikipedia.org/wiki/Precision_Time_Protocol), which 714 is also used by the White Rabbit protocol in High Energy Physics: 715 https://white-rabbit.web.cern.ch. 716 717 Caveats: 718 719 - Before 1972, fractional adjustments to UTC were made, so the difference 720 between ``unix`` and ``unix_tai`` time is no longer an integer. 721 - Because of the fractional adjustments, to be very precise, ``unix_tai`` 722 is the number of seconds since ``1970-01-01 00:00:00 TAI`` or equivalently 723 ``1969-12-31 23:59:51.999918 UTC``. The difference between TAI and UTC 724 at that epoch was 8.000082 sec. 725 - On the day of a positive leap second the difference between ``unix`` and 726 ``unix_tai`` times increases linearly through the day by 1.0. See also the 727 documentation for the `~astropy.time.TimeUnix` class. 728 - Negative leap seconds are possible, though none have been needed to date. 729 730 Examples 731 -------- 732 733 >>> # get the current offset between TAI and UTC 734 >>> from astropy.time import Time 735 >>> t = Time('2020-01-01', scale='utc') 736 >>> t.unix_tai - t.unix 737 37.0 738 739 >>> # Before 1972, the offset between TAI and UTC was not integer 740 >>> t = Time('1970-01-01', scale='utc') 741 >>> t.unix_tai - t.unix # doctest: +FLOAT_CMP 742 8.000082 743 744 >>> # Initial offset of 10 seconds in 1972 745 >>> t = Time('1972-01-01', scale='utc') 746 >>> t.unix_tai - t.unix 747 10.0 748 """ 749 name = 'unix_tai' 750 epoch_val = '1970-01-01 00:00:00' 751 epoch_scale = 'tai' 752 753 754class TimeCxcSec(TimeFromEpoch): 755 """ 756 Chandra X-ray Center seconds from 1998-01-01 00:00:00 TT. 757 For example, 63072064.184 is midnight on January 1, 2000. 758 """ 759 name = 'cxcsec' 760 unit = 1.0 / erfa.DAYSEC # in days (1 day == 86400 seconds) 761 epoch_val = '1998-01-01 00:00:00' 762 epoch_val2 = None 763 epoch_scale = 'tt' 764 epoch_format = 'iso' 765 766 767class TimeGPS(TimeFromEpoch): 768 """GPS time: seconds from 1980-01-06 00:00:00 UTC 769 For example, 630720013.0 is midnight on January 1, 2000. 770 771 Notes 772 ===== 773 This implementation is strictly a representation of the number of seconds 774 (including leap seconds) since midnight UTC on 1980-01-06. GPS can also be 775 considered as a time scale which is ahead of TAI by a fixed offset 776 (to within about 100 nanoseconds). 777 778 For details, see https://www.usno.navy.mil/USNO/time/gps/usno-gps-time-transfer 779 """ 780 name = 'gps' 781 unit = 1.0 / erfa.DAYSEC # in days (1 day == 86400 seconds) 782 epoch_val = '1980-01-06 00:00:19' 783 # above epoch is the same as Time('1980-01-06 00:00:00', scale='utc').tai 784 epoch_val2 = None 785 epoch_scale = 'tai' 786 epoch_format = 'iso' 787 788 789class TimePlotDate(TimeFromEpoch): 790 """ 791 Matplotlib `~matplotlib.pyplot.plot_date` input: 792 1 + number of days from 0001-01-01 00:00:00 UTC 793 794 This can be used directly in the matplotlib `~matplotlib.pyplot.plot_date` 795 function:: 796 797 >>> import matplotlib.pyplot as plt 798 >>> jyear = np.linspace(2000, 2001, 20) 799 >>> t = Time(jyear, format='jyear', scale='utc') 800 >>> plt.plot_date(t.plot_date, jyear) 801 >>> plt.gcf().autofmt_xdate() # orient date labels at a slant 802 >>> plt.draw() 803 804 For example, 730120.0003703703 is midnight on January 1, 2000. 805 """ 806 # This corresponds to the zero reference time for matplotlib plot_date(). 807 # Note that TAI and UTC are equivalent at the reference time. 808 name = 'plot_date' 809 unit = 1.0 810 epoch_val = 1721424.5 # Time('0001-01-01 00:00:00', scale='tai').jd - 1 811 epoch_val2 = None 812 epoch_scale = 'utc' 813 epoch_format = 'jd' 814 815 @lazyproperty 816 def epoch(self): 817 """Reference epoch time from which the time interval is measured""" 818 try: 819 # Matplotlib >= 3.3 has a get_epoch() function 820 from matplotlib.dates import get_epoch 821 except ImportError: 822 # If no get_epoch() then the epoch is '0001-01-01' 823 _epoch = self._epoch 824 else: 825 # Get the matplotlib date epoch as an ISOT string in UTC 826 epoch_utc = get_epoch() 827 from erfa import ErfaWarning 828 with warnings.catch_warnings(): 829 # Catch possible dubious year warnings from erfa 830 warnings.filterwarnings('ignore', category=ErfaWarning) 831 _epoch = Time(epoch_utc, scale='utc', format='isot') 832 _epoch.format = 'jd' 833 834 return _epoch 835 836 837class TimeStardate(TimeFromEpoch): 838 """ 839 Stardate: date units from 2318-07-05 12:00:00 UTC. 840 For example, stardate 41153.7 is 00:52 on April 30, 2363. 841 See http://trekguide.com/Stardates.htm#TNG for calculations and reference points 842 """ 843 name = 'stardate' 844 unit = 0.397766856 # Stardate units per day 845 epoch_val = '2318-07-05 11:00:00' # Date and time of stardate 00000.00 846 epoch_val2 = None 847 epoch_scale = 'tai' 848 epoch_format = 'iso' 849 850 851class TimeUnique(TimeFormat): 852 """ 853 Base class for time formats that can uniquely create a time object 854 without requiring an explicit format specifier. This class does 855 nothing but provide inheritance to identify a class as unique. 856 """ 857 858 859class TimeAstropyTime(TimeUnique): 860 """ 861 Instantiate date from an Astropy Time object (or list thereof). 862 863 This is purely for instantiating from a Time object. The output 864 format is the same as the first time instance. 865 """ 866 name = 'astropy_time' 867 868 def __new__(cls, val1, val2, scale, precision, 869 in_subfmt, out_subfmt, from_jd=False): 870 """ 871 Use __new__ instead of __init__ to output a class instance that 872 is the same as the class of the first Time object in the list. 873 """ 874 val1_0 = val1.flat[0] 875 if not (isinstance(val1_0, Time) and all(type(val) is type(val1_0) 876 for val in val1.flat)): 877 raise TypeError('Input values for {} class must all be same ' 878 'astropy Time type.'.format(cls.name)) 879 880 if scale is None: 881 scale = val1_0.scale 882 883 if val1.shape: 884 vals = [getattr(val, scale)._time for val in val1] 885 jd1 = np.concatenate([np.atleast_1d(val.jd1) for val in vals]) 886 jd2 = np.concatenate([np.atleast_1d(val.jd2) for val in vals]) 887 888 # Collect individual location values and merge into a single location. 889 if any(tm.location is not None for tm in val1): 890 if any(tm.location is None for tm in val1): 891 raise ValueError('cannot concatenate times unless all locations ' 892 'are set or no locations are set') 893 locations = [] 894 for tm in val1: 895 location = np.broadcast_to(tm.location, tm._time.jd1.shape, 896 subok=True) 897 locations.append(np.atleast_1d(location)) 898 899 location = np.concatenate(locations) 900 901 else: 902 location = None 903 else: 904 val = getattr(val1_0, scale)._time 905 jd1, jd2 = val.jd1, val.jd2 906 location = val1_0.location 907 908 OutTimeFormat = val1_0._time.__class__ 909 self = OutTimeFormat(jd1, jd2, scale, precision, in_subfmt, out_subfmt, 910 from_jd=True) 911 912 # Make a temporary hidden attribute to transfer location back to the 913 # parent Time object where it needs to live. 914 self._location = location 915 916 return self 917 918 919class TimeDatetime(TimeUnique): 920 """ 921 Represent date as Python standard library `~datetime.datetime` object 922 923 Example:: 924 925 >>> from astropy.time import Time 926 >>> from datetime import datetime 927 >>> t = Time(datetime(2000, 1, 2, 12, 0, 0), scale='utc') 928 >>> t.iso 929 '2000-01-02 12:00:00.000' 930 >>> t.tt.datetime 931 datetime.datetime(2000, 1, 2, 12, 1, 4, 184000) 932 """ 933 name = 'datetime' 934 935 def _check_val_type(self, val1, val2): 936 if not all(isinstance(val, datetime.datetime) for val in val1.flat): 937 raise TypeError('Input values for {} class must be ' 938 'datetime objects'.format(self.name)) 939 if val2 is not None: 940 raise ValueError( 941 f'{self.name} objects do not accept a val2 but you provided {val2}') 942 return val1, None 943 944 def set_jds(self, val1, val2): 945 """Convert datetime object contained in val1 to jd1, jd2""" 946 # Iterate through the datetime objects, getting year, month, etc. 947 iterator = np.nditer([val1, None, None, None, None, None, None], 948 flags=['refs_ok', 'zerosize_ok'], 949 op_dtypes=[None] + 5*[np.intc] + [np.double]) 950 for val, iy, im, id, ihr, imin, dsec in iterator: 951 dt = val.item() 952 953 if dt.tzinfo is not None: 954 dt = (dt - dt.utcoffset()).replace(tzinfo=None) 955 956 iy[...] = dt.year 957 im[...] = dt.month 958 id[...] = dt.day 959 ihr[...] = dt.hour 960 imin[...] = dt.minute 961 dsec[...] = dt.second + dt.microsecond / 1e6 962 963 jd1, jd2 = erfa.dtf2d(self.scale.upper().encode('ascii'), 964 *iterator.operands[1:]) 965 self.jd1, self.jd2 = day_frac(jd1, jd2) 966 967 def to_value(self, timezone=None, parent=None, out_subfmt=None): 968 """ 969 Convert to (potentially timezone-aware) `~datetime.datetime` object. 970 971 If ``timezone`` is not ``None``, return a timezone-aware datetime 972 object. 973 974 Parameters 975 ---------- 976 timezone : {`~datetime.tzinfo`, None}, optional 977 If not `None`, return timezone-aware datetime. 978 979 Returns 980 ------- 981 `~datetime.datetime` 982 If ``timezone`` is not ``None``, output will be timezone-aware. 983 """ 984 if out_subfmt is not None: 985 # Out_subfmt not allowed for this format, so raise the standard 986 # exception by trying to validate the value. 987 self._select_subfmts(out_subfmt) 988 989 if timezone is not None: 990 if self._scale != 'utc': 991 raise ScaleValueError("scale is {}, must be 'utc' when timezone " 992 "is supplied.".format(self._scale)) 993 994 # Rather than define a value property directly, we have a function, 995 # since we want to be able to pass in timezone information. 996 scale = self.scale.upper().encode('ascii') 997 iys, ims, ids, ihmsfs = erfa.d2dtf(scale, 6, # 6 for microsec 998 self.jd1, self.jd2_filled) 999 ihrs = ihmsfs['h'] 1000 imins = ihmsfs['m'] 1001 isecs = ihmsfs['s'] 1002 ifracs = ihmsfs['f'] 1003 iterator = np.nditer([iys, ims, ids, ihrs, imins, isecs, ifracs, None], 1004 flags=['refs_ok', 'zerosize_ok'], 1005 op_dtypes=7*[None] + [object]) 1006 1007 for iy, im, id, ihr, imin, isec, ifracsec, out in iterator: 1008 if isec >= 60: 1009 raise ValueError('Time {} is within a leap second but datetime ' 1010 'does not support leap seconds' 1011 .format((iy, im, id, ihr, imin, isec, ifracsec))) 1012 if timezone is not None: 1013 out[...] = datetime.datetime(iy, im, id, ihr, imin, isec, ifracsec, 1014 tzinfo=TimezoneInfo()).astimezone(timezone) 1015 else: 1016 out[...] = datetime.datetime(iy, im, id, ihr, imin, isec, ifracsec) 1017 1018 return self.mask_if_needed(iterator.operands[-1]) 1019 1020 value = property(to_value) 1021 1022 1023class TimeYMDHMS(TimeUnique): 1024 """ 1025 ymdhms: A Time format to represent Time as year, month, day, hour, 1026 minute, second (thus the name ymdhms). 1027 1028 Acceptable inputs must have keys or column names in the "YMDHMS" set of 1029 ``year``, ``month``, ``day`` ``hour``, ``minute``, ``second``: 1030 1031 - Dict with keys in the YMDHMS set 1032 - NumPy structured array, record array or astropy Table, or single row 1033 of those types, with column names in the YMDHMS set 1034 1035 One can supply a subset of the YMDHMS values, for instance only 'year', 1036 'month', and 'day'. Inputs have the following defaults:: 1037 1038 'month': 1, 'day': 1, 'hour': 0, 'minute': 0, 'second': 0 1039 1040 When the input is supplied as a ``dict`` then each value can be either a 1041 scalar value or an array. The values will be broadcast to a common shape. 1042 1043 Example:: 1044 1045 >>> from astropy.time import Time 1046 >>> t = Time({'year': 2015, 'month': 2, 'day': 3, 1047 ... 'hour': 12, 'minute': 13, 'second': 14.567}, 1048 ... scale='utc') 1049 >>> t.iso 1050 '2015-02-03 12:13:14.567' 1051 >>> t.ymdhms.year 1052 2015 1053 """ 1054 name = 'ymdhms' 1055 1056 def _check_val_type(self, val1, val2): 1057 """ 1058 This checks inputs for the YMDHMS format. 1059 1060 It is bit more complex than most format checkers because of the flexible 1061 input that is allowed. Also, it actually coerces ``val1`` into an appropriate 1062 dict of ndarrays that can be used easily by ``set_jds()``. This is useful 1063 because it makes it easy to get default values in that routine. 1064 1065 Parameters 1066 ---------- 1067 val1 : ndarray or None 1068 val2 : ndarray or None 1069 1070 Returns 1071 ------- 1072 val1_as_dict, val2 : val1 as dict or None, val2 is always None 1073 1074 """ 1075 if val2 is not None: 1076 raise ValueError('val2 must be None for ymdhms format') 1077 1078 ymdhms = ['year', 'month', 'day', 'hour', 'minute', 'second'] 1079 1080 if val1.dtype.names: 1081 # Convert to a dict of ndarray 1082 val1_as_dict = {name: val1[name] for name in val1.dtype.names} 1083 1084 elif val1.shape == (0,): 1085 # Input was empty list [], so set to None and set_jds will handle this 1086 return None, None 1087 1088 elif (val1.dtype.kind == 'O' 1089 and val1.shape == () 1090 and isinstance(val1.item(), dict)): 1091 # Code gets here for input as a dict. The dict input 1092 # can be either scalar values or N-d arrays. 1093 1094 # Extract the item (which is a dict) and broadcast values to the 1095 # same shape here. 1096 names = val1.item().keys() 1097 values = val1.item().values() 1098 val1_as_dict = {name: value for name, value 1099 in zip(names, np.broadcast_arrays(*values))} 1100 1101 else: 1102 raise ValueError('input must be dict or table-like') 1103 1104 # Check that the key names now are good. 1105 names = val1_as_dict.keys() 1106 required_names = ymdhms[:len(names)] 1107 1108 def comma_repr(vals): 1109 return ', '.join(repr(val) for val in vals) 1110 1111 bad_names = set(names) - set(ymdhms) 1112 if bad_names: 1113 raise ValueError(f'{comma_repr(bad_names)} not allowed as YMDHMS key name(s)') 1114 1115 if set(names) != set(required_names): 1116 raise ValueError(f'for {len(names)} input key names ' 1117 f'you must supply {comma_repr(required_names)}') 1118 1119 return val1_as_dict, val2 1120 1121 def set_jds(self, val1, val2): 1122 if val1 is None: 1123 # Input was empty list [] 1124 jd1 = np.array([], dtype=np.float64) 1125 jd2 = np.array([], dtype=np.float64) 1126 1127 else: 1128 jd1, jd2 = erfa.dtf2d(self.scale.upper().encode('ascii'), 1129 val1['year'], 1130 val1.get('month', 1), 1131 val1.get('day', 1), 1132 val1.get('hour', 0), 1133 val1.get('minute', 0), 1134 val1.get('second', 0)) 1135 1136 self.jd1, self.jd2 = day_frac(jd1, jd2) 1137 1138 @property 1139 def value(self): 1140 scale = self.scale.upper().encode('ascii') 1141 iys, ims, ids, ihmsfs = erfa.d2dtf(scale, 9, 1142 self.jd1, self.jd2_filled) 1143 1144 out = np.empty(self.jd1.shape, dtype=[('year', 'i4'), 1145 ('month', 'i4'), 1146 ('day', 'i4'), 1147 ('hour', 'i4'), 1148 ('minute', 'i4'), 1149 ('second', 'f8')]) 1150 out['year'] = iys 1151 out['month'] = ims 1152 out['day'] = ids 1153 out['hour'] = ihmsfs['h'] 1154 out['minute'] = ihmsfs['m'] 1155 out['second'] = ihmsfs['s'] + ihmsfs['f'] * 10**(-9) 1156 out = out.view(np.recarray) 1157 1158 return self.mask_if_needed(out) 1159 1160 1161class TimezoneInfo(datetime.tzinfo): 1162 """ 1163 Subclass of the `~datetime.tzinfo` object, used in the 1164 to_datetime method to specify timezones. 1165 1166 It may be safer in most cases to use a timezone database package like 1167 pytz rather than defining your own timezones - this class is mainly 1168 a workaround for users without pytz. 1169 """ 1170 @u.quantity_input(utc_offset=u.day, dst=u.day) 1171 def __init__(self, utc_offset=0 * u.day, dst=0 * u.day, tzname=None): 1172 """ 1173 Parameters 1174 ---------- 1175 utc_offset : `~astropy.units.Quantity`, optional 1176 Offset from UTC in days. Defaults to zero. 1177 dst : `~astropy.units.Quantity`, optional 1178 Daylight Savings Time offset in days. Defaults to zero 1179 (no daylight savings). 1180 tzname : str or None, optional 1181 Name of timezone 1182 1183 Examples 1184 -------- 1185 >>> from datetime import datetime 1186 >>> from astropy.time import TimezoneInfo # Specifies a timezone 1187 >>> import astropy.units as u 1188 >>> utc = TimezoneInfo() # Defaults to UTC 1189 >>> utc_plus_one_hour = TimezoneInfo(utc_offset=1*u.hour) # UTC+1 1190 >>> dt_aware = datetime(2000, 1, 1, 0, 0, 0, tzinfo=utc_plus_one_hour) 1191 >>> print(dt_aware) 1192 2000-01-01 00:00:00+01:00 1193 >>> print(dt_aware.astimezone(utc)) 1194 1999-12-31 23:00:00+00:00 1195 """ 1196 if utc_offset == 0 and dst == 0 and tzname is None: 1197 tzname = 'UTC' 1198 self._utcoffset = datetime.timedelta(utc_offset.to_value(u.day)) 1199 self._tzname = tzname 1200 self._dst = datetime.timedelta(dst.to_value(u.day)) 1201 1202 def utcoffset(self, dt): 1203 return self._utcoffset 1204 1205 def tzname(self, dt): 1206 return str(self._tzname) 1207 1208 def dst(self, dt): 1209 return self._dst 1210 1211 1212class TimeString(TimeUnique): 1213 """ 1214 Base class for string-like time representations. 1215 1216 This class assumes that anything following the last decimal point to the 1217 right is a fraction of a second. 1218 1219 **Fast C-based parser** 1220 1221 Time format classes can take advantage of a fast C-based parser if the times 1222 are represented as fixed-format strings with year, month, day-of-month, 1223 hour, minute, second, OR year, day-of-year, hour, minute, second. This can 1224 be a factor of 20 or more faster than the pure Python parser. 1225 1226 Fixed format means that the components always have the same number of 1227 characters. The Python parser will accept ``2001-9-2`` as a date, but the C 1228 parser would require ``2001-09-02``. 1229 1230 A subclass in this case must define a class attribute ``fast_parser_pars`` 1231 which is a `dict` with all of the keys below. An inherited attribute is not 1232 checked, only an attribute in the class ``__dict__``. 1233 1234 - ``delims`` (tuple of int): ASCII code for character at corresponding 1235 ``starts`` position (0 => no character) 1236 1237 - ``starts`` (tuple of int): position where component starts (including 1238 delimiter if present). Use -1 for the month component for format that use 1239 day of year. 1240 1241 - ``stops`` (tuple of int): position where component ends. Use -1 to 1242 continue to end of string, or for the month component for formats that use 1243 day of year. 1244 1245 - ``break_allowed`` (tuple of int): if true (1) then the time string can 1246 legally end just before the corresponding component (e.g. "2000-01-01" 1247 is a valid time but "2000-01-01 12" is not). 1248 1249 - ``has_day_of_year`` (int): 0 if dates have year, month, day; 1 if year, 1250 day-of-year 1251 """ 1252 1253 def __init_subclass__(cls, **kwargs): 1254 if 'fast_parser_pars' in cls.__dict__: 1255 fpp = cls.fast_parser_pars 1256 fpp = np.array(list(zip(map(chr, fpp['delims']), 1257 fpp['starts'], 1258 fpp['stops'], 1259 fpp['break_allowed'])), 1260 _parse_times.dt_pars) 1261 if cls.fast_parser_pars['has_day_of_year']: 1262 fpp['start'][1] = fpp['stop'][1] = -1 1263 cls._fast_parser = _parse_times.create_parser(fpp) 1264 1265 super().__init_subclass__(**kwargs) 1266 1267 def _check_val_type(self, val1, val2): 1268 if val1.dtype.kind not in ('S', 'U') and val1.size: 1269 raise TypeError(f'Input values for {self.name} class must be strings') 1270 if val2 is not None: 1271 raise ValueError( 1272 f'{self.name} objects do not accept a val2 but you provided {val2}') 1273 return val1, None 1274 1275 def parse_string(self, timestr, subfmts): 1276 """Read time from a single string, using a set of possible formats.""" 1277 # Datetime components required for conversion to JD by ERFA, along 1278 # with the default values. 1279 components = ('year', 'mon', 'mday', 'hour', 'min', 'sec') 1280 defaults = (None, 1, 1, 0, 0, 0) 1281 # Assume that anything following "." on the right side is a 1282 # floating fraction of a second. 1283 try: 1284 idot = timestr.rindex('.') 1285 except Exception: 1286 fracsec = 0.0 1287 else: 1288 timestr, fracsec = timestr[:idot], timestr[idot:] 1289 fracsec = float(fracsec) 1290 1291 for _, strptime_fmt_or_regex, _ in subfmts: 1292 if isinstance(strptime_fmt_or_regex, str): 1293 try: 1294 tm = time.strptime(timestr, strptime_fmt_or_regex) 1295 except ValueError: 1296 continue 1297 else: 1298 vals = [getattr(tm, 'tm_' + component) 1299 for component in components] 1300 1301 else: 1302 tm = re.match(strptime_fmt_or_regex, timestr) 1303 if tm is None: 1304 continue 1305 tm = tm.groupdict() 1306 vals = [int(tm.get(component, default)) for component, default 1307 in zip(components, defaults)] 1308 1309 # Add fractional seconds 1310 vals[-1] = vals[-1] + fracsec 1311 return vals 1312 else: 1313 raise ValueError(f'Time {timestr} does not match {self.name} format') 1314 1315 def set_jds(self, val1, val2): 1316 """Parse the time strings contained in val1 and set jd1, jd2""" 1317 # If specific input subformat is required then use the Python parser. 1318 # Also do this if Time format class does not define `use_fast_parser` or 1319 # if the fast parser is entirely disabled. Note that `use_fast_parser` 1320 # is ignored for format classes that don't have a fast parser. 1321 if (self.in_subfmt != '*' 1322 or '_fast_parser' not in self.__class__.__dict__ 1323 or conf.use_fast_parser == 'False'): 1324 jd1, jd2 = self.get_jds_python(val1, val2) 1325 else: 1326 try: 1327 jd1, jd2 = self.get_jds_fast(val1, val2) 1328 except Exception: 1329 # Fall through to the Python parser unless fast is forced. 1330 if conf.use_fast_parser == 'force': 1331 raise 1332 else: 1333 jd1, jd2 = self.get_jds_python(val1, val2) 1334 1335 self.jd1 = jd1 1336 self.jd2 = jd2 1337 1338 def get_jds_python(self, val1, val2): 1339 """Parse the time strings contained in val1 and get jd1, jd2""" 1340 # Select subformats based on current self.in_subfmt 1341 subfmts = self._select_subfmts(self.in_subfmt) 1342 # Be liberal in what we accept: convert bytes to ascii. 1343 # Here .item() is needed for arrays with entries of unequal length, 1344 # to strip trailing 0 bytes. 1345 to_string = (str if val1.dtype.kind == 'U' else 1346 lambda x: str(x.item(), encoding='ascii')) 1347 iterator = np.nditer([val1, None, None, None, None, None, None], 1348 flags=['zerosize_ok'], 1349 op_dtypes=[None] + 5 * [np.intc] + [np.double]) 1350 for val, iy, im, id, ihr, imin, dsec in iterator: 1351 val = to_string(val) 1352 iy[...], im[...], id[...], ihr[...], imin[...], dsec[...] = ( 1353 self.parse_string(val, subfmts)) 1354 1355 jd1, jd2 = erfa.dtf2d(self.scale.upper().encode('ascii'), 1356 *iterator.operands[1:]) 1357 jd1, jd2 = day_frac(jd1, jd2) 1358 1359 return jd1, jd2 1360 1361 def get_jds_fast(self, val1, val2): 1362 """Use fast C parser to parse time strings in val1 and get jd1, jd2""" 1363 # Handle bytes or str input and convert to uint8. We need to the 1364 # dtype _parse_times.dt_u1 instead of uint8, since otherwise it is 1365 # not possible to create a gufunc with structured dtype output. 1366 # See note about ufunc type resolver in pyerfa/erfa/ufunc.c.templ. 1367 if val1.dtype.kind == 'U': 1368 # Note: val1.astype('S') is *very* slow, so we check ourselves 1369 # that the input is pure ASCII. 1370 val1_uint32 = val1.view((np.uint32, val1.dtype.itemsize // 4)) 1371 if np.any(val1_uint32 > 127): 1372 raise ValueError('input is not pure ASCII') 1373 1374 # It might be possible to avoid making a copy via astype with 1375 # cleverness in parse_times.c but leave that for another day. 1376 chars = val1_uint32.astype(_parse_times.dt_u1) 1377 1378 else: 1379 chars = val1.view((_parse_times.dt_u1, val1.dtype.itemsize)) 1380 1381 # Call the fast parsing ufunc. 1382 time_struct = self._fast_parser(chars) 1383 jd1, jd2 = erfa.dtf2d(self.scale.upper().encode('ascii'), 1384 time_struct['year'], 1385 time_struct['month'], 1386 time_struct['day'], 1387 time_struct['hour'], 1388 time_struct['minute'], 1389 time_struct['second']) 1390 return day_frac(jd1, jd2) 1391 1392 def str_kwargs(self): 1393 """ 1394 Generator that yields a dict of values corresponding to the 1395 calendar date and time for the internal JD values. 1396 """ 1397 scale = self.scale.upper().encode('ascii'), 1398 iys, ims, ids, ihmsfs = erfa.d2dtf(scale, self.precision, 1399 self.jd1, self.jd2_filled) 1400 1401 # Get the str_fmt element of the first allowed output subformat 1402 _, _, str_fmt = self._select_subfmts(self.out_subfmt)[0] 1403 1404 yday = None 1405 has_yday = '{yday:' in str_fmt 1406 1407 ihrs = ihmsfs['h'] 1408 imins = ihmsfs['m'] 1409 isecs = ihmsfs['s'] 1410 ifracs = ihmsfs['f'] 1411 for iy, im, id, ihr, imin, isec, ifracsec in np.nditer( 1412 [iys, ims, ids, ihrs, imins, isecs, ifracs], 1413 flags=['zerosize_ok']): 1414 if has_yday: 1415 yday = datetime.datetime(iy, im, id).timetuple().tm_yday 1416 1417 yield {'year': int(iy), 'mon': int(im), 'day': int(id), 1418 'hour': int(ihr), 'min': int(imin), 'sec': int(isec), 1419 'fracsec': int(ifracsec), 'yday': yday} 1420 1421 def format_string(self, str_fmt, **kwargs): 1422 """Write time to a string using a given format. 1423 1424 By default, just interprets str_fmt as a format string, 1425 but subclasses can add to this. 1426 """ 1427 return str_fmt.format(**kwargs) 1428 1429 @property 1430 def value(self): 1431 # Select the first available subformat based on current 1432 # self.out_subfmt 1433 subfmts = self._select_subfmts(self.out_subfmt) 1434 _, _, str_fmt = subfmts[0] 1435 1436 # TODO: fix this ugly hack 1437 if self.precision > 0 and str_fmt.endswith('{sec:02d}'): 1438 str_fmt += '.{fracsec:0' + str(self.precision) + 'd}' 1439 1440 # Try to optimize this later. Can't pre-allocate because length of 1441 # output could change, e.g. year rolls from 999 to 1000. 1442 outs = [] 1443 for kwargs in self.str_kwargs(): 1444 outs.append(str(self.format_string(str_fmt, **kwargs))) 1445 1446 return np.array(outs).reshape(self.jd1.shape) 1447 1448 1449class TimeISO(TimeString): 1450 """ 1451 ISO 8601 compliant date-time format "YYYY-MM-DD HH:MM:SS.sss...". 1452 For example, 2000-01-01 00:00:00.000 is midnight on January 1, 2000. 1453 1454 The allowed subformats are: 1455 1456 - 'date_hms': date + hours, mins, secs (and optional fractional secs) 1457 - 'date_hm': date + hours, mins 1458 - 'date': date 1459 """ 1460 1461 name = 'iso' 1462 subfmts = (('date_hms', 1463 '%Y-%m-%d %H:%M:%S', 1464 # XXX To Do - use strftime for output ?? 1465 '{year:d}-{mon:02d}-{day:02d} {hour:02d}:{min:02d}:{sec:02d}'), 1466 ('date_hm', 1467 '%Y-%m-%d %H:%M', 1468 '{year:d}-{mon:02d}-{day:02d} {hour:02d}:{min:02d}'), 1469 ('date', 1470 '%Y-%m-%d', 1471 '{year:d}-{mon:02d}-{day:02d}')) 1472 1473 # Define positions and starting delimiter for year, month, day, hour, 1474 # minute, seconds components of an ISO time. This is used by the fast 1475 # C-parser parse_ymdhms_times() 1476 # 1477 # "2000-01-12 13:14:15.678" 1478 # 01234567890123456789012 1479 # yyyy-mm-dd hh:mm:ss.fff 1480 # Parsed as ('yyyy', '-mm', '-dd', ' hh', ':mm', ':ss', '.fff') 1481 fast_parser_pars = dict( 1482 delims=(0, ord('-'), ord('-'), ord(' '), ord(':'), ord(':'), ord('.')), 1483 starts=(0, 4, 7, 10, 13, 16, 19), 1484 stops=(3, 6, 9, 12, 15, 18, -1), 1485 # Break allowed *before* 1486 # y m d h m s f 1487 break_allowed=(0, 0, 0, 1, 0, 1, 1), 1488 has_day_of_year=0) 1489 1490 def parse_string(self, timestr, subfmts): 1491 # Handle trailing 'Z' for UTC time 1492 if timestr.endswith('Z'): 1493 if self.scale != 'utc': 1494 raise ValueError("Time input terminating in 'Z' must have " 1495 "scale='UTC'") 1496 timestr = timestr[:-1] 1497 return super().parse_string(timestr, subfmts) 1498 1499 1500class TimeISOT(TimeISO): 1501 """ 1502 ISO 8601 compliant date-time format "YYYY-MM-DDTHH:MM:SS.sss...". 1503 This is the same as TimeISO except for a "T" instead of space between 1504 the date and time. 1505 For example, 2000-01-01T00:00:00.000 is midnight on January 1, 2000. 1506 1507 The allowed subformats are: 1508 1509 - 'date_hms': date + hours, mins, secs (and optional fractional secs) 1510 - 'date_hm': date + hours, mins 1511 - 'date': date 1512 """ 1513 1514 name = 'isot' 1515 subfmts = (('date_hms', 1516 '%Y-%m-%dT%H:%M:%S', 1517 '{year:d}-{mon:02d}-{day:02d}T{hour:02d}:{min:02d}:{sec:02d}'), 1518 ('date_hm', 1519 '%Y-%m-%dT%H:%M', 1520 '{year:d}-{mon:02d}-{day:02d}T{hour:02d}:{min:02d}'), 1521 ('date', 1522 '%Y-%m-%d', 1523 '{year:d}-{mon:02d}-{day:02d}')) 1524 1525 # See TimeISO for explanation 1526 fast_parser_pars = dict( 1527 delims=(0, ord('-'), ord('-'), ord('T'), ord(':'), ord(':'), ord('.')), 1528 starts=(0, 4, 7, 10, 13, 16, 19), 1529 stops=(3, 6, 9, 12, 15, 18, -1), 1530 # Break allowed *before* 1531 # y m d h m s f 1532 break_allowed=(0, 0, 0, 1, 0, 1, 1), 1533 has_day_of_year=0) 1534 1535 1536class TimeYearDayTime(TimeISO): 1537 """ 1538 Year, day-of-year and time as "YYYY:DOY:HH:MM:SS.sss...". 1539 The day-of-year (DOY) goes from 001 to 365 (366 in leap years). 1540 For example, 2000:001:00:00:00.000 is midnight on January 1, 2000. 1541 1542 The allowed subformats are: 1543 1544 - 'date_hms': date + hours, mins, secs (and optional fractional secs) 1545 - 'date_hm': date + hours, mins 1546 - 'date': date 1547 """ 1548 1549 name = 'yday' 1550 subfmts = (('date_hms', 1551 '%Y:%j:%H:%M:%S', 1552 '{year:d}:{yday:03d}:{hour:02d}:{min:02d}:{sec:02d}'), 1553 ('date_hm', 1554 '%Y:%j:%H:%M', 1555 '{year:d}:{yday:03d}:{hour:02d}:{min:02d}'), 1556 ('date', 1557 '%Y:%j', 1558 '{year:d}:{yday:03d}')) 1559 1560 # Define positions and starting delimiter for year, month, day, hour, 1561 # minute, seconds components of an ISO time. This is used by the fast 1562 # C-parser parse_ymdhms_times() 1563 # 1564 # "2000:123:13:14:15.678" 1565 # 012345678901234567890 1566 # yyyy:ddd:hh:mm:ss.fff 1567 # Parsed as ('yyyy', ':ddd', ':hh', ':mm', ':ss', '.fff') 1568 # 1569 # delims: character at corresponding `starts` position (0 => no character) 1570 # starts: position where component starts (including delimiter if present) 1571 # stops: position where component ends (-1 => continue to end of string) 1572 1573 fast_parser_pars = dict( 1574 delims=(0, 0, ord(':'), ord(':'), ord(':'), ord(':'), ord('.')), 1575 starts=(0, -1, 4, 8, 11, 14, 17), 1576 stops=(3, -1, 7, 10, 13, 16, -1), 1577 # Break allowed before: 1578 # y m d h m s f 1579 break_allowed=(0, 0, 0, 1, 0, 1, 1), 1580 has_day_of_year=1) 1581 1582 1583class TimeDatetime64(TimeISOT): 1584 name = 'datetime64' 1585 1586 def _check_val_type(self, val1, val2): 1587 if not val1.dtype.kind == 'M': 1588 if val1.size > 0: 1589 raise TypeError('Input values for {} class must be ' 1590 'datetime64 objects'.format(self.name)) 1591 else: 1592 val1 = np.array([], 'datetime64[D]') 1593 if val2 is not None: 1594 raise ValueError( 1595 f'{self.name} objects do not accept a val2 but you provided {val2}') 1596 1597 return val1, None 1598 1599 def set_jds(self, val1, val2): 1600 # If there are any masked values in the ``val1`` datetime64 array 1601 # ('NaT') then stub them with a valid date so downstream parse_string 1602 # will work. The value under the mask is arbitrary but a "modern" date 1603 # is good. 1604 mask = np.isnat(val1) 1605 masked = np.any(mask) 1606 if masked: 1607 val1 = val1.copy() 1608 val1[mask] = '2000' 1609 1610 # Make sure M(onth) and Y(ear) dates will parse and convert to bytestring 1611 if val1.dtype.name in ['datetime64[M]', 'datetime64[Y]']: 1612 val1 = val1.astype('datetime64[D]') 1613 val1 = val1.astype('S') 1614 1615 # Standard ISO string parsing now 1616 super().set_jds(val1, val2) 1617 1618 # Finally apply mask if necessary 1619 if masked: 1620 self.jd2[mask] = np.nan 1621 1622 @property 1623 def value(self): 1624 precision = self.precision 1625 self.precision = 9 1626 ret = super().value 1627 self.precision = precision 1628 return ret.astype('datetime64') 1629 1630 1631class TimeFITS(TimeString): 1632 """ 1633 FITS format: "[±Y]YYYY-MM-DD[THH:MM:SS[.sss]]". 1634 1635 ISOT but can give signed five-digit year (mostly for negative years); 1636 1637 The allowed subformats are: 1638 1639 - 'date_hms': date + hours, mins, secs (and optional fractional secs) 1640 - 'date': date 1641 - 'longdate_hms': as 'date_hms', but with signed 5-digit year 1642 - 'longdate': as 'date', but with signed 5-digit year 1643 1644 See Rots et al., 2015, A&A 574:A36 (arXiv:1409.7583). 1645 """ 1646 name = 'fits' 1647 subfmts = ( 1648 ('date_hms', 1649 (r'(?P<year>\d{4})-(?P<mon>\d\d)-(?P<mday>\d\d)T' 1650 r'(?P<hour>\d\d):(?P<min>\d\d):(?P<sec>\d\d(\.\d*)?)'), 1651 '{year:04d}-{mon:02d}-{day:02d}T{hour:02d}:{min:02d}:{sec:02d}'), 1652 ('date', 1653 r'(?P<year>\d{4})-(?P<mon>\d\d)-(?P<mday>\d\d)', 1654 '{year:04d}-{mon:02d}-{day:02d}'), 1655 ('longdate_hms', 1656 (r'(?P<year>[+-]\d{5})-(?P<mon>\d\d)-(?P<mday>\d\d)T' 1657 r'(?P<hour>\d\d):(?P<min>\d\d):(?P<sec>\d\d(\.\d*)?)'), 1658 '{year:+06d}-{mon:02d}-{day:02d}T{hour:02d}:{min:02d}:{sec:02d}'), 1659 ('longdate', 1660 r'(?P<year>[+-]\d{5})-(?P<mon>\d\d)-(?P<mday>\d\d)', 1661 '{year:+06d}-{mon:02d}-{day:02d}')) 1662 # Add the regex that parses the scale and possible realization. 1663 # Support for this is deprecated. Read old style but no longer write 1664 # in this style. 1665 subfmts = tuple( 1666 (subfmt[0], 1667 subfmt[1] + r'(\((?P<scale>\w+)(\((?P<realization>\w+)\))?\))?', 1668 subfmt[2]) for subfmt in subfmts) 1669 1670 def parse_string(self, timestr, subfmts): 1671 """Read time and deprecated scale if present""" 1672 # Try parsing with any of the allowed sub-formats. 1673 for _, regex, _ in subfmts: 1674 tm = re.match(regex, timestr) 1675 if tm: 1676 break 1677 else: 1678 raise ValueError(f'Time {timestr} does not match {self.name} format') 1679 tm = tm.groupdict() 1680 # Scale and realization are deprecated and strings in this form 1681 # are no longer created. We issue a warning but still use the value. 1682 if tm['scale'] is not None: 1683 warnings.warn("FITS time strings should no longer have embedded time scale.", 1684 AstropyDeprecationWarning) 1685 # If a scale was given, translate from a possible deprecated 1686 # timescale identifier to the scale used by Time. 1687 fits_scale = tm['scale'].upper() 1688 scale = FITS_DEPRECATED_SCALES.get(fits_scale, fits_scale.lower()) 1689 if scale not in TIME_SCALES: 1690 raise ValueError("Scale {!r} is not in the allowed scales {}" 1691 .format(scale, sorted(TIME_SCALES))) 1692 # If no scale was given in the initialiser, set the scale to 1693 # that given in the string. Realization is ignored 1694 # and is only supported to allow old-style strings to be 1695 # parsed. 1696 if self._scale is None: 1697 self._scale = scale 1698 if scale != self.scale: 1699 raise ValueError("Input strings for {} class must all " 1700 "have consistent time scales." 1701 .format(self.name)) 1702 return [int(tm['year']), int(tm['mon']), int(tm['mday']), 1703 int(tm.get('hour', 0)), int(tm.get('min', 0)), 1704 float(tm.get('sec', 0.))] 1705 1706 @property 1707 def value(self): 1708 """Convert times to strings, using signed 5 digit if necessary.""" 1709 if 'long' not in self.out_subfmt: 1710 # If we have times before year 0 or after year 9999, we can 1711 # output only in a "long" format, using signed 5-digit years. 1712 jd = self.jd1 + self.jd2 1713 if jd.size and (jd.min() < 1721425.5 or jd.max() >= 5373484.5): 1714 self.out_subfmt = 'long' + self.out_subfmt 1715 return super().value 1716 1717 1718class TimeEpochDate(TimeNumeric): 1719 """ 1720 Base class for support floating point Besselian and Julian epoch dates 1721 """ 1722 _default_scale = 'tt' # As of astropy 3.2, this is no longer 'utc'. 1723 1724 def set_jds(self, val1, val2): 1725 self._check_scale(self._scale) # validate scale. 1726 epoch_to_jd = getattr(erfa, self.epoch_to_jd) 1727 jd1, jd2 = epoch_to_jd(val1 + val2) 1728 self.jd1, self.jd2 = day_frac(jd1, jd2) 1729 1730 def to_value(self, **kwargs): 1731 jd_to_epoch = getattr(erfa, self.jd_to_epoch) 1732 value = jd_to_epoch(self.jd1, self.jd2) 1733 return super().to_value(jd1=value, jd2=np.float64(0.0), **kwargs) 1734 1735 value = property(to_value) 1736 1737 1738class TimeBesselianEpoch(TimeEpochDate): 1739 """Besselian Epoch year as floating point value(s) like 1950.0""" 1740 name = 'byear' 1741 epoch_to_jd = 'epb2jd' 1742 jd_to_epoch = 'epb' 1743 1744 def _check_val_type(self, val1, val2): 1745 """Input value validation, typically overridden by derived classes""" 1746 if hasattr(val1, 'to') and hasattr(val1, 'unit'): 1747 raise ValueError("Cannot use Quantities for 'byear' format, " 1748 "as the interpretation would be ambiguous. " 1749 "Use float with Besselian year instead. ") 1750 # FIXME: is val2 really okay here? 1751 return super()._check_val_type(val1, val2) 1752 1753 1754class TimeJulianEpoch(TimeEpochDate): 1755 """Julian Epoch year as floating point value(s) like 2000.0""" 1756 name = 'jyear' 1757 unit = erfa.DJY # 365.25, the Julian year, for conversion to quantities 1758 epoch_to_jd = 'epj2jd' 1759 jd_to_epoch = 'epj' 1760 1761 1762class TimeEpochDateString(TimeString): 1763 """ 1764 Base class to support string Besselian and Julian epoch dates 1765 such as 'B1950.0' or 'J2000.0' respectively. 1766 """ 1767 _default_scale = 'tt' # As of astropy 3.2, this is no longer 'utc'. 1768 1769 def set_jds(self, val1, val2): 1770 epoch_prefix = self.epoch_prefix 1771 # Be liberal in what we accept: convert bytes to ascii. 1772 to_string = (str if val1.dtype.kind == 'U' else 1773 lambda x: str(x.item(), encoding='ascii')) 1774 iterator = np.nditer([val1, None], op_dtypes=[val1.dtype, np.double], 1775 flags=['zerosize_ok']) 1776 for val, years in iterator: 1777 try: 1778 time_str = to_string(val) 1779 epoch_type, year_str = time_str[0], time_str[1:] 1780 year = float(year_str) 1781 if epoch_type.upper() != epoch_prefix: 1782 raise ValueError 1783 except (IndexError, ValueError, UnicodeEncodeError): 1784 raise ValueError(f'Time {val} does not match {self.name} format') 1785 else: 1786 years[...] = year 1787 1788 self._check_scale(self._scale) # validate scale. 1789 epoch_to_jd = getattr(erfa, self.epoch_to_jd) 1790 jd1, jd2 = epoch_to_jd(iterator.operands[-1]) 1791 self.jd1, self.jd2 = day_frac(jd1, jd2) 1792 1793 @property 1794 def value(self): 1795 jd_to_epoch = getattr(erfa, self.jd_to_epoch) 1796 years = jd_to_epoch(self.jd1, self.jd2) 1797 # Use old-style format since it is a factor of 2 faster 1798 str_fmt = self.epoch_prefix + '%.' + str(self.precision) + 'f' 1799 outs = [str_fmt % year for year in years.flat] 1800 return np.array(outs).reshape(self.jd1.shape) 1801 1802 1803class TimeBesselianEpochString(TimeEpochDateString): 1804 """Besselian Epoch year as string value(s) like 'B1950.0'""" 1805 name = 'byear_str' 1806 epoch_to_jd = 'epb2jd' 1807 jd_to_epoch = 'epb' 1808 epoch_prefix = 'B' 1809 1810 1811class TimeJulianEpochString(TimeEpochDateString): 1812 """Julian Epoch year as string value(s) like 'J2000.0'""" 1813 name = 'jyear_str' 1814 epoch_to_jd = 'epj2jd' 1815 jd_to_epoch = 'epj' 1816 epoch_prefix = 'J' 1817 1818 1819class TimeDeltaFormat(TimeFormat): 1820 """Base class for time delta representations""" 1821 1822 _registry = TIME_DELTA_FORMATS 1823 1824 def _check_scale(self, scale): 1825 """ 1826 Check that the scale is in the allowed list of scales, or is `None` 1827 """ 1828 if scale is not None and scale not in TIME_DELTA_SCALES: 1829 raise ScaleValueError("Scale value '{}' not in " 1830 "allowed values {}" 1831 .format(scale, TIME_DELTA_SCALES)) 1832 1833 return scale 1834 1835 1836class TimeDeltaNumeric(TimeDeltaFormat, TimeNumeric): 1837 1838 def set_jds(self, val1, val2): 1839 self._check_scale(self._scale) # Validate scale. 1840 self.jd1, self.jd2 = day_frac(val1, val2, divisor=1. / self.unit) 1841 1842 def to_value(self, **kwargs): 1843 # Note that 1/unit is always exactly representable, so the 1844 # following multiplications are exact. 1845 factor = 1. / self.unit 1846 jd1 = self.jd1 * factor 1847 jd2 = self.jd2 * factor 1848 return super().to_value(jd1=jd1, jd2=jd2, **kwargs) 1849 1850 value = property(to_value) 1851 1852 1853class TimeDeltaSec(TimeDeltaNumeric): 1854 """Time delta in SI seconds""" 1855 name = 'sec' 1856 unit = 1. / erfa.DAYSEC # for quantity input 1857 1858 1859class TimeDeltaJD(TimeDeltaNumeric): 1860 """Time delta in Julian days (86400 SI seconds)""" 1861 name = 'jd' 1862 unit = 1. 1863 1864 1865class TimeDeltaDatetime(TimeDeltaFormat, TimeUnique): 1866 """Time delta in datetime.timedelta""" 1867 name = 'datetime' 1868 1869 def _check_val_type(self, val1, val2): 1870 if not all(isinstance(val, datetime.timedelta) for val in val1.flat): 1871 raise TypeError('Input values for {} class must be ' 1872 'datetime.timedelta objects'.format(self.name)) 1873 if val2 is not None: 1874 raise ValueError( 1875 f'{self.name} objects do not accept a val2 but you provided {val2}') 1876 return val1, None 1877 1878 def set_jds(self, val1, val2): 1879 self._check_scale(self._scale) # Validate scale. 1880 iterator = np.nditer([val1, None, None], 1881 flags=['refs_ok', 'zerosize_ok'], 1882 op_dtypes=[None, np.double, np.double]) 1883 1884 day = datetime.timedelta(days=1) 1885 for val, jd1, jd2 in iterator: 1886 jd1[...], other = divmod(val.item(), day) 1887 jd2[...] = other / day 1888 1889 self.jd1, self.jd2 = day_frac(iterator.operands[-2], 1890 iterator.operands[-1]) 1891 1892 @property 1893 def value(self): 1894 iterator = np.nditer([self.jd1, self.jd2, None], 1895 flags=['refs_ok', 'zerosize_ok'], 1896 op_dtypes=[None, None, object]) 1897 1898 for jd1, jd2, out in iterator: 1899 jd1_, jd2_ = day_frac(jd1, jd2) 1900 out[...] = datetime.timedelta(days=jd1_, 1901 microseconds=jd2_ * 86400 * 1e6) 1902 1903 return self.mask_if_needed(iterator.operands[-1]) 1904 1905 1906def _validate_jd_for_storage(jd): 1907 if isinstance(jd, (float, int)): 1908 return np.array(jd, dtype=np.float_) 1909 if (isinstance(jd, np.generic) 1910 and (jd.dtype.kind == 'f' and jd.dtype.itemsize <= 8 1911 or jd.dtype.kind in 'iu')): 1912 return np.array(jd, dtype=np.float_) 1913 elif (isinstance(jd, np.ndarray) 1914 and jd.dtype.kind == 'f' 1915 and jd.dtype.itemsize == 8): 1916 return jd 1917 else: 1918 raise TypeError( 1919 f"JD values must be arrays (possibly zero-dimensional) " 1920 f"of floats but we got {jd!r} of type {type(jd)}") 1921 1922 1923def _broadcast_writeable(jd1, jd2): 1924 if jd1.shape == jd2.shape: 1925 return jd1, jd2 1926 # When using broadcast_arrays, *both* are flagged with 1927 # warn-on-write, even the one that wasn't modified, and 1928 # require "C" only clears the flag if it actually copied 1929 # anything. 1930 shape = np.broadcast(jd1, jd2).shape 1931 if jd1.shape == shape: 1932 s_jd1 = jd1 1933 else: 1934 s_jd1 = np.require(np.broadcast_to(jd1, shape), 1935 requirements=["C", "W"]) 1936 if jd2.shape == shape: 1937 s_jd2 = jd2 1938 else: 1939 s_jd2 = np.require(np.broadcast_to(jd2, shape), 1940 requirements=["C", "W"]) 1941 return s_jd1, s_jd2 1942 1943 1944# Import symbols from core.py that are used in this module. This succeeds 1945# because __init__.py imports format.py just before core.py. 1946from .core import Time, TIME_SCALES, TIME_DELTA_SCALES, ScaleValueError # noqa 1947