Source code for mpcorbfile.mpcorbfile

import datetime
import json
import logging
import numpy as np
from tqdm import tqdm
from typing import Generator

logging.basicConfig(level=logging.INFO)

def json_serial(obj):
    """JSON serializer for objects not serializable by default json code"""
    if isinstance(obj, (datetime.datetime, datetime.date)):
        return obj.isoformat()
    raise TypeError("Type %s not serializable" % type(obj))


def hook(obj):
    """ tqdm hook for json.loads """
    #TBD
    return obj

# convenience fn
[docs] def add_asteroids_to_rebound(simulation, bodies=None): """ Add asteroids to a REBOUND simulation. Example:: import rebound import mpcorbfile import numpy as np sim=rebound.Simulation() rebound.data.add_solar_system(sim) mpcorb = 'MPCORB_TEST.DAT' f = mpcorbfile.mpcorb_file(mpcorb) mpcorbfile.add_asteroids_to_rebound(sim) """ for body in bodies: # append to simulation simulation.add( m=0, # Masa del cuerpo (0 para asteroides) a=body["a"], e=body["e"], inc=np.radians(body["i"]), omega=np.radians(body["Peri"]), Omega=np.radians(body["Node"]), M=np.radians(body["M"]), date=body["epochJD"], hash=body["Name"], )
[docs] def set_elliptical_body_elements(eliptical_body, body): """ Set orbital elements of eliptical_body for futher calculation using pyephem:: pyephem parameters: _inc — Inclination (°) _Om — Longitude of ascending node (°) _om — Argument of perihelion (°) _a — Mean distance from sun (AU) _M — Mean anomaly from the perihelion (°) _epoch_M — Date for measurement _M _size — Angular size (arcseconds at 1 AU) _e — Eccentricity _epoch — Epoch for _inc, _Om, and _om _H, _G — Parameters for the H/G magnitude model _g, _k — Parameters for the g/k magnitude model eliptical_body._H = body["H"] eliptical_body._G = body["G"] eliptical_body._a = body["a"] eliptical_body._M = body["M"] eliptical_body._om = body["Peri"] eliptical_body._Om = body["Node"] eliptical_body._inc = body["i"] eliptical_body._e = body["e"] eliptical_body._epoch = body["Epoch"].strftime("%Y/%m/%d %H:%M:%S") eliptical_body._epoch_M = body["Epoch"].strftime("%Y/%m/%d %H:%M:%S") """ eliptical_body._H = body["H"] eliptical_body._G = body["G"] eliptical_body._a = body["a"] eliptical_body._M = body["M"] eliptical_body._om = body["Peri"] eliptical_body._Om = body["Node"] eliptical_body._inc = body["i"] eliptical_body._e = body["e"] eliptical_body._epoch = body["Epoch"].strftime("%Y/%m/%d %H:%M:%S") eliptical_body._epoch_M = body["Epoch"].strftime("%Y/%m/%d %H:%M:%S") return eliptical_body
[docs] class mpcorb_file: """ Read and write MPCORB files ussing the format stated in https://www.minorplanetcenter.net/iau/info/MPOrbitFormat.html on march 4, 2025 format:: Export Format for Minor-Planet Orbits This document describes the format used for both unperturbed and perturbed orbits of minor planets, as used in the Extended Computer Service and in the Minor Planet Ephemeris Service. Orbital elements for minor planets are heliocentric. The column headed `F77' indicates the Fortran 77/90/95/2003/2008 format specifier that should be used to read the specified value. Columns F77 Use 1 - 7 a7 Number or provisional designation (in packed form) 9 - 13 f5.2 Absolute magnitude, H 15 - 19 f5.2 Slope parameter, G 21 - 25 a5 Epoch (in packed form, .0 TT) 27 - 35 f9.5 Mean anomaly at the epoch, in degrees 38 - 46 f9.5 Argument of perihelion, J2000.0 (degrees) 49 - 57 f9.5 Longitude of the ascending node, J2000.0 (degrees) 60 - 68 f9.5 Inclination to the ecliptic, J2000.0 (degrees) 71 - 79 f9.7 Orbital eccentricity 81 - 91 f11.8 Mean daily motion (degrees per day) 93 - 103 f11.7 Semimajor axis (AU) 106 i1 Uncertainty parameter, U or a1 If this column contains `E' it indicates that the orbital eccentricity was assumed. For one-opposition orbits this column can also contain `D' if a double (or multiple) designation is involved or `F' if an e-assumed double (or multiple) designation is involved. 108 - 116 a9 Reference 118 - 122 i5 Number of observations 124 - 126 i3 Number of oppositions For multiple-opposition orbits: 128 - 131 i4 Year of first observation 132 a1 '-' 133 - 136 i4 Year of last observation For single-opposition orbits: 128 - 131 i4 Arc length (days) 133 - 136 a4 'days' 138 - 141 f4.2 r.m.s residual (") 143 - 145 a3 Coarse indicator of perturbers (blank if unperturbed one-opposition object) 147 - 149 a3 Precise indicator of perturbers (blank if unperturbed one-opposition object) 151 - 160 a10 Computer name There may sometimonth be additional information beyond column 160 as follows: 162 - 165 z4.4 4-hexdigit flags The bottom 6 bits (bits 0 to 5) are used to encode a value representing the orbit type (other values are undefined): Value 1 Atira 2 Aten 3 Apollo 4 Amor 5 Object with q < 1.665 AU 6 Hungaria 7 Unused or internal MPC use only 8 Hilda 9 Jupiter Trojan 10 Distant object Additional information is conveyed by adding in the following bit values: Bit Value 6 64 Unused or internal MPC use only 7 128 Unused or internal MPC use only 8 256 Unused or internal MPC use only 9 512 Unused or internal MPC use only 10 1024 Unused or internal MPC use only 11 2048 Object is NEO 12 4096 Object is 1-km (or larger) NEO 13 8192 1-opposition object seen at earlier opposition 14 16384 Critical list numbered object 15 32768 Object is PHA Note that the orbit classification is based on cuts in osculating element space and is not 100% reliable. Note also that certain of the flags are for internal MPC use and are not documented. 167 - 194 a Readable designation 195 - 202 i8 Date of last observation included in orbit solution (YYYYMMDD format) """ def __init__(self, file=None): self.bodies = list() # This fields dont throw exceptions if missing self.optional_fields = [ "G", "H", "U", "Ref", "Num_obs", "Num_opps", "Arc_length", "rms", "Perturbers", "Perturbers_2", "Computer", "Hex_flags", "Last_obs", "Number", "Name", ] # if type is not present field is consider string self.format_dict = { "packed_designation": {"from": 1, "to": 8, "ljust": True}, "H": {"from": 9, "to": 14, "type": float, "ljust": False, "format": "5.2f"}, "G": { "from": 15, "to": 20, "type": float, "ljust": False, "format": "5.2f", }, "Epoch": { "from": 21, "to": 26, "type": self.compressed_epoch, "ljust": False, "format": "", }, "M": { "from": 27, "to": 36, "type": float, "ljust": False, "format": "9.5f", }, "Peri": { "from": 38, "to": 47, "type": float, "ljust": False, "format": "9.5f", }, "Node": { "from": 49, "to": 58, "type": float, "ljust": False, "format": "9.5f", }, "i": { "from": 60, "to": 69, "type": float, "ljust": False, "format": "9.5f", }, "e": { "from": 71, "to": 80, "type": float, "ljust": False, "format": "9.7f", }, "n": { "from": 81, "to": 92, "type": float, "ljust": False, "format": "11.8f", }, "a": { "from": 93, "to": 104, "type": float, "ljust": False, "format": "11.7f", }, "U": {"from": 106, "to": 107, "ljust": False, "format": ""}, "Ref": {"from": 108, "to": 117, "ljust": False, "format": ""}, "Num_obs": { "from": 118, "to": 123, "type": int, "ljust": False, "format": "5d", }, "Num_opps": { "from": 124, "to": 127, "type": int, "ljust": False, "format": "3d", }, "Arc_length": {"from": 128, "to": 137, "ljust": False, "format": ""}, "rms": { "from": 138, "to": 142, "type": float, "ljust": True, "format": "4.2f", }, "Perturbers": {"from": 143, "to": 146, "ljust": False, "format": ""}, "Perturbers_2": {"from": 147, "to": 150, "ljust": True, "format": ""}, "Computer": {"from": 151, "to": 161, "ljust": True, "format": ""}, "Hex_flags": { "from": 162, "to": 166, "type": "hex", "ljust": False, "format": "04X", }, "Number": {"from": 167, "to": 175, "ljust": False, "format": ""}, "Name": {"from": 176, "to": 194, "ljust": True, "format": ""}, "Last_obs": {"from": 195, "to": 203, "ljust": False, "format": ""}, } if file is not None: self.read(file) def datetime_compressed_epoch(self, epoch: datetime.datetime) -> str: year_letter_map = {"18": "I", "19": "J", "20": "K"} day_map = "123456789ABCDEFGHIJKLMNOPQRSTUV" year_str = str(epoch.year) year_letter = year_letter_map[year_str[0:2]] result = f"{year_letter}{year_str[2:4]}{epoch.month:1X}{day_map[epoch.day - 1]}" return result def compressed_epoch(self, epoch: str) -> datetime.datetime: return self.compressed_epoch_to_datetime(epoch) # Función para convertir el formato comprimido de la época a fecha juliana
[docs] def compressed_epoch_to_datetime(self, epoch: str) -> datetime.datetime: """ Convert compressed epoch to python datetime following the below rules:: Dates of the form YYYYMMDD may be packed into five characters to conserve space. The first two digits of the year are packed into a single character in column 1 (I = 18, J = 19, K = 20). Columns 2-3 contain the last two digits of the year. Column 4 contains the month and column 5 contains the day, coded as detailed below: Month Day Character Day Character in Col 4 or 5 in Col 4 or 5 Jan. 1 1 17 H Feb. 2 2 18 I Mar. 3 3 19 J Apr. 4 4 20 K May 5 5 21 L June 6 6 22 M July 7 7 23 N Aug. 8 8 24 O Sept. 9 9 25 P Oct. 10 A 26 Q Nov. 11 B 27 R Dec. 12 C 28 S 13 D 29 T 14 E 30 U 15 F 31 V 16 G Examples: 1996 Jan. 1 = J9611 1996 Jan. 10 = J961A 1996 Sept.30 = J969U 1996 Oct. 1 = J96A1 2001 Oct. 22 = K01AM This system can be extended to dates with non-integral days. The decimal fraction of the day is simply appended to the five characters defined above. Examples: 1998 Jan. 18.73 = J981I73 2001 Oct. 22.138303 = K01AM138303 """ year_letter = {"I": "18", "J": "19", "K": "20"} month_map = { "1": 1, "2": 2, "3": 3, "4": 4, "5": 5, "6": 6, "7": 7, "8": 8, "9": 9, "A": 10, "B": 11, "C": 12, } day_map = { "1": 1, "2": 2, "3": 3, "4": 4, "5": 5, "6": 6, "7": 7, "8": 8, "9": 9, "A": 10, "B": 11, "C": 12, "D": 13, "E": 14, "F": 15, "G": 16, "H": 17, "I": 18, "J": 19, "K": 20, "L": 21, "M": 22, "N": 23, "O": 24, "P": 25, "Q": 26, "R": 27, "S": 28, "T": 29, "U": 30, "V": 31, } year_letter_epoch = epoch[0] month_epoch = epoch[3] day_epoch = epoch[4] if year_letter.get(year_letter_epoch, 0) == 0: raise ValueError(f"Invalid epoch format: {epoch}") year = f"{year_letter.get(year_letter_epoch, 0)}{epoch[1:3]}" month = month_map.get(month_epoch, 0) day = day_map.get(day_epoch, 0) if month == 0 or day == 0: raise ValueError(f"Invalid epoch format: {epoch}") date_str = f"{year}-{month:02d}-{day:02d}" date = datetime.datetime.strptime(date_str, "%Y-%m-%d") return date
def __add_calculate_fields(self, body: dict) -> dict: """ Add some calculated fields to the body dict """ newbody = body.copy() newbody["epochJD"] = self.datetime_to_julian_date(newbody["Epoch"]) # Add calculate new fields # print(newbody['packed_designation']) newbody["designation"] = self.expand_packed_designation( newbody["packed_designation"] ) # newbody["discover_date"] = self.date_from_packed_designation(newbody['packed_designation']) # Better use designation. Packed designation losses his date meaning when asteroid get numbered #'name' field has discovery date meaning while it is provisional (no given name). newbody["discover_date"] = self.date_from_designation(newbody["Name"]) newbody["orbit_type"] = self.orbit_type( newbody["a"], newbody["e"], newbody["i"] ) return newbody
[docs] def datetime_to_julian_date(self, my_date: datetime.datetime) -> float: """ Convert a datetime to julian date """ return my_date.toordinal() + 1721424.5
[docs] def add(self, body_dict: dict): """ Add new body from a dict. """ # TODO check keys _body_dict = self.__add_calculate_fields(body_dict) self.bodies.append(_body_dict)
def __parse_line(self, line: str) -> dict: """ Parse one line an return a dict with all the variables fullfilled. """ # line = " " + l # padding to sync index with mpcorb description body = dict() for k, v in self.format_dict.items(): if "type" in v: try: if v["type"] == "hex": body[k] = int(line[v["from"] - 1 : v["to"] - 1], 16) else: body[k] = v["type"](line[v["from"] - 1 : v["to"] - 1]) except ValueError: body[k] = np.nan else: body[k] = line[v["from"] - 1 : v["to"] - 1].strip() body = self.__add_calculate_fields(body) return body def __make_line(self, body: dict) -> str: """ Compose one line with the body data """ # Ceres data used to dim line ceres = "00001 3.34 0.15 K2555 188.70269 73.27343 80.25221 10.58780 0.0794013 0.21424651 2.7660512 0 E2024-V47 7330 125 1801-2024 0.80 M-v 30k MPCLINUX 4000 (1) Ceres 20241101" line = [" " for x in range(len(ceres))] if True: for k, v in self.format_dict.items(): if k not in body: if k in self.optional_fields: # Fill with default values if "type" not in v: body[k] = "" elif v["type"] == "hex": body[k] = 0x0 elif v["type"] is float: body[k] = v["type"](np.nan) else: body[k] = 0 else: raise Exception(f"Required field: {k} not in body:{body}") if "format" in v: if "type" in v: if v["type"] == self.compressed_epoch: txt = self.datetime_compressed_epoch(body[k]) elif np.isnan(body[k]): txt = "" else: txt = f"{body[k]:{v['format']}}" else: txt = f"{body[k]:{v['format']}}" else: txt = body[k] if v["ljust"]: text = txt.ljust(v["to"] - v["from"]) else: text = txt.rjust(v["to"] - v["from"]) line[v["from"] - 1 : v["to"] - 1] = text # except: # print('Fail to typer',k,v,body[k],type(body[k])) return "".join(line)
[docs] def read(self, filename: str) -> list: """ Read the MPCORB.DAT file. """ bodies = [] with open(filename, "r") as fd: lines = fd.readlines() # skip header if any (all text above '---') start_line = [i for i, line in enumerate(lines) if "---" in line] if len(start_line) > 0: lines = lines[start_line[-1] + 1 :] # load all bodies bodies = list() for line in tqdm( lines, colour="green", unit=" bodies", desc="reading", unit_scale=True ): if ( line.startswith("#") or len(line.strip()) < 1 ): # Ignore empty lines or comments continue body = self.__parse_line(line) bodies.append(body) self.bodies = bodies # save classwise to caching when called by other fn self.colnames = list(bodies[0].keys()) return bodies
[docs] def read_json(self, filename: str) -> list: """ read json files https://minorplanetcenter.net/Extended_Files/mpcorb_extended.json.gz """ with open(filename, "r") as f: self.bodies = json.load(f, object_hook=hook) # TO BE DONE for body in tqdm(self.bodies): if "Name" not in body: body["Name"] = body["Principal_desig"] if "Number" in body: body["packed_designation"] = self.pack_designation(body["Number"]) elif "Name" in body: body["packed_designation"] = self.pack_designation(body["Name"]) else: body["packed_designation"] = "" body["Epoch"] = datetime.date.fromordinal( int(body["Epoch"] - 1721424.5) ) # From julian date if "Arc_years" in body: body["Arc_length"] = body["Arc_years"] elif "Arc_length" in body: body["Arc_length"] = f"{body['Arc_length']} days" pass else: body["Arc_length"] = "" body["Last_obs"] = body["Last_obs"].replace("-", "") body["Hex_flags"] = int(body["Hex_flags"], 16) return self.bodies
[docs] def write(self, filename: str, header: str = "") -> bool: """ Write a file formated as MPCORB with the bodies data """ if self.bodies is None: return False Note = "\n Create with mpcorbfile python library/utility. See: https://github.com/nachoplus/mpcorbfile\n" colnames = "Des'n H G Epoch M Peri. Node Incl. e n a Reference #Obs #Opp Arc rms Perts Computer" with open(filename, "w") as fd: fd.write(f"{header}\n") fd.write(f"{Note}\n") fd.write(f"{colnames}\n") fd.write("".join(["-" for x in range(len(colnames) + 2)])) fd.write("\n") for body in tqdm( self.bodies, colour="blue", unit=" bodies", desc="writting", unit_scale=True, ): fd.write(self.__make_line(body)) fd.write("\n") return True
[docs] def write_json(self, filename: str): """Write json file compatible with https://minorplanetcenter.net/Extended_Files/mpcorb_extended.json.gz" files""" with open(filename, "w") as f: json.dump(self.bodies, f, indent=2, default=json_serial)
def json(self): return json.dumps(self.bodies, indent=2, default=json_serial)
[docs] def get_chunks(self, n: int) -> list: """return a n list of lists with len(list)/n bodies each""" N = int(np.ceil(len(self.bodies) / n)) return self.__group(self.bodies, N)
# Internal fn def __group(self, lst: list, n: int) -> Generator[list, None, None]: for i in range(0, len(lst), n): val = lst[i : i + n] yield val def __hex2dec(self, letter: chr) -> str: """ Convert 0..F hex digit to 0..15 decimal """ try: int(letter) return letter except ValueError: if letter.isupper(): return str(ord(letter) - ord("A") + 10) if letter.islower(): return str(ord(letter) - ord("a") + 36)
[docs] def pack_designation(self, designation: str) -> str: """ Create packed designation from designation following format: https://www.minorplanetcenter.net/iau/info/PackedDes.html """ year_letter_map = {"18": "I", "19": "J", "20": "K"} base62 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" isdigit = str.isdigit if designation[0] == "(" and designation[-1] == ")": # Numered designation number = int(designation[1:-1]) if number < 100000: result = f"{number:05d}" elif number < 620000: first_two = int(number / 10000) modu = number % 10000 result = f"{base62[first_two]}{modu:04d}" else: number = number - 620000 bit4, rest = divmod(number, 62**3) bit3, rest = divmod(rest, 62**2) bit2, rest = divmod(rest, 62**1) bit1, rest = divmod(rest, 1) # print(bit1,bit2,bit3,bit4,rest) result = f"~{base62[bit4]}{base62[bit3]}{base62[bit2]}{base62[bit1]}" return result elif isdigit(designation[0:4]) and designation[4] == " ": # Provisional designation # Survey if designation[5:8] in ["P-L", "T-1", "T-2", "T-3"]: return f"{designation[5:8].replace('-', '')}{designation[0:4]}" # Regular year = f"{year_letter_map[designation[0:2]]}{designation[2:4]}" halfmonth = designation[5:6] letter = designation[6:7] if len(designation) > 7: number = int(designation[7:]) # print(year,halfmonth,letter,number) if int(number) > 99: # first two digits first_two = designation[7:9] last = designation[9] number_txt = f"{base62[int(first_two)]}{int(last)}" result = f"{year}{halfmonth}{number_txt}{letter}" else: result = f"{year}{halfmonth}{number:02d}{letter}" else: result = f"{year}{halfmonth}00{letter}" return result
[docs] def expand_packed_designation(self, packed: str) -> str: """ Convert the packed designation format to formal designation following format: https://www.minorplanetcenter.net/iau/info/PackedDes.html """ isdigit = str.isdigit desig = "" try: packed = packed.strip() except ValueError: print("ValueError: Input is not convertable to string.") if isdigit(packed): desig = packed.lstrip("0") # ex: 00123 elif ( not isdigit(packed[0]) and packed[0] != "~" ): # ex: A7659 = 107659 but not ~0000 if isdigit(packed[1:]): # ex: A7659 desig = self.__hex2dec(packed[0]) + packed[1:] elif isdigit(packed[1:3]): # ex: J98SG2S = 1998 SS162 if isdigit(packed[4:6]) and packed[4:6] != "00": desig = ( self.__hex2dec(packed[0]) + packed[1:3] + " " + packed[3] + packed[-1] + packed[4:6].lstrip("0") ) if isdigit(packed[4:6]) and packed[4:6] == "00": desig = ( self.__hex2dec(packed[0]) + packed[1:3] + " " + packed[3] + packed[-1] ) if not isdigit(packed[4:6]): desig = ( self.__hex2dec(packed[0]) + packed[1:3] + " " + packed[3] + packed[-1] + self.__hex2dec(packed[4]) + packed[5] ) elif packed[2] == "S": # ex: T1S3138 = 3138 T-1 desig = packed[3:] + " " + packed[0] + "-" + packed[1] elif packed[0] == "~": base62 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" i4 = base62.index(packed[1]) i3 = base62.index(packed[2]) i2 = base62.index(packed[3]) i1 = base62.index(packed[4]) n = i4 * 62 ^ 3 + i3 * 62 ^ 2 + i2 * 62 ^ 1 + i1 * 62 ^ 0 desig = f"({n + 620000})" # print(f'{packed} {i4} {i3} {i2} {i1} {n} {desig}') else: print("fail to expand packed designation") return desig
def date_from_designation(self, name: str) -> datetime.datetime: isdigit = str.isdigit halfmonth_letter = { "A": (1, 1), "B": (1, 15), "C": (2, 1), "D": (2, 15), "E": (3, 1), "F": (3, 15), "G": (4, 1), "H": (4, 15), "J": (5, 1), "K": (5, 15), "L": (6, 1), "M": (6, 15), "N": (7, 1), "O": (7, 15), "P": (8, 1), "Q": (8, 15), "R": (9, 1), "S": (9, 15), "T": (10, 1), "U": (10, 15), "V": (11, 1), "W": (11, 15), "X": (12, 1), "Y": (12, 15), } if isdigit(name[0:4]) and name[4] == " ": # start with year year = int(name[0:4]) halfmonth = name[5] month, day = halfmonth_letter[halfmonth] date = datetime.datetime.strptime(f"{year}-{month}-{day}", "%Y-%m-%d") else: date = np.nan return date def date_from_packed_designation(self, packed: str) -> datetime.datetime: isdigit = str.isdigit if ( not isdigit(packed[0]) and isdigit(packed[1:3]) and (packed[0] in ["I", "J", "K"]) and len(packed.strip()) == 7 ): try: packdt = str(packed).strip() except ValueError: print("ValueError: Input is not convertable to string.") year = self.__hex2dec(packdt[0]) + packdt[1:3] halfmonth = float(self.__hex2dec(packdt[3])) - 9 if halfmonth > 9: halfmonth -= 1 month = str(int(np.ceil(halfmonth / 2))) if (halfmonth % 2) == 1: day = "01" else: day = "15" result = datetime.datetime.strptime(f"{year}-{month}-{day}", "%Y-%m-%d") else: result = None return result
[docs] def orbit_type(self, a: float, e: float, i: float) -> str: """ Classify asteroid orbit type following http://en.wikipedia.org/wiki/Near-Earth_object """ Qt = 1.017 qt = 0.983 at = 1 neo = 1.3 Q = a * (1 + e) q = a * (1 - e) t = [] if q <= neo: t.append("NEO") if a <= at: if Q > qt: t.append("Athen") else: t.append("Atira") else: if q < Qt: t.append("Apollo") # Amors (1.0167 < q < 1.3 AU) elif Qt < q < neo: t.append("Amor") # Mars crossers (1.3 < q < 1.6660 AU) if neo < q < 1.6660: t.append("MarsCrosser") # HUNGARIAN Semi-major axis between 1.78 and 2.00 AU. Orbital period of approximately 2.5 years. # Low eccentricity of below 0.18. An inclination of 16° to 34° if 1.78 <= a <= 2.0 and e <= 0.18 and 16 <= i <= 34: t.append("Hungaria") # MB:Zona I (2,06-2,5 UA), Zona II (2,5-2,82 UA) y Zona III (2,82-3,28 UA). if 2.06 <= a <= 2.5: # main belter I t.append("MB I") if 2.5 <= a <= 2.82: # main belter II t.append("MB II") if 2.82 <= a <= 3.28: # main belter II t.append("MB III") # HILDA: semi-major axis between 3.7 AU and 4.2 AU, an eccentricity less than 0.3, and an inclination less than 20° if 3.7 <= a <= 4.2 and e <= 0.3 and i <= 20: t.append("Hilda") # TNOs 30,103 if a >= 30.103: t.append("TNO") # print a,Q,q,t t_ = ";".join(t) return t_