Skip to content

Commit cc8dc54

Browse files
authored
Merge pull request #452 from lsst/tickets/SP-2082
tickets/SP-2082: Support getting obsloctac visits from the pre-night sim archive
2 parents fa12d71 + 8ace21f commit cc8dc54

File tree

3 files changed

+242
-14
lines changed

3 files changed

+242
-14
lines changed

rubin_sim/maf/utils/opsim_utils.py

+25-14
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
import os
88
import sqlite3
9+
import urllib
10+
from contextlib import closing
911

1012
import numpy as np
1113
import pandas as pd
@@ -15,8 +17,8 @@
1517

1618
def get_sim_data(
1719
db_con,
18-
sqlconstraint,
19-
dbcols,
20+
sqlconstraint=None,
21+
dbcols=None,
2022
stackers=None,
2123
table_name=None,
2224
full_sql_query=None,
@@ -77,26 +79,35 @@ def get_sim_data(
7779
# that's probably fine, keep people from getting fancy with old sims
7880
table_name = "observations"
7981

80-
if isinstance(db_con, str):
81-
con = sqlite3.connect(db_con)
82-
else:
83-
con = db_con
84-
8582
if full_sql_query is None:
86-
col_str = ""
87-
for colname in dbcols:
88-
col_str += colname + ", "
89-
col_str = col_str[0:-2] + " "
83+
col_str = "*" if dbcols is None else ", ".join(dbcols)
9084

9185
query = "SELECT %s FROM %s" % (col_str, table_name)
9286
if len(sqlconstraint) > 0:
9387
query += " WHERE %s" % (sqlconstraint)
9488
query += ";"
95-
sim_data = pd.read_sql(query, con).to_records(index=False)
96-
9789
else:
9890
query = full_sql_query
99-
sim_data = pd.read_sql(query, con).to_records(index=False)
91+
92+
if isinstance(db_con, sqlite3.Connection):
93+
sim_data = pd.read_sql(query, db_con).to_records(index=False)
94+
elif isinstance(db_con, str) and os.path.isfile(db_con):
95+
with closing(sqlite3.connect(db_con)) as con:
96+
sim_data = pd.read_sql(query, con).to_records(index=False)
97+
elif (not isinstance(db_con, str)) or urllib.parse.urlparse(db_con).scheme != "":
98+
try:
99+
from lsst.resources import ResourcePath
100+
101+
with ResourcePath(db_con).as_local() as local_db_path:
102+
with closing(sqlite3.connect(local_db_path.ospath)) as con:
103+
sim_data = pd.read_sql(query, con).to_records(index=False)
104+
except ModuleNotFoundError:
105+
raise RuntimeError(
106+
f"Cannot read visits from {db_con}."
107+
"Maybe it does not exist, or maybe you need to install lsst.resources."
108+
)
109+
else:
110+
raise RuntimeError("Cannot find {db_con}.")
100111

101112
if len(sim_data) == 0:
102113
raise UserWarning("No data found matching sqlconstraint %s" % (sqlconstraint))

rubin_sim/sim_archive/sim_archive.py

+189
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@
3333
from astropy.time import Time
3434
from rubin_scheduler.scheduler import sim_runner
3535
from rubin_scheduler.scheduler.utils import SchemaConverter
36+
from rubin_scheduler.site_models.almanac import Almanac
37+
38+
from rubin_sim.maf.utils.opsim_utils import get_sim_data
3639

3740
LOGGER = logging.getLogger(__name__)
3841

@@ -931,3 +934,189 @@ def compile_sim_archive_metadata_cli(*args):
931934
compilation_resource = ResourcePath(compilation_uri)
932935

933936
compilation_resource = compile_sim_metadata(archive_uri, compilation_resource, append=append)
937+
938+
939+
def find_latest_prenight_sim_for_nights(
940+
first_day_obs: str | None = None,
941+
last_day_obs: str | None = None,
942+
tags: tuple[str] = ("ideal", "nominal"),
943+
max_simulation_age: int = 2,
944+
archive_uri: str = "s3://rubin:rubin-scheduler-prenight/opsim/",
945+
compilation_uri: str = "s3://rubin:rubin-scheduler-prenight/opsim/compiled_metadata_cache.h5",
946+
) -> pd.DataFrame:
947+
"""Find the most recent prenight simulation that covers a night.
948+
949+
Parameters
950+
----------
951+
first_day_obs : `str` or `None`
952+
The date of the evening for the first night for which to get
953+
a simulation. If `None`, then the current date will be used.
954+
last_day_obs : `str` or `None`
955+
The date of the evening for the last night for which to get
956+
a simulation. If `None`, then the current date will be used.
957+
tags : `tuple[str]`
958+
A tuple of tags to filter simulations by.
959+
Defaults to ``('ideal', 'nominal')``.
960+
max_simulation_age : `int`
961+
The maximum age of simulations to consider, in days.
962+
Simulations older than ``max_simulation_age`` will not be considered.
963+
Defaults to 2.
964+
archive_uri : `str`
965+
The URI of the archive from which to fetch the simulation.
966+
Defaults to ``s3://rubin:rubin-scheduler-prenight/opsim/``.
967+
compilation_uri : `str`
968+
The URI of the compiled metadata HDF5 file for efficient querying.
969+
Defaults to
970+
``s3://rubin:rubin-scheduler-prenight/opsim/compiled_metadata_cache.h5``.
971+
972+
Returns
973+
-------
974+
sim_metadata : `dict`
975+
A dictionary with metadata for the simulation.
976+
"""
977+
978+
if first_day_obs is None:
979+
first_day_obs = Time(Time.now().mjd - 0.5, format="mjd").iso[:10]
980+
if last_day_obs is None:
981+
last_day_obs = first_day_obs
982+
983+
sim_metadata = read_archived_sim_metadata(
984+
archive_uri, num_nights=max_simulation_age, compilation_resource=compilation_uri
985+
)
986+
987+
best_sim = None
988+
for uri, sim in sim_metadata.items():
989+
sim["uri"] = uri
990+
sim["exec_date"] = uri.split("/")[-3]
991+
sim["date_index"] = int(uri.split("/")[-2])
992+
993+
if sim["simulated_dates"]["first"] > first_day_obs:
994+
continue
995+
if sim["simulated_dates"]["last"] < last_day_obs:
996+
continue
997+
if not set(tags).issubset(sim["tags"]):
998+
continue
999+
if best_sim is not None:
1000+
if sim["exec_date"] < best_sim["exec_date"]:
1001+
continue
1002+
if sim["date_index"] < best_sim["date_index"]:
1003+
continue
1004+
best_sim = sim
1005+
1006+
if best_sim is not None:
1007+
best_sim["opsim_rp"] = (
1008+
ResourcePath(archive_uri)
1009+
.join(best_sim["exec_date"], forceDirectory=True)
1010+
.join(f"{best_sim['date_index']}", forceDirectory=True)
1011+
.join(best_sim["files"]["observations"]["name"])
1012+
)
1013+
1014+
return best_sim
1015+
1016+
1017+
def fetch_latest_prenight_sim_for_nights(
1018+
first_day_obs: str | None = None,
1019+
last_day_obs: str | None = None,
1020+
tags: tuple[str] = ("ideal", "nominal"),
1021+
max_simulation_age: int = 2,
1022+
archive_uri: str = "s3://rubin:rubin-scheduler-prenight/opsim/",
1023+
compilation_uri: str = "s3://rubin:rubin-scheduler-prenight/opsim/compiled_metadata_cache.h5",
1024+
**kwargs,
1025+
) -> pd.DataFrame:
1026+
"""Fetches visit parameters from the latest archived pre-night simulation
1027+
with requested tags for a specified day of observing.
1028+
1029+
Parameters
1030+
----------
1031+
first_day_obs : `str` or `None`
1032+
The date of the evening for the first night for which to get
1033+
a simulation. If `None`, then the current date will be used.
1034+
last_day_obs : `str` or `None`
1035+
The date of the evening for the last night for which to get
1036+
a simulation. If `None`, then the current date will be used.
1037+
tags : `tuple[str]`
1038+
A tuple of tags to filter simulations by.
1039+
Defaults to ``('ideal', 'nominal')``.
1040+
max_simulation_age : `int`
1041+
The maximum age of simulations to consider, in days. Simulations older
1042+
than ``max_simulation_age`` will not be considered. Defaults to 2.
1043+
archive_uri : `str`
1044+
The URI of the archive from which to fetch the simulation.
1045+
Defaults to ``s3://rubin:rubin-scheduler-prenight/opsim/``.
1046+
compilation_uri : `str`
1047+
The URI of the compiled metadata HDF5 file for efficient querying.
1048+
Defaults to ``s3://rubin:rubin-scheduler-prenight/opsim/compiled_metadata_cache.h5``.
1049+
**kwargs
1050+
Additional keyword arguments passed to
1051+
`rubin_sim.maf.get_sim_data`.
1052+
1053+
Returns
1054+
-------
1055+
visits : `pd.DataFrame`
1056+
A pandas DataFrame containing visit parameters.
1057+
"""
1058+
1059+
sim_metadata = find_latest_prenight_sim_for_nights(
1060+
first_day_obs, last_day_obs, tags, max_simulation_age, archive_uri, compilation_uri
1061+
)
1062+
visits = get_sim_data(sim_metadata["opsim_rp"], **kwargs)
1063+
1064+
return visits
1065+
1066+
1067+
def fetch_obsloctap_visits(day_obs: str | None = None, nights: int = 2) -> pd.DataFrame:
1068+
"""Return visits from latest nominal prenight briefing simulation.
1069+
1070+
Parameters
1071+
----------
1072+
day_obs : `str`
1073+
The day_obs of the night, in YYYY-MM-DD format (e.g. 2025-03-26).
1074+
Default None will use the date of the next sunset.
1075+
nights : `int`
1076+
The number of nights of observations to return.
1077+
Defaults to 2.
1078+
1079+
Returns
1080+
-------
1081+
visits : `pd.DataFrame`
1082+
The visits from the prenight simulation.
1083+
"""
1084+
dbcols = [
1085+
"observationStartMJD",
1086+
"fieldRA",
1087+
"fieldDec",
1088+
"rotSkyPos",
1089+
"band",
1090+
"visitExposureTime",
1091+
"night",
1092+
"target_name",
1093+
]
1094+
1095+
# Start with the first night that starts after the reference time,
1096+
# which is the current time by default.
1097+
# So, if the reference time is during a night, it starts with the
1098+
# following night.
1099+
night_bounds = pd.DataFrame(Almanac().sunsets)
1100+
reference_time = Time.now() if day_obs is None else Time(day_obs, format="iso", scale="utc")
1101+
first_night = night_bounds.query(f"sunset > {reference_time.mjd}").night.min()
1102+
last_night = first_night + nights - 1
1103+
1104+
night_bounds.set_index("night", inplace=True)
1105+
start_mjd = night_bounds.loc[first_night, "sunset"]
1106+
end_mjd = night_bounds.loc[last_night, "sunrise"]
1107+
1108+
sqlconstraint = (f"observationStartMJD BETWEEN {start_mjd} AND {end_mjd}",)
1109+
max_simulation_age = int(np.ceil(Time.now().mjd - reference_time.mjd)) + 1
1110+
1111+
first_day_obs = Time(start_mjd - 0.5, format="mjd").iso[:10]
1112+
last_day_obs = Time(end_mjd - 0.5, format="mjd").iso[:10]
1113+
visits = fetch_latest_prenight_sim_for_nights(
1114+
first_day_obs,
1115+
last_day_obs,
1116+
tags=("ideal", "nominal"),
1117+
max_simulation_age=max_simulation_age,
1118+
sqlconstraint=sqlconstraint,
1119+
dbcols=dbcols,
1120+
)
1121+
1122+
return visits

tests/sim_archive/test_sim_archive.py

+28
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
from pathlib import Path
77
from tempfile import TemporaryDirectory
88

9+
import numpy as np
10+
import pandas as pd
11+
from astropy.time import Time
912
from rubin_scheduler.scheduler import sim_runner
1013
from rubin_scheduler.scheduler.example import example_scheduler
1114
from rubin_scheduler.scheduler.model_observatory import ModelObservatory
@@ -18,6 +21,9 @@
1821
from rubin_sim.sim_archive.sim_archive import (
1922
check_opsim_archive_resource,
2023
compile_sim_metadata,
24+
fetch_latest_prenight_sim_for_nights,
25+
fetch_obsloctap_visits,
26+
find_latest_prenight_sim_for_nights,
2127
make_sim_archive_cli,
2228
make_sim_archive_dir,
2329
read_archived_sim_metadata,
@@ -134,6 +140,28 @@ def test_cli(self):
134140
test_archive_uri,
135141
)
136142

143+
@unittest.skipIf(not HAVE_LSST_RESOURCES, "No lsst.resources")
144+
def test_find_latest_prenight_sim_for_night(self):
145+
day_obs = "2025-03-25"
146+
max_simulation_age = int(np.ceil(Time.now().mjd - Time(day_obs).mjd)) + 1
147+
sim_metadata = find_latest_prenight_sim_for_nights(day_obs, max_simulation_age=max_simulation_age)
148+
assert sim_metadata["simulated_dates"]["first"] <= day_obs <= sim_metadata["simulated_dates"]["last"]
149+
150+
@unittest.skipIf(not HAVE_LSST_RESOURCES, "No lsst.resources")
151+
def test_fetch_latest_prenight_sim_for_night(self):
152+
day_obs = "2025-03-25"
153+
max_simulation_age = int(np.ceil(Time.now().mjd - Time(day_obs).mjd)) + 1
154+
visits = fetch_latest_prenight_sim_for_nights(day_obs, max_simulation_age=max_simulation_age)
155+
assert len(visits) > 0
156+
157+
@unittest.skipIf(not HAVE_LSST_RESOURCES, "No lsst.resources")
158+
def test_fetch_obsloctap_visits(self):
159+
day_obs = "2025-03-25"
160+
num_nights = 2
161+
visits = pd.DataFrame(fetch_obsloctap_visits(day_obs, nights=num_nights))
162+
assert np.floor(visits["observationStartMJD"].min() - 0.5) == Time(day_obs).mjd
163+
assert np.floor(visits["observationStartMJD"].max() - 0.5) == Time(day_obs).mjd + num_nights - 1
164+
137165

138166
if __name__ == "__main__":
139167
unittest.main()

0 commit comments

Comments
 (0)