Source code for rubin_sim.sim_archive.prenightindex

__all__ = ["get_prenight_index", "select_latest_prenight_sim", "get_sim_uuid", "get_sim_metadata"]

import logging
import os
from datetime import date
from typing import Any
from uuid import UUID

import numpy as np
import pandas as pd
from lsst.resources import ResourcePath

if "PRENIGHT_INDEX_URL" in os.environ:
    PRENIGHT_INDEX_URL = os.environ["PRENIGHT_INDEX_URL"]
else:
    from rubin_sim.sim_archive.vseqarchive import PRENIGHT_INDEX_URL

from rubin_sim.sim_archive.vseqmetadata import VisitSequenceArchiveMetadata

from .util import dayobs_to_date

MAX_AGE = 365
LOGGER = logging.getLogger(__name__)
TELESCOPES = ("simonyi", "auxtel")


def get_prenight_index_from_database(
    day_obs: str | int | date,
    telescope: str = "simonyi",
    schema: str | None = None,
    host: str | None = None,
    user: str | None = None,
    database: str | None = None,
) -> pd.DataFrame:
    """
    Retrieve the pre‑night observation index for a given night from the
    LSST SIM Archive.

    Parameters
    ----------
    day_obs : `str` or `int` or `date`
        The night for which to fetch the index.  Accepts a date string,
        YYYYMMDD encoded into an integer, or a :class:`datetime.date`
    telescope : `str`, optional
        Telescope name (default ``"simonyi"``).  Passed directly to
        :class:`~rubin_sim.sim_archive.vseqmetadata.VisitSequenceArchiveMetadata`.
    schema : `str` or `None`, optional
        Optional database schema name.
    host : `str` or `None`, optional
        Database host address.
    user : `str` or `None`, optional
        Database user name.
    database : `str` or `None`, optional
        Database name.

    Returns
    -------
    prenights: `pandas.DataFrame`
        A DataFrame indexed by ``visitseq_uuid`` containing all pre‑night
        entries for the requested night.  Columns mirror those returned
        by the underlying `sims_on_nights` call.

    Notes
    -----
    * The function uses a hard‑coded maximum simulation age of
      :data:`MAX_AGE` (365 days) to limit the search.
    * If any of ``host``, ``user`` or ``database`` are supplied, they
      override the defaults in the ``metadata_dsn`` dictionary passed to
      :class:`~rubin_sim.sim_archive.vseqmetadata.VisitSequenceArchiveMetadata`.
    """

    day_obs_date = dayobs_to_date(day_obs)
    assert isinstance(day_obs_date, date)

    metadata_dsn = {}
    if host is not None:
        metadata_dsn["host"] = host
    if user is not None:
        metadata_dsn["user"] = user
    if database is not None:
        metadata_dsn["database"] = database

    visit_seq_archive_metadata = VisitSequenceArchiveMetadata(metadata_dsn, schema)
    prenights = visit_seq_archive_metadata.sims_on_nights(
        day_obs_date, day_obs_date, tags=["prenight"], telescope=telescope, max_simulation_age=MAX_AGE
    ).set_index("visitseq_uuid")
    LOGGER.info(f"Got metadata on {len(prenights)} simulations from database")
    return prenights


def get_prenight_index_from_bucket(
    day_obs: str | int | date,
    telescope: str = "simonyi",
    prenight_index_path: str | ResourcePath = PRENIGHT_INDEX_URL,
) -> pd.DataFrame:
    """
    Load the pre‑night observation index for a given night from a remote
    bucket.

    Parameters
    ----------
    day_obs : `str` or `int` or `date`
        The night for which to fetch the index.  Accepts a date string,
        YYYYMMDD encoded into an integer, a ``datetime.date``
    telescope : `str`, optional
        Telescope name (default ``"simonyi"``).
    prenight_index_path : `str` or `ResourcePath`, optional
        The root path where the pre‑night index
        JSON files are stored.  It can be a string URL or a
        ``ResourcePath`` instance.

    Returns
    -------
    prenights : `pandas.DataFrame`
        A DataFrame indexed by the JSON key (``visitseq_uuid``) containing
        all pre‑night entries for the requested night.  The columns match
        those stored in the JSON file.
    """
    day_obs_date = dayobs_to_date(day_obs)
    assert isinstance(day_obs_date, date)

    prenight_index_path = ResourcePath(prenight_index_path, forceDirectory=True)
    assert isinstance(prenight_index_path, ResourcePath)

    year = day_obs_date.year
    month = day_obs_date.month
    isodate = day_obs_date.isoformat()

    prenight_index_resource_path = (
        prenight_index_path.join(telescope)
        .join(str(year))
        .join(str(month))
        .join(f"{telescope}_prenights_for_{isodate}.json")
    )
    with prenight_index_resource_path.as_local() as local_resource_path:
        prenights = pd.read_json(local_resource_path.ospath, orient="index")

    prenights.index.name = "visitseq_uuid"

    # Dates in json are loaded into strings, but in the
    # DB query code they are returned as instances of
    # datetime.date.
    # Convert correctly formatted date strings to datetime.date
    # so they come back the same regardless of origin.
    date_keys = ("sim_creation_day_obs", "first_day_obs", "last_day_obs", "parent_last_day_obs")
    for key in date_keys:
        replacement_values = None
        try:
            replacement_values = pd.to_datetime(prenights[key]).dt.date
        except ValueError, KeyError:
            pass
        if replacement_values is not None:
            prenights[key] = replacement_values

    return prenights


[docs] def get_prenight_index( day_obs: str | int | date, telescope: str = "simonyi", schema: str | None = None, host: str | None = None, user: str | None = None, database: str | None = None, prenight_index_path: str | ResourcePath | None = None, ) -> pd.DataFrame: """Retrieve the pre‑night observation index for a given night. Parameters ---------- day_obs : `str` or `int` or `date` Night for which to fetch the index. The function will return all simulations that simulate the night designated by the day_obs. telescope : `str` Telescope name (default ``simonyi``). schema : `str` or `None`, optional Optional database schema name. host : `str` or `None`, optional Database host address. user : `str` or `None`, optional Database user name. database : `str` or `None`, optional Database name. prenight_index_path : `str` or `ResourcePath`, optional Root path to the bucket files (used only if the database call fails). Returns ------- prenights: `pd.DataFrame` Pre‑night index indexed by ``visitseq_uuid``. """ try: # Try the database first prenights = get_prenight_index_from_database(day_obs, telescope, schema, host, user, database) except Exception: # Fall back to the bucket LOGGER.info("Database not accessible, falling back on the index in the S3 bucket.") if prenight_index_path is None: prenight_index_path = PRENIGHT_INDEX_URL try: prenights = get_prenight_index_from_bucket(day_obs, telescope, prenight_index_path) except FileNotFoundError: prenights = pd.DataFrame() return prenights
[docs] def get_sim_uuid( sim_date: date, daily_id: int | str, day_obs: int | date | str | None = None, vsmd: VisitSequenceArchiveMetadata | None = None, **kwargs: Any, ) -> UUID: """Get the UUID of a simulation given its observation night, creation date, and daily ID. Parameters ---------- sim_date : `date` The creation date of the simulation to find. daily_id : `int` or `str` The daily ID of the simulation to find. The daily ID is an index of simulations in the metadata added at a given sim_date: A simulation with ``sim_date`` of 2026-04-28 and ``daily_id`` of 4 is the 4th simulation added to the metadata database with a creation date of 2026-04-28. The combination ``sim_date`` and ``daily_id`` uniquely identifies a simulation in a given metadata database, but unlike UUID is not guaranteed to be unique across different metadata databases. day_obs : `int` or `str` or `date` or `None` The dayobs of a night simulated by the simulation. Can be a date string, YYYYMMDD encoded into an integer, or a `datetime.date` object. The date rollover follows SITCOMTN-032 (-12hr timezone). If ``None``, the metadata database must be used. vsmd : `VisitSequenceArchiveMetadata` or `None` The interface to the visit sequence metadata database to query. **kwargs Additional keyword arguments passed to :func:`get_prenight_index`. Returns ------- uuid : `uuid.UUID` The UUID of the matching simulation. Raises ------ ValueError If no simulation is found matching the specified criteria. """ if day_obs is None or vsmd is not None: if vsmd is None: # If an instance of VisitSequenceArchiveMetada is not # provided, make one with default configuration. vsmd = VisitSequenceArchiveMetadata() sim_uuid = vsmd.get_sim_uuid(sim_date, daily_id) else: sim_uuid = _get_sim_uuid_from_metadata_index(day_obs, sim_date, daily_id, **kwargs) return sim_uuid
def _get_sim_uuid_from_metadata_index( day_obs: int | date | str, sim_date: date, daily_id: int | str, **kwargs: Any ) -> UUID: """Get the UUID of a simulation given its observation night, creation date, and daily ID. Parameters ---------- day_obs : `int` or `str` or `date` The observation night for which to search for the simulation. Can be a date string, YYYYMMDD encoded into an integer, or a `datetime.date` object. The date rollover follows SITCOMTN-032 (-12hr timezone). This can be any night included in the simulation. sim_date : `date` The creation date of the simulation to find. daily_id : `int` or `str` The daily ID of the simulation to find. **kwargs Additional keyword arguments passed to :func:`get_prenight_index`. Returns ------- uuid : `uuid.UUID` The UUID of the matching simulation. Raises ------ ValueError If no simulation is found matching the specified criteria. """ # Accept ISO style string for day_obs if isinstance(day_obs, str): day_obs = day_obs.replace("-", "") day_obs = int( f"{day_obs.year:04d}{day_obs.month:02d}{day_obs.day:02d}" if isinstance(day_obs, date) else day_obs ) assert isinstance(day_obs, int) maybe_uuid: UUID | None = None for telescope in TELESCOPES: prenight_index = get_prenight_index(day_obs, telescope, **kwargs) matching_sims = (prenight_index.sim_creation_day_obs == sim_date) & ( prenight_index.daily_id == int(daily_id) ) if np.any(matching_sims): matching_uuids = prenight_index.index[matching_sims].values assert matching_uuids.shape == (1,) maybe_uuid = UUID(str(matching_uuids.item())) if maybe_uuid is None: raise ValueError(f"No simulation found for {sim_date}, {daily_id}") assert isinstance(maybe_uuid, UUID) return maybe_uuid
[docs] def get_sim_metadata( visitseq_uuid: UUID | str, day_obs: int | date | str | None = None, vsmd: VisitSequenceArchiveMetadata | None = None, **kwargs: Any, ) -> pd.Series: """Get metadata for a simulation given its observation night and UUID. Parameters ---------- visitseq_uuid : `uuid.UUID` or `str` The UUID of the simulation to retrieve information for. day_obs : `int` or `str` or `date` or `None` The observation night for which to search for the simulation. Can be a date string, YYYYMMDD encoded into an integer, or a :class:`datetime.date` object. The date rollover follows SITCOMTN-032 (-12hr timezone). This can be any night included in the simulation. **kwargs Additional keyword arguments passed to :func:`get_prenight_index`. Returns ------- info : `pandas.Series` A Series containing the index information for the simulation. Raises ------ ValueError If no simulation with the given UUID is found, or if multiple simulations are found with the same UUID (indicating a problem in the metadata database). """ sim_metadata: pd.Series if day_obs is None: if vsmd is None: # If an instance of VisitSequenceArchiveMetada is not # provided, make one with default configuration. vsmd = VisitSequenceArchiveMetadata() sim_metadata = vsmd.get_visitseq_metadata(visitseq_uuid, "simulations_extra") else: sim_metadata = _get_sim_metadata_from_metadata_index(day_obs, visitseq_uuid, **kwargs) return sim_metadata
def _get_sim_metadata_from_metadata_index( day_obs: int | date | str, visitseq_uuid: UUID | str, **kwargs: Any ) -> pd.Series: """Get metadata for a simulation given its observation night and UUID. Parameters ---------- day_obs : `int` or `str` or `date` The observation night for which to search for the simulation. Can be a date string, YYYYMMDD encoded into an integer, or a :class:`datetime.date` object. The date rollover follows SITCOMTN-032 (-12hr timezone). visitseq_uuid : `uuid.UUID` or `str` The UUID of the simulation to retrieve information for. **kwargs Additional keyword arguments passed to :func:`get_prenight_index`. Returns ------- info : `pandas.Series` A Series containing the index information for the simulation. Raises ------ ValueError If no simulation with the given UUID is found, or if multiple simulations are found with the same UUID (indicating a problem in the metadata database). """ day_obs = int( f"{day_obs.year:04d}{day_obs.month:02d}{day_obs.day:02d}" if isinstance(day_obs, date) else day_obs ) visitseq_uuid_str: str | None = None if isinstance(visitseq_uuid, str): assert isinstance(visitseq_uuid, str) visitseq_uuid_str = visitseq_uuid visitseq_uuid = UUID(visitseq_uuid) else: visitseq_uuid_str = str(visitseq_uuid) assert isinstance(visitseq_uuid_str, str) assert isinstance(visitseq_uuid, UUID) maybe_sim_index_info: None | pd.Series | pd.DataFrame = None for telescope in TELESCOPES: prenight_index = get_prenight_index(day_obs, telescope, **kwargs) if len(prenight_index) > 0 and not isinstance(prenight_index.index[0], str): prenight_index.index = prenight_index.index.map(str) if visitseq_uuid_str in prenight_index.index: maybe_sim_index_info = prenight_index.loc[visitseq_uuid_str, :] maybe_sim_index_info[prenight_index.index.name] = visitseq_uuid break if isinstance(maybe_sim_index_info, pd.DataFrame): raise ValueError( f"Multiple sims for UUID {visitseq_uuid}: there is a problem in the sim metadata database!" ) if maybe_sim_index_info is None: raise ValueError(f"No sim with UUID {visitseq_uuid} found") assert isinstance(maybe_sim_index_info, pd.Series) return maybe_sim_index_info
[docs] def select_latest_prenight_sim( prenights: pd.DataFrame, tags: tuple[str, ...] = ("ideal", "nominal"), max_simulation_age: int = 2, ) -> dict | None: """Select the best prenight simulation from a DataFrame. Parameters ---------- prenights: `pd.DataFrame` The table of prenights from which to select. tags : `tuple` [`str`] A tuple of tags to filter simulations by. Defaults to ``('ideal', 'nominal')``. max_simulation_age : `int` The maximum age of simulations to consider, in days. Simulations older than ``max_simulation_age`` will not be considered. Defaults to 2. Returns ------- best_sim : `dict` or `None` A dictionary with metadata for the simulation. """ best_sim = None for _, sim in prenights.reset_index().iterrows(): if not set(tags).issubset(sim["tags"]): continue if best_sim is not None: if sim["creation_time"] < best_sim["creation_time"]: continue age = (pd.Timestamp.now(tz="UTC") - sim["creation_time"]).days if age > max_simulation_age: continue best_sim = sim if best_sim is not None: LOGGER.info(f"Most recent simulation meeting requested criteria is {best_sim['visitseq_url']}.") else: LOGGER.debug("No simulations met the requested criteria.") return best_sim