Source code for rubin_sim.sim_archive.vseqarchive

import datetime
import hashlib
import logging
import os
from pathlib import Path
from tempfile import TemporaryDirectory
from typing import Sequence, Tuple
from uuid import UUID

import click
import pandas as pd
from astropy.time import Time
from lsst.resources import ResourcePath

from rubin_sim.maf.stackers import BaseStacker

from .util import compute_conda_env, dayobs_to_date, hdf5_to_opsimdb, opsimdb_to_hdf5
from .vseqmetadata import VSARCHIVE_PGSCHEMA, VisitSequenceArchiveMetadata

__all__ = [
    "compute_nightly_stats",
    "get_visits",
    "record_visitseq_metadata",
    "set_visitseq_url",
    "get_visitseq_url",
    "update_visitseq_metadata",
    "is_tagged",
    "tag",
    "untag",
    "comment",
    "get_comments",
    "archive_file",
    "get_file",
    "add_nightly_stats",
    "query_nightly_stats",
    "record_conda_env",
    "import_proto",
    "export_proto",
    "ARCHIVE_URL",
    "PRENIGHT_INDEX_URL",
]

ARCHIVE_URL = "s3://rubin:rubin-scheduler-prenight/opsim/vseq/"
PRENIGHT_INDEX_URL = "s3://rubin:rubin-scheduler-prenight/opsim/prenight_index/"
SQLITE_EXTINSIONS = {".db", ".sqlite", ".sqlite3", ".db3"}

LOGGER = logging.getLogger(__name__)

#
# Computation
#


def compute_nightly_stats(
    visits: pd.DataFrame, columns: Tuple[str, ...] = ("altitude", "azimuth")
) -> pd.DataFrame:
    if "day_obs" not in visits.columns:
        # Pandas seems to work better with type hinding that astropy Time
        start_mjd_field = ""
        for candidate_field in ["obs_start_mjd", "observationStartMJD"]:
            if candidate_field in visits.columns:
                start_mjd_field = candidate_field
                break
        if len(start_mjd_field) == 0:
            raise ValueError("No day_obs or start mjd field found")

        times = pd.to_datetime(visits.loc[:, start_mjd_field] + 2400000, unit="D", origin="julian")
        visits = visits.assign(day_obs=pd.Series(times).dt.date)

    stats_df = (
        visits.groupby("day_obs")[list(columns)]
        .describe(percentiles=[0.05, 0.25, 0.5, 0.75, 0.95])
        .stack(level=0, future_stack=True)
        .reset_index()
        .rename(
            columns={
                "level_1": "value_name",
                "5%": "p05",
                "25%": "q1",
                "50%": "median",
                "75%": "q3",
                "95%": "p95",
            }
        )
    )
    stats_df["accumulated"] = False
    return stats_df


def construct_base_resource_path(
    archive_base: ResourcePath, telescope: str, creation_time: Time, visitseq_uuid: UUID
) -> ResourcePath:
    """Build the base ``ResourcePath`` for a visit‑sequence
    archive entry.

    Parameters
    ----------
    archive_base : `ResourcePath`
        The base of the archive relative to which the visit sequence
        will be added.
    telescope : `str`
        Identifier for the telescope (e.g. ``"simonyi"`` or ``"auxtel"`).
    creation_time : `Time`
        The timestamp when the visit sequence was created.
    visitseq_uuid : `UUID`
        The unique identifier of the visit sequence.

    Returns
    -------
    ResourcePath
        A ``ResourcePath`` instance representing the base directory for
        this visit sequence.  The directory is structured as:

        ``<archive_base>/<telescope>/<date>/<visitseq_uuid>/``

        where ``<date>`` is the UTC‑12 date derived from ``creation_time``.
    """
    creation_time_iso_dayobs = dayobs_to_date(Time(creation_time.mjd, format="mjd")).isoformat()
    visitseq_base_rp = (
        archive_base.join(telescope)
        .join(creation_time_iso_dayobs)
        .join(str(visitseq_uuid), forceDirectory=True)
    )
    return visitseq_base_rp


#
# Interaction with the file store
#


def _write_data_to_archive(content: bytes, visitseq_base_rp: ResourcePath, file_name: str) -> ResourcePath:
    # Make sure the base for the visit sequence exists
    if not visitseq_base_rp.exists():
        visitseq_base_rp.mkdir()
        LOGGER.debug(f"Created {visitseq_base_rp}.")

    destination_rp = visitseq_base_rp.join(file_name)
    LOGGER.debug(f"Writing {destination_rp}.")
    destination_rp.write(content)
    LOGGER.debug(f"Wrote {destination_rp}.")
    return destination_rp


def _write_file_to_archive(
    origin: str | Path,
    visitseq_base_rp: ResourcePath,
) -> Tuple[ResourcePath, bytes]:
    file_path = origin if isinstance(origin, Path) else Path(origin)
    with open(file_path, "rb") as origin_io:
        content = origin_io.read()

    content_sha256 = bytes.fromhex(hashlib.sha256(content).hexdigest())
    destination_rp = _write_data_to_archive(content, visitseq_base_rp, file_path.name)
    return destination_rp, content_sha256


def add_file(
    vsarch: VisitSequenceArchiveMetadata,
    uuid: UUID,
    origin: str | Path,
    file_type: str,
    archive_base: str | ResourcePath,
    update: bool = False,
) -> ResourcePath:
    """Archive a file associated with a visit sequence and
    register its location in the metadata database.

    Parameters
    ----------
    vsarch : `VisitSequenceArchiveMetadata`
        Instance of `VisitSequenceArchiveMetadata` used to
        query the sequence metadata and register the file.
    uuid : `UUID`
        The unique identifier of the visit sequence that the file
        should be linked to.
    origin : `str` or `pathlib.Path`
        Path to the local file to be archived.
    file_type : `str`
        Identifier for the type of file being archived.
    archive_base : `lsst.resources.ResourcePath`
        Base location of the archive.
    update : `bool`, optional
        If ``True`` and a record for the same ``visitseq_uuid`` and
        ``file_type`` already exists, the existing row will be
        updated with the new SHA‑256 hash and URL.  If ``False`` a
        `ValueError` will be raised on duplicate.

    Returns
    -------
    file_rp : `lsst.resources.ResourcePath`
        The `ResourcePath` pointing to the archived file
        on the file store.
    """
    if isinstance(archive_base, str):
        archive_base = ResourcePath(archive_base)
    assert isinstance(archive_base, ResourcePath)

    visitseq_metadata = vsarch.get_visitseq_metadata(uuid)
    uuid = visitseq_metadata["visitseq_uuid"]
    telescope = visitseq_metadata["telescope"]
    creation_time = Time(visitseq_metadata["creation_time"])
    visitseq_base_rp = construct_base_resource_path(archive_base, telescope, creation_time, uuid)

    # If we are adding a file that looks like opsim sqlite3 output but
    # specify the file type as visits, convert to hdf5.
    if file_type == "visits" and Path(origin).suffix.lower() in SQLITE_EXTINSIONS:
        with TemporaryDirectory() as temp_dir:
            new_origin = Path(temp_dir).joinpath("visits.h5")
            opsimdb_to_hdf5(origin, new_origin)
            location = add_file(vsarch, uuid, new_origin, "visits", archive_base, update)
        return location

    location, file_sha256 = _write_file_to_archive(origin, visitseq_base_rp)
    if file_type == "visits":
        vsarch.set_visitseq_url(uuid, location.geturl())
    else:
        vsarch.register_file(uuid, file_type, file_sha256, location, update=update)

    return location


[docs] def get_visits( visits_url: str | ResourcePath, query: str = "", stackers: Sequence[BaseStacker] = (), ) -> pd.DataFrame: """Retrieve visits for a visit sequence. Parameters ---------- visits_url : `str` The origin of the visits. query : `str` A query in `pandas.DataFrame.query` syntax to select visits from the returned sequence. Applied before stackers are run. stackers : `Sequence` [`BaseStacker`] A sequence of maf stackers to apply. Returns ------- visits: `pd.DataFrame` The visits in the sequence. """ origin_rp = ResourcePath(visits_url) with TemporaryDirectory() as temp_dir: h5_destination = ResourcePath(temp_dir).join("visits.h5") h5_destination.transfer_from(origin_rp, "copy") visits = pd.read_hdf(h5_destination.ospath, key="observations") assert isinstance(visits, pd.DataFrame) if len(query) > 0: visits.query(query, inplace=True) if len(stackers) > 0: visit_records = visits.to_records() for stacker in stackers: visit_records = stacker.run(visit_records) visits = pd.DataFrame(visit_records) return visits
# # API # @click.group() @click.option( "--database", default=None, help="PostgreSQL database name of the metadata database", ) @click.option( "--host", default=None, help="PostgreSQL host address of the metadata database", ) @click.option( "--user", default=None, help="PostgreSQL user name to use to connect to the metadata database", ) @click.option( "--schema", default=None, help="Schema of the metadata database containing the visit‑sequence tables", ) @click.pass_context def vseqarchive( click_context: click.Context, database: str, host: str, user: str | None, schema: str, ) -> None: """visitseq command line interface.""" metadata_db_kwargs = {} if database is not None: metadata_db_kwargs["database"] = database if user is not None: metadata_db_kwargs["user"] = user if host is not None: metadata_db_kwargs["host"] = host if schema is None: if "VSARCHIVE_PGSCHEMA" in os.environ: schema = os.environ["VSARCHIVE_PGSCHEMA"] else: schema = VSARCHIVE_PGSCHEMA assert isinstance(schema, str) click_context.obj = VisitSequenceArchiveMetadata(metadata_db_kwargs, schema) @vseqarchive.command() @click.argument("table", type=str) @click.argument("visits_file", type=click.Path(exists=True)) @click.argument("label") @click.option( "--telescope", default="simonyi", show_default=True, help="Telescope identifier (e.g. simonyi or auxtel)", ) @click.option("--url", default=None, help="Optional URL for the visits file") @click.option("--first_day_obs", default=None, help="First night of observations (YYYY-MM-DD or int)") @click.option("--last_day_obs", default=None, help="Last night of observations (YYYY-MM-DD or int)") @click.option( "--creation_time", default=None, help="Creation time of the visit sequence in ISO format (e.g. 2025-01-01T00:00:00)", ) @click.pass_obj def record_visitseq_metadata( vsarch: VisitSequenceArchiveMetadata, table: str, visits_file: str, label: str, telescope: str, url: str | None, first_day_obs: str | int | None, last_day_obs: str | int | None, creation_time: str | None, ) -> None: """Record visit‑sequence metadata from an HDF5 visits file. Parameters ---------- vsarch : `VisitSequenceArchiveMetadata` The database interface instance. table : `str` Target table for the record (e.g. ``visitseq``, ``simulations``, ``completed`` or ``mixedvisitseq``). visits_file : `str` Path to the HDF5 file (or SQLite3 file) containing the ``visits`` dataset. label : `str` Human‑readable label for the visit sequence. telescope : `str`, optional Telescope identifier, default ``simonyi``. url : `str`, optional URL where the visits file can be downloaded. first_day_obs : `str` or `int`, optional First day of observation in SMTN‑032 format, ISO string, or ``YYYY‑MM‑DD``. last_day_obs : `str` or `int`, optional Last day of observation (same formats as ``first_day_obs``). creation_time : `str`, optional ISO‑formatted timestamp of when the sequence was created; if omitted, the current time is used. Returns ------- None The UUID of the inserted visit sequence is printed to stdout. """ if Path(visits_file).suffix.lower() in SQLITE_EXTINSIONS: with TemporaryDirectory() as temp_dir: hdf5_path = Path(temp_dir).joinpath("visits.h5") opsimdb_to_hdf5(visits_file, hdf5_path) visits_df = pd.read_hdf(str(hdf5_path), key="observations") else: visits_df = pd.read_hdf(visits_file, key="observations") assert isinstance(visits_df, pd.DataFrame) # Convert the optional creation time string to an astropy Time object creation_ap_time = Time(creation_time) if creation_time is not None else Time.now() # Record the visit sequence metadata visitseq_uuid = vsarch.record_visitseq_metadata( visits_df, label, telescope=telescope, table=table, url=url, first_day_obs=first_day_obs, last_day_obs=last_day_obs, creation_time=creation_ap_time, ) print(visitseq_uuid) @vseqarchive.command() @click.argument("uuid", type=click.UUID) @click.option( "--table", default="visitseq", show_default=True, help="Table to query (e.g. visitseq, simulations, completed, mixedvisitseq)", ) @click.pass_obj def get_visitseq_metadata(vsarch: VisitSequenceArchiveMetadata, uuid: UUID, table: str) -> None: """Retrieve and display metadata for a visit sequence. Parameters ---------- vsarch : `VisitSequenceArchiveMetadata` The metadata interface instance. uuid : `UUID` The UUID of the visit sequence to query. table : `str`, optional Name of the child table to search; one of ``visitseq``, ``simulations``, ``completed`` or ``mixedvisitseq``. Defaults to ``visitseq``. """ sequence_metadata = vsarch.get_visitseq_metadata(uuid, table=table) # Print the DataFrame as a tab‑separated table print(sequence_metadata.to_frame().T.to_csv(sep="\t", index=False).rstrip("\n")) @vseqarchive.command() @click.argument("uuid", type=click.UUID) @click.argument("url", type=click.STRING) @click.pass_obj def set_visitseq_url(vsarch: VisitSequenceArchiveMetadata, uuid: UUID, url: str) -> None: """Update the stored URL for a visit‑sequence file. Parameters ---------- vsarch : `VisitSequenceArchiveMetadata` The metadata interface instance. uuid : `UUID` The UUID of the visit sequence whose URL is to be updated. url : `str` The new URL pointing to the visits file. """ vsarch.set_visitseq_url(uuid, url) @vseqarchive.command() @click.argument("uuid", type=click.UUID) @click.pass_obj def get_visitseq_url(vsarch: VisitSequenceArchiveMetadata, uuid: UUID) -> None: """Retrieve and print the URL for the visits in a visit sequence. Parameters ---------- vsarch : `VisitSequenceArchiveMetadata` The metadata interface instance. uuid : `UUID` The UUID of the visit sequence whose URL is to be retrieved. """ url = vsarch.get_visitseq_url(uuid) click.echo(url) @vseqarchive.command() @click.argument("uuid", type=click.UUID) @click.argument("field", type=str) @click.argument("value", type=str) @click.pass_obj def update_visitseq_metadata( vsarch: VisitSequenceArchiveMetadata, uuid: UUID, field: str, value: str, ) -> None: """Update a single metadata field for a visit sequence. Parameters ---------- vsarch : `VisiteSequenceArchiveMetadata` An instance of the interface to the metadata archive. uuid : `UUID` The UUID of the visit sequence to update. field : `str` The name of the column in the metadata table to modify. value : `str` The new value for the field. Postgresql will cast the type to what is expected in the database schema. """ # psycopg2 usually converts strings to the right # type base on column type in the database, but if # hex values are provided for bytes colums it # gets it wrong. # So, if we're asking to update a bytes column, convert # it from hex ourselves. update_value: str | bytes = value if field.endswith("sha256"): update_value = bytes.fromhex(value) vsarch.update_visitseq_metadata(uuid, field, update_value) @vseqarchive.command() @click.argument("uuid", type=click.UUID) @click.argument("tag", type=str) @click.pass_obj def is_tagged(vsarch: VisitSequenceArchiveMetadata, uuid: UUID, tag: str) -> None: """Return whether a visit sequence is tagged with a given tag. The command prints ``true`` if the tag exists for the visit sequence and ``false`` otherwise. Parameters ---------- vsarch : `VisitSequenceArchiveMetadata` The interface to the metadata database. uuid : `UUID` The UUID of the visit sequence to check. tag : `str` The tag name to query. """ seq_is_tagged = vsarch.is_tagged(uuid, tag) if seq_is_tagged: click.echo("true") else: click.echo("false") @vseqarchive.command() @click.argument("uuid", type=click.UUID) @click.argument("tags", nargs=-1, required=True) @click.pass_obj def tag(vsarch: VisitSequenceArchiveMetadata, uuid: UUID, tags: Tuple[str, ...]) -> None: """Add one or more tags to a visit sequence. Parameters ---------- vsarch : `VisitSequenceArchiveMetadata` The interface to the metadata database. uuid : `UUID` The UUID of the visit sequence to tag. tags : `str` One or more tag names to add. """ if len(tags) < 1: raise ValueError("At least one tag must be requested.") vsarch.tag(uuid, *tags) @vseqarchive.command() @click.argument("uuid", type=click.UUID) @click.argument("tag", type=str) @click.pass_obj def untag(vsarch: VisitSequenceArchiveMetadata, uuid: UUID, tag: str) -> None: """Remove a tag from a visit sequence. Parameters ---------- vsarch : `VisitSequenceArchiveMetadata` The interface to the metadata database. uuid : `UUID` The UUID of the visit sequence to modify. tag : `str` The name of the tag to remove. """ vsarch.untag(uuid, tag) @vseqarchive.command() @click.argument("uuid", type=click.UUID) @click.argument("comment") @click.option( "--author", default=None, help="The author of the comment", ) @click.pass_obj def comment( vsarch: VisitSequenceArchiveMetadata, uuid: UUID, comment: str, author: str | None = None ) -> None: """Attach a comment to a visit sequence in the metadata database. Parameters ---------- vsarch : `VisitSequenceArchiveMetadata` The interface to the metadata database. uuid : `UUID` The UUID of the visit sequence to comment on. comment : `str` The comment text to attach. author : `str`, optional The author of the comment. If omitted the comment will be recorded without an author field. """ vsarch.comment(uuid, comment, author) @vseqarchive.command() @click.argument("uuid", type=click.UUID) @click.pass_obj def get_comments(vsarch: VisitSequenceArchiveMetadata, uuid: UUID) -> None: """Retrieve all comments attached to a specific visit sequence. Parameters ---------- uuid : `UUID` The UUID of the visit sequence whose comments should be fetched. """ comments_df = vsarch.get_comments(uuid) if not comments_df.empty: # Pandas will include the index by default; omit it. click.echo(comments_df.to_csv(sep="\t", index=False).rstrip("\n")) @vseqarchive.command() @click.argument("uuid", type=click.UUID) @click.argument("origin", type=click.Path(exists=True)) @click.argument("file_type", type=click.STRING) @click.option( "--archive-base", default=ARCHIVE_URL, show_default=True, help="Base directory for the archive (e.g. file://data/archive).", ) @click.option( "--update", is_flag=True, help="If set, overwrite an existing record for the same visitseq_uuid and file_type.", ) @click.pass_obj def archive_file( vsarch: VisitSequenceArchiveMetadata, uuid: UUID, origin: str, file_type: str, archive_base: str, update: bool, ) -> None: """Archive a file and register its location in the metadata database. Parameters ---------- vsarch : `VisitSequenceArchiveMetadata` The metadata interface instance. uuid : `UUID` The UUID of the visit sequence that the file should be linked to. origin : `str` Path to the local file to be archived. file_type : `str` Identifier for the type of file being archived. archive_base : `str`, optional Base location of the archive. update : `bool`, optional If true, an existing record for the same ``visitseq_uuid``/``file_type`` combination is updated rather than raising an error. """ # Convert the base path string into a ResourcePath archive_base_rp = ResourcePath(archive_base) archived_location = add_file(vsarch, uuid, origin, file_type, archive_base_rp, update=update) click.echo(archived_location.geturl()) @vseqarchive.command() @click.argument("uuid", type=click.UUID) @click.argument("file_type", type=click.STRING) @click.argument("destination", type=click.Path(exists=False)) @click.pass_obj def get_file( vsarch: VisitSequenceArchiveMetadata, uuid: UUID, file_type: str, destination: str, ) -> None: """Retrieve a registered file and copy it to a local destination. Parameters ---------- vsarch : `VisitSequenceArchiveMetadata` The metadata interface used to look up the file URL. uuid : `UUID` The unique identifier of the visit sequence containing the requested file. file_type : `str` The handle for the type of file to retrieve (e.g. ``visits``). destination : `str` Path where the retrieved file should be written. If the destination does not exist it will be created. For files registered with the ``visits`` type and a SQLite3 extension suffix, the file will be downloaded as an HDF5 ``visits.h5`` file, converted back to the original opsim SQLite format, and written to ``destination``. """ file_url = vsarch.get_file_url(uuid, file_type) origin_rp = ResourcePath(file_url) if file_type == "visits" and Path(destination).suffix.lower() in SQLITE_EXTINSIONS: with TemporaryDirectory() as temp_dir: h5_destination = ResourcePath(temp_dir).join("visits.h5") h5_destination.transfer_from(origin_rp, "copy") click.echo(f"Copied {origin_rp.geturl()} to {h5_destination.geturl()}") hdf5_to_opsimdb(h5_destination.ospath, destination) click.echo(f"Converted to opsim and wrote to {destination}") else: destination_rp = ResourcePath(destination) destination_rp.transfer_from(origin_rp, "copy") click.echo(f"Copied {origin_rp.geturl()} to {destination_rp.geturl()}") @vseqarchive.command() @click.argument("uuid", type=click.UUID) @click.argument("visits_file", type=click.Path(exists=True)) @click.argument( "columns", type=str, required=False, nargs=-1, ) @click.pass_obj def add_nightly_stats( vsarch: VisitSequenceArchiveMetadata, uuid: UUID, visits_file: str, columns: tuple[str, ...], ) -> None: """Compute nightly statistics for a visit sequence and add them to the visit sequence metadata database. Parameters ---------- vsarch : `VisitSequenceArchiveMetadata` The metadata interface. uuid : `UUID` The UUID of the visit sequence to attach the statistics to. visits_file : `str` Path to an HDF5 file containing a ``visits`` dataset. columns : `tuple` List of columns for which stats should be added. """ # Load the visits table from the HDF5 file visits_df = pd.read_hdf(visits_file, key="observations") assert isinstance(visits_df, pd.DataFrame), "Expected a DataFrame from key 'observations'" # Compute the nightly statistics, passing the requested columns if any if columns: stats_df = compute_nightly_stats(visits_df, columns=tuple(columns)) else: stats_df = compute_nightly_stats(visits_df) # Insert into the database vsarch.insert_nightly_stats(uuid, stats_df) # Print the statistics as TSV tsv = stats_df.to_csv(sep="\t", index=False).rstrip("\n") click.echo(tsv) @vseqarchive.command() @click.argument("uuid", type=click.UUID) @click.pass_obj def query_nightly_stats(vsarch: VisitSequenceArchiveMetadata, uuid: UUID) -> None: """Retrieve nightly statistics for a visit sequence and print them as a tab‑separated table. Parameters ---------- vsarch : `VisitSequenceArchiveMetadata` The metadata interface. uuid : `UUID` The unique identifier of the visit sequence. """ stats_df = vsarch.query_nightly_stats(uuid) output = stats_df.to_csv(sep="\t", index=False).rstrip("\n") if not stats_df.empty else "" click.echo(output) @vseqarchive.command() @click.pass_obj def record_conda_env(vsarch: VisitSequenceArchiveMetadata) -> None: """Record the current Conda environment in the metadata database, and print the hash used as a key for the env in the database. Parameters ---------- vsarch : `VisitSequenceArchiveMetadata` The metadata interface. """ env_hash, env_json = compute_conda_env() vsarch.record_conda_env(env_hash, env_json) click.echo(env_hash.hex()) @vseqarchive.command("import-proto") @click.argument("archive_base", type=str) @click.argument("sim_date", type=str) @click.argument("sim_index", type=str) @click.option( "--proto-sim-archive-url", default="s3://rubin:rubin-scheduler-prenight/opsim/", show_default=True, help="Base URI of the prototype simulation archive.", ) @click.pass_obj def import_proto( vsarch: VisitSequenceArchiveMetadata, archive_base: str, sim_date: str, sim_index: str, proto_sim_archive_url: str = "s3://rubin:rubin-scheduler-prenight/opsim/", ) -> None: """Import a simulation from the prototype simulation archive, and print the UUID of the resultant entry in the destintation archive. Parameters ---------- vsarch : `VisitSequenceArchiveMetadata` The metadata interface instance. archive_base :`str` Base location of the destination archive. sim_date : `str` The ISO‑formatted date (e.g. ``'2025-03-12'``) by which the prototype simulation is indexed. sim_index : `str` The index of the simulation which completes the identification of the simulation in the prototype. proto_sim_archive_url : `str`, optional Base URI of the prototype simulation archive. Defaults to ``s3://rubin:rubin-scheduler-prenight/opsim/``. """ sim_uuid = vsarch.import_sim_from_prototype_sim_archive( archive_base, sim_date, sim_index, proto_sim_archive_url, ) click.echo(sim_uuid) @vseqarchive.command("export-proto") @click.argument("uuid", type=click.UUID) @click.option( "--proto-sim-archive-url", default="s3://rubin:rubin-scheduler-prenight/opsim/", show_default=True, help="Base URL of the prototype simulation archive to which to export.", ) @click.pass_obj def export_proto( vsarch: VisitSequenceArchiveMetadata, uuid: UUID, proto_sim_archive_url: str = "s3://rubin:rubin-scheduler-prenight/opsim/", ) -> None: """Export a simulation from the visit sequence archive to the prototype simulation archive, and print the URL in the prototype sim archive. Parameters ---------- vsarch : `VisitSequenceArchiveMetadata` The metadata interface instance. uuid : `UUID` UUID of the simulation to export. proto_sim_archive_url : `str` Base URL of the prototype simulation archive to which the simulation should be exported. """ # Import here to avoid circular imports from .prototype import export_sim_to_prototype_sim_archive destination_rp = export_sim_to_prototype_sim_archive( archive_metadata=vsarch, sim_uuid=uuid, proto_sim_archive_url=proto_sim_archive_url, ) click.echo(destination_rp.geturl()) @vseqarchive.command() @click.argument("dayobs", type=click.DateTime(formats=["%Y-%m-%d", "%Y%m%d"])) @click.argument("telescope", type=click.Choice(["simonyi", "auxtel"])) @click.option( "--destination", default=PRENIGHT_INDEX_URL, type=str, help="Base of the resource to which to write the prenight index.", ) @click.pass_obj def make_prenight_index( vsarch: VisitSequenceArchiveMetadata, dayobs: str | datetime.datetime | datetime.date | int | Time, telescope: str, destination: str, ) -> None: """Write a json index of prenight for a telescope and dayobs to a resource. Parameters ---------- vsarch : `VisitSequenceArchiveMetadata` The metadata interface instance. dayobs : `datetime` or `date` or `int` or `str` A datetime in the desired dayobs telescope : `str` The telescope, either ``simonyi`` or ``auxtel`` destination : `str` Base of the resource to which to write the prenight index. """ dayobs = dayobs_to_date(dayobs) assert isinstance(dayobs, datetime.date) prenights = vsarch.sims_on_night_with_stats( dayobs, tags=("prenight",), telescope=telescope, max_simulation_age=4000 ).set_index("visitseq_uuid") def to_hex(data: memoryview | None) -> str | None: if data is None: return None assert isinstance(data, memoryview) return data.hex() prenights["conda_env_sha256"] = prenights["conda_env_sha256"].apply(to_hex) def to_str_dayobs(d: datetime.date | None) -> str | None: if d is None: return None return d.isoformat() for column in ("sim_creation_day_obs", "first_day_obs", "last_day_obs", "parent_last_day_obs"): prenights[column] = prenights[column].apply(to_str_dayobs) # Convert UUID type into something to_json can deal with. prenights.index = prenights.index.map(str) for row in prenights.index: parent_visitseq_uuid = prenights.loc[row, "parent_visitseq_uuid"] if isinstance(parent_visitseq_uuid, UUID): prenights.loc[row, "parent_visitseq_uuid"] = str(parent_visitseq_uuid) table_json = prenights.to_json(orient="index", date_format="iso", indent=4) destination_dir_rp = ( ResourcePath(destination) .join(telescope) .join(str(dayobs.year)) .join(str(dayobs.month), forceDirectory=True) ) destination_rp = destination_dir_rp.join(f"{telescope}_prenights_for_{dayobs.isoformat()}.json") destination_rp.write(table_json.encode()) click.echo(destination_rp.geturl()) if __name__ == "__main__": vseqarchive()