Source code for jorbit.utils.mpc

"""A function for parsing an MPC observations file."""

import astropy.units as u
import pandas as pd
from astropy.coordinates import SkyCoord
from astropy.time import Time


[docs] def read_mpc_file( mpc_file: str, ) -> tuple[SkyCoord, list[Time], list[str], list[u.Quantity]]: """Read an MPC observations file and extract the relevant data. Haven't checked on this in a while - it may be out of date. Args: mpc_file (str): Path to the MPC observations file. Returns: tuple[SkyCoord, list[Time], list[str], list[u.Quantity]]: A tuple containing the following elements. (SkyCoord, The observed coordinates; list[Time], The times of observation; list[str], The observatory locations; list[u.Quantity], The astrometric uncertainties) """ cols = [ (0, 5), (5, 12), (12, 13), (13, 14), (14, 15), (15, 32), (32, 44), (44, 56), (65, 70), (70, 71), (77, 80), ] names = [ "Packed number", "Packed provisional designation", "Discovery asterisk", "Note 1", "Note 2", "Date of observation", "Observed RA (J2000.0)", "Observed Decl. (J2000.0)", "Observed magnitude", "Band", "Observatory code", ] data = pd.read_fwf(mpc_file, colspecs=cols, names=names) def parse_time(mpc_time: str) -> Time: t = mpc_time.replace(" ", "-").split(".") return Time(t[0], format="iso", scale="utc") + float(f"0.{t[1]}") * u.day def parse_uncertainty(dec_coord: str) -> u.Quantity: if len(dec_coord.split(".")) == 1: return 1 * u.arcsec return 10 ** (-len(dec_coord.split(".")[1])) * u.arcsec observed_coordinates = SkyCoord( data["Observed RA (J2000.0)"], data["Observed Decl. (J2000.0)"], unit=(u.hourangle, u.deg), ) times = list(map(parse_time, data["Date of observation"])) observatory_locations = [s + "@399" for s in list(data["Observatory code"])] astrometric_uncertainties = list( map(parse_uncertainty, data["Observed Decl. (J2000.0)"]) ) return ( observed_coordinates, times, observatory_locations, astrometric_uncertainties, )
[docs] def unpacked_to_packed_designation(number_str: str) -> str: """Convert an unpacked designation to a packed designation. Useful for translating between the leftmost and rightmost columns of a mpcorb file. Correctly handles provisional designations, low-numbered objects, medium-numbered objects, and high-numbered objects. Args: number_str (str): The unpacked designation. If is 7 digits and begins with a letter, it's assumed to be a provisional designation and is returned unchanged. Otherwise it's assumed to be a numbered object and will be packed into a 5 digit form. Returns: str: The packed designation. """ # If it's a provisional designation (7 characters), return as is # adding this isalpha check in case we reach > 10^7 numbered objects soonish if (len(number_str) == 7) and number_str[0].isalpha(): return number_str # Convert to integer for numerical comparisons num = int(number_str) # Low numbers (purely numeric) - return as is if num < 100000: return number_str # Medium numbers (10000-619999) - convert to letter + 4 digits if num < 620000: # Calculate the letter prefix and remaining digits prefix_num = num // 10000 remaining = num % 10000 # Convert prefix number to letter (matching the original letter_to_number function) if prefix_num >= 36: # a-z for 36+ prefix = chr(ord("a") + (prefix_num - 36)) else: # A-Z for 10-35 prefix = chr(ord("A") + (prefix_num - 10)) # Format the remaining digits with leading zeros return f"{prefix}{remaining:04d}" # High numbers (620000+) - convert to tilde + base62 def decimal_to_base62(n: int) -> str: """Convert decimal number to base62 string.""" chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" if n == 0: return "0" result = "" while n > 0: n, remainder = divmod(n, 62) result = chars[remainder] + result return result # Subtract the offset and convert to base62 base62_num = decimal_to_base62(num - 620000) # Pad to ensure total length of 5 characters (including the tilde) return f"~{base62_num:0>4}"
[docs] def packed_to_unpacked_designation(code: str) -> str: """Convert a packed designation to an unpacked designation. Useful for translating between the leftmost and rightmost columns of a mpcorb file. Correctly handles provisional designations, low-numbered objects, medium-numbered objects, and high-numbered objects. Args: code (str): The packed designation. 5 characters for numbered objects, 7 for provisional. Returns: str: The unpacked designation. """ # if it's a provisional designation, just return it if len(code) == 7: return code # if it's a numbered object, it could be written 3 forms: # low numbered objects are just numbers if code.isdigit(): return code # medium-numbered objects are a letter followed by 4 digits def letter_to_number(char: str) -> int: if char.isupper(): return ord(char) - ord("A") + 10 else: return ord(char) - ord("a") + 36 if code[0].isalpha() and code[1:].isdigit(): prefix_value = letter_to_number(code[0]) num = (prefix_value * 10000) + int(code[1:]) return str(num) # high-numbered objects are a tilde followed by a base-62 number def base62_to_decimal(char: str) -> int: if char.isdigit(): return int(char) elif char.isupper(): return ord(char) - ord("A") + 10 else: return ord(char) - ord("a") + 36 if code.startswith("~"): # Convert each character to its decimal value and calculate total total = 0 for position, char in enumerate(reversed(code[1:])): decimal_value = base62_to_decimal(char) total += decimal_value * (62**position) num = total + 620000 return str(num) raise ValueError(f"Invalid MPC code format: {code}")
[docs] def unpack_epoch(epoch_str: str) -> Time: """Convert a packed epoch string from the MPCORB format to an astropy Time object.""" century = epoch_str[0] if century == "I": year_prefix = "18" elif century == "J": year_prefix = "19" elif century == "K": year_prefix = "20" else: raise ValueError(f"Unknown century code: {century}") decade = epoch_str[1:3] if epoch_str[3].isdigit(): month = epoch_str[3] else: month = str(ord(epoch_str[3].lower()) - ord("a") + 10) if epoch_str[4].isdigit(): day = epoch_str[4] else: day = str(ord(epoch_str[4].lower()) - ord("a") + 10) date = Time( f"{year_prefix}{decade}-{month.zfill(2)}-{day.zfill(2)}", format="iso", scale="utc", ) if len(epoch_str) == 5: return date elif len(epoch_str) > 5: day_frac = float(epoch_str[5:]) return date + day_frac * u.day