Source code for mpcutilities.ele220

#!/usr/bin/env python3
"""

--------------------------------------------------------------

Nov 2018

- Payne

Originally written by Sonia Keys

Parsing and formatting for four orbit formats:

* the 220 column format for minor planets
* the 90 column format for minor planets
* the 220 column format for natural satellites of outer planets
* the 255 column format for comets

Parsing is provided with classes Ele220, Ele90, EleSat, EleComet.  

Formatting is provided class EleFields, but currently only implemented for the 220 and 90
column minor planet formats, not for the natural satellite or comet formats.

--------------------------------------------------------------

"""

import datetime
import math
import novas.compat as novas

__all__ = ['Ele220', 'Ele90', 'EleSat', 'EleComet']

k = 0.01720209895


_ps = {
    'd': 'DE200',
    'f': 'DE245',
    'h': 'DE403',
    'j': 'DE405'
}


class _Ele(str):
    """
        
    Base class for 220 and 90 character element lines.

    Parameters
    ----------
    str : string
        The 220 or character element line.  A trailing newline is okay.
        
    """

    # slice objects hold column bounds for the raw fields within the 220
    # character line.  Primarily for internal use, not as part of the API.
    _timePeri = slice(12, 24)
    _argPeri = slice(24, 34)
    _node = slice(34, 44)
    _inc = slice(44, 54)
    _periDist = slice(54, 64)
    _ecc = slice(64, 74)

    def timePeri(self):
        """
        Returns
        -------
        float
            Date of perihelion passage/JDT
        """
        return JDFromPacked(self[_Ele._timePeri])

    def argPeri(self):
        """
        Returns
        -------
        float
            Argument of perihelion (deg/J2000.0)
        """
        return float7(self[_Ele._argPeri])

    def node(self):
        """
        Returns
        -------
        float
            Longitude of ascending node (deg/J2000.0)
        """
        return float7(self[_Ele._node])

    def inc(self):
        """
        Returns
        -------
        float
            Inclination of orbit (deg/J2000.0)
        """
        return float7(self[_Ele._inc])

    def periDist(self):
        """
        Returns
        -------
        float
            Perihelion distance (AU)
        """
        return float9p(self[_Ele._periDist])

    def ecc(self):
        """
        Returns
        -------
        float
            Eccentricity
        """
        return float9(self[_Ele._ecc])


[docs]class Ele220(_Ele): """ Holds a 220 character element line. Class Ele220 instantiates on a 220 character line, then methods interpret individual field values. A design assumption is that many applications will not need all fields but only some subset. Instantiation therefore does not do any parsing. Individual methods interpret and return single field values. Thus depending on the application, Ele220 may not be best for orbit represention but only for parsing. For example you might define an orbit class in some way suitable for computation, then instantiate an Ele220 on a line from an orbit file, then use the methods of the Ele220 object in constrution of your computation-suitable orbit object. This way only the fields you need are parsed; they are parsed once and then the Ele220 object is no longer needed. Note the methods ma1, argPeri1, node1, inc1, ecc3, and a3 have little computational use. They interpret fields appearing in the 220 column format as "readable" values with reduced precision. Full precision values are either available as other methods or can be computed from other methods. Parameters ---------- str : string 220 character element line. Extra length such as a trailing newline is okay. """ _desig = slice(0, 7) _g = slice(7, 12) # 12:74 are standard elements _pertEpoch = slice(75, 80) _designation = slice(80, 90) _h = slice(91, 96) _maEpoch = slice(97, 102) _ma1 = slice(103, 108) _argPeri1 = slice(109, 114) _node1 = slice(115, 120) _inc1 = slice(121, 126) _ecc3 = slice(127, 132) _a3 = slice(133, 140) _numOpp = slice(141, 144) _arc = slice(144, 150) _numObs = slice(150, 156) _uNote = 157 # overloaded column _comp = slice(159, 168) _ref = slice(169, 178) _first = slice(179, 184) _last = slice(185, 190) _rms = slice(191, 196) _pertScheme = 197 _pertCoarse = slice(199, 202) _pert = slice(203, 207) # there can be some capital letters after _pert. no method. _numScore = slice(212, 214) _curOppScore = 214
[docs] def desig(self): """ Returns ------- string a short designation, no longer than 7 characters, whitespace trimmed. This may be a temporary or packed designation, either permanent or provisional. See also -------- designation : Unpacked designation """ return self[Ele220._desig].strip()
[docs] def g(self): """ Returns ------- float Slope parameter, G """ return float(self[Ele220._g])
[docs] def pertEpoch(self): """ Returns ------- None or float float is epoch jd """ pe = self[Ele220._pertEpoch] if pe.isspace(): return None return JDFromPacked(pe)
[docs] def designation(self): """ Returns ------- string a non-packed designation, up to 10 characters, whitespace trimmed. See also -------- desig : Packed designation """ return self[Ele220._designation].strip()
[docs] def h(self): """ Returns ------- float Absolute magnitude, H """ return float(self[Ele220._h])
[docs] def maEpoch(self): """ Returns ------- float Epoch JD """ return JDFromPacked(self[Ele220._maEpoch])
[docs] def ma1(self): """ Returns ------- float Mean anomaly in degrees, precision limited to one decimal place. """ return float(self[Ele220._ma1])
[docs] def argPeri1(self): """ Returns ------- float Argument of perihelion in degrees, precision limited to one decimal place. """ return float(self[Ele220._argPeri1])
[docs] def node1(self): """ Returns ------- float Longitude of ascending node in degrees, precision limited to one decimal place. """ return float(self[Ele220._node1])
[docs] def inc1(self): """ Returns ------- float Inclination in degrees, precision limited to one decimal place. """ return float(self[Ele220._inc1])
[docs] def ecc3(self): """ Returns ------- float Eccentricity, precision limited to three decimal places. """ return float(self[Ele220._ecc3])
[docs] def a3(self): """ Returns ------- float Semi-major axis in AU, precision limited to three decimal places. """ return float(self[Ele220._a3])
[docs] def numOpp(self): """ Returns ------- int Number of oppositions included in solution """ return int(self[Ele220._numOpp])
[docs] def arc(self): """ Returns ------- int Arc length of observations included in solution (/days) """ return int(self[Ele220._arc])
[docs] def numObs(self): """ Returns ------- int Number of observations included in solution """ return int(self[Ele220._numObs])
[docs] def u(self): """ Returns ------- int U value (a number from 0 to 9) if present. 10 if not present. (10 is not a valid U value.) """ try: return int(self[Ele220._uNote]) except ValueError: return 10
[docs] def uNote(self): """ Returns ------- string single character note from the U field, if one exists, '' otherwise """ n = self[Ele220._uNote] if n == ' ' or n.isdigit(): return '' return n
[docs] def comp(self): """ Returns ------- string Computer name, up to 9 characters, whitespace trimmed """ return self[Ele220._comp].strip()
[docs] def ref(self): """ Returns ------- string Reference, up to 9 characters, whitespace trimmed """ return self[Ele220._ref].strip()
[docs] def first(self): """ Returns ------- float Date of first included observation as JD """ return JDFromPacked(self[Ele220._first])
[docs] def last(self): """ Returns ------- float Date of last included observation as JD """ return JDFromPacked(self[Ele220._last])
[docs] def rms(self): """ Returns ------- float r.m.s. fit of included obserations (/") """ return float(self[Ele220._rms])
[docs] def pertScheme(self): """ Returns ------- string a string such as 'DE200', 'DE403', 'DE405', or '' """ if self[Ele220._pertEpoch.start] == ' ': return '' return _ps.get(self[Ele220._pertScheme], '')
[docs] def pertCoarse(self): """ Returns ------- string a string such as 'M-v' or '' """ if self[Ele220._pertEpoch.start] == ' ': return '' return self[Ele220._pertCoarse].strip()
[docs] def perturbers(self): """ Returns ------- string 4 character perturber code if present, '' otherwise """ if self[Ele220._pertEpoch.start] == ' ': return '' return self[Ele220._pert]
[docs] def numScore(self): """ Returns ------- int a number from 0 to 219 if present. -1 if not present. """ s = self[Ele220._numScore] if s.isspace(): return -1 n = int(s[1]) return n if s[0] == ' ' else b62(s[0]) * 10 + n
[docs] def curOppScore(self): """ Returns ------- int a number from 0 to 9 if present. -1 if not present. """ s = self[Ele220._curOppScore] return -1 if s == ' ' else int(s)
[docs]class Ele90(_Ele): """holds a 90 character element line. Parameters ---------- str : string The 90 character element line. A trailing newline is okay. See also -------- Ele220 """ _desig = slice(0, 7) _h = slice(7, 12) # 12:74 are standard elements _first = slice(80, 85) _last = slice(85, 90)
[docs] def desig(self): """ Returns ------- string a short designation, no longer than 7 characters, whitespace trimmed. This may be a temporary or packed designation, either permanent or provisional. See also -------- designation : Unpacked designation """ return self[Ele90._desig].strip()
[docs] def h(self): """ Returns ------- float Absolute magnitude, H, if field is filled None if field is blank """ try: return float(self[Ele90._h]) except ValueError: if not self[Ele90._h].isspace(): raise
[docs] def first(self): """ Returns ------- float Date of first included observation as JD """ return JDFromPacked(self[Ele90._first])
[docs] def last(self): """ Returns ------- float Date of last included observation as JD """ return JDFromPacked(self[Ele90._last])
[docs]class EleSat(_Ele): """ Holds a 220 character element line for a natural satellite Class EleSat instantiates on a 220 character line, then methods interpret individual field values. A design assumption is that many applications will not need all fields but only some subset. Instantiation therefore does not do any parsing. Individual methods interpret and return single field values. Parameters ---------- str : string 220 character element line. Extra length such as a trailing newline is okay. """ _permDesig = slice(0, 4) # [4] is always 'S' _tempDesig = slice(5, 12) # 12:74 are standard elements _pertEpoch = slice(75, 80) _designation = slice(80, 91) _h = slice(91, 96) _maEpoch = slice(97, 102) _ma1 = slice(103, 108) _argPeri1 = slice(109, 114) _node1 = slice(115, 120) _inc1 = slice(121, 126) _ecc3 = slice(127, 132) _a3 = slice(133, 140) _arc = slice(144, 150) _numObs = slice(150, 156) _comp = slice(159, 168) _ref = slice(169, 178) _first = slice(179, 184) _last = slice(185, 190) _rms = slice(191, 196) _pertScheme = 197 _pertCoarse = slice(199, 202) _pert = slice(203, 207) _numScore = slice(212, 214) _curOppScore = 214
[docs] def permDesig(self): """ Returns ------- string permanent designation, 4 characters if present, '' if not present. """ return self[EleSat._permDesig].strip()
[docs] def tempDesig(self): """ Returns ------- string temporary designation, 7 characters if present, '' if not present. """ return self[EleSat._tempDesig].strip()
[docs] def pertEpoch(self): """ Returns ------- float epoch jd """ return JDFromPacked(self[EleSat._pertEpoch])
[docs] def designation(self): """ Returns ------- string unpacked designation. """ return self[EleSat._designation].strip()
[docs] def h(self): """ Returns ------- float Absolute magnitude, H """ return float(self[EleSat._h])
[docs] def maEpoch(self): """ Returns ------- float Epoch JD """ return JDFromPacked(self[EleSat._maEpoch])
[docs] def ma1(self): """ Returns ------- float Mean anomaly in degrees, precision limited to one decimal place. """ return float(self[EleSat._ma1])
[docs] def argPeri1(self): """ Returns ------- float Argument of perihelion in degrees, precision limited to one decimal place. """ return float(self[EleSat._argPeri1])
[docs] def node1(self): """ Returns ------- float Longitude of ascending node in degrees, precision limited to one decimal place. """ return float(self[EleSat._node1])
[docs] def inc1(self): """ Returns ------- float Inclination in degrees, precision limited to one decimal place. """ return float(self[EleSat._inc1])
[docs] def ecc3(self): """ Returns ------- float Eccentricity, precision limited to three decimal places. """ return float(self[EleSat._ecc3])
[docs] def a3(self): """ Returns ------- float Semi-major axis in AU, precision limited to three decimal places. """ return float(self[EleSat._a3])
[docs] def arc(self): """ Returns ------- int Arc length of observations included in solution """ return int(self[EleSat._arc])
[docs] def numObs(self): """ Returns ------- int Number of observations included in solution """ return int(self[EleSat._numObs])
[docs] def comp(self): """ Returns ------- string Computer name, up to 9 characters, whitespace trimmed """ return self[EleSat._comp].strip()
[docs] def ref(self): """ Returns ------- string Reference, up to 9 characters, whitespace trimmed """ return self[EleSat._ref].strip()
[docs] def first(self): """ Returns ------- float Date of first included observation as JD """ return JDFromPacked(self[EleSat._first])
[docs] def last(self): """ Returns ------- float Date of last included observation as JD """ return JDFromPacked(self[EleSat._last])
[docs] def rms(self): """ Returns ------- float r.m.s. fit of included obserations """ return float(self[EleSat._rms])
[docs] def pertScheme(self): """ Returns ------- string a string such as 'DE200', 'DE403', 'DE405', or '' """ return _ps.get(self[EleSat._pertScheme], '')
[docs] def pertCoarse(self): """ Returns ------- string a string such as 'M-v' or '' """ return self[EleSat._pertCoarse].strip()
[docs] def perturbers(self): """ Returns ------- string 4 character perturber code if present, '' otherwise """ return self[EleSat._pert]
[docs] def numScore(self): """ Returns ------- int a number from 0 to 219 if present. -1 if not present. """ s = self[EleSat._numScore] if s.isspace(): return -1 n = int(s[1]) return n if s[0] == ' ' else b62(s[0]) * 10 + n
[docs] def curOppScore(self): """ Returns ------- int a number from 0 to 9 if present. -1 if not present. """ s = self[EleSat._curOppScore] return -1 if s == ' ' else int(s)
[docs]class EleComet(_Ele): """holds a 256 character comet element line. Parameters ---------- str : string The 256 character element line. A trailing newline is okay. """ _permDesig = slice(0, 5) # includes orbit type at [4] _tempDesig = slice(4, 12) # includes orbit type at [4] _tempDes1 = 5 # first position after the orbit type _frag = 11 _frag2 = slice(10, 12) # 12:74 are standard elements # [74] is usually blank but sometimes has an 'a', or an 'S'. # there is no method for this. _pertEpoch = slice(75, 80) _name = slice(80, 109) _comp = slice(109, 120) # there can be a leading @ _pubYear = slice(121, 125) _pub = slice(125, 129) # unknown fields here. looks like float, int, float, int, and a # date formatted as a string. _timeScale = 163 _pubVol = slice(165, 174) _numObs = slice(176, 181) # first char can be A or N, last can be + _nonGravStar = 181 _first = slice(183, 188) _last = slice(189, 194) _pertScheme = 195 # there can be a capital letter in 196. no method. _pertCoarse = slice(197, 200) _pert = slice(201, 205) # [206] typically has a digit in it. # 208, 209, and 210 are note fields, no methods for these, except # that a ! in 210 obviously means that fields in [183:210] cannot be # interpreted by _first, _last, _pertScheme, _pertCoarse, _pert. # if 210 contains a !, [183:210] has unknown meaning. _notes = slice(208, 210) # a '!' sometimes appears in [219] # [222:251] hold non-grav parameters, at least if [_nonGravStar] is '*'. # Interpretation of these columns in non-* cases is unknown. _ng_style = 221 _ng_a1 = slice(222, 232) _ng_a2 = slice(232, 243) _ng_a3 = slice(243, 251) _rms = slice(251, 255)
[docs] def permDesig(self): """ Returns ------- string permanent designation, 5 characters if present, '' if not. """ return self[EleComet._permDesig] if self[0] > ' ' else ''
[docs] def tempDesig(self): """ Returns ------- string temporary designation, including leading orbit type character, 8 characters altogether if present, '' if not present. """ if self[EleComet._tempDes1] > ' ': return self[EleComet._tempDesig] return ''
[docs] def frag(self): """ Returns ------- string 1 or 2 character fragment identifer if present. '' if not present. """ # _frag is the last position of _tempDesig. if there's no lowercase # letter there, there's no fragment identifier. if self[EleComet._frag] < 'a': return '' # non-space after the orbit type character at the start of tempDesig # means the tempDesig is present and the fragment can only be the # single character. if self[EleComet._tempDesig.start + 1] > ' ': return self[EleComet._frag] # else, with no temp des, the field is free to hold two character # fragment designations. if self[EleComet._frag2.start] == ' ': return self[EleComet._frag] return self[EleComet._frag2]
[docs] def pertEpoch(self): """ Returns ------- None or float float is epoch jd """ pe = self[EleComet._pertEpoch] if pe.isspace(): return None return JDFromPacked(pe)
[docs] def name(self): """ Returns ------- string Comet name """ return self[EleComet._name].strip()
[docs] def comp(self): """ Returns ------- string Orbit computer name. The first character can be '@', which means it can be expanded with an external list, comet_computers.txt. """ return self[EleComet._comp].strip()
[docs] def pubYear(self): """ Returns ------- string """ return self[EleComet._pubYear].strip()
[docs] def pub(self): """ Returns ------- string """ return self[EleComet._pub].strip()
[docs] def timeScale(self): """ Returns ------- string Either "TT/TDT" or "UTC". """ if self[EleComet._timePeri.start] < 'J': return "TT/TDT" if self[EleComet._timeScale] == 'T' else "UTC" return "UTC" if self[EleComet._timeScale] == 'U' else "TT/TDT"
[docs] def pubVol(self): """ Returns ------- string """ return self[EleComet._pubVol].strip()
[docs] def numObs(self): """ Returns ------- int Number of observations used in orbit solution. """ n = self[EleComet._numObs] if n[0] in "AN": return int(n[1:]) if n[4] == '+': return int(n[:4]) return int(n)
[docs] def numObsAN(self): """ Returns ------- string 'A' or 'N' where one of these characters appears before numObs. """ n0 = self[EleComet._numObs.start] return n0 if n0 in "AN" else ""
[docs] def numObsPlus(self): """ Returns ------- bool True if a '+' appears following numObs. """ return self[EleComet._numObs.stop - 1] == '+'
[docs] def nonGrav(self): """ Returns ------- None or tuple None if no non-grav parameters present, otherwise: 3-tuple of (style, a1, a2), or 4-tuple of (style, a1, a2, a3) where style is a single character and a1-a3 are floats """ if self[EleComet._nonGravStar] != '*': return None st = self[EleComet._ng_style] a1 = float(self[EleComet._ng_a1]) a2 = float(self[EleComet._ng_a2]) s3 = self[EleComet._ng_a3].strip() if s3 == '': return st, a1, a2 return st, a1, a2, float(s3)
[docs] def first(self): """ Returns ------- float or None Date of first included observation as JD if valid, None otherwise """ if self[210] == '!': return None return JDFromPacked(self[EleComet._first].strip())
[docs] def last(self): """ Returns ------- float or None Date of last included observation as JD if valid, None otherwise """ if self[210] == '!': return None return JDFromPacked(self[EleComet._last].strip())
[docs] def pertScheme(self): """ Returns ------- string a string such as 'DE200', 'DE403', 'DE405', or '' """ if self[EleComet._pertEpoch.start] == ' ' or self[210] == '!': return '' return _ps.get(self[EleComet._pertScheme], '')
[docs] def pertCoarse(self): """ Returns ------- string a string such as 'M-v' or '' """ if self[EleComet._pertEpoch.start] == ' ' or self[210] == '!': return '' return self[EleComet._pertCoarse].strip()
[docs] def perturbers(self): """ Returns ------- string 4 character perturber code if present, '' otherwise """ if self[EleComet._pertEpoch.start] == ' ' or self[210] == '!': return '' return self[EleComet._pert]
[docs] def notes(self): """ Returns ------- string up to two note codes. """ return self[EleComet._notes].strip()
[docs] def rms(self): """ Returns ------- float r.m.s. fit of included obserations if rms is present. -1 if rms is not present. """ s = self[EleComet._rms] return -1 if s.isspace() else float(s)
_i62 = [None] * (ord('z') + 1) for i in range(10): _i62[ord('0') + i] = i for i in range(26): _i62[ord('A') + i] = i + 10 for i in range(26): _i62[ord('a') + i] = i + 10 + 26 def b62(c): try: n = _i62[ord(c)] if n is not None: return n except IndexError: pass raise ValueError(c) def unpackDate(s): """ Returns ------- int year int month int day float fraction of day """ if len(s) < 3: # allow as little as a packed year raise ValueError(s) # century c = ord(s[0]) if c >= ord('A'): c = 10 + c - ord('A') else: c -= ord('0') if c > 25: # allow a few centuries into the future, raise ValueError(s) # call anything beyond that an error try: y = int(s[1:3]) except ValueError: raise ValueError(s) if c >= 0: y = c * 100 + y else: y = (c + 1) * 100 - y m = 1 if len(s) > 3: try: m = b62(s[3]) except ValueError: raise ValueError(s) if m == 0 or m > 12: raise ValueError(s) d = 1 if len(s) > 4: try: d = b62(s[4]) except ValueError: raise ValueError(s) if d == 0 or d > 31: raise ValueError(s) frac = 0. if len(s) > 5: try: frac = float('.' + s[5:]) except ValueError: raise ValueError(s) return y, m, d, frac def JDFromPacked(s): y, m, d, frac = unpackDate(s) return to_julian_date(y, m, d + frac) def to_julian_date(year, month, day): frac, d = math.modf(day) j = novas.julian_date(year, month, int(d), frac * 24) if j >= 2299159.5: # start of Gregorian calendar, October 15, 1582 return j # else compute correction for Julian calendar y = year - (14 - month) // 12 return j + y // 100 - y // 400 - 2 def packDate(j, w): y, m, d = from_julian_date(j) prec = w - 5 # split fraction from day frac, d = math.modf(d) d = int(d) # round fraction to target precision p = 10. ** prec frac = math.floor(p * frac + .5) if frac == p: # round up to next day d = datetime.date(y, m, d) + datetime.timedelta(days=1) y = d.year m = d.month d = d.day frac = 0 # after rounding check year can be packed if y < 1800 or y > 2099: raise ValueError('cannot pack date {}'.format(j)) if prec == 0: frac = '' else: frac = '%0*d' % (prec, frac) if d < 10: d = str(d) else: d = chr(int(d) - 10 + ord('A')) return '{}{:02d}{:X}{}{}'.format(chr(y // 100 - 18 + ord('I')), y % 100, m, d, frac) def from_julian_date(j): if j < 2299159.5: # start of Gregorian calendar, October 15, 1582 # this could be implemented if needed. not sure it is needed. # one possible implementation would be to compute a correction as # was done for to_julian_date. raise ValueError('JD before start of Gregorian calendar unsupported') y, m, d, hr = novas.cal_date(j) return y, m, d + hr / 24 def float7(s): return int(s) * 1e-7 def float9(s): return int(s) * 1e-9 def float9p(s): if '.' in s: return float(s) return int(s) * 1e-9 class EleFields: """ Abstract base class providing formatting methods. Formatting methods fmt220 and fmt90 format EleFields attributes but the attributes are not part of the EleFields class definition. It may be most convenient to use EleFields as an abstract base class for a class that defines the needed attributes. Alternatively the constructor can define attributes. Parameters ---------- kw : keywords keyword parameters will be set as attributes. """ def __init__(self, **kw): self.__dict__.update(kw) def fmt220(self): """ formats attributes into a string in the 220 character format. Recognized attributes: desig, g, timePeri, argPeri, node, inc, periDist, ecc, designation, h, maEpoch, ma, a, numOpp, arc, numObs, comp, ref, first, last, rms. Fields of the 220 character format without defined attributes are left blank. Note there are not generally separate high and low precision attributes. Mean anomaly ma and semi-major axis a, only being formatted in low precision, may be derived from high precision attributes such as periDist and ecc. The reverse computations are not automatically performed however. No assumption is made on the precision of attributes ma and a and so no high precision fields will be derived from them. Returns ------- string formatted 220 character string Examples -------- >>> # use EleFields as an abstract base class >>> class aei(EleFields): ... def __init__(self, a, e, i): ... self.a = a ... self.ecc = e ... self.inc = i ... >>> x = aei(3.1, .1, 20) >>> line = x.fmt220() >>> line[Ele220._inc] '0200000000' >>> # periDist not automatically computed from a and ecc. >>> line[Ele220._periDist] ' ' >>> line[Ele220._ecc] '0100000000' >>> line ' 0200000000 0100000000 20.0 0.100 3.100 ' >>> # construct with kwargs >>> line = EleFields(periDist=2.79, ecc=.1, inc=20).fmt220() >>> line[Ele220._periDist] '2790000000' >>> line[Ele220._a3] # a3 computed from periDist and ecc ' 3.100' >>> line ' 020000000027900000000100000000 20.0 0.100 3.100 ' """ return ''.join([ self._desig(), self._g(), self._timePeri(), self._argPeri(), self._node(), self._inc(), self._periDist(), self._ecc(), ' ', # undocumented field, appears to be a packed date. self._designation(), ' ', self._h(), ' ', self._maEpoch(), ' ', self._ma1(), ' ', self._argPeri1(), ' ', self._node1(), ' ', self._inc1(), ' ', self._ecc3(), ' ', self._a3(), ' ', self._numOpp(), ' ', self._arc(), ' ', self._numObs(), ' ', # undocumented field self._comp(), ' ', self._ref(), ' ', self._first(), ' ', self._last(), ' ', self._rms(), ' ']) # multiple undocumented fields def _desig(self): # all field formatters roughly follow this pattern: try: v = self.desig # first just attempt to access the field, except AttributeError: return ' ' # and return blank if it's not there. if v is None: return ' ' # also return blank if it's None. if len(v) <= 7: # perform any range check return v.ljust(7) # return the formatted field if it's okay raise ValueError('desig > 7 characters') # raise ValueError otherwise def _g(self): try: v = self.g except AttributeError: return ' ' if v is None: return ' ' # 0 to .4 is the typical range for g. the limits here hopefully # are enough to allow some unusual yet intentional values. if v > -.5 and v < 1: return '{:5.2f}'.format(v) raise ValueError('invalid g') def _timePeri(self): try: v = self.timePeri except AttributeError: return ' ' if v is None: return ' ' try: return packDate(v, 12) except ValueError: raise ValueError('invalid timePeri') def _argPeri(self): try: v = self.argPeri except AttributeError: return ' ' if v is None: return ' ' s = '{:011.7f}'.format(v % 360.) if s == '360.0000000': return '0000000000' return s[:3] + s[4:] def _node(self): try: v = self.node except AttributeError: return ' ' if v is None: return ' ' s = '{:011.7f}'.format(v % 360.) if s == '360.0000000': return '0000000000' return s[:3] + s[4:] def _inc(self): try: v = self.inc except AttributeError: return ' ' if v is None: return ' ' if v >= 0 and v <= 180: s = '{:011.7f}'.format(v) return s[:3] + s[4:] raise ValueError('invalid inc') def _periDist(self): try: v = self.periDist except AttributeError: return ' ' if v is None: return ' ' if v > 0: s = '{:011.9f}'.format(v) if len(s) == 11: return s[:1] + s[2:] return '%.*f' % (19 - len(s), v) raise ValueError('invalid periDist') def _ecc(self): try: v = self.ecc except AttributeError: return ' ' if v is None: return ' ' if v >= 0 and v < 1.1: s = '{:011.9f}'.format(v) return s[:1] + s[2:] raise ValueError('invalid ecc') def _designation(self): try: v = self.designation except AttributeError: return ' ' if v is None: return ' ' if len(v) <= 10: return v.ljust(10) raise ValueError('designation > 10 characters') def _h(self): try: v = self.h except AttributeError: return ' ' if v is None: return ' ' if v > -1 and v < 60: return '{:4.1f}'.format(v) raise ValueError('invalid h') def _maEpoch(self): try: v = self.maEpoch except AttributeError: return ' ' if v is None: return ' ' try: return packDate(v, 5) except ValueError: raise ValueError('invalid maEpoch') def _ma1(self): try: v = self.ma except AttributeError: try: a = self.periDist / (1 - self.ecc) n = k / math.sqrt(a * a * a) * 180 / math.pi v = n * (self.maEpoch - self.timePeri) except AttributeError: return ' ' if v is None: return ' ' s = '{:5.1f}'.format(v % 360) if s == '360.0': return ' 0.0' return s def _argPeri1(self): try: v = self.argPeri except AttributeError: return ' ' if v is None: return ' ' s = '{:5.1f}'.format(v % 360.) if s == '360.0': return ' 0.0' return s def _node1(self): try: v = self.node except AttributeError: return ' ' if v is None: return ' ' s = '{:5.1f}'.format(v % 360.) if s == '360.0': return ' 0.0' return s def _inc1(self): try: v = self.inc except AttributeError: return ' ' if v is None: return ' ' if v >= 0 and v <= 180: return '{:5.1f}'.format(v) raise ValueError('invalid inc') def _ecc3(self): try: v = self.ecc except AttributeError: return ' ' if v is None: return ' ' if v >= 0 and v < 1.1: return '{:5.3f}'.format(v) raise ValueError('invalid ecc') def _a3(self): try: v = self.a except AttributeError: try: v = self.periDist / (1 - self.ecc) except AttributeError: return ' ' if v is None: return ' ' if v > 0 and v <= 999.999: return '{:7.3f}'.format(v) # if a doesn't fit in the format, just leave it blank. return ' ' def _numOpp(self): try: v = self.numOpp except AttributeError: return ' ' if v is None: return ' ' # numOpp must be strictly > 0. 300 is still a quite generous upper # limit while disallowing any use of 999 to mean anything special. if v > 0 and v < 300: return '{:3d}'.format(v) raise ValueError('invalid numOpp') def _arc(self): try: v = self.arc except AttributeError: return ' ' if v is None: return ' ' # arc can be 0 in the case of rounding an arc < 12 hours. # 30000 a generous upper limit disallowing 99999. if v >= 0 and v <= 30000: return '{:5d}'.format(v) raise ValueError('invalid arc') def _numObs(self): try: v = self.numObs except AttributeError: return ' ' if v is None: return ' ' # numObs must be strictly > 0. # 30000 a generous upper limit disallowing 99999. if v > 0 and v < 30000: return '{:5d}'.format(v) raise ValueError('invalid numObs') def _comp(self): try: v = self.comp except AttributeError: return ' ' if v is None: return ' ' if len(v) <= 9: return v.ljust(9) raise ValueError('comp > 9 characters') def _ref(self): try: v = self.ref except AttributeError: return ' ' if v is None: return ' ' if len(v) <= 9: return v.ljust(9) raise ValueError('ref > 9 characters') def _first(self): try: v = self.first except AttributeError: return ' ' if v is None: return ' ' try: return packDate(v, 5) except ValueError: raise ValueError('invalid first') def _last(self): try: v = self.last except AttributeError: return ' ' if v is None: return ' ' try: return packDate(v, 5) except ValueError: raise ValueError('invalid last') def _rms(self): try: v = self.rms except AttributeError: return ' ' if v is None: return ' ' if v >= 0 and v < 60: return '{:5.2f}'.format(v) raise ValueError('invalid rms') def fmt90(self): """ formats attributes into a string in the 90 character format. Recognized attributes: desig, h, timePeri, argPeri, node, inc, periDist, ecc, first, last. Returns ------- string formatted 90 character string """ return ''.join([ self._desig(), self._h(), ' ', self._timePeri(), self._argPeri(), self._node(), self._inc(), self._periDist(), self._ecc(), ' ', # unknown purpose self._first(), self._last()])