diff --git a/.gitignore b/.gitignore index 8e1bff13..ddea0ef6 100644 --- a/.gitignore +++ b/.gitignore @@ -128,6 +128,9 @@ dmypy.json # Pyre type checker .pyre/ +# dynamically created files +schedview/version.py + # Scratch notebooks and data notebooks/202?-??-*.ipynb notebooks/scratch @@ -135,3 +138,8 @@ schedview/data/local # Externally supplied data schedview/data/bsc5.dat +util/sample_data/sample_opsim.db +util/sample_data/sample_rewards.h5 +util/sample_data/sample_scheduler.pickle.xz +notebooks/rewards.db +notebooks/example_scheduler.p.xz diff --git a/environment_080a2.yaml b/environment_080a2.yaml deleted file mode 100644 index 2451b6c0..00000000 --- a/environment_080a2.yaml +++ /dev/null @@ -1,13 +0,0 @@ -name: surveyvis080a2 -channels: - - conda-forge - - lsstts -dependencies: - - rubin-sim=0.8.0a2 - - bokeh - - pytest-flake8 - - pytest-black - - selenium - - firefox - - geckodriver - - build diff --git a/notebooks/prenight.ipynb b/notebooks/prenight.ipynb index 6ff2b201..0b48136b 100644 --- a/notebooks/prenight.ipynb +++ b/notebooks/prenight.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "markdown", - "id": "f7ab0a55-4966-4caf-8c59-3d62fa97c859", + "id": "bd7b7073-27e7-49ea-9e9d-5e50355e6d23", "metadata": {}, "source": [ "# Running the pre-night briefing dashboard within a notebook" @@ -10,7 +10,7 @@ }, { "cell_type": "markdown", - "id": "af9d7773-c301-4b9b-b0a1-1c9e4c8610cd", + "id": "5329df4e-4793-47ff-9ad1-55236fd4f10d", "metadata": {}, "source": [ "## Notebook perparation" @@ -18,7 +18,7 @@ }, { "cell_type": "markdown", - "id": "47f2dccd-39f0-4c6a-ae3c-af4a3a865373", + "id": "a9667f16-58e3-40b2-bc35-1a16923bdb08", "metadata": {}, "source": [ "### Load jupyter extensions" @@ -27,8 +27,10 @@ { "cell_type": "code", "execution_count": null, - "id": "4d155494-c0a5-4fdb-8031-4f5ceb7d7395", - "metadata": {}, + "id": "2385888e-2285-4f86-95c7-d616013e1674", + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "%load_ext lab_black\n", @@ -38,7 +40,7 @@ }, { "cell_type": "markdown", - "id": "8b8d23b2-5f83-4598-b080-5d346bd599e3", + "id": "9868fc2a-49f1-4743-b3c2-11f00239d2ea", "metadata": {}, "source": [ "### Imports\n", @@ -49,7 +51,7 @@ { "cell_type": "code", "execution_count": null, - "id": "c448f750-c648-4024-8678-a0b031ce6e1e", + "id": "7c2adaf3-7d2c-44cd-8978-036e7b7814b0", "metadata": { "tags": [] }, @@ -57,26 +59,38 @@ "source": [ "import warnings\n", "import math\n", + "import logging\n", + "from pathlib import Path\n", "import panel as pn\n", - "import numpy as np" + "import numpy as np\n", + "import pandas as pd\n", + "import param\n", + "import bokeh\n", + "from copy import deepcopy\n", + "import datetime\n", + "from pytz import timezone\n", + "import lzma\n", + "import pickle\n", + "from tempfile import TemporaryDirectory, NamedTemporaryFile" ] }, { "cell_type": "code", "execution_count": null, - "id": "20e41df3-f40a-4a61-b44b-503be2e27cca", + "id": "95bf450f-f267-40f6-bf5b-d6d7b755e625", "metadata": { "tags": [] }, "outputs": [], "source": [ - "from astropy.time import Time, TimeDelta" + "from astropy.time import Time, TimeDelta\n", + "from zoneinfo import ZoneInfo" ] }, { "cell_type": "code", "execution_count": null, - "id": "5e9a6ca1-1a78-4fcd-91a5-23554e045430", + "id": "4a8c3442-8881-4c9e-b341-54fa699c7e14", "metadata": { "tags": [] }, @@ -84,35 +98,27 @@ "source": [ "from rubin_sim.scheduler.example import example_scheduler\n", "from rubin_sim.scheduler import sim_runner\n", - "from rubin_sim.scheduler.model_observatory import ModelObservatory" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "384e7112-dd5b-481f-9940-607184c898c1", - "metadata": {}, - "outputs": [], - "source": [ - "%aimport schedview\n", - "%aimport schedview.app.prenight" + "from rubin_sim.scheduler.model_observatory import ModelObservatory\n", + "from rubin_sim.scheduler.utils import SchemaConverter" ] }, { "cell_type": "code", "execution_count": null, - "id": "2ac473e0-eac2-4566-8955-628675df90af", + "id": "c86fdcf4-e7cb-4636-bee8-cb07f3a4374c", "metadata": { "tags": [] }, "outputs": [], "source": [ - "# from schedview.app.prenight import prenight_app" + "%aimport schedview\n", + "%aimport schedview.app.prenight\n", + "%aimport schedview.compute.scheduler" ] }, { "cell_type": "markdown", - "id": "4bb6e2dc-28d7-4f0c-b47a-1b193bbc0c38", + "id": "95c0762e-051a-48ee-9ae0-1ce429574d43", "metadata": {}, "source": [ "### Further preparation of the notebook" @@ -121,18 +127,19 @@ { "cell_type": "code", "execution_count": null, - "id": "f89baf7b-f8f7-4e6d-b0dd-34a3044817f7", + "id": "1a86b787-1ca5-441d-b42e-a113645b8bee", "metadata": { "tags": [] }, "outputs": [], "source": [ - "pn.extension()" + "# pn.extension()\n", + "pn.extension(\"terminal\")" ] }, { "cell_type": "markdown", - "id": "c66be7e2-51cb-4c70-883d-e149f8a7cd08", + "id": "56ccf5c1-9cdc-4fee-bef3-b53f04a17ab0", "metadata": {}, "source": [ "### Filter warnings\n", @@ -144,7 +151,7 @@ { "cell_type": "code", "execution_count": null, - "id": "b2f12164-0b3b-49d7-9237-7b480531abbd", + "id": "28ed2cbd-a3ae-48ff-91fd-2f04f4a22bbd", "metadata": { "tags": [] }, @@ -189,7 +196,7 @@ }, { "cell_type": "markdown", - "id": "16a6ab95-c37f-4eaa-92fb-b742cfc538c5", + "id": "3d001704-da46-4fb3-985c-a54837dffb8d", "metadata": {}, "source": [ "## Configuration and initial configuration" @@ -197,7 +204,7 @@ }, { "cell_type": "markdown", - "id": "e21a7b40-d718-4329-8b01-36ecd48b3f4a", + "id": "7823d462-e524-4337-9f44-2f9bd0bc34fa", "metadata": {}, "source": [ "Setting `keep_rewards` to `True` results in a dashboard that includes plots of rewards." @@ -217,7 +224,7 @@ }, { "cell_type": "markdown", - "id": "3a63ca50-a5e1-4a6c-8dc9-0253f4df80a3", + "id": "cac858ec-27da-40b2-8a62-75050a34584e", "metadata": {}, "source": [ "Set the start date, scheduler, and observatory for the night:" @@ -237,7 +244,7 @@ }, { "cell_type": "markdown", - "id": "94d1a65e-eb2b-4bd1-9b9f-0894139d0c35", + "id": "6bf6f1e8-aad2-4400-ad5e-9d2a098319c3", "metadata": {}, "source": [ "Set `evening_mjd` to the integer calendar MJD of the local calendar day on which sunset falls on the night of interest." @@ -252,12 +259,16 @@ }, "outputs": [], "source": [ - "evening_mjd = Time(\"2025-01-01\").mjd" + "evening_iso8601 = \"2025-01-01\"\n", + "\n", + "night_date = datetime.date.fromisoformat(evening_iso8601)\n", + "evening_mjd = Time(evening_iso8601).mjd\n", + "night_date, evening_mjd" ] }, { "cell_type": "markdown", - "id": "7e30b3b3-0545-48dc-9f90-27bf64872f97", + "id": "88c4336c-28f0-4e9d-991d-b0f92229db8f", "metadata": {}, "source": [ "If we just use this day as the start and make the simulation duration 1 day, the begin and end of the simulation will probably begin in the middle on one night and end in the middle of the next.\n", @@ -283,7 +294,8 @@ "mjd_end = observatory.almanac.sunsets[this_night][\"sunrise\"][0]\n", "\n", "night_duration = mjd_end - mjd_start\n", - "Time(mjd_start, format=\"mjd\").iso, night_duration" + "time_start = Time(mjd_start, format=\"mjd\")\n", + "time_start.iso, night_duration" ] }, { @@ -312,7 +324,15 @@ }, { "cell_type": "markdown", - "id": "fecdf1fb-1d05-4433-a756-0605355c3a93", + "id": "cfeccfee-22bf-448e-b587-5e15c4a4dd81", + "metadata": {}, + "source": [ + "Record the date of local day in the evening. " + ] + }, + { + "cell_type": "markdown", + "id": "da0e327b-50ae-490a-aeb2-6b02c90cc6ea", "metadata": {}, "source": [ "## Run a simulation and create the app instance" @@ -320,7 +340,7 @@ }, { "cell_type": "markdown", - "id": "5596ff14-86c5-46dc-a523-593107dbc1df", + "id": "d7ed376c-caac-4bb0-af91-702a648ab25f", "metadata": {}, "source": [ "For this example, simulate starting the default first day of observing:" @@ -339,7 +359,6 @@ " observatory, scheduler, observations = sim_runner(\n", " observatory, scheduler, mjd_start=mjd_start, survey_length=night_duration\n", " )\n", - " app = schedview.app.prenight.prenight_app(observatory, scheduler, observations)\n", "else:\n", " scheduler.keep_rewards = True\n", " observatory, scheduler, observations, reward_df, obs_rewards = sim_runner(\n", @@ -348,69 +367,161 @@ " mjd_start=mjd_start,\n", " survey_length=night_duration,\n", " record_rewards=True,\n", - " )\n", - " app = schedview.app.prenight.prenight_app(\n", - " observatory,\n", - " scheduler,\n", - " observations,\n", - " reward_df=reward_df,\n", - " obs_rewards=obs_rewards,\n", " )" ] }, { - "cell_type": "raw", - "id": "9a34dfaa-b659-4524-b55d-00258de0b345", + "cell_type": "markdown", + "id": "6eb63645-c4d9-4d0b-86fe-21a9b0320512", + "metadata": {}, + "source": [ + "## Save the simulation" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a6c2f16b-c54c-41bc-8daa-1f107e1f48a4", "metadata": { - "execution": { - "iopub.execute_input": "2023-04-27T16:27:03.918688Z", - "iopub.status.busy": "2023-04-27T16:27:03.918295Z", - "iopub.status.idle": "2023-04-27T16:27:28.103381Z", - "shell.execute_reply": "2023-04-27T16:27:28.102586Z", - "shell.execute_reply.started": "2023-04-27T16:27:03.918671Z" - } + "tags": [] }, + "outputs": [], "source": [ - "app2 = schedview.app.prenight.prenight_app(\n", - " observatory,\n", - " scheduler,\n", - " observations,\n", - " reward_df=reward_df,\n", - " obs_rewards=obs_rewards,\n", - " )" + "data_dir = TemporaryDirectory()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1840ee54-90e9-44b3-a425-7de490e325bc", + "metadata": {}, + "outputs": [], + "source": [ + "with NamedTemporaryFile(prefix=\"opsim-\", suffix=\".db\", dir=data_dir.name) as temp_file:\n", + " opsim_output_fname = temp_file.name\n", + "\n", + "SchemaConverter().obs2opsim(observations, filename=opsim_output_fname)\n", + "opsim_output_fname" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2fec714c-ea8c-48b8-b9e9-426889b5e3f6", + "metadata": {}, + "outputs": [], + "source": [ + "with NamedTemporaryFile(\n", + " prefix=\"scheduler-\", suffix=\".pickle.xz\", dir=data_dir.name\n", + ") as temp_file:\n", + " scheduler_fname = temp_file.name\n", + "\n", + "with lzma.open(scheduler_fname, \"wb\", format=lzma.FORMAT_XZ) as pio:\n", + " pickle.dump(scheduler, pio)\n", + "\n", + "scheduler_fname" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4c8dc1c2-43f1-4195-bdf1-8c0b5d211340", + "metadata": {}, + "outputs": [], + "source": [ + "with NamedTemporaryFile(\n", + " prefix=\"rewards-\", suffix=\".h5\", dir=data_dir.name\n", + ") as temp_file:\n", + " rewards_fname = temp_file.name\n", + "\n", + "reward_df.to_hdf(rewards_fname, \"reward_df\")\n", + "obs_rewards.to_hdf(rewards_fname, \"obs_rewards\")" ] }, { "cell_type": "markdown", - "id": "2cb29a00-3857-4179-906e-5feb93662cd3", + "id": "6a0a04da-a982-4487-9725-6f83fba81b83", "metadata": {}, "source": [ - "## Display the dashboard" + "If you're host doesn't have a lot of memory, you may need to clean out some memory before trying to start the dashboard." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "529560b0-810c-4894-8a79-cd547ca425b7", + "metadata": {}, + "outputs": [], + "source": [ + "# del observations\n", + "del scheduler\n", + "del reward_df\n", + "del obs_rewards" ] }, { "cell_type": "markdown", - "id": "576838fd-8414-4695-8414-c2f078b2e53f", + "id": "0c8c4987-fa33-449a-b7d8-4595874e4621", "metadata": {}, "source": [ - "Let's look at the last (and only) full night we simulated:" + "## Make the dashboard" + ] + }, + { + "cell_type": "markdown", + "id": "d843b92c-33c1-44b1-9679-c0bc2e9b628d", + "metadata": {}, + "source": [ + "Including two instances of the scheduler takes too much memory, crashes the kernel. Bummer." ] }, { "cell_type": "code", "execution_count": null, - "id": "153a3969-e63b-42fb-bfff-49dd0b9a44f2", + "id": "12d50d47-869b-4b5e-89bf-1d0e6aabe0b6", "metadata": { "tags": [] }, "outputs": [], "source": [ - "app" + "pn_app = schedview.app.prenight.prenight_app(\n", + " night_date,\n", + " observations=opsim_output_fname,\n", + " scheduler=scheduler_fname,\n", + " rewards=rewards_fname,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2b0b3358-221a-49c8-b0c1-4ab0c2293feb", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "if False:\n", + " out = \"Show with panel button at top of jupyter tab\"\n", + "else:\n", + " out = pn_app\n", + "\n", + "out" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b218a897-ea5c-4828-9a74-ae4eb7af74db", + "metadata": {}, + "outputs": [], + "source": [ + "assert False" ] }, { "cell_type": "markdown", - "id": "71fa7945-59a5-4481-ad06-50e76b244e83", + "id": "629e19e4-0b3e-4430-b43d-779f8fc7d08f", "metadata": {}, "source": [ "# Adjusting plot parameters beyond what the explore interface does" @@ -418,7 +529,7 @@ }, { "cell_type": "markdown", - "id": "128b3815-8daf-4565-a5d4-16cb1a31ff0b", + "id": "491a5f5a-0934-4fce-b499-226d648ace01", "metadata": {}, "source": [ "Build an independent explorer.\n", @@ -430,7 +541,7 @@ }, { "cell_type": "markdown", - "id": "f533bd75-20b5-46aa-840c-f6253b921470", + "id": "1a613c56-839c-4e0c-a91b-d8b8aec5cfa4", "metadata": {}, "source": [ "Start by getting the data set used by the explorer:" @@ -452,6 +563,7 @@ "\n", "schema_converter = SchemaConverter()\n", "visits = schema_converter.obs2opsim(observations)\n", + "\n", "visits[\"start_date\"] = pd.to_datetime(\n", " visits[\"observationStartMJD\"] + 2400000.5, origin=\"julian\", unit=\"D\", utc=True\n", ")\n", @@ -473,7 +585,7 @@ }, { "cell_type": "markdown", - "id": "facd4b08-c225-430d-8ca1-200d80e135b4", + "id": "b07fcefe-0010-4158-a238-ffc39e3cd2a0", "metadata": {}, "source": [ "Use the explorer GUI above to get the plot as close as you can to what you want, then use the cell below to capture the python needed to generate that plot, and make further adjustments as necessary." @@ -512,7 +624,7 @@ { "cell_type": "code", "execution_count": null, - "id": "957db80b-7757-4f74-aad5-ca5c996be399", + "id": "bad03ab6-f07e-4194-a980-dcacdf6a898a", "metadata": {}, "outputs": [], "source": [] @@ -520,9 +632,9 @@ ], "metadata": { "kernelspec": { - "display_name": "updated0", + "display_name": "schedview", "language": "python", - "name": "updated0" + "name": "schedview" }, "language_info": { "codemirror_mode": { @@ -534,12 +646,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.11" - }, - "vscode": { - "interpreter": { - "hash": "8f716438b432a9cce0d1718507c983c697ec5d817d7dabcbee39092aa596e59c" - } + "version": "3.11.4" } }, "nbformat": 4, diff --git a/notebooks/scheduler.ipynb b/notebooks/scheduler.ipynb index 68df7c0c..e34334a0 100644 --- a/notebooks/scheduler.ipynb +++ b/notebooks/scheduler.ipynb @@ -56,7 +56,8 @@ "import dateutil\n", "from datetime import timezone\n", "from zoneinfo import ZoneInfo\n", - "import bokeh" + "import bokeh\n", + "from astropy.utils.iers import IERSDegradedAccuracyWarning" ] }, { @@ -210,7 +211,8 @@ " \"ignore\",\n", " module=\"rubin_sim\",\n", " message=\"All-NaN slice encountered\",\n", - ")" + ")\n", + "warnings.filterwarnings(\"ignore\", category=IERSDegradedAccuracyWarning, append=True)" ] }, { @@ -833,7 +835,7 @@ { "cell_type": "code", "execution_count": null, - "id": "9ea00171-a224-40ae-a1b3-e5da04566c7c", + "id": "c3e8591c-436e-458c-9e4c-75ea229dea4b", "metadata": {}, "outputs": [], "source": [] @@ -841,9 +843,9 @@ ], "metadata": { "kernelspec": { - "display_name": "ehn311", + "display_name": "schedview", "language": "python", - "name": "ehn311" + "name": "schedview" }, "language_info": { "codemirror_mode": { diff --git a/notebooks/spheremap.ipynb b/notebooks/spheremap.ipynb deleted file mode 100644 index 794db246..00000000 --- a/notebooks/spheremap.ipynb +++ /dev/null @@ -1,1157 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": null, - "id": "c0436180-4435-41b8-812f-e219625d1883", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "import numpy as np\n", - "import healpy as hp\n", - "import bokeh\n", - "import colorcet as cc\n", - "import pandas as pd\n", - "import panel as pn\n", - "from astropy.time import Time\n", - "import sqlite3\n", - "import astropy.coordinates\n", - "\n", - "import healsparse as hsp\n", - "\n", - "import schedview.collect.scheduler_pickle\n", - "from schedview.plot.spheremap import (\n", - " Planisphere,\n", - " ArmillarySphere,\n", - " MollweideMap,\n", - " HorizonMap,\n", - " split_healpix_by_resolution,\n", - ")\n", - "from schedview.compute.camera import LsstCameraFootprintPerimeter\n", - "from rubin_sim.scheduler.model_observatory.model_observatory import ModelObservatory\n", - "import schedview.compute.astro\n", - "from schedview.collect.stars import load_bright_stars\n", - "import rubin_sim\n", - "from rubin_sim.scheduler.utils.footprints import get_dustmap\n", - "\n", - "from rubin_sim.scheduler.utils.sky_area import SkyAreaGenerator" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "4494afa4-4fec-49d2-828e-bc35f1bae14b", - "metadata": {}, - "outputs": [], - "source": [ - "pn.extension()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "1fc0a1d5-8b29-4475-b844-9a8af1dfb26c", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "%%html\n", - "" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "4a8443c8-a8fe-454b-b20a-fba675991294", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "BAND_COLORS = dict(zip((\"u\", \"g\", \"r\", \"i\", \"z\", \"y\"), bokeh.palettes.Bokeh[6]))\n", - "\n", - "BAND_HATCH_PATTERNS = dict(\n", - " u=\"dot\",\n", - " g=\"ring\",\n", - " r=\"horizontal_line\",\n", - " i=\"vertical_line\",\n", - " z=\"right_diagonal_line\",\n", - " y=\"left_diagonal_line\",\n", - ")\n", - "BAND_HATCH_SCALES = dict(u=6, g=6, r=6, i=6, z=12, y=12)\n", - "\n", - "NSIDE_LOW = 8" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "045d6d28-0d43-4e0d-9fa6-77bc27113039", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "observatory = ModelObservatory()\n", - "night = Time(\"2026-05-30\", location=observatory.location)" - ] - }, - { - "cell_type": "markdown", - "id": "e8a9d739-b3b6-420b-9535-dac5e4c38c7e", - "metadata": {}, - "source": [ - "# Simple planisphere example" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "de150d15-b9eb-4c9f-8eae-ffc9a7663ab4", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "plot = bokeh.plotting.figure(\n", - " plot_width=512,\n", - " plot_height=512,\n", - " match_aspect=True,\n", - " title=\"Sample 1\",\n", - ")\n", - "psphere = Planisphere(mjd=night.mjd, plot=plot)\n", - "psphere.add_mjd_slider()\n", - "psphere.add_graticules(\n", - " graticule_kwargs={\n", - " \"min_decl\": -80,\n", - " \"max_decl\": 80,\n", - " \"decl_space\": 20,\n", - " \"min_ra\": 0,\n", - " \"max_ra\": 360,\n", - " \"ra_space\": 30,\n", - " },\n", - " line_kwargs={\"color\": \"lightgray\"},\n", - ")\n", - "psphere.add_ecliptic(color=\"green\")\n", - "psphere.add_galactic_plane(color=\"blue\")\n", - "psphere.add_horizon()\n", - "psphere.add_horizon(zd=70, line_kwargs={\"color\": \"red\", \"line_width\": 2})\n", - "pn.Row(psphere.figure)" - ] - }, - { - "cell_type": "markdown", - "id": "35bc25e3-c300-4a93-a9a0-2a3cce4e4529", - "metadata": {}, - "source": [ - "# Simple armillary sphere example" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "48fd12d3-0622-4775-892e-b9c0021d3750", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "plot = bokeh.plotting.figure(\n", - " plot_width=512,\n", - " plot_height=512,\n", - " match_aspect=True,\n", - " title=\"Sample 2\",\n", - ")\n", - "asphere = ArmillarySphere(mjd=night.mjd, plot=plot)\n", - "asphere.add_mjd_slider()\n", - "asphere.add_graticules()\n", - "asphere.add_ecliptic()\n", - "asphere.add_galactic_plane()\n", - "asphere.add_horizon()\n", - "asphere.add_horizon(zd=70, line_kwargs={\"color\": \"red\", \"line_width\": 2})\n", - "pn.Row(asphere.figure)" - ] - }, - { - "cell_type": "markdown", - "id": "52d24c01-0386-4aae-8667-b5d8f8f7890d", - "metadata": { - "execution": { - "iopub.execute_input": "2023-04-21T19:10:07.171869Z", - "iopub.status.busy": "2023-04-21T19:10:07.171375Z", - "iopub.status.idle": "2023-04-21T19:10:07.173953Z", - "shell.execute_reply": "2023-04-21T19:10:07.173605Z", - "shell.execute_reply.started": "2023-04-21T19:10:07.171844Z" - } - }, - "source": [ - "# Multiple connected views" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "ed283de5-f0af-498c-9db9-13ad20afae46", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "data_source = {}\n", - "\n", - "asphere_plot = bokeh.plotting.figure(\n", - " plot_width=512,\n", - " plot_height=512,\n", - " match_aspect=True,\n", - " title=\"Sample 3a\",\n", - ")\n", - "asphere = ArmillarySphere(mjd=night.mjd, plot=asphere_plot)\n", - "mjd_slider = asphere.add_mjd_slider()\n", - "\n", - "asphere.add_graticules()\n", - "asphere.add_ecliptic()\n", - "asphere.add_galactic_plane()\n", - "data_source[\"horizon\"] = asphere.add_horizon()\n", - "data_source[\"high_X\"] = asphere.add_horizon(\n", - " zd=70, line_kwargs={\"color\": \"red\", \"line_width\": 2}\n", - ")\n", - "\n", - "psphere_plot = bokeh.plotting.figure(\n", - " plot_width=512,\n", - " plot_height=512,\n", - " match_aspect=True,\n", - " title=\"Sample 3b\",\n", - ")\n", - "psphere = Planisphere(mjd=night.mjd, plot=psphere_plot)\n", - "psphere.add_graticules(\n", - " graticule_kwargs={\n", - " \"min_decl\": -80,\n", - " \"max_decl\": 80,\n", - " \"decl_space\": 20,\n", - " \"min_ra\": 0,\n", - " \"max_ra\": 360,\n", - " \"ra_space\": 30,\n", - " },\n", - " line_kwargs={\"color\": \"lightgray\"},\n", - ")\n", - "psphere.add_ecliptic()\n", - "psphere.add_galactic_plane()\n", - "psphere.add_horizon(data_source=data_source[\"horizon\"])\n", - "psphere.add_horizon(\n", - " data_source=data_source[\"high_X\"], line_kwargs={\"color\": \"red\", \"line_width\": 2}\n", - ")\n", - "\n", - "msphere_plot = bokeh.plotting.figure(\n", - " plot_width=1024,\n", - " plot_height=512,\n", - " match_aspect=False,\n", - " title=\"Sample 3c\",\n", - ")\n", - "msphere = MollweideMap(mjd=night.mjd, plot=msphere_plot)\n", - "\n", - "msphere.add_graticules(\n", - " graticule_kwargs={\n", - " \"min_decl\": -80,\n", - " \"max_decl\": 80,\n", - " \"decl_space\": 20,\n", - " \"min_ra\": 0,\n", - " \"max_ra\": 360,\n", - " \"ra_space\": 30,\n", - " },\n", - " line_kwargs={\"color\": \"lightgray\"},\n", - ")\n", - "# HACK to make the RA=180 graticule appear both on the left and right\n", - "msphere.add_graticules(\n", - " graticule_kwargs={\n", - " \"min_decl\": -80,\n", - " \"max_decl\": 80,\n", - " \"decl_space\": 160,\n", - " \"min_ra\": 180 - 1e-6,\n", - " \"max_ra\": 180,\n", - " \"ra_space\": 30,\n", - " },\n", - " line_kwargs={\"color\": \"lightgray\"},\n", - ")\n", - "\n", - "msphere.add_ecliptic()\n", - "msphere.add_galactic_plane()\n", - "msphere.add_horizon(data_source=data_source[\"horizon\"])\n", - "msphere.add_horizon(\n", - " data_source=data_source[\"high_X\"], line_kwargs={\"color\": \"red\", \"line_width\": 2}\n", - ")\n", - "\n", - "pn.Column(msphere.figure, pn.Row(asphere.figure, psphere.figure))" - ] - }, - { - "cell_type": "markdown", - "id": "c4049de8-6e22-4101-a900-0760e8a2ea14", - "metadata": {}, - "source": [ - "# Add the sun, moon, and stars" - ] - }, - { - "cell_type": "markdown", - "id": "0046c616-a926-41af-a716-1c60a2ef9d94", - "metadata": {}, - "source": [ - "Use `astropy` to get the position of the sun and moon:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "5a3b5f60-2f8d-4c9a-9a44-fab495f922b3", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "sun_coords = astropy.coordinates.get_sun(night)\n", - "moon_coords = astropy.coordinates.get_moon(night)\n", - "sun_coords, moon_coords" - ] - }, - { - "cell_type": "markdown", - "id": "c2102e3c-c019-441e-9a81-bae8310b0939", - "metadata": {}, - "source": [ - "Load the Yale bright star catalog:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "0492de54-b2ac-458e-9641-b1d15c0dcf9f", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "try:\n", - " stars = load_bright_stars()\n", - "except FileNotFoundError:\n", - " stars = load_bright_stars(\"http://tdc-www.harvard.edu/catalogs/bsc5.dat.gz\")\n", - "\n", - "stars" - ] - }, - { - "cell_type": "markdown", - "id": "d1982da4-5ef6-41b6-9252-218339034570", - "metadata": {}, - "source": [ - "This is way too many stars. Only consider the brightest:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "d18e70f1-5474-42b6-9d3e-21df88c32333", - "metadata": {}, - "outputs": [], - "source": [ - "stars.query(\"Vmag<3.5\", inplace=True)" - ] - }, - { - "cell_type": "markdown", - "id": "147765d5-d15d-46a3-be4b-04f8bb4e590f", - "metadata": {}, - "source": [ - "Now show the figure:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "33a8cdba-939b-4beb-ab35-f21c898983a4", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "asphere = ArmillarySphere(mjd=night.mjd)\n", - "asphere.add_mjd_slider()\n", - "asphere.add_graticules()\n", - "asphere.add_ecliptic()\n", - "asphere.add_galactic_plane()\n", - "asphere.add_horizon()\n", - "asphere.add_horizon(zd=70, line_kwargs={\"color\": \"red\", \"line_width\": 2})\n", - "\n", - "asphere.add_marker(\n", - " sun_coords.ra.deg,\n", - " sun_coords.dec.deg,\n", - " name=\"Sun\",\n", - " glyph_size=15,\n", - " circle_kwargs={\"color\": \"brown\"},\n", - ")\n", - "asphere.add_marker(\n", - " moon_coords.ra.deg,\n", - " moon_coords.dec.deg,\n", - " name=\"Moon\",\n", - " glyph_size=15,\n", - " circle_kwargs={\"color\": \"orange\"},\n", - ")\n", - "\n", - "# Scale the size of the star markers with the magnitude of the stars\n", - "stars[\"glyph_size\"] = 15 * (1.01 - stars[\"Vmag\"] / stars[\"Vmag\"].max())\n", - "\n", - "# Actually add the stars\n", - "asphere.add_stars(stars, mag_limit_slider=True, star_kwargs={\"color\": \"black\"})\n", - "\n", - "# Set the limit of the slider according to the stars we've included\n", - "asphere.sliders[\"mag_limit\"].end = stars[\"Vmag\"].max()\n", - "\n", - "pn.Row(asphere.figure)" - ] - }, - { - "cell_type": "markdown", - "id": "e897f1fe-44ab-49c3-bedf-a1d4cc2155b9", - "metadata": {}, - "source": [ - "# Horizon coordinates" - ] - }, - { - "cell_type": "markdown", - "id": "b52ea98c-6d16-4e47-a5ed-5bea22cca072", - "metadata": {}, - "source": [ - "You can show a map in a horizon (az/alt polar) projection:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "548f8bd2-3fe9-4434-aeb1-b8921f45c9fa", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "hsphere = HorizonMap(mjd=night.mjd)\n", - "hsphere.add_mjd_slider()\n", - "hsphere.add_horizon_graticules()\n", - "hsphere.add_horizon()\n", - "hsphere.add_ecliptic()\n", - "hsphere.add_galactic_plane()\n", - "\n", - "hsphere.add_marker(\n", - " sun_coords.ra.deg,\n", - " sun_coords.dec.deg,\n", - " name=\"Sun\",\n", - " glyph_size=15,\n", - " circle_kwargs={\"color\": \"brown\"},\n", - ")\n", - "hsphere.add_marker(\n", - " moon_coords.ra.deg,\n", - " moon_coords.dec.deg,\n", - " name=\"Moon\",\n", - " glyph_size=15,\n", - " circle_kwargs={\"color\": \"orange\"},\n", - ")\n", - "hsphere.add_stars(stars, mag_limit_slider=True, star_kwargs={\"color\": \"black\"})\n", - "\n", - "pn.Row(hsphere.figure)" - ] - }, - { - "cell_type": "markdown", - "id": "8a459ad0-0eb9-4d2e-ada3-b13c47940b26", - "metadata": {}, - "source": [ - "You can show horizon graticules in any of the projections:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "e76b1824-6cd6-4d4c-82ca-9be7848296dd", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "asphere = ArmillarySphere(mjd=night.mjd)\n", - "asphere.add_mjd_slider()\n", - "asphere.add_graticules()\n", - "asphere.add_ecliptic()\n", - "asphere.add_galactic_plane()\n", - "asphere.add_horizon_graticules(line_kwargs={\"color\": \"red\", \"line_dash\": \"dotted\"})\n", - "pn.Row(asphere.figure)" - ] - }, - { - "cell_type": "markdown", - "id": "57429ef7-89ad-430e-b0a5-ecfdd140b03d", - "metadata": {}, - "source": [ - "# Show a healpix map" - ] - }, - { - "cell_type": "markdown", - "id": "6d3a3fa0-5462-4e0f-8d11-177b8aa38a56", - "metadata": {}, - "source": [ - "Get the healpix dust map from `rubin_sim` as an example healpix map." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "89181297-5c50-403e-aee8-959610ac65be", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "dust = get_dustmap(nside=32)" - ] - }, - { - "cell_type": "markdown", - "id": "caf67741-e22a-4e4b-8101-abb45cdd432a", - "metadata": {}, - "source": [ - "Make a `bokeh` color map:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "d2196559-2014-4f7d-9611-7a4242027da9", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "cmap = bokeh.transform.log_cmap(\n", - " \"value\", cc.palette[\"CET_L18\"], np.quantile(dust, 0.75), np.quantile(dust, 0.99)\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "364620e5-71a0-4f74-adea-86d76a6df940", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "asphere = ArmillarySphere(mjd=night.mjd)\n", - "asphere.add_healpix(dust, nside=32, cmap=cmap)\n", - "asphere.add_graticules()\n", - "asphere.add_ecliptic(color=\"lightgreen\")\n", - "asphere.add_galactic_plane(color=\"lightblue\")\n", - "pn.Row(asphere.figure)" - ] - }, - { - "cell_type": "markdown", - "id": "f0112fb8-ee73-472b-ae71-7275e1d4b366", - "metadata": {}, - "source": [ - "# Show a healsparse map" - ] - }, - { - "cell_type": "markdown", - "id": "3417164d-8c0e-40f3-878f-4e71faa5fb45", - "metadata": {}, - "source": [ - "At nsides greater than 32, interactivity can be sluggish. Sometimes you can reduce the number of pixels by only displaying some regions of the sky. For example, we can show only the high dust areas in the dust map above:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "f80c1f00-9593-4c4d-9dbb-c70b13479333", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "# Get a higher resolution healpix map\n", - "dust64 = hp.reorder(get_dustmap(nside=64), inp=\"RING\", out=\"NESTED\")\n", - "\n", - "# Cut off any pixels lower than the bottom for our color map\n", - "dust64[dust64 < cmap[\"transform\"].low] = hp.UNSEEN\n", - "\n", - "# Make a healsparse map with just the seen healpixel\n", - "dust_hsp = hsp.HealSparseMap(nside_coverage=16, healpix_map=dust64)" - ] - }, - { - "cell_type": "markdown", - "id": "0f516747-6012-4d8e-861e-3cdb65712f0b", - "metadata": {}, - "source": [ - "Make a color map such that low dust areas are near white, and thus fall to white as dust drops:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "7b63b2c8-cfb0-451e-b093-193944ff5c69", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "asphere = ArmillarySphere(mjd=night.mjd)\n", - "asphere.add_healpix(dust_hsp, nside=64, cmap=cmap)\n", - "asphere.add_graticules()\n", - "asphere.add_ecliptic(color=\"lightgreen\")\n", - "asphere.add_galactic_plane(color=\"lightblue\")\n", - "pn.Row(asphere.figure)" - ] - }, - { - "cell_type": "markdown", - "id": "cf48ff72-1dfd-4279-b85b-166919b7d8f9", - "metadata": { - "execution": { - "iopub.execute_input": "2023-04-24T19:58:39.400081Z", - "iopub.status.busy": "2023-04-24T19:58:39.399848Z", - "iopub.status.idle": "2023-04-24T19:58:39.402451Z", - "shell.execute_reply": "2023-04-24T19:58:39.402013Z", - "shell.execute_reply.started": "2023-04-24T19:58:39.400062Z" - }, - "tags": [] - }, - "source": [ - "# Survey footprint" - ] - }, - { - "cell_type": "markdown", - "id": "5c673a62-be4d-4cfd-a9b0-418bc920cd29", - "metadata": {}, - "source": [ - "Get the final target survey footprint:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "0394579d-f66a-4833-8749-9ce23739fa89", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "nside = 64\n", - "sky_area_generator = SkyAreaGenerator(nside=nside)\n", - "footprint, footprint_pix_labels = sky_area_generator.return_maps()" - ] - }, - { - "cell_type": "markdown", - "id": "9c171d9c-134b-48c2-ba75-b8efc55084af", - "metadata": {}, - "source": [ - "Split the desired footprint between edges between regions, which we will show using the full nside healsparse map, and a lower-nside healsparse map on large uniform areas:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "4ef74b2b-fe5c-4908-934b-11ecce128562", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "low_nside = 16\n", - "this_footprint = footprint[\"g\"].copy()\n", - "this_footprint[this_footprint == 0] = hp.UNSEEN\n", - "footprint_high, footprint_low = split_healpix_by_resolution(\n", - " this_footprint, low_nside, nside\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "f8cc36ee-b8d3-42b3-b6d8-29c790751723", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "asphere = ArmillarySphere(mjd=night.mjd)\n", - "\n", - "cmap = bokeh.transform.linear_cmap(\"value\", cc.palette[\"rainbow4\"], 0, 1)\n", - "\n", - "asphere.add_healpix(footprint_high, nside=nside, cmap=cmap)\n", - "asphere.add_healpix(footprint_low, nside=low_nside, cmap=cmap)\n", - "asphere.add_graticules()\n", - "asphere.add_ecliptic(color=\"lightgreen\")\n", - "asphere.add_galactic_plane(color=\"lightblue\")\n", - "pn.Row(asphere.figure)" - ] - }, - { - "cell_type": "markdown", - "id": "9a9e8511-ed77-4e65-93fc-7a333c0880fa", - "metadata": {}, - "source": [ - "# Arbitrary bokeh\n", - "\n", - "A planned refactor of `spheremap` will result in a change in the API used in this section\n", - "\n", - "See PREOPS-3405." - ] - }, - { - "cell_type": "markdown", - "id": "ba4290eb-5ffb-46fb-817f-c03df07917a1", - "metadata": {}, - "source": [ - "## Data sources with point locations" - ] - }, - { - "cell_type": "markdown", - "id": "a8877108-80e7-4003-9bbe-b30bcba1d027", - "metadata": {}, - "source": [ - "Use `asphere.make_points` to create the data source.\n", - "\n", - "The `plot` member of `SphereMap` and its children (`ArmillarySphere`, etc.) is just a normal `bokeh.plotting.Figure`.\n", - "\n", - "The `make_points` method of any of these objects generate a `bokeh.models.ColumnDataSource` with columns with the coordinates on the projection plane for the different projections. A client-side javascript callback updates these columns as necessary. The `x_col` and `y_col` members of `SphereMap`'s children hold the names of the columns that have the `x` and `y` coordinates in the projection plane.\n", - "\n", - "So, to plot on the projection plane using any of the varous methods of `bokeh.plotting.Figure` that take single sets of coordinates for each value, create the appropriate data source using `make_points`, and call the appropriate member of" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "1f547ba3-7d8b-47df-afba-5fa0d0607424", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "npoints = 100\n", - "sample_df = pd.DataFrame(\n", - " {\n", - " \"name\": \"sample\",\n", - " \"glyph_size\": 10,\n", - " \"ra\": np.random.random(npoints) * 360,\n", - " \"decl\": np.random.random(npoints) * 180 - 90,\n", - " }\n", - ")\n", - "asphere = ArmillarySphere(mjd=night.mjd)\n", - "\n", - "sample_ds = asphere.make_points(sample_df)\n", - "asphere.plot.asterisk(asphere.x_col, asphere.y_col, size=\"glyph_size\", source=sample_ds)\n", - "asphere.add_graticules()\n", - "asphere.add_ecliptic(color=\"lightgreen\")\n", - "asphere.add_galactic_plane(color=\"lightblue\")\n", - "pn.Row(asphere.figure)" - ] - }, - { - "cell_type": "markdown", - "id": "8118911b-306e-4a5b-9757-8258b995b65c", - "metadata": {}, - "source": [ - "## Data sources with paths" - ] - }, - { - "cell_type": "markdown", - "id": "959c82f0-ea1a-4380-bc58-cfde90b59923", - "metadata": {}, - "source": [ - "Use `asphere.make_patches_data_source` to create the data source." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "786bf14f-c768-4462-b582-64a38739eaf0", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "npoints = 25\n", - "sample_df = pd.DataFrame(\n", - " {\n", - " \"center_ra\": np.random.random(npoints) * 360,\n", - " \"center_decl\": np.random.random(npoints) * 180 - 90,\n", - " \"rotation\": 180 * np.random.random(npoints),\n", - " }\n", - ")\n", - "\n", - "camera_perimeter = LsstCameraFootprintPerimeter()\n", - "ras, decls = camera_perimeter(\n", - " sample_df.center_ra, sample_df.center_decl, sample_df.rotation\n", - ")\n", - "sample_df[\"ra\"] = ras\n", - "sample_df[\"decl\"] = decls\n", - "sample_df.head()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "88ff5309-4917-4c5d-bcee-e2571db025de", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "asphere = ArmillarySphere(mjd=night.mjd)\n", - "sample_ds = asphere.make_patches_data_source(sample_df)\n", - "asphere.plot.patches(\n", - " xs=asphere.x_col,\n", - " ys=asphere.y_col,\n", - " source=sample_ds,\n", - " line_color=\"blue\",\n", - " fill_color=\"red\",\n", - " fill_alpha=0.2,\n", - ")\n", - "asphere.add_graticules()\n", - "asphere.add_ecliptic(color=\"lightgreen\")\n", - "asphere.add_galactic_plane(color=\"lightblue\")\n", - "pn.Row(asphere.figure)" - ] - }, - { - "cell_type": "markdown", - "id": "e2caecdc-3141-4c4f-bdcb-f8ca4586f1cf", - "metadata": {}, - "source": [ - "# Plotting visits for a night, with bells and whistles" - ] - }, - { - "cell_type": "markdown", - "id": "d28b5cb1-3734-4aea-b920-603d31aaf2b4", - "metadata": {}, - "source": [ - "Get a visit table from the baseline:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "c99058a8-21ff-4d3d-a25b-4527dff167d8", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "baseline_visits_fname = rubin_sim.data.get_baseline()\n", - "with sqlite3.connect(baseline_visits_fname) as connection:\n", - " night_index = (\n", - " pd.read_sql_query(\n", - " f\"SELECT ROUND(AVG(night)) FROM Observations WHERE ROUND(observationStartMJD)={night.mjd}\",\n", - " connection,\n", - " )\n", - " .values[0, 0]\n", - " .astype(int)\n", - " )\n", - " visits = pd.read_sql_query(\n", - " f\"\"\"\n", - " SELECT observationStartMJD AS mjd,\n", - " fieldRA AS center_ra,\n", - " fieldDec AS center_decl,\n", - " rotSkyPos AS rotation,\n", - " filter AS band\n", - " FROM Observations\n", - " WHERE night={night_index}\"\"\",\n", - " connection,\n", - " )\n", - "\n", - "camera_perimeter = LsstCameraFootprintPerimeter()\n", - "visits[\"ra\"], visits[\"decl\"] = camera_perimeter(\n", - " visits.center_ra, visits.center_decl, visits.rotation\n", - ")\n", - "visits" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "b34feb91-36b0-4c6d-a941-e4f277b210b5", - "metadata": {}, - "outputs": [], - "source": [ - "initial_mjd = visits.mjd.max()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "6fde4f69-e2e9-4ada-af0a-90175b67ae63", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "# Only show the visit if the slider has a value greater than the observation MJD\n", - "visits[\"min_mjd\"] = visits.mjd\n", - "visits[\"in_mjd_window\"] = np.where(visits.mjd<=initial_mjd, 0.5, 0)\n", - "\n", - "# Emphasize the visits just behind the MJD window,\n", - "# that is, recent as of the time on the slider\n", - "visits[\"fade_scale\"] = 2.0 / (24 * 60)\n", - "visits[\"recent_mjd\"] = np.clip((initial_mjd - visits.mjd) * visits.fade_scale, 0, 1)\n", - "\n", - "# Use the best categorical bokeh palette for the bands actually used\n", - "used_bands = [b for b in \"ugrizy\" if b in visits['band'].values]\n", - "num_bands = len(used_bands)\n", - "base_palette = bokeh.palettes.Spectral\n", - "try:\n", - " band_palette = dict(zip(used_bands, base_palette[num_bands]))\n", - "except KeyError:\n", - " band_palette = dict(zip(used_bands, base_palette[3][:num_bands]))\n", - "\n", - "visits[\"color\"] = visits.band.map(band_palette)" - ] - }, - { - "cell_type": "markdown", - "id": "62fed4d1-6b3b-451d-85a8-cbb139b59611", - "metadata": { - "execution": { - "iopub.execute_input": "2023-04-25T15:45:20.136277Z", - "iopub.status.busy": "2023-04-25T15:45:20.136030Z", - "iopub.status.idle": "2023-04-25T15:45:20.139561Z", - "shell.execute_reply": "2023-04-25T15:45:20.139232Z", - "shell.execute_reply.started": "2023-04-25T15:45:20.136247Z" - }, - "tags": [] - }, - "source": [ - "Sun and moon coordinates:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "fa8d7ad9-a7f7-4c10-b5f0-e9dcc8cea240", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "sun_coords = astropy.coordinates.get_sun(night)\n", - "moon_coords = astropy.coordinates.get_moon(night)" - ] - }, - { - "cell_type": "markdown", - "id": "0424707d-0efe-4ec7-940e-060d46899608", - "metadata": {}, - "source": [ - "Include the survey footprint:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "37d9a94f-9441-48b1-9e3a-c828b109ba85", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "nside = 32\n", - "sky_area_generator = SkyAreaGenerator(nside=nside)\n", - "footprint, footprint_pix_labels = sky_area_generator.return_maps()\n", - "\n", - "low_nside = 16\n", - "this_footprint = footprint[\"r\"].copy()\n", - "this_footprint[this_footprint == 0] = hp.UNSEEN\n", - "footprint_high, footprint_low = split_healpix_by_resolution(\n", - " this_footprint, low_nside, nside\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "f631b214-9caf-4050-a3e1-b559ad52176c", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "data_source = {}\n", - "\n", - "asphere_plot = bokeh.plotting.figure(\n", - " plot_width=768,\n", - " plot_height=512,\n", - " match_aspect=True,\n", - " title=\"Armillary Sphere\",\n", - ")\n", - "\n", - "asphere = ArmillarySphere(mjd=initial_mjd, plot=asphere_plot)\n", - "\n", - "visit_ds = {}\n", - "for band in band_palette.keys():\n", - " band_visits = visits.query(f\"band=='{band}'\")\n", - " if len(band_visits)>0:\n", - " visit_ds[band] = asphere.make_patches_data_source(visits.query(f\"band=='{band}'\"))\n", - "\n", - "# Faint gray footprint in the background\n", - "cmap = bokeh.transform.linear_cmap(\n", - " \"value\", list(reversed(bokeh.palettes.Greys256))[:32], 0, 1\n", - ")\n", - "data_source['footprint_high'], cm, gl = asphere.add_healpix(footprint_high, nside=nside, cmap=cmap)\n", - "data_source['footprint_low'], cm, gl = asphere.add_healpix(footprint_low, nside=low_nside, cmap=cmap)\n", - "\n", - "# The visits\n", - "for band, band_visit_ds in visit_ds.items():\n", - " asphere.plot.patches(\n", - " xs=asphere.x_col,\n", - " ys=asphere.y_col,\n", - " source=band_visit_ds,\n", - " fill_alpha=\"in_mjd_window\",\n", - " line_alpha=\"recent_mjd\",\n", - " line_color=\"black\",\n", - " line_width=2,\n", - " fill_color=\"color\",\n", - " legend_label=band\n", - " )\n", - "\n", - "# The sun\n", - "data_source['sun'] = asphere.add_marker(\n", - " sun_coords.ra.deg,\n", - " sun_coords.dec.deg,\n", - " name=\"Sun\",\n", - " glyph_size=15,\n", - " circle_kwargs={\"color\": \"yellow\", \"legend_label\": \"Sun\"},\n", - ")\n", - "\n", - "# The moon\n", - "data_source['moon'] = asphere.add_marker(\n", - " moon_coords.ra.deg,\n", - " moon_coords.dec.deg,\n", - " name=\"Moon\",\n", - " glyph_size=15,\n", - " circle_kwargs={\"color\": \"orange\", \"legend_label\": \"Moon\"},\n", - ")\n", - "\n", - "asphere.add_graticules()\n", - "asphere.add_ecliptic(color=\"lightgreen\", legend_label=\"Ecliptic\")\n", - "asphere.add_galactic_plane(color=\"lightblue\", legend_label=\"Galactic plane\")\n", - "data_source['horizon'] = asphere.add_horizon(line_kwargs={\"legend_label\": \"Horizon\"})\n", - "data_source['zd70'] = asphere.add_horizon(zd=70, line_kwargs={\"color\": \"red\", \"line_width\": 2, \"legend_label\": \"ZD=70\" + u'\\N{DEGREE SIGN}'})\n", - "\n", - "# Tweak the MJD slider end points to match the first and last visit\n", - "asphere.sliders['mjd'].start = visits.mjd.min()\n", - "asphere.sliders['mjd'].end = visits.mjd.max()\n", - "asphere.sliders['mjd'].value = initial_mjd\n", - "asphere.sliders['mjd'].step = 40./(24*60*60)\n", - "\n", - "asphere.plot.add_layout(asphere.plot.legend[0], \"right\")\n", - "# pn.Row(asphere.figure)" - ] - }, - { - "cell_type": "markdown", - "id": "76a3fd3c-922a-4cf4-97f8-82e57b30fa65", - "metadata": {}, - "source": [ - "Show the planisphere beside the armillary sphere:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "2094fac8-82ff-49d8-99ab-7e752c9353f9", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "psphere = Planisphere(mjd=initial_mjd)\n", - "psphere.add_healpix(data_source['footprint_high'], cmap=cmap)\n", - "psphere.add_healpix(data_source['footprint_low'], cmap=cmap)\n", - "\n", - "# The visits\n", - "for band, band_visit_ds in visit_ds.items():\n", - " psphere.plot.patches(\n", - " xs=psphere.x_col,\n", - " ys=psphere.y_col,\n", - " source=band_visit_ds,\n", - " fill_alpha=\"in_mjd_window\",\n", - " line_alpha=\"recent_mjd\",\n", - " line_color=\"black\",\n", - " line_width=2,\n", - " fill_color=\"color\",\n", - " )\n", - "\n", - "psphere.add_marker(\n", - " data_source=data_source['sun'],\n", - " name=\"Sun\",\n", - " glyph_size=15,\n", - " circle_kwargs={\"color\": \"yellow\", \"legend_label\": \"Sun\"},\n", - ")\n", - "\n", - "psphere.add_marker(\n", - " data_source=data_source['moon'],\n", - " name=\"Moon\",\n", - " glyph_size=15,\n", - " circle_kwargs={\"color\": \"orange\", \"legend_label\": \"Moon\"},\n", - ")\n", - "\n", - "psphere.add_graticules()\n", - "asphere.add_ecliptic(color=\"lightgreen\")\n", - "asphere.add_galactic_plane(color=\"lightblue\")\n", - "psphere.add_horizon(data_source=data_source[\"horizon\"])\n", - "psphere.add_horizon(\n", - " data_source=data_source[\"zd70\"], line_kwargs={\"color\": \"red\", \"line_width\": 2}\n", - ")\n", - "\n", - "\n", - "pn.Row(psphere.figure, asphere.figure)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "9ed7579b-bd3c-4a00-9088-9fe3a32f737f", - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "ehn310", - "language": "python", - "name": "ehn310" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.6" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/schedview/app/prenight/prenight.py b/schedview/app/prenight/prenight.py index 336ce3fa..a4b0c6bd 100644 --- a/schedview/app/prenight/prenight.py +++ b/schedview/app/prenight/prenight.py @@ -1,200 +1,271 @@ -import panel as pn +import param import logging -from copy import deepcopy -import pickle -import lzma -from tempfile import TemporaryDirectory, NamedTemporaryFile - import numpy as np +import pandas as pd +import os + from astropy.time import Time +import astropy.utils.iers -import rubin_sim from rubin_sim.scheduler.model_observatory import ModelObservatory import rubin_sim.scheduler.example -from rubin_sim.scheduler.utils import SchemaConverter import schedview.compute.astro import schedview.collect.opsim import schedview.compute.scheduler import schedview.collect.footprint +import schedview.plot.visits import schedview.plot.visitmap import schedview.plot.rewards import schedview.plot.visits import schedview.plot.maf import schedview.plot.nightbf +import schedview.param -TEMP_DIR = TemporaryDirectory() -DEFAULT_TIMEZONE = "Chile/Continental" +import panel as pn -pn.extension("tabulator", css_files=[pn.io.resources.CSS_URLS["font-awesome"]]) +AVAILABLE_TIMEZONES = [ + "Chile/Continental", + "US/Pacific", + "US/Arizona", + "US/Mountain", + "US/Central", + "US/Eastern", +] +DEFAULT_TIMEZONE = AVAILABLE_TIMEZONES[0] +DEFAULT_CURRENT_TIME = Time.now() +DEFAULT_OPSIM_FNAME = "opsim.db" +DEFAULT_SCHEDULER_FNAME = "scheduler.pickle.xz" +DEFAULT_REWARDS_FNAME = "rewards.h5" +USE_EXAMPLE_SCHEDULER = False + +astropy.utils.iers.conf.iers_degraded_accuracy = "warn" + +pn.extension( + "tabulator", + css_files=[pn.io.resources.CSS_URLS["font-awesome"]], + sizing_mode="stretch_width", +) +pn.config.console_output = "accumulate" + +logging.basicConfig(format="%(asctime)s %(message)s", level=logging.INFO) + +debug_info = pn.widgets.Debugger( + name="Debugger info level", level=logging.DEBUG, sizing_mode="stretch_both" +) + + +class Prenight(param.Parameterized): + opsim_output_fname = param.String( + default=DEFAULT_OPSIM_FNAME, + doc="The file name or URL of the OpSim output database.", + label="OpSim output database path or URL", + ) -logging.basicConfig(format="%(asctime)s %(message)s", level=logging.DEBUG) + scheduler_fname_doc = """URL or file name of the scheduler pickle file. +Such a pickle file can either be of an instance of a subclass of +rubin_sim.scheduler.schedulers.CoreScheduler, or a tuple of the form +(scheduler, conditions), where scheduler is an instance of a subclass of +rubin_sim.scheduler.schedulers.CoreScheduler, and conditions is an +instance of rubin_sim.scheduler.conditions.Conditions.""" + scheduler_fname = param.String( + default=DEFAULT_SCHEDULER_FNAME, + doc=scheduler_fname_doc, + label="URL or file name of scheduler pickle file", + ) + rewards_fname = param.String( + default=DEFAULT_REWARDS_FNAME, + doc="URL or file name of the rewards HDF5 file.", + label="URL or file name of rewards HDF5 file", + ) -def prenight_app( - observatory=ModelObservatory(), - scheduler=None, - observations=rubin_sim.data.get_baseline(), - obs_night=None, - reward_df=None, - obs_rewards=None, - timezone=DEFAULT_TIMEZONE, - nside=None, -): - """Create the prenight briefing app instance. + # Express the night as an instance of datetime.date, so that it can be + # used with the panel.widgets.DatePicker widget. + # It represents the local calendar date of sundown for the night to view. + night = param.Date( + default=DEFAULT_CURRENT_TIME.datetime.date(), + doc="The local calendar date of sundown for the night to view.", + label="Night to view (local time at sunset)", + ) - Parameters - ---------- - observatory : `ModelObservatory` or `None` - The model observatory to use. - By default, None. - scheduler : `rubin_sim.scheduler.schedulers.core_scheduler.CoreScheduler` - The scheduler instance to use. - observations : `str` - The name of the sqlite3 file with the simulation results. - Defaults to the baseline as specified by the `rubin_sim` dependency. - obs_night : `astropy.time.Time` - The night for which to display data. Defaults to the last full night - in observations. - reward_df : `pandas.DataFrame` or `None` - The rewards by survey, as recorded by the `scheduler` instance - when running the simulation. - Defaults to None. - obs_rewards : `pandas.DataFrame` or `None` - The mapping between scheduler calls and simulated observations, - as recorded by the `scheduler` instance. - Defaults to None. - timezone : `str` or `None` - The timezone to use for localtime. - nside : `int` or `None` - The nside to use for healpix maps to be shown. - timezone : `str`, optional - Timezone for horizontal axis, by default "Chile/Continental" + timezone = param.ObjectSelector( + objects=AVAILABLE_TIMEZONES, + default=DEFAULT_TIMEZONE, + doc='The timezone in which "local time" is to be shown.', + label="Timezone", + ) - Returns - ------- - app : `panel.viewable.Viewable` - The dashboard app. - """ + tier = param.ObjectSelector( + default="", + objects=[""], + doc="The label for the first index into the CoreScheduler.survey_lists.", + label="Tier", + ) + surveys = param.ListSelector( + objects=[], + default=[], + doc="The labels for the second index into the CoreScheduler.survey_lists.", + label="Surveys", + ) + basis_function = param.ObjectSelector( + default="", + objects=[""], + doc="The label for the basis function to be shown.", + label="Basis function", + ) - if nside is None: - try: - nside = scheduler.nside - except AttributeError: - nside = observatory.nside + _observatory = ModelObservatory() + _site = _observatory.location + # Must declare all of these as Parameters, even though they should not + # be set by the user, because they are used in the @depends methods, + # and otherwise Parametrized will assume that they depend on + # everything. + _visits = schedview.param.DataFrame( + None, + doc="The visits for the night.", + columns={ + "fieldRA": float, + "fieldDec": float, + "observationStartMJD": float, + "filter": str, + "rotSkyPos": float, + "rotSkyPos_desired": float, + }, + ) - if isinstance(observations, str): - opsim_output_fname = observations - else: - # If we are passed an array of observations, write them to a file. - converter = SchemaConverter() + # An instance of rubin_sim.scheduler.schedulers.CoreScheduler + _scheduler = param.Parameter() - # Get a unique temp file name - with NamedTemporaryFile( - prefix="opsim-", suffix=".db", dir=TEMP_DIR.name - ) as temp_file: - opsim_output_fname = temp_file.name + _almanac_events = schedview.param.DataFrame( + None, + doc="Events from the rubin_sim alamanc", + columns={"MJD": float, "LST": float, "UTC": pd.Timestamp}, + ) - converter.obs2opsim(observations, filename=opsim_output_fname) + _reward_df = schedview.param.DataFrame( + None, + columns={ + "basis_function": str, + "feasible": np.bool_, + "max_basis_reward": float, + "basis_area": float, + "basis_weight": float, + "tier_label": str, + "survey_label": str, + "survey_class": str, + "survey_reward": float, + "basis_function_class": object, + "queue_start_mjd": float, + "queue_fill_mjd_ns": np.int64, + }, + ) + _obs_rewards = schedview.param.Series() - if isinstance(scheduler, str) or scheduler is None: - scheduler_fname = scheduler - else: - # Get a unique temp file name - with NamedTemporaryFile( - prefix="scheduler-", suffix=".pickle.xz", dir=TEMP_DIR.name - ) as temp_file: - scheduler_fname = temp_file.name - - with lzma.open(scheduler_fname, "wb", format=lzma.FORMAT_XZ) as pio: - pickle.dump(scheduler, pio) - - site = observatory.location - - if obs_night is None: - if isinstance(observations, str): - # We were provided a database filename, not actual observations - converter = SchemaConverter() - observations = converter.opsim2obs(opsim_output_fname) - - end_mjd = observations["mjd"].max() - if end_mjd > observatory.sky_model.mjd_right.max(): - # If we cannot look at the last night of the observations, - # look at the first. - end_mjd = observations["mjd"].min() + 1 - - end_mjd_almanac = observatory.almanac.get_sunset_info(end_mjd) - - # If the last observation is in the first half (pm) of the night, - # guess that we want to look at the night before. If the simulator - # is configured to end on an integer mjd, it can happen that that - # the start of a night is just after the mjd rollover, so we can - # get just a few observations on the last night, and this last - # night is probably not the one we want to look at. - end_mjd_night_middle = 0.5 * ( - end_mjd_almanac["sunset"] + end_mjd_almanac["sunrise"] + @param.depends("night", "timezone", watch=True) + def _update_almanac_events(self): + logging.info("Updating almanac events.") + night_events = schedview.compute.astro.night_events( + self.night, self._site, self.timezone ) - if end_mjd < end_mjd_night_middle: - end_mjd_almanac = observatory.almanac.get_sunset_info(end_mjd - 1) - - # Get the night MJD based on local noon of sunset. - sunset_mjd_ut = end_mjd_almanac["sunset"] - sunset_mjd_local = sunset_mjd_ut + site.lon.deg / 360 - sunset_night_mjd = np.floor(sunset_mjd_local) - obs_night = Time(sunset_night_mjd, format="mjd", scale="utc") - - night = pn.widgets.DatePicker(name="Night", value=obs_night.datetime.date()) - timezone = pn.widgets.Select( - name="Timezone", - options=[ - "Chile/Continental", - "US/Pacific", - "US/Arizona", - "US/Mountain", - "US/Central", - "US/Eastern", - ], - ) - scheduler_fname = pn.widgets.TextInput( - name="Scheduler file name", value=scheduler_fname - ) - opsim_output_fname = pn.widgets.TextInput( - name="Opsim output", value=opsim_output_fname - ) + # Bokeh automatically converts all datetimes to UTC + # when displaying, which we do not want. So, turn the localized + # datetimes to naive datetimes so bokeh leaves them alone. + night_events[self.timezone] = night_events[self.timezone].dt.tz_localize(None) - visits_cache = {} - scheduler_cache = {} + self._almanac_events = night_events - def almanac_events(night, timezone): - logging.info("Updating almanac.") - night_time = Time(night.isoformat()) - almanac_events = schedview.compute.astro.night_events( - night_time, site, timezone - ) - almanac_events[timezone] = almanac_events[timezone].dt.tz_localize(None) - almanac_table = pn.widgets.Tabulator(almanac_events) - logging.info("Finished updating almanac.") + @param.depends("_almanac_events") + def almanac_events_table(self): + if self._almanac_events is None: + return "No almanac events computed." + + logging.info("Updating almanac table.") + almanac_table = pn.widgets.Tabulator(self._almanac_events) return almanac_table - def visit_explorer(opsim_output_fname, night): - logging.info("Updating visit explorer") - night_time = Time(night.isoformat()) + @param.depends("opsim_output_fname", "_almanac_events", watch=True) + def _update_visits(self): + if self.opsim_output_fname is None: + self._visits = None + return - visits_cache_key = (opsim_output_fname, night_time) - visits = visits_cache.get(visits_cache_key, opsim_output_fname) + if self._almanac_events is None: + self._update_almanac_events() + + logging.info("Updating visits.") + try: + if not os.path.exists(self.opsim_output_fname): + raise FileNotFoundError(f"File not found: {self.opsim_output_fname}") + + visits = schedview.collect.opsim.read_opsim( + self.opsim_output_fname, + Time(self._almanac_events.loc["sunset", "UTC"]), + Time(self._almanac_events.loc["sunrise", "UTC"]), + ) + self._visits = visits + except Exception as e: + logging.error(e) + self._visits = None + + @param.depends("_visits") + def visit_table(self): + """Create a tabuler display widget with visits. + + Returns + ------- + visit_table : `pn.widgets.Tabulator` + The table of visits. + """ + if self._visits is None: + return "No visits loaded." + + logging.info("Updating visit table") + columns = [ + "start_date", + "fieldRA", + "fieldDec", + "altitude", + "azimuth", + "filter", + "airmass", + "slewTime", + "moonDistance", + "block_id", + "note", + ] + + visit_table = pn.widgets.Tabulator(self._visits[columns]) + + if len(self._visits) < 1: + visit_table = "No visits on this night" + + logging.info("Finished updating visit table") + return visit_table + + @param.depends("_visits") + def visit_explorer(self): + """Create holoviz explorer on the visits. + + Returns + ------- + visit_explorer : `hvplot.ui.hvDataFrameExplorer` + The holoviz plot of visits. + """ + if self._visits is None: + return "No visits loaded." + + logging.info("Updating visit explorer") ( visit_explorer, visit_explorer_data, ) = schedview.plot.visits.create_visit_explorer( - visits=visits, - night_date=night_time, + visits=self._visits, + night_date=self.night, ) - visits_cache.clear() - visits_cache[visits_cache_key] = visit_explorer_data["visits"] - if len(visit_explorer_data["visits"]) < 1: visit_explorer = "No visits on this night." @@ -202,38 +273,53 @@ def visit_explorer(opsim_output_fname, night): return visit_explorer - def visit_skymaps(opsim_output_fname, scheduler_fname, night, timezone="UTC"): + @param.depends("scheduler_fname", watch=True) + def _update_scheduler(self): + logging.info("Updating scheduler.") + try: + ( + scheduler, + conditions, + ) = schedview.collect.scheduler_pickle.read_scheduler(self.scheduler_fname) + + self._scheduler = scheduler + except Exception as e: + logging.error(f"Could not load scheduler from {self.scheduler_fname} {e}") + if USE_EXAMPLE_SCHEDULER: + logging.info("Loading example scheduler.") + self._scheduler = rubin_sim.scheduler.example.example_scheduler( + nside=self._nside + ) + + @param.depends( + "_scheduler", + "_visits", + ) + def visit_skymaps(self): + """Create an interactive skymap of the visits. + + Returns + ------- + vmap : `bokeh.models.layouts.LayoutDOM` + The bokeh maps of visits. + """ + + if self._visits is None: + return "No visits are loaded." + + if self._scheduler is None: + return "No scheduler is loaded." + logging.info("Updating skymaps") - night_time = Time(night.isoformat()) - - visits_cache_key = (opsim_output_fname, night_time) - visits = visits_cache.get(visits_cache_key, opsim_output_fname) - - if scheduler_fname not in scheduler_cache: - if scheduler_fname is None: - scheduler = rubin_sim.scheduler.example.example_scheduler(nside=nside) - else: - ( - scheduler, - conditions, - ) = schedview.collect.scheduler_pickle.read_scheduler(scheduler_fname) - - scheduler_cache.clear() - scheduler_cache[scheduler_fname] = scheduler - else: - scheduler = deepcopy(scheduler_cache[scheduler_fname]) vmap, vmap_data = schedview.plot.visitmap.create_visit_skymaps( - visits=visits, - scheduler=scheduler, - night_date=night_time, - timezone=timezone, - observatory=observatory, + visits=self._visits, + scheduler=self._scheduler, + night_date=self.night, + timezone=self.timezone, + observatory=self._observatory, ) - visits_cache.clear() - visits_cache[visits_cache_key] = vmap_data["visits"] - if len(vmap_data["visits"]) < 1: vmap = "No visits on this night." @@ -241,178 +327,299 @@ def visit_skymaps(opsim_output_fname, scheduler_fname, night, timezone="UTC"): return vmap - def visit_table(opsim_output_fname, night, timezone="UTC"): - logging.info("Updating visit table") - columns = [ - "start_date", - "fieldRA", - "fieldDec", - "altitude", - "azimuth", - "filter", - "airmass", - "slewTime", - "moonDistance", - "block_id", - "note", - ] + @param.depends("rewards_fname", watch=True) + def _update_reward_df(self): + if self.rewards_fname is None: + return None + + logging.info("Updating reward dataframe.") + + try: + reward_df = pd.read_hdf(self.rewards_fname, "reward_df") + except Exception as e: + logging.error(e) + + self._reward_df = reward_df + + @param.depends("_reward_df", watch=True) + def _update_tier_selector(self): + if self._reward_df is None: + self.param["tier"].objects = [""] + self.tier = "" + return + + logging.info("Updating tier selector.") + tiers = self._reward_df.tier_label.unique().tolist() + self.param["tier"].objects = tiers + self.tier = tiers[0] + + @param.depends("_reward_df", "tier", watch=True) + def _update_surveys_selector(self): + init_displayed_surveys = 7 + + if self._reward_df is None or self.tier == "": + self.param["surveys"].objects = [""] + self.surveys = "" + return + + logging.info("Updating surveys selector.") + + surveys = ( + self._reward_df.set_index("tier_label") + .loc[self.tier, "survey_label"] + .unique() + .tolist() + ) + self.param["surveys"].objects = surveys + self.surveys = ( + surveys[:init_displayed_surveys] + if len(surveys) > init_displayed_surveys + else surveys + ) - night_time = Time(night.isoformat()) + @param.depends("_reward_df", "tier", "surveys", watch=True) + def _update_basis_function_selector(self): + if self._reward_df is None or self.tier == "" or self.surveys == "": + self.param["basis_function"].objects = [""] + self.basis_function = "" + return - visits_cache_key = (opsim_output_fname, night_time) - visits = visits_cache.get(visits_cache_key, opsim_output_fname) + logging.info("Updating basis function selector.") - night_events = schedview.compute.astro.night_events( - night_date=night_time, site=site, timezone=timezone + tier_reward_df = self._reward_df.set_index("tier_label").loc[self.tier, :] + + basis_functions = ["Total"] + ( + tier_reward_df.set_index("survey_label") + .loc[self.surveys, "basis_function"] + .unique() + .tolist() ) - start_time = Time(night_events.loc["sunset", "UTC"]) - end_time = Time(night_events.loc["sunrise", "UTC"]) + self.param["basis_function"].objects = basis_functions + self.basis_function = "Total" - # Collect - if isinstance(visits, str): - visits = schedview.collect.opsim.read_opsim( - visits, Time(start_time).iso, Time(end_time).iso - ) + @param.depends("rewards_fname", watch=True) + def _update_obs_rewards(self): + if self.rewards_fname is None: + return None - visits_cache.clear() - visits_cache[visits_cache_key] = visits + try: + obs_rewards = pd.read_hdf(self.rewards_fname, "obs_rewards") + except Exception as e: + logging.error(e) - visit_table = pn.widgets.Tabulator(visits[columns]) + self._obs_rewards = obs_rewards - if len(visits) < 1: - visit_table = "No visits on this night" - logging.info("Finished updating visit table") - return visit_table + @param.depends("_reward_df", "tier") + def reward_params(self): + """Create a param set for the reward plot. - # ########################## - # Basis function examination - # ########################## + Returns + ------- + param_set : `panel.Param` + """ + if self._reward_df is None: + this_param_set = pn.Param( + self.param, + parameters=["rewards_fname"], + ) + return this_param_set - if reward_df is not None: - tier = pn.widgets.Select( - name="Tier", - options=reward_df.tier_label.unique().tolist(), - value="tier 2", - width_policy="fit", + if len(self.param["surveys"].objects) > 10: + survey_widget = pn.widgets.CrossSelector + else: + survey_widget = pn.widgets.MultiSelect + + this_param_set = pn.Param( + self.param, + parameters=["rewards_fname", "tier", "basis_function", "surveys"], + default_layout=pn.Row, + name="", + widgets={"surveys": survey_widget}, ) + return this_param_set + + @param.depends( + "_reward_df", + "tier", + "_obs_rewards", + "night", + "surveys", + "basis_function", + ) + def reward_plot(self): + """Create a plot of the rewards. - # Survey selection - surveys = pn.widgets.MultiSelect( - name="Displayed surveys", options=["foo"], value=["foo"], width_policy="fit" + Returns + ------- + fig : `bokeh.plotting.Figure` + The figure with the reward plot. + """ + if self._reward_df is None: + return "No rewards are loaded." + + fig = schedview.plot.nightbf.plot_rewards( + reward_df=self._reward_df, + tier_label=self.tier, + night=self.night, + observatory=self._observatory, + obs_rewards=self._obs_rewards, + surveys=self.surveys, + basis_function=self.basis_function, + plot_kwargs={}, ) + return fig + + @param.depends( + "_reward_df", + "tier", + "_obs_rewards", + "night", + "surveys", + ) + def infeasible_plot(self): + """Create a plot of infeasible basis functions. - def configure_survey_selector(survey_selector, reward_df, tier): - surveys = ( - reward_df.set_index("tier_label") - .loc[tier, "survey_label"] - .unique() - .tolist() - ) - survey_selector.options = surveys - survey_selector.value = surveys[:10] if len(surveys) > 10 else surveys + Returns + ------- + fig : `bokeh.plotting.Figure` + The figure.. + """ + if self._reward_df is None: + return "No rewards are loaded." + + fig = schedview.plot.nightbf.plot_infeasible( + reward_df=self._reward_df, + tier_label=self.tier, + night=self.night, + observatory=self._observatory, + surveys=self.surveys, + ) + return fig - configure_survey_selector(surveys, reward_df, tier.value) - def survey_selector_update_callback(surveys, event): - new_tier = event.new - configure_survey_selector(surveys, reward_df, new_tier) +def prenight_app( + night_date=None, observations=None, scheduler=None, rewards=None, return_app=False +): + """Create the pre-night briefing app. - tier.link(surveys, {"value": survey_selector_update_callback}) + Parameters + ---------- + night_date : `datetime.date`, optional + The date of the night to display. + observations : `str`, optional + Path to the opsim output databas file. + scheduler : `str`, optional + Path to the scheduler pickle file. + rewards : `str`, optional + Path to the rewards hdf5 file. + return_app : `bool`, optional + Return the instances of the app class itself. - # Basis function selection + Returns + ------- + pn_app : `panel.viewable.Viewable` or + `tuple` ['panel.viewable.Viewable', `schedview.prenight.Prenight`] + The pre-night briefing app. + """ + prenight = Prenight() - basis_function = pn.widgets.Select( - name="Reward (Total or basis function maximum)", - options=["Total"], - value="Total", - width_policy="fit", - ) + # Rather than set each parameter one at a time, and execute the callbacks + # for each as they are set, we can use the batch_call_watchers context + # manager to set all the parameters at once, and only execute the callbacks + # once. + with param.parameterized.batch_call_watchers(prenight): + if night_date is not None: + prenight.night = night_date - def configure_basis_function_selector(basis_function_selector, reward_df, tier): - basis_functions = ( - reward_df.set_index("tier_label") - .loc[tier, "basis_function"] - .unique() - .tolist() - ) - basis_function_selector.options = ["Total"] + basis_functions - basis_function_selector.value = "Total" - - configure_basis_function_selector(basis_function, reward_df, tier.value) - - def basis_function_selector_update_callback(basis_function, event): - new_tier = event.new - configure_basis_function_selector(basis_function, reward_df, new_tier) - - tier.link(basis_function, {"value": basis_function_selector_update_callback}) - - reward_plot = pn.bind( - schedview.plot.nightbf.plot_rewards, - reward_df, - tier, - night, - None, - obs_rewards, - surveys, - basis_function, - plot_kwargs={"width": 1024}, - ) + if observations is not None: + prenight.opsim_output_fname = observations - infeasible_plot = pn.bind( - schedview.plot.nightbf.plot_infeasible, - reward_df, - tier, - night, - None, - surveys, - plot_kwargs={"width": 1024}, - ) + if scheduler is not None: + prenight.scheduler_fname = scheduler - # Top level layout + if rewards is not None: + prenight.rewards_fname = rewards - basic_app = pn.Column( + pn_app = pn.Column( "