"""Various conveniences. Higher-level libraries like Skyfield that use this one usually have their own date and time handling. But for folks using this library by itself, native Python datetime handling could be convenient. """ import datetime as dt import sgp4 from math import pi from .functions import days2mdhms, jday class _UTC(dt.tzinfo): 'UTC' zero = dt.timedelta(0) def __repr__(self): return 'UTC' def dst(self, datetime): return self.zero def tzname(self, datetime): return 'UTC' def utcoffset(self, datetime): return self.zero UTC = _UTC() def jday_datetime(datetime): """Return two floats that, when added, produce the specified Julian date. The first float returned gives the date, while the second float provides an additional offset for the particular hour, minute, and second of that date. Because the second float is much smaller in magnitude it can, unlike the first float, be accurate down to very small fractions of a second. >>> jd, fr = jday(2020, 2, 11, 13, 57, 0) >>> jd 2458890.5 >>> fr 0.58125 Note that the first float, which gives the moment of midnight that commences the given calendar date, always carries the fraction ``.5`` because Julian dates begin and end at noon. This made Julian dates more convenient for astronomers in Europe, by making the whole night belong to a single Julian date. The input is a native `datetime` object. Timezone of the input is converted internally to UTC. """ u = datetime.astimezone(UTC) year = u.year mon = u.month day = u.day hr = u.hour minute = u.minute sec = u.second + u.microsecond * 1e-6 return jday(year, mon, day, hr, minute, sec) def sat_epoch_datetime(sat): """Return the epoch of the given satellite as a Python datetime.""" year = sat.epochyr year += 1900 + (year < 57) * 100 days = sat.epochdays month, day, hour, minute, second = days2mdhms(year, days) if month == 12 and day > 31: # for that time the ISS epoch was "Dec 32" year += 1 month = 1 day -= 31 second, fraction = divmod(second, 1.0) second = int(second) micro = int(fraction * 1e6) return dt.datetime(year, month, day, hour, minute, second, micro, UTC) _ATTRIBUTES = [] _ATTR_MAXES = {} _MAX_VALUES = {'2pi': 2*pi, 'pi': pi} def _load_attributes(): for line in sgp4.__doc__.splitlines(): if line.endswith('*'): title = line.strip('*') _ATTRIBUTES.append(title) elif line.startswith('| ``'): pieces = line.split('``') name = pieces[1] _ATTRIBUTES.append(name) i = 2 while pieces[i] == ', ': another_name = pieces[i+1] _ATTRIBUTES.append(another_name) i += 2 if '<' in line: _ATTR_MAXES[name] = '2pi' if ('2pi' in line) else 'pi' def check_satrec(sat): """Check whether satellite orbital elements are within range.""" if not _ATTRIBUTES: _load_attributes() e = [] for name, max_name in sorted(_ATTR_MAXES.items()): value = getattr(sat, name) if 0.0 <= value < _MAX_VALUES[max_name]: continue e.append(' {0} = {1:f} is outside the range 0 <= {0} < {2}\n' .format(name, value, max_name)) if e: raise ValueError('satellite parameters out of range:\n' + '\n'.join(e)) def dump_satrec(sat, sat2=None): """Yield lines that list the attributes of one or two satellites.""" if not _ATTRIBUTES: _load_attributes() for item in _ATTRIBUTES: if item[0].isupper(): title = item yield '\n' yield '# -------- {0} --------\n'.format(title) else: name = item value = getattr(sat, item, '(not set)') line = '{0} = {1!r}\n'.format(item, value) if sat2 is not None: value2 = getattr(sat2, name, '(not set)') verdict = '==' if (value == value2) else '!=' line = '{0:39} {1} {2!r}\n'.format(line[:-1], verdict, value2) yield line