102.1. Rubin Schedule Viewer#
102.1. Rubin Schedule Viewer¶
For the Rubin Science Platform at data.lsst.cloud.
Data Release: Prompt Products
Container Size: Large
LSST Science Pipelines version: r30.0.11
Last verified to run: 2026-10-02
Repository: github.com/lsst/tutorial-notebooks
DOI: 10.11578/rubin/dc.20250909.20
Learning objective: How to programmatically query the Rubin Schedule Viewer.
Rubin service: The Rubin Schedule Viewer service runs at the USDF and is publicly available at Rubin Schedule Viewer.
Packages: requests, pandas
Credit: Originally developed by the Rubin Community Science team. Please consider acknowledging them if this notebook is used for the preparation of journal articles, software releases, or other notebooks.
Get Support: Everyone is encouraged to ask questions or raise issues in the Support Category of the Rubin Community Forum. Rubin staff will respond to all questions posted there.
1. Introduction¶
This tutorial demonstrates how to query the Rubin schedule, which is currently openly available via the Rubin Schedule Viewer service at the US Data Facility:
If the table viewer page appears blank, refresh the page and give it time to execute the default query.
The Rubin schedule viewer service publishes both the next scheduled visit and the predicted observing schedule for Rubin Observatory. It provides a user-friendly web interface as well as a programmatic web API, enabling the community to access up-to-date information for planning observations. The Rubin Schedule Viewer provides a nominal survey schedule at least 24 hours in advance, followed by updated schedules published at least 2 hours before each observing visit.
The design of the service is described in DMTN-263.
The all-sky map also supports filtering, downloading the underlying data in several formats, and switching between equatorial and Galactic projections.
For a record of what has already been observed, the Commissioning tutorial 101 queries a temporary LSSTCam visits database covering April 2025 to June 2026, updated approximately monthly. Once the Prompt Products Database (PPDB) is released, it will be the better tool for querying what was observed in the past.
1.1. ObsLocTAP - Observation Locator Table Access Protocol¶
The Rubin Schedule Viewer is built using the IVOA Observation Locator Table Access Protocol (ObsLocTAP henceforth) standard (Salgado et al.) and incorporates information from an observatory's scheduler, including both forecasted observations and the historical record of completed visits.
ObsLocTAP specifies - in a standard format - services to retrieve information about planned, scheduled and performed observations of a given target (or coordinates) for a given astronomical observatory based on the existing ObsCore data model. This standard does not describe the access to data obtained after the processing of the observational activity, as that is the goal of ObsCore (archived observations), although the discovery could be done in a similar way.
ObsLocTAP defines a data model for scheduled observations and a method to run queries over compliant data, using several Virtual Observatory technologies.
1.2. Key columns¶
The ObsLocTAP data model (table schema) for the scheduled observations is defined in the ivoa.obsplan table -- Observation Locator
Table Access Protocol Version 1.0
The database contains 28 columns in total, and these are the key columns.
t_min: The start time (MJD).t_max: The stop time (MJD).s_ra: The Right Ascension of the visit or planned observation (deg).s_dec: The Declination of the visit or planned observation (deg).em_min: The filter minimum wavelength (m).em_max: The filter maximum wavelength (m).target_name: The name of the astronomical object observed, if any.tracking_type: One of the following values: Sidereal, Solar-system-object-tracking, Fixed-az-el-transit.priority: The Priority level { 0, 1, 2}. 0 = in the target queue as the next visit (meaning its execution is highly likely), 1 = in the 1 hour look-ahead summit queue (meaning likely), 2 = 24 hour pre-night simulation, meaning it is not in the summit queue and has a high degree of uncertainty.t_planning: Time (MJD) when this observation has been added or modified into the planning log.execution_status: Execution status of the prediction: One of the following values: Planned, Scheduled, Unscheduled, Performed, Aborted.
Key columns that remained unpopulated as of the last update of this tutorial.
s_region: Sky region covered by the data product (expressed in ICRS frame).obs_id: The observation ID from the Camera (currently the same astarget_name).
These are also columns of interest, but are generally the same for most observations taken with LSSTCam.
t_plan_exptime: The planned or scheduled exposure time (s).t_exptime: The total exposure time =t_max - t_min(s).s_fov: The diameter (bounds) of the covered region (deg).s_resolution: The spatial resolution of the data expressed as FWHM (arcsec/pixel).facility_name: Name of the facility used for this observation e.g Rubin Simonyi, AuxTel.instrument_name: Name of the instrument used for this observation e.g LSSTCam, LSSTComCam.
See DMTN-263 for a description of all fields in the schema.
1.3. Import packages¶
Import the requests package to query the service, pandas and numpy to work with the results, and astropy's Time and zoneinfo packages to handle times.
import requests
import pandas as pd
import numpy as np
from astropy.time import Time
from IPython.display import display
from zoneinfo import ZoneInfo
from datetime import datetime, timezone
1.4. Define functions¶
Define a function to convert the em_min and em_max columns into the bandpass (filter) name.
def em_min_max_to_band(em_min, em_max):
"""
Convert the wavelength minimum and maximum to band name.
Parameters
----------
em_min: float
Wavelength minimum, in m.
em_max: float
Wavelength maximum, in m.
Returns
-------
band: string
Band (filter) name.
"""
band = None
band_dict = {"u": (2.95e-7, 4.05e-7), "g": (4.00e-7, 5.55e-7),
"r": (5.50e-7, 6.92e-7), "i": (6.90e-7, 8.20e-7),
"z": (8.15e-7, 9.25e-7), "y": (9.20e-7, 11.1e-7)}
for b in band_dict.keys():
if em_min > band_dict[b][0] and em_max < band_dict[b][1]:
band = b
if band is None:
print(em_min, em_max, " no band found")
return band
2. Rubin Schedule Viewer¶
The Rubin Schedule Viewer runs at the US Data Facility at SLAC.
Define the Rubin Schedule Viewer's base URL.
obsloctap_url = "https://usdf-rsp.slac.stanford.edu/obsloctap"
2.1. Graphical User Interface¶
The Rubin Schedule Viewer User Interface (UI) provides a convenient graphical user interface to query and explore the Rubin schedule of observations.
It provides the facility to interactively query the historical and future schedule from a specified point in time (MJD or ISO) for a number of hours into the future.
It provides a filter by execution status, e.g. executed or scheduled, and a filter by target_name, e.g low-dust, LMC_SMC, dusty_plane.
The publicly accessible user interface is: https://usdf-rsp.slac.stanford.edu/obsloctap/static/viewer.html .
Programmatically check the health of the static UI by executing the following cell, and assert that the static user interface service must be alive.
Warning: If
RemoteDisconnecterrors are returned, it means the service is temporarily unstable or unavailable.
rubin_static_schedule_url = obsloctap_url + "/static/viewer.html"
response = requests.get(rubin_static_schedule_url)
assert response.status_code == 200, f"request failed with status {response.status_code}"
print(f"Rubin Schedule Viewer at {response.url} is alive.")
Rubin Schedule Viewer at https://usdf-rsp.slac.stanford.edu/obsloctap/static/viewer.html is alive.
2.2. Programmatic access¶
The service is also accessible programmatically, via the ObsLocTAP protocol, allowing users to extract metadata from the Schedule Viewer itself, including links such as the GitHub repository for the service code. The Schedule Viewer does not require authentication, which is essential for publicly distributing the observing schedule.
Connect to the service file using requests package and assert that the service is alive.
response = requests.get(obsloctap_url)
assert response.status_code == 200, f"request failed with status {response.status_code}"
print(f"Rubin Schedule Viewer API at {response.url} is alive.")
Rubin Schedule Viewer API at https://usdf-rsp.slac.stanford.edu/obsloctap/ is alive.
2.3. Query the service header information¶
Retrieve the header metadata and print the keys.
headers = response.json()
assert headers is not None
meta_data = headers["metadata"]
print(meta_data.keys())
dict_keys(['name', 'version', 'description', 'repository_url', 'documentation_url'])
Print the service's description.
print(
f'Rubin Schedule Viewer running at USDF for "{meta_data.get("description")}" \
with source code at {meta_data.get("repository_url")}.'
)
Rubin Schedule Viewer running at USDF for "Look at scheduler forecast and track what was observed" with source code at https://github.com/lsst-dm/obsloctap.
3. Retrieve the future schedule¶
Query the Rubin Schedule Service to get the upcoming forecast for up to 24 hours in advance.
Define the schedule URL.
schedule_url = obsloctap_url + "/schedule"
Assert that the schedule forecast service is alive.
response = requests.get(schedule_url)
assert response.status_code == 200, f"request failed with status {response.status_code}"
print(f"Rubin Schedule Forecast at {response.url} is alive.")
Rubin Schedule Forecast at https://usdf-rsp.slac.stanford.edu/obsloctap/schedule is alive.
3.1. Retrieve the next 24 hours of schedule¶
Request the schedule with a time parameter of 24 hours. time accepts any value from 1 to 48 (hours); changing it, e.g. to "2", retrieves a shorter or longer lookahead window instead.
Extract the list of upcoming visits as a json object and convert to a pandas dataframe, then display the first five rows.
params = {"time": "24"}
response = requests.get(schedule_url, params=params)
forward_24h_schedule = pd.DataFrame(response.json())
print(len(forward_24h_schedule))
0
Print the first five rows, if any visits were retrieved.
Note: The live forecast above may be empty, depending on what is currently scheduled. Section 4 below queries a past night instead, which reliably returns a populated table in the same format.
if len(forward_24h_schedule) > 0:
display(forward_24h_schedule.head(5))
else:
print("There were no scheduled visits retrieved for the next 24 hours.")
There were no scheduled visits retrieved for the next 24 hours.
If any visits were retrieved, note that the values in the "Execution Status" field are all "Scheduled".
3.1.1. Planned DDF observations¶
Alerts stream only in regions with template images, primarily the DDFs; see prompt-products.lsst.io for current template coverage.
Print the number of planned observations per DDF in the next 24h, and in which filters the observations are planned.
First, add a column for the band (filter), converted from the em_min and em_max columns.
if len(forward_24h_schedule) > 0:
em_min = forward_24h_schedule["em_min"].values
em_max = forward_24h_schedule["em_max"].values
temp = []
for i in range(len(forward_24h_schedule)):
temp.append(em_min_max_to_band(em_min[i], em_max[i]))
forward_24h_schedule["band"] = temp
del temp
else:
print("There were no scheduled visits retrieved")
There were no scheduled visits retrieved
Define the names of the DDF fields, in the format of the target_name column.
For each field, sum the number of planned observations and print the unique filters for those observations.
ddf_names = ["ddf_cosmos", "ddf_ecdfs", "ddf_elaiss1",
"ddf_edfs_a", "ddf_edfs_b", "ddf_xmm_lss"]
if len(forward_24h_schedule) > 0:
for ddf_name in ddf_names:
temp = forward_24h_schedule.query("target_name.str.contains(@ddf_name)")
band_list = "".join(str(value) for value in np.unique(temp["band"]))
print("%-12s %3i bands: %-15s" % (ddf_name, len(temp), band_list))
del temp, band_list
else:
print("There were no scheduled visits retrieved")
There were no scheduled visits retrieved
4. Retrieve a past planned schedule¶
A start date can also be specified that is in the past to retrieve the observations that were planned on previous nights. The date can be MJD or ISO format.
Note that these are the observations that were scheduled for that night, they are not necessarily what was observed.
The time window duration and start date can be defined in a dictionary as follows.
# params = {"time": "24", "start": "2026-06-08"}
Warning: A
startdate given without a time of day defaults to midnight (00:00:00 UTC), so 24 hours from it covers the ends of two different nights rather than one.
Times can also be defined with the astropy package's Time module.
Convert local midday at Cerro Pachon on June 08 2026 to UTC with zoneinfo, and define past_time as the equivalent MJD.
local_noon = datetime(2026, 6, 8, 12, 0, 0, tzinfo=ZoneInfo("America/Santiago"))
utc_noon = local_noon.astimezone(timezone.utc)
print(utc_noon)
past_time = Time(utc_noon).mjd
print(past_time)
2026-06-08 16:00:00+00:00 61199.666666666664
Use the astropy Time in the dictionary instead of a string.
params = {"time": "24", "start": past_time}
response = requests.get(schedule_url, params=params)
assert response.status_code == 200, f"request failed with status {response.status_code}"
print(response.url)
https://usdf-rsp.slac.stanford.edu/obsloctap/schedule?time=24&start=61199.666666666664
next_visits_from_date_iso = pd.DataFrame(response.json())
print(
f"There were {len(next_visits_from_date_iso)} visits scheduled in the \
{params['time']} hours from {Time(params['start'], format='mjd').iso}."
)
There were 1041 visits scheduled in the 24 hours from 2026-06-08 16:00:00.000.
Display 5 of the visits of the retrieved table.
next_visits_from_date_iso.head(5)
| t_planning | target_name | obs_id | obs_collection | s_ra | s_dec | s_fov | s_region | s_resolution | t_min | ... | pol_xel | facility_name | instrument_name | t_plan_exptime | category | priority | execution_status | tracking_type | rubin_rot_sky_pos | rubin_nexp | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 61200.443349 | lowdust | 2026071000022 | BLOCK-407| pair_15, ri, a | 46.691518 | -25.856146 | 3.5 | 0.2 | 61231.962928 | ... | 0 | Rubin:Simonyi | LSSTCam | 30.0 | Fixed | 0 | Performed | Sidereal | 52.500141 | 1 | |
| 1 | 61200.442927 | lowdust | 2026071000022 | BLOCK-407| pair_15, ri, a | 48.637723 | -23.598704 | 3.5 | 0.2 | 61231.962928 | ... | 0 | Rubin:Simonyi | LSSTCam | 30.0 | Fixed | 0 | Performed | Sidereal | 52.500141 | 1 | |
| 2 | 61200.442497 | lowdust | 2026071000022 | BLOCK-407| pair_15, ri, a | 47.456368 | -20.608415 | 3.5 | 0.2 | 61231.962928 | ... | 0 | Rubin:Simonyi | LSSTCam | 30.0 | Fixed | 0 | Performed | Sidereal | 52.500141 | 1 | |
| 3 | 61200.442075 | lowdust | 2026071000022 | BLOCK-407| pair_15, ri, a | 50.507052 | -21.302340 | 3.5 | 0.2 | 61231.962928 | ... | 0 | Rubin:Simonyi | LSSTCam | 30.0 | Fixed | 0 | Performed | Sidereal | 52.500141 | 1 | |
| 4 | 61200.441645 | lowdust | 2026071000022 | BLOCK-407| pair_15, ri, a | 51.837071 | -24.251713 | 3.5 | 0.2 | 61231.962928 | ... | 0 | Rubin:Simonyi | LSSTCam | 30.0 | Fixed | 0 | Performed | Sidereal | 52.500141 | 1 |
5 rows × 28 columns
4.1. Execution status¶
Print the unique values in the "Execution Status" field for the night of June 08 2026.
temp = next_visits_from_date_iso["execution_status"].unique().tolist()
print('"Execution Status" field types: ', temp)
del temp
aborted_visits = len(
next_visits_from_date_iso[next_visits_from_date_iso["execution_status"] == "Aborted"]
)
performed_visits = len(
next_visits_from_date_iso[next_visits_from_date_iso["execution_status"] == "Performed"]
)
print(
f"There were {performed_visits} visits Performed and {aborted_visits} visits Aborted in the \
{params['time']} hours from {Time(params['start'], format='mjd').iso}."
)
"Execution Status" field types: ['Performed', 'Aborted'] There were 678 visits Performed and 363 visits Aborted in the 24 hours from 2026-06-08 16:00:00.000.