#!/usr/bin/python #/data_systems/opt/bin/python3 """ convert_incoming_cips_tle Convert the incoming TLE into the format used by tHE AIM CIPS software. This is done by removing the comments and reversing the order of the elements A TLE is a NORAD Two Line Element file that contains spacecraft geolocation data. Created June 2019 @author: Bill Barrett """ import os.path import time import traceback import sys from collections import namedtuple import re # The name of the TLE file as received from LASP Mission Operations RAW_FLE_FILE_NAME = 'aim_tle.save' CIPS_PRODUCTION_PATH = '/aim/sds/common/input_data/tle/' # Each TLE (Two Line Element) is composed of two lines TlePair = namedtuple('TlePair', 'first_line second_line') # first line of TLE, e.g. # 1 31304U 07015A 19156.69163316 +.00000529 +00000-0 +33928-4 0 9996 FIRST_LINE_PATTERN = re.compile(r""" (?P 1\s # Line Number of Element Data 31304 # Satellite Number (for AIM) U\s # Classification (U=Unclassified) (for AIM) 07 # International Designator (Last two digits of launch year (for AIM) 015 # International Designator (Launch number of the year) (for AIM) A\s{3} # International Designator (Piece of the launch) (for AIM) (?P \d{2} # Epoch Year (Last two digits of year) \d{3}\.\d{8}) # Epoch (Day of the year and fractional portion of the day) \s[\s\+\s-]\.\d{8} # First Time Derivative of the Mean Motion \s[\s\+\s-]\d{5}[\s\+\-]\d # Second Time Derivative of Mean Motion (decimal point assumed) \s[\s\+\s-]\d{5}[\s\+\-]\d # BSTAR drag term (decimal point assumed) \s\d # Ephemeris type \s[\s\d]{4} # Element number ) # end of (?P\d) # Checksum (Modulo 10) # (Letters, blanks, periods, plus signs = 0; minus signs = 1) """, re.VERBOSE) # second line of TLE, e.g. # 2 31304 097.9581 019.2278 0004579 192.3684 167.7434 15.13101935663555 SECOND_LINE_PATTERN = re.compile(r""" (?P 2\s # Line Number of Element Data 31304 # Satellite Number (for AIM) \s[\s\d]{3}\.\d{4} # Inclination [Degrees] \s[\s\d]{3}\.\d{4} # Right Ascension of the Ascending Node [Degrees] \s\d{7} # Eccentricity (decimal point assumed) \s[\s\d]{3}\.\d{4} # Argument of Perigee [Degrees] \s[\s\d]{3}\.\d{4} # Mean Anomaly [Degrees] \s\d{2}\.\d{8} # Mean Motion [Revs per day] [\s\d]{4}\d # Revolution number at epoch [Revs] ) # end of (?P\d) # Checksum (Modulo 10) """, re.VERBOSE) class TleFormatter: """ Convert the incoming tle into the format used by the AIM CIPS software. """ def __init__(self, raw_tle_directory): """ Initialize the place to find the raw TLE file and the dictionary of TLE's. Parameters ---------- raw_tle_directory : string the directory where the raw TLE file will be found """ assert raw_tle_directory, 'no directory path specified for raw tle file' self.raw_tle_file = os.path.join(raw_tle_directory, RAW_FLE_FILE_NAME) # A dictionary containing the TLE data indexed by the element time self.tles = {} def __enter__(self): """ Read, remove duplicates and validate the raw TLE file Returns ------- self : object Per standard Python convention enter returns 'self' """ self.read_raw_tle() return self def __exit__(self, exception_type, exception_value, traceback_info): """ Necessary if __enter__ is defined. Prints message on error exit. True for a normal exit, False if the exception_type is not None """ if exception_type is not None: print(time.strftime("%Y-%m-%d %H:%M:%S") + ' type: {0}, value: {1}'.format(exception_type, exception_value)) traceback.print_exception(exception_type, exception_value, traceback_info) return False # Comment to pass exception through return True def read_raw_tle(self): """ Read the raw tle file, storing the TLE pairs in a dictionary indexed by the element date (a year, day of year, fraction of day) Duplicate items are ignored as are pairs where one or the other lines has a bad checksum Items with duplicated time tags are sometimes corrections for earlier lines and sometimes simply duplications Note that because the raw file has its elements in reverse chronological order that in the case of duplication the most recently added line, which may be a correction, is the one retained. """ first_line = None with open(self.raw_tle_file, 'r') as raw_tle: for line in raw_tle: line = line.replace('\n', '') # Ignore comments if line.startswith('#') or line.startswith(';'): continue if first_line is None: tle_match = self.validate_line(line, FIRST_LINE_PATTERN) if tle_match is not None: element_date = tle_match.group('element_date') # This is where in the case of time tag duplication, the most # recently acquired element is kept, rather than the older one. if element_date in self.tles: continue else: first_line = line else: if self.validate_line(line, SECOND_LINE_PATTERN) is not None: self.tles[element_date] = TlePair(first_line, line) first_line = None @staticmethod def validate_line(line, regex): """ Verify both that the line matches the expected regular expression and that the line contains a valid checksum Parameters ---------- line : string the TLE line regex : compiled regular expression the regular expression against which to compare the line Returns ------- : obj the match object from the regular expression if the line matches it and the line contains a valid checksum None otherwise """ tle_match = regex.match(line) if tle_match is not None: if TleFormatter.validate_tle_checksum(tle_match.group('line_data'), int(tle_match.group('checksum'))): return tle_match # Fall through for the cases where the line does not match the pattern, # or the checksum is invalid return None @staticmethod def validate_tle_checksum(line_data, checksum): """ Validate the lines checksum following the NASA rule 1) Digits are added as their integer value to the checksum 2) A minus sign '-' counts as a '1' 3) All other characters are ignored Parameters ---------- line_data : string the TLE line less the checksum checksum : int the line checksum Returns ------- : bool True if the calculated checksum matches the input checksum, False otherwise """ # Count the number of minus signs ('-') count = line_data.count('-') # Strip out everything but the digits [0-9] result = re.sub(r'\D', '', line_data) for digit in result: count += int(digit) return checksum == (count % 10) if __name__ == '__main__': """ In the operational directory this is executed from the shell script via the following lines from 'create_cips_vetted_files_metadata_db.sh' Arguments ---------- argv[1] : str a data type in the keys of QUERIES_AND_FILES_TYPES argv[2] : str the name of the database file """ if len(sys.argv) >= 2: RAW_TLE_PATH = sys.argv[1] else: RAW_TLE_PATH = CIPS_PRODUCTION_PATH with TleFormatter(RAW_TLE_PATH) as tle_formatter: assert tle_formatter is not None # This loops is done as a print rather than a write to a file so # that the output can be visually inspected. In the shell script # that calls this the output is redirected to a file. for key in sorted(tle_formatter.tles.keys()): print(tle_formatter.tles[key].first_line) print(tle_formatter.tles[key].second_line)