diff --git a/data/targets.csv b/data/targets.csv deleted file mode 100644 index d595877..0000000 --- a/data/targets.csv +++ /dev/null @@ -1,3 +0,0 @@ -# target,start_time,end_time,step -Ceres,2024-01-01 00:00,2025-12-31 23:59,1h -2,2024-01-01 00:00,2024-06-30 23:59,1h diff --git a/src/forcedphot/horizons_interface.py b/src/forcedphot/horizons_interface.py index 52cdd46..7d104d9 100644 --- a/src/forcedphot/horizons_interface.py +++ b/src/forcedphot/horizons_interface.py @@ -1,14 +1,37 @@ -import numpy as np import logging import time -import pandas as pd + import astropy.units as u +import numpy as np +import pandas as pd from astropy.time import Time from astroquery.jplhorizons import Horizons +from local_dataclasses import EphemerisData, QueryInput, QueryResult -from local_dataclasses import QueryInput, EphemerisData, QueryResult class HorizonsInterface: + """ + A class for querying ephemeris data from JPL Horizons for celestial objects. + + This class provides methods to query ephemeris data for single time ranges + or multiple objects from a CSV file. It uses the astropy Horizons module + to interact with the JPL Horizons system. + + Attributes: + DEFAULT_OBSERVER_LOCATION (str): The default observer location code ('X05' for Rubin). + logger (logging.Logger): Logger for the class. + observer_location (str): The observer location code used for queries. + + Methods: + query_single_range(query: QueryInput) -> QueryResult: + Query ephemeris for a single time range. + + query_ephemeris_from_csv(csv_filename: str, observer_location=DEFAULT_OBSERVER_LOCATION): + Query ephemeris for multiple celestial objects from a CSV file and save results to ECSV files. + + The class handles large queries by splitting them into smaller time ranges + when necessary to avoid exceeding JPL Horizons' limit of 10,000 instances per query.""" + # Set up logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) @@ -21,7 +44,11 @@ def __init__(self, observer_location=DEFAULT_OBSERVER_LOCATION): Initialize the HorizonsInterface with an optional observer location. Parameters: - observer_location (str): The observer location code. Default is 'X05' (Rubin location). + ---------- + self : HorizonsInterface + The instance of the class being initialized. + observer_location : str, optional + The observer location code. Default is 'X05' (Rubin location). """ self.observer_location = observer_location @@ -30,20 +57,33 @@ def query_single_range(self, query: QueryInput) -> QueryResult: """ Query ephemeris for a single time range. - Parameters: - query (QueryInput): The query parameters. - - Returns: - QueryResult: The queried ephemeris data. + Parameters + ---------- + self : HorizonsInterface + The instance of the class calling this method. + query : QueryInput + he query parameters containing target, start time, end time, and step. + + Returns + ------- + QueryResult or None + The queried ephemeris data wrapped in a QueryResult object if successful, + or None if an error occurs. + + Raises + ------ + Exception + If an error occurs during the query process. The error is logged, + but not re-raised. """ try: start_time = time.time() - obj = Horizons(id_type='smallbody', id=query.target, location=self.observer_location, + obj = Horizons(id_type='smallbody', id=query.target, location=self.observer_location, epochs={'start': query.start.iso, 'stop': query.end.iso, 'step': query.step}) ephemeris = obj.ephemerides() end_time = time.time() - self.logger.info(f"Query for range {query.start} to {query.end} successful for target \ - {query.target}. Time taken: {end_time - start_time:.2f} seconds.") + self.logger.info(f"Query for range {query.start} to {query.end} successful for target" + f"{query.target}. Time taken: {end_time - start_time:.2f} seconds.") ephemeris_data = EphemerisData( datetime_jd=Time(ephemeris['datetime_jd'], format='jd'), @@ -63,17 +103,42 @@ def query_single_range(self, query: QueryInput) -> QueryResult: return QueryResult(query.target, query.start, query.end, ephemeris_data) except Exception as e: - self.logger.error(f"An error occurred during query for range {query.start} to {query.end} for target {query.target}: {e}") + self.logger.error(f"An error occurred during query for range {query.start} to {query.end}" + f"for target {query.target}: {e}") return None - + @classmethod def query_ephemeris_from_csv(cls, csv_filename: str, observer_location=DEFAULT_OBSERVER_LOCATION): """ - Query ephemeris for multiple celestial objects from JPL Horizons based on a CSV file and save the data to ECSV files. - - Parameters: - csv_filename (str): The filename of the input CSV file containing target, start time, end time, and step. - observer_location (str): The observer location code. Default is 'X05' (Rubin location). + Query ephemeris for multiple celestial objects from JPL Horizons based on a CSV file and save + the data to CSV files. + + Parameters + ---------- + cls : type + The class itself, as this is a class method. + csv_filename : str + The filename of the input CSV file containing target, start time, end time, and step. + observer_location : str, optional + The observer location code. Default is 'X05' (Rubin location). + + Returns + ------- + None + This method doesn't return any value, but saves the queried data to CSV files. + + Raises + ------ + Exception + If an error occurs during the CSV processing or querying. The error is logged, + but not re-raised. + + Notes + ----- + - The input CSV file should have columns for target, start time, end time, and step. + - The method creates a separate CSV file for each target in the input file. + - If a query would exceed 10,000 instances, it is automatically split into multiple queries. + - The method logs information about the query process and any errors that occur. """ try: total_start_time = time.time() @@ -94,14 +159,15 @@ def query_ephemeris_from_csv(cls, csv_filename: str, observer_location=DEFAULT_O # Calculate the total number of instances total_days = (query.end - query.start).jd - step_hours = float(query.step[:-1]) - step_days = step_hours / 24.0 + step_hours = float(query.step[:-1]) + step_days = step_hours / 24.0 max_instances = 10000 step_instances = total_days / step_days # Check if multiple queries are needed if step_instances > max_instances: - cls.logger.info(f"Total instances exceed 10,000 for target {query.target}. Splitting the queries.") + cls.logger.info(f"Total instances exceed 10,000 for target {query.target}. Splitting" + f"the queries.") time_splits = int(step_instances // max_instances) + 1 time_ranges = [(query.start + i * (total_days / time_splits) * u.day, @@ -155,14 +221,15 @@ def query_ephemeris_from_csv(cls, csv_filename: str, observer_location=DEFAULT_O cls.logger.info(f"Ephemeris data successfully saved to {output_filename}") total_end_time = time.time() - cls.logger.info(f"Total time taken for processing the CSV file: {total_end_time - total_start_time:.2f} seconds.") + cls.logger.info(f"Total time taken for processing the CSV file:" + f"{total_end_time - total_start_time:.2f} seconds.") except Exception as e: cls.logger.error(f"An error occurred while processing the CSV file: {e}") # Example usage if __name__ == "__main__": - HorizonsInterface.query_ephemeris_from_csv('targets.csv') + HorizonsInterface.query_ephemeris_from_csv('./data/targets.csv') # Different observer location - # HorizonsInterface.query_ephemeris_from_csv('targets.csv', observer_location='500') # Example: Geocentric \ No newline at end of file + # HorizonsInterface.query_ephemeris_from_csv('targets.csv', observer_location='500')