diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f86fe6..9ffd530 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,18 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Added + +- Added support for loading orbit information from JPL Horizons + +### Changed + +- Changed the return signature for `fov_static_check`, this now returns indices of + the inputs, instead of the original inputs themselves. + + ## [v1.0.2] ### Added diff --git a/src/kete/horizons.py b/src/kete/horizons.py index 24d1d87..bfc8915 100644 --- a/src/kete/horizons.py +++ b/src/kete/horizons.py @@ -1,10 +1,17 @@ +""" +Interface functions and classes to JPL Horizons web services. +""" + +import gzip import requests import os from typing import Union, Optional +from functools import lru_cache import base64 import json import numpy as np +import pandas as pd from ._core import HorizonsProperties, Covariance, NonGravModel, CometElements from .mpc import unpack_designation, pack_designation @@ -12,7 +19,7 @@ from .cache import cache_path from .covariance import generate_sample_from_cov -__all__ = ["HorizonsProperties", "fetch_spice_kernel"] +__all__ = ["HorizonsProperties", "fetch_spice_kernel", "fetch_known_orbit_data"] _PARAM_MAP = { @@ -442,3 +449,56 @@ def fetch_spice_kernel( with open(filename, "wb") as f: f.write(base64.b64decode(response.json()["spk"])) + + +@lru_cache() +def fetch_known_orbit_data(update_cache=False): + """ + Fetch the known orbit data from JPL Horizons for all known asteroids and comets. + + This gets loaded as a pandas table, and if the file already exists in cache, then + the contents of this file are returned by default. + + The constructed pandas table may be turned into states using the + :func:`~kete.mpc.table_to_states` function. + + Parameters + ========== + update_cache : + Force download a new file from JPL Horizons, this can be used to update the + orbit fits which are currently saved. + """ + filename = os.path.join(cache_path(), "horizons_orbits.json.gz") + if update_cache or not os.path.isfile(filename): + res = requests.get( + ( + "https://ssd-api.jpl.nasa.gov/sbdb_query.api?fields=" + "pdes,spkid,orbit_id,rms,H,diameter,epoch,e,i,q,w,tp,om,A1,A2,A3,DT" + "&full-prec=1&sb-xfrag=1" + ) + ) + res.raise_for_status() + with gzip.open(filename, "wb") as f: + f.write(res.content) + file_contents = res.json() + else: + with gzip.open(filename, "rb") as f: + file_contents = json.load(f) + columns = file_contents["fields"] + + # relabel some of the columns so that they match the contents of the MPC orbit file + # this allows user to reuse the table_to_state function in mpc.py + lookup = { + "e": "ecc", + "i": "incl", + "q": "peri_dist", + "w": "peri_arg", + "tp": "peri_time", + "om": "lon_node", + "pdes": "desig", + } + columns = [lookup.get(c, c) for c in columns] + table = pd.DataFrame.from_records(file_contents["data"], columns=columns) + others = table.columns.difference(["desig", "spkid", "orbit_id"]) + table[others] = table[others].apply(pd.to_numeric, errors="coerce") + return table diff --git a/src/kete/mpc.py b/src/kete/mpc.py index 06d5f57..a5973f5 100644 --- a/src/kete/mpc.py +++ b/src/kete/mpc.py @@ -14,7 +14,26 @@ from . import _core -__all__ = ["unpack_designation", "pack_designation", "find_obs_code"] +__all__ = [ + "unpack_designation", + "pack_designation", + "fetch_known_packed_designations", + "fetch_known_designations", + "fetch_known_orbit_data", + "fetch_known_comet_orbit_data", + "find_obs_code", + "table_to_states", + "unpack_permanent_designation", + "pack_permanent_designation", + "unpack_provisional_designation", + "pack_provisional_designation", + "unpack_satellite_designation", + "pack_satellite_designation", + "unpack_comet_designation", + "pack_comet_designation", + "normalize_names", +] + _mpc_hex = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" logger = logging.getLogger(__name__) diff --git a/src/kete/rust/fovs/checks.rs b/src/kete/rust/fovs/checks.rs index 07dc760..6b0a9b7 100644 --- a/src/kete/rust/fovs/checks.rs +++ b/src/kete/rust/fovs/checks.rs @@ -3,10 +3,7 @@ use kete_core::{fov::FOV, propagation::propagate_n_body_spk}; use pyo3::prelude::*; use rayon::prelude::*; -use crate::{ - simult_states::PySimultaneousStates, - vector::{Vector, VectorLike}, -}; +use crate::{simult_states::PySimultaneousStates, vector::VectorLike}; /// Given states and field of view, return only the objects which are visible to the /// observer, adding a correction for optical light delay. @@ -135,15 +132,20 @@ pub fn fov_spk_checks_py(obj_ids: Vec, fovs: FOVListLike) -> Vec, fovs: FOVListLike) -> Vec, fovs: FOVListLike, -) -> Vec<(Vec, AllowedFOV)> { +) -> Vec<(Vec, AllowedFOV)> { let fovs = fovs.into_sorted_vec_fov(); let pos: Vec<_> = pos .into_iter() @@ -163,15 +165,7 @@ pub fn fov_static_checks_py( let vis: Vec<_> = fov .check_statics(&pos) .into_iter() - .filter_map(|pop| { - pop.map(|(p_vec, fov)| { - let p_vec = p_vec - .into_iter() - .map(|p| Vector::new(p.into(), crate::frame::PyFrames::Ecliptic)) - .collect(); - (p_vec, fov.into()) - }) - }) + .filter_map(|pop| pop.map(|(p_vec, fov)| (p_vec, fov.into()))) .collect(); match vis.is_empty() { true => None, diff --git a/src/kete_core/src/fov/fov_like.rs b/src/kete_core/src/fov/fov_like.rs index 0edeaca..8db6401 100644 --- a/src/kete_core/src/fov/fov_like.rs +++ b/src/kete_core/src/fov/fov_like.rs @@ -225,21 +225,16 @@ pub trait FovLike: Sync + Sized { .collect() } - /// Given a collection of static positions, return all which are visible. - fn check_statics(&self, pos: &[Vector3]) -> Vec>, FOV)>> { - let mut visible: Vec>> = vec![Vec::new(); self.n_patches()]; - - let vis_pos: Vec<_> = pos - .iter() - .filter_map(|p| match self.check_static(p) { - (idx, Contains::Inside) => Some((idx, p)), - _ => None, - }) - .collect(); - - vis_pos - .into_iter() - .for_each(|(patch_idx, p)| visible[patch_idx].push(*p)); + /// Given a collection of static positions, return the index of the input vector + /// which was visible. + fn check_statics(&self, pos: &[Vector3]) -> Vec, FOV)>> { + let mut visible: Vec> = vec![Vec::new(); self.n_patches()]; + + pos.iter().enumerate().for_each(|(vec_idx, p)| { + if let (patch_idx, Contains::Inside) = self.check_static(p) { + visible[patch_idx].push(vec_idx) + } + }); visible .into_iter() diff --git a/src/kete_core/src/fov/mod.rs b/src/kete_core/src/fov/mod.rs index 561d42f..21e1d2d 100644 --- a/src/kete_core/src/fov/mod.rs +++ b/src/kete_core/src/fov/mod.rs @@ -98,7 +98,7 @@ impl FOV { /// Check if static sources are visible in this FOV. /// Position must be in the correct frame! - pub fn check_statics(&self, pos: &[Vector3]) -> Vec>, FOV)>> { + pub fn check_statics(&self, pos: &[Vector3]) -> Vec, FOV)>> { match self { FOV::Wise(fov) => fov.check_statics(pos), FOV::NeosCmos(fov) => fov.check_statics(pos),