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( "

Pre-night briefing

", - pn.pane.PNG( - "https://project.lsst.org/sites/default/files/Rubin-O-Logo_0.png", height=50 + pn.Row( + pn.Param( + prenight, + parameters=[ + "night", + "timezone", + "scheduler_fname", + "opsim_output_fname", + ], + name="

Parameters

", + widgets={"night": pn.widgets.DatePicker}, + ), + pn.Column( + "

Astronomical Events

", + pn.param.ParamMethod( + prenight.almanac_events_table, loading_indicator=True + ), + ), ), - "

Parameters

", - night, - timezone, - scheduler_fname, - opsim_output_fname, - "

Astronomical Events

", - pn.Row(pn.bind(almanac_events, night, timezone)), - "

Simulated visits

", - pn.Row(pn.bind(visit_explorer, opsim_output_fname, night)), - pn.Row(pn.bind(visit_table, opsim_output_fname, night)), - pn.Row(pn.bind(visit_skymaps, opsim_output_fname, scheduler_fname, night)), - ) + pn.Tabs( + ( + "Visit explorer", + pn.param.ParamMethod(prenight.visit_explorer, loading_indicator=True), + ), + ( + "Table of visits", + pn.param.ParamMethod(prenight.visit_table, loading_indicator=True), + ), + ( + "Sky maps", + pn.param.ParamMethod(prenight.visit_skymaps, loading_indicator=True), + ), + ( + "Reward plots", + pn.Column( + pn.param.ParamMethod( + prenight.reward_params, loading_indicator=True + ), + pn.param.ParamMethod(prenight.reward_plot, loading_indicator=True), + pn.param.ParamMethod( + prenight.infeasible_plot, loading_indicator=True + ), + ), + ), + dynamic=False, # When true, visit_table never renders. Why? + ), + debug_info, + ).servable() - if reward_df is not None: - reward_rows = pn.Column( - "

Reward values through the night

", - pn.Row(tier, surveys, basis_function), - reward_plot, - infeasible_plot, - ) - app = pn.Column(basic_app, reward_rows) - else: - app = basic_app + def clear_caches(session_context): + logging.info("session cleared") + pn_app.stop() - return app + try: + pn.state.on_session_destroyed(clear_caches) + except RuntimeError as e: + logging.info("RuntimeError: %s", e) + + if return_app: + return pn_app, prenight + else: + return pn_app if __name__ == "__main__": - app = prenight_app() + print("Starting prenight dashboard") - template = pn.Template( - """ - {% extends base %} - {% block postamble %} - - {% endblock %} - """ + if "PRENIGHT_PORT" in os.environ: + prenight_port = int(os.environ["PRENIGHT_PORT"]) + else: + prenight_port = 8080 + + pn.serve( + prenight_app, + port=prenight_port, + title="Prenight Dashboard", + show=True, + start=True, + autoreload=True, + threaded=True, ) - template.add_panel("main", app) - - template.show() diff --git a/schedview/app/sched_maps/sched_maps.py b/schedview/app/sched_maps/sched_maps.py index 05e3cd59..623a00b0 100644 --- a/schedview/app/sched_maps/sched_maps.py +++ b/schedview/app/sched_maps/sched_maps.py @@ -27,7 +27,7 @@ class SchedulerDisplayApp(SchedulerDisplay): def make_pickle_entry_box(self): """Make the entry box for a file name from which to load state.""" file_input_box = bokeh.models.TextInput( - value=sample_pickle("baseline.pickle.gz") + " ", + value=sample_pickle() + " ", title="Pickle path:", ) diff --git a/schedview/collect/opsim.py b/schedview/collect/opsim.py index e2adb990..43e40604 100644 --- a/schedview/collect/opsim.py +++ b/schedview/collect/opsim.py @@ -17,7 +17,7 @@ def read_opsim(filename, start_time="2000-01-01", end_time="2100-01-01"): Returns ------- - visits : `holoviews.Dataset` + visits : `pandas.DataFrame` The visits and their parameters. """ start_mjd = Time(start_time).mjd diff --git a/schedview/collect/scheduler_pickle.py b/schedview/collect/scheduler_pickle.py index 1723f5a4..da1d1e70 100644 --- a/schedview/collect/scheduler_pickle.py +++ b/schedview/collect/scheduler_pickle.py @@ -110,7 +110,7 @@ def read_scheduler(file_name_or_url=None): return scheduler, conditions -def sample_pickle(base_fname="baseline.pickle.gz"): +def sample_pickle(base_fname="sample_scheduler.pickle.xz"): """Return the path of the sample pickle Parameters diff --git a/schedview/compute/astro.py b/schedview/compute/astro.py index 915c9a3f..297bd54d 100644 --- a/schedview/compute/astro.py +++ b/schedview/compute/astro.py @@ -1,17 +1,65 @@ +from functools import cache +import datetime import numpy as np import pandas as pd from astropy.time import Time +import pytz from rubin_sim.site_models.almanac import Almanac from rubin_sim.scheduler.model_observatory import ModelObservatory +@cache +def _compute_all_night_events(): + # Loading the alamac takes a while, so generate it with a function + # that caches the result (using the functools.cache decorator). + almanac = Almanac() + all_nights_events = pd.DataFrame(almanac.sunsets) + all_nights_events["night_middle"] = ( + all_nights_events["sunrise"] + all_nights_events["sunset"] + ) / 2 + return all_nights_events + + +@cache +def convert_evening_date_to_night_of_survey(night_date, timezone="Chile/Continental"): + """Convert a calendar date in the evening to the night of survey. + + Parameters + ---------- + night_date : `datetime.date` + The calendar date in the evening local time. + timezone: `str` + The string designating the time zone. Defaults to 'Chile/Continental' + + Returns + ------- + night_of_survey : `int` + The night of survey, starting from 0. + """ + sample_time = Time( + pytz.timezone(timezone) + .localize( + datetime.datetime( + night_date.year, night_date.month, night_date.day, 23, 59, 59 + ) + ) + .astimezone(pytz.timezone("UTC")) + ) + all_nights_events = _compute_all_night_events() + closest_middle_iloc = np.abs( + sample_time.mjd - all_nights_events["night_middle"] + ).argsort()[0] + night_of_survey = all_nights_events.iloc[closest_middle_iloc, :]["night"] + return night_of_survey + + def night_events(night_date=None, site=None, timezone="Chile/Continental"): """Creata a pandas.DataFrame with astronomical events. Parameters ---------- - night_date : `str`, `astropy.time.Time` - The night for which to get events. Defaults to now. + night_date : `datetime.date` + The calendar date in the evening local time. site : `astropy.coordinates.earth.EarthLocation` The observatory location. Defaults to Rubin observatory. timezone: `str` @@ -19,24 +67,20 @@ def night_events(night_date=None, site=None, timezone="Chile/Continental"): Returns ------- - events : `holoviews.Dataset` - A Dataset of night events. + events : `pandas.DataFrame` + A DataFrame of night events. """ if night_date is None: - night_date = Time.now() + night_date = datetime.date.today() if site is None: site = ModelObservatory().location - night_mjd = Time(night_date).mjd - all_nights_events = pd.DataFrame(Almanac().sunsets).set_index("night") - first_night = all_nights_events.index.min() - night = ( - first_night - + np.floor(night_mjd) - - np.floor(all_nights_events.loc[first_night, "sunset"]) + all_nights_events = _compute_all_night_events().set_index("night") + night_of_survey = convert_evening_date_to_night_of_survey( + night_date, timezone=timezone ) - mjds = all_nights_events.loc[night] + mjds = all_nights_events.loc[night_of_survey] ap_times = Time(mjds, format="mjd", scale="utc", location=site) time_df = pd.DataFrame( diff --git a/schedview/compute/camera.py b/schedview/compute/camera.py index f0df8c73..7a71affd 100644 --- a/schedview/compute/camera.py +++ b/schedview/compute/camera.py @@ -12,7 +12,6 @@ class LsstCameraFootprintPerimeter(object): footprint_width_deg = 3.5 def __init__(self): - raft_width_deg = self.footprint_width_deg / self.footprint_wide_rafts offsets = ( @@ -64,7 +63,8 @@ def single_eq_vertices(self, ra, decl, rotation=0): # rotation matches the sense used by # rubin_sim.utils.camera_footprint.LsstCameraFootprint eq_vertices = center.directional_offset_by( - (self.vertices.angle + rotation) * u.deg, self.vertices.r * u.deg + (self.vertices.angle.values + rotation) * u.deg, + self.vertices.r.values * u.deg, ) ra = eq_vertices.ra.deg decl = eq_vertices.dec.deg diff --git a/schedview/compute/maf_metrics.py b/schedview/compute/maf_metrics.py index 9e33297b..bf7b4fcc 100644 --- a/schedview/compute/maf_metrics.py +++ b/schedview/compute/maf_metrics.py @@ -20,8 +20,8 @@ def compute_night_metric_bundle( Name of file with opsim output. data_dir : `str` MAF output directory. - night_date : `astropy.time.Time` - The night to examine. + night_date : `datetime.date` + The local calendar date of the start of the night. metric : `rubin_sim.maf.metrics.base_metric.BaseMetric` The MAF metric to compute. slicer : `rubin_sim.maf.slicers.base_slicer.BaseSlicer` @@ -73,8 +73,8 @@ def compute_sample_metric_bundle(opsim_fname, data_dir, night_date, observatory= Name of file with opsim output data_dir : `str` MAF output directory - night_date : `astropy.time.Time` - Night to make the metric bundle for. + night_date : `datetime.date` + The local calendar date of the start of the night. observatory : `ModelObservatory`, optional The observatory to use, by default None diff --git a/schedview/compute/scheduler.py b/schedview/compute/scheduler.py index 0916fc20..05043703 100644 --- a/schedview/compute/scheduler.py +++ b/schedview/compute/scheduler.py @@ -202,6 +202,7 @@ def create_example( simulate=True, scheduler_pickle_fname=None, opsim_db_fname=None, + rewards_fname=None, ): """Create an example scheduler and observatory. @@ -219,6 +220,14 @@ def create_example( nside : `int` The nside to use for the scheduler and observatory. If None, use the default nside for the example scheduler. + simulate : `bool` + Run a sample simulation from survey_start to current_time + scheduler_pickle_fname : `str` + The filename to save the scheduler to. + opsim_db_fname : `str` + The filename to save the opsim database to. + rewards_fname : `str` + The filename to save the rewards to. Returns ------- @@ -232,6 +241,8 @@ def create_example( The observations from the simulation. """ + record_rewards = rewards_fname is not None + current_time = _normalize_time(current_time) survey_start = _normalize_time(survey_start) @@ -245,12 +256,28 @@ def create_example( if simulate: sim_duration = current_time.mjd - survey_start.mjd - observatory, scheduler, observations = sim_runner( - observatory, - scheduler, - mjd_start=survey_start.mjd, - survey_length=sim_duration, - ) + if record_rewards: + scheduler.keep_rewards = True + observatory, scheduler, observations, reward_df, obs_rewards = sim_runner( + observatory, + scheduler, + mjd_start=survey_start.mjd, + survey_length=sim_duration, + record_rewards=True, + ) + reward_df.to_hdf(rewards_fname, "reward_df") + obs_rewards.to_hdf(rewards_fname, "obs_rewards") + else: + observatory, scheduler, observations = sim_runner( + observatory, + scheduler, + mjd_start=survey_start.mjd, + survey_length=sim_duration, + ) + + if opsim_db_fname is not None: + converter = SchemaConverter() + converter.obs2opsim(observations, filename=opsim_db_fname, delete_past=True) else: observations = None @@ -268,11 +295,11 @@ def create_example( with open(scheduler_pickle_fname, "wb") as out_file: pickle.dump((scheduler, scheduler.conditions), out_file) - if opsim_db_fname is not None: - converter = SchemaConverter() - converter.obs2opsim(observations, filename=opsim_db_fname, delete_past=True) + result = (scheduler, observatory, conditions, observations) + if record_rewards: + result += (reward_df, obs_rewards) - return scheduler, observatory, conditions, observations + return result def make_unique_survey_name(scheduler, survey_index=None): @@ -387,7 +414,6 @@ def get_survey_url(row): summary_df["survey_url"] = summary_df.apply(get_survey_url, axis=1) def make_survey_row(survey_bfs): - infeasible_bf = ", ".join( survey_bfs.loc[~survey_bfs.feasible.astype(bool)].basis_function.to_list() ) diff --git a/schedview/compute/survey.py b/schedview/compute/survey.py index b376d46f..6b694ae0 100644 --- a/schedview/compute/survey.py +++ b/schedview/compute/survey.py @@ -128,7 +128,7 @@ def can_be_healpix_map(values): values = survey.calc_reward_function(conditions) if not can_be_healpix_map(values): - values = np.fill(np.empty(hp.nside2npix(nside)), values) + values = np.full(np.empty(hp.nside2npix(nside)).shape, values) survey_maps["reward"] = values diff --git a/schedview/dashboards/prenight.py b/schedview/dashboards/prenight.py deleted file mode 100644 index e596baaf..00000000 --- a/schedview/dashboards/prenight.py +++ /dev/null @@ -1,108 +0,0 @@ -import warnings - -import numpy as np - -from astropy.time import Time - -import rubin_sim -from rubin_sim.scheduler.model_observatory import ModelObservatory - -import hvplot.pandas - -import panel as pn -import holoviews as hv -import hvplot -import hvplot.pandas - -import schedview -import schedview.plot -import schedview.compute -import schedview.compute.astro -import schedview.compute.scheduler -import schedview.collect -import schedview.collect.stars -import schedview.collect.opsim -import schedview.collect.footprint -import schedview.collect.scheduler_pickle -import schedview.app.metric_maps -import schedview.app.sched_maps -import schedview.plot.scheduler -import schedview.plot.visitmap - - -def prenight_panel( - scheduler_fname, - night, - visit_fname=rubin_sim.data.get_baseline(), - timezone="Chile/Continental", -): - observatory = ModelObservatory() - - # Build astronomical events table - astro_events = schedview.compute.astro.night_events( - night, observatory.location, timezone - ) - - sunset = Time(astro_events.loc["sunset", "UTC"]) - sunrise = Time(astro_events.loc["sunrise", "UTC"]) - night_middle = Time((sunset.mjd + sunrise.mjd) / 2, format="mjd") - - # Compute basis function using the state at the start of the current night - - # Prepare the scheduler instance to calculate the rewards - scheduler, conditions = schedview.collect.scheduler_pickle.read_scheduler( - scheduler_fname - ) - scheduler.update_conditions(conditions) - old_visits = schedview.collect.opsim.read_opsim( - visit_fname, Time(scheduler.conditions.mjd, format="mjd").iso, sunset.iso - ) - schedview.compute.scheduler.replay_visits(scheduler, old_visits) - - observatory.mjd = night_middle.mjd - conditions = observatory.return_conditions() - scheduler.update_conditions(conditions) - - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=FutureWarning) - night_rewards = schedview.compute.scheduler.compute_basis_function_rewards( - scheduler - ) - - night_reward_plot = ( - night_rewards.replace([np.inf, -np.inf], np.nan) - .hvplot( - by=["survey_name"], x="time", y=["reward"], title="Rewards for each survey" - ) - .options({"Curve": {"color": hv.Cycle("Category20")}}) - ) - - # Load visits up to the start of the night and create corresponding pane - new_visits = schedview.collect.opsim.read_opsim( - rubin_sim.data.get_baseline(), sunset.iso, sunrise.iso - ) - - visit_explorer = hvplot.explorer( - new_visits, kind="scatter", x="start_date", y="airmass", by=["note"] - ) - - # Visit map - footprint = schedview.collect.footprint.get_footprint(scheduler) - observatory.mjd = night_middle.mjd - conditions = observatory.return_conditions() - vmap = schedview.plot.visitmap.plot_visit_skymaps(new_visits, footprint, conditions) - - # Combine the panes into a panel - dashboard = pn.Column( - f"

Pre-night briefing for {night.iso.split()[0]}

", - "

Astronomical Events

", - astro_events, - "

Rewards by survey, with time

", - night_reward_plot, - "

Simulated visits

", - visit_explorer, - "

Visit map

", - vmap, - ) - - return dashboard diff --git a/schedview/data/sample_opsim.db b/schedview/data/sample_opsim.db new file mode 100644 index 00000000..3101a36e Binary files /dev/null and b/schedview/data/sample_opsim.db differ diff --git a/schedview/data/sample_rewards.h5 b/schedview/data/sample_rewards.h5 new file mode 100644 index 00000000..ff5b54ab Binary files /dev/null and b/schedview/data/sample_rewards.h5 differ diff --git a/schedview/data/sample_scheduler.pickle.xz b/schedview/data/sample_scheduler.pickle.xz new file mode 100644 index 00000000..0fa32066 Binary files /dev/null and b/schedview/data/sample_scheduler.pickle.xz differ diff --git a/schedview/js/__init_.py b/schedview/js/__init_.py deleted file mode 100644 index e69de29b..00000000 diff --git a/schedview/js/update_map.js b/schedview/js/update_map.js deleted file mode 100644 index f608ca3c..00000000 --- a/schedview/js/update_map.js +++ /dev/null @@ -1,462 +0,0 @@ -function get_gpu() { - try { - return gpu; - } catch (e) { - return new GPU(); - } -} - -function rotateCart(ux, uy, uz, angle, x0, y0, z0) { - // ux, uy, uz is a vector that defines the axis of rotation - // angle is in radians - // following https://en.wikipedia.org/wiki/Rotation_matrix - - const cosa = Math.cos(angle) - const ccosa = 1 - cosa - const sina = Math.sin(angle) - const rxx = cosa + ux * ux * ccosa - const rxy = ux * uy * ccosa - uz * sina - const rxz = ux * uz * ccosa + uy * sina - const ryx = uy * ux * ccosa + uz * sina - const ryy = cosa + uy * uy * ccosa - const ryz = uy * uz * ccosa - ux * sina - const rzx = uz * ux * ccosa - uy * sina - const rzy = uz * uy * ccosa + ux * sina - const rzz = cosa + uz * uz * ccosa - const x = rxx * x0 + rxy * y0 + rxz * z0 - const y = ryx * x0 + ryy * y0 + ryz * z0 - const z = rzx * x0 + rzy * y0 + rzz * z0 - return [x, y, z] -} - -function applyRotations(hpx, hpy, hpz, codecl, ra, orient, npoleCoords1) { - // We are looking out of the sphere from the inside, so the center is 180 degrees - // from the front of the sphere, hence the pi. - const decl_rot = Math.PI + codecl - const ra_rot = ra - Math.PI / 2 - - const coords1 = rotateCart(1, 0, 0, decl_rot, hpx, hpy, hpz) - const coords2 = rotateCart(npoleCoords1[0], npoleCoords1[1], npoleCoords1[2], ra_rot, coords1[0], coords1[1], coords1[2]) - let coords = rotateCart(0, 0, 1, orient, coords2[0], coords2[1], coords2[2]) - - // In astronomy, we are looking out of the sphere from the center to the back - // (which naturally results in west to the right). - // Positive z is out of the screen behind us, and we are at the center, - // so to visible part is when z is negative (coords[2]<=0). - // So, stuff the points with positive z to NaN so they are - // not shown, because they are behind the observer. - - // Use 5*Number.EPSILON instead of exactly 0, because the - // assorted trig operations result in values slightly above or below - // 0 when the horizon is in principle exactly 0, and this gives an - // irregularly dotted/dashed appearance to the horizon if - // a cutoff of exactly 0 is used. - if (coords[2] > 5 * Number.EPSILON) { - coords[0] = NaN - coords[1] = NaN - } - return coords -} - -function horizonToEq(lat, alt, az, lst) { - // Stupid simple rough approximation, ignores aberration, precession, diffraction, etc. - // Doing this "correctly" would make this much more complicated and much slower, and - // of the dates of relevance won't make a significant difference. - const decl = Math.asin(Math.sin(alt) * Math.sin(lat) + Math.cos(lat) * Math.cos(alt) * Math.cos(az)) - const ha = Math.atan2( - -1 * Math.cos(alt) * Math.cos(lat) * Math.sin(az), - Math.sin(alt) - Math.sin(lat) * Math.sin(decl) - ) - const ra = lst - ha - const coords = [ra, decl] - return coords -} - -function eqToHorizon(ra, decl, lat, lst) { - // Stupid simple rough approximation, ignores aberration, precession, diffraction, etc. - const ha = lst - ra - const alt = Math.asin( - Math.sin(decl) * Math.sin(lat) + Math.cos(decl) * Math.cos(lat) * Math.cos(ha) - ) - const az = Math.atan2( - -1 * Math.cos(decl) * Math.cos(lat) * Math.sin(ha), - Math.sin(decl) - Math.sin(lat) * Math.sin(alt) - ) - const coords = [alt, az] - return coords -} - -function eqToHorizonCart(ra, decl, lat, lst) { - const horizon = eqToHorizon(ra, decl, lat, lst) - const alt = horizon[0] - const az = horizon[1] - const zd = Math.PI / 2 - alt - const x = -1 * zd * Math.sin(az) - const y = zd * Math.cos(az) - let coords = [x, y] - if ((x ** 2 + y ** 2) > ((Math.PI / 2) ** 2)) { - coords[0] = NaN - coords[1] = NaN - } - return coords -} - -function eqToCart(ra, decl) { - const theta = Math.PI / 2 - decl - const phi = ra - const x = Math.sin(theta) * Math.cos(phi) - const y = Math.sin(theta) * Math.sin(phi) - const z = Math.cos(theta) - return [x, y, z] -} - -function cartToEq(x, y, z) { - const theta = Math.acos(z) - const ra = Math.atan2(y, x) - const decl = Math.PI / 2 - theta - return [ra, decl] -} - -function eqToLambertAEA(ra, decl, hemisphere, west_right) { - // Follow notation of Snyder p. 87-88 - let theta = (west_right === (hemisphere === 'south')) ? -1 * ra : ra - const phi = (hemisphere === 'south') ? -1 * decl : decl - - // Choose an R to match that used by healpy - const R = 1 - - const rho = 2 * R * Math.sin((Math.PI / 2 - phi) / 2) - let x = rho * Math.sin(theta) - let y = rho * Math.cos(theta) - if (phi > 89.9 * Math.PI / 180) { - x = Math.NaN - y = Math.NaN - } - return [x, y] -} - -function eqToMollweide(ra, decl, west_right) { - const tolerance = 0.001 - const max_iter = 1000 - const R = 1 / Math.sqrt(2) - const dir_sign = west_right ? -1 : 1 - const wra = (ra + Math.PI) % (2 * Math.PI) - Math.PI - - // Return NaNs if near the discontinuity - if (Math.abs(ra - Math.PI) < (Math.PI / 180) / Math.cos(decl)) { - let xy = [Math.NaN, Math.NaN] - return xy - } - - function compute_xy(theta) { - const x = dir_sign * R * (2 / Math.PI) * Math.sqrt(2) * wra * Math.cos(theta) - const y = Math.sqrt(2) * R * Math.sin(theta) - return [x, y] - } - - let theta = decl - let xy = compute_xy(theta) - - for (let iter = 1; iter < max_iter; iter++) { - if (Math.cos(theta) ** 2 <= Math.cos(Math.PI / 2) ** 2) { - // We are too close to a pole to refine further - break - } - - const theta0 = theta - const xy0 = compute_xy(theta0) - - const delta_theta = -(2 * theta0 + Math.sin(2 * theta0) - Math.PI * Math.sin(decl)) / ( - 4 * (Math.cos(theta0) ** 2) - ) - theta = theta0 + delta_theta - xy = compute_xy(theta) - - if ((Math.abs(xy[0] - xy0[0]) < tolerance) & (Math.abs(xy[1] - xy0[1]) < tolerance)) { - break - } - } - - return xy -} - -function computeLocalSiderealTime(mjd, longitude) { - // Computes the Mean Sidereal Time - - // Follow Meeus's _Astronomical_Algorithms_ 2nd edition, equation 12.4 - // Avoid obvious simplifications to make it easier to check the - // numbers in the equation exactly. - // Meeus follows tho IAU recommendation of 1982. - const jd = mjd + 2400000.5 - const t = (jd - 2451545.0) / 36525.0 - const theta0 = 280.46061837 + (360.98564736629 * (jd - 2451545.0)) + (0.000387933 * t * t) - (t * t * t / 38710000) - const lon_deg = longitude * (180.0 / Math.PI) - const lst_deg = ((theta0 + lon_deg) % 360 + 360) % 360 - const lst = lst_deg * Math.PI / 180.0 - return lst -} - -const multiplyMultiMatrix = get_gpu().createKernel(function (coeff, data) { - let sum = 0; - let hpix = this.thread.y - let corner = this.thread.x - let out_coord = this.thread.z - for (let in_coord = 0; in_coord < 3; in_coord++) { - sum += coeff[out_coord][in_coord] * data[in_coord][hpix][corner]; - } - return sum; -}) - -function multiplyRotationMatrix(a, b) { - let result = [[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]] - for (let x = 0; x < 3; x++) { - for (let y = 0; y < 3; y++) { - for (let i = 0; i < 3; i++) { - result[y][x] = result[y][x] + a[i][x] * b[y][i] - } - } - } - return result; -} - -function computeRotationMatrix(ux, uy, uz, angle) { - const cosa = Math.cos(angle) - const ccosa = 1 - cosa - const sina = Math.sin(angle) - const rxx = cosa + ux * ux * ccosa - const rxy = ux * uy * ccosa - uz * sina - const rxz = ux * uz * ccosa + uy * sina - const ryx = uy * ux * ccosa + uz * sina - const ryy = cosa + uy * uy * ccosa - const ryz = uy * uz * ccosa - ux * sina - const rzx = uz * ux * ccosa - uy * sina - const rzy = uz * uy * ccosa + ux * sina - const rzz = cosa + uz * uz * ccosa - const matrix = [[rxx, rxy, rxz], [ryx, ryy, ryz], [rzx, rzy, rzz]] - return matrix -} - -function applyHealpixRotations(hpx, hpy, hpz, codecl, ra, orient, npoleCoords1) { - // We are looking out of the sphere from the inside, so the center is 180 degrees - // from the front of the sphere, hence the pi. - const decl_rot = Math.PI + codecl - const ra_rot = ra - Math.PI / 2 - - const matrix1 = computeRotationMatrix(1, 0, 0, decl_rot) - const matrix2 = computeRotationMatrix(npoleCoords1[0], npoleCoords1[1], npoleCoords1[2], ra_rot) - const matrix3 = computeRotationMatrix(0, 0, 1, orient) - const rotMatrix = multiplyRotationMatrix(multiplyRotationMatrix(matrix1, matrix2), matrix3) - let coords = multiplyMultiMatrix(rotMatrix, [hpx, hpy, hpz]) - - // In astronomy, we are looking out of the sphere from the center to the back - // (which naturally results in west to the right). - // Positive z is out of the screen behind us, and we are at the center, - // so to visible part is when z is negative (coords[2]<=0). - // So, stuff the points with positive z to NaN so they are - // not shown, because they are behind the observer. - - // Use 5*Number.EPSILON instead of exactly 0, because the - // assorted trig operations result in values slightly above or below - // 0 when the horizon is in principle exactly 0, and this gives an - // irregularly dotted/dashed appearance to the horizon if - // a cutoff of exactly 0 is used. - for (let hpix = 0; hpix < coords[0].length; hpix++) { - for (let corner = 0; corner < coords[0][0].length; corner++) { - if (coords[2][hpix][corner] > 5 * Number.EPSILON) { - coords[0][hpix][corner] = NaN - coords[1][hpix][corner] = NaN - } - } - } - return coords -} - -const data = data_source.data - -lat = lat * Math.PI / 180 -lon = lon * Math.PI / 180 - -const alt = center_alt_slider.value * Math.PI / 180 -const az = center_az_slider.value * Math.PI / 180 -const mjd = mjd_slider.value -const lst = computeLocalSiderealTime(mjd, lon) - -const eqCoords = horizonToEq(lat, alt, az, lst) -const ra = eqCoords[0] -const decl = eqCoords[1] -const codecl = Math.PI / 2 - decl -const npoleCoords1 = rotateCart(1, 0, 0, codecl, 0, 0, 1) - -const hemisphere = (lat > 0) ? 'north' : 'south' - -/* To get the orientation - - Get the coordinates of a point we want to be directly "up" from center (slightly higher alt, same az) - - Look where it would end up with orientation 0 - - Get the angle of the desired top with the actual top - - Reverse to get the rotation - */ -let upAlt = alt + Math.PI / 180 -let upAz = az -const upEq = horizonToEq(lat, upAlt, upAz, lst) -const upCart0 = eqToCart(upEq[0], upEq[1]) -const upCart3 = applyRotations(upCart0[0], upCart0[1], upCart0[2], codecl, ra, 0, npoleCoords1) -const orient = Math.PI / 2 - Math.atan2(upCart3[1], upCart3[0]) - -function updateData() { - let pointEq = NaN - let eqUpdated = false - let hzUpdated = true - - if (data['x_hp'].length == 0) { - return - } - - // Columns can contain lists of coords (eg for corners of healpixels), or individual points. - // If they are lists, iteratate over each element. Otherwise, just apply the rotation to the point. - if (typeof (data['x_hp'][0]) === 'number') { - if ('alt' in data) { - hzUpdated = false - for (let i = 0; i < data['x_hp'].length; i++) { - pointEq = horizonToEq(lat, data['alt'][i] * Math.PI / 180, data['az'][i] * Math.PI / 180, lst) - let new_ra = pointEq[0] * 180 / Math.PI - let new_decl = pointEq[1] * 180 / Math.PI - // If the point does not actually change, be user eqUpdated remains false - // so we do not call the expensive eqToMollweide below. - if ((data['ra'][i] != new_ra) || (data['decl'][i] != new_decl)) { - data['ra'][i] = new_ra - data['decl'][i] = new_decl - eqUpdated = true - - let cartCoords = eqToCart(pointEq[0], pointEq[1]) - data['x_hp'][i] = cartCoords[0] - data['y_hp'][i] = cartCoords[1] - data['z_hp'][i] = cartCoords[2] - } - } - } - for (let i = 0; i < data['x_hp'].length; i++) { - const coords = applyRotations(data['x_hp'][i], data['y_hp'][i], data['z_hp'][i], codecl, ra, orient, npoleCoords1) - data['x_orth'][i] = coords[0] - data['y_orth'][i] = coords[1] - data['z_orth'][i] = coords[2] - } - - if (eqUpdated && ('x_laea' in data)) { - for (let i = 0; i < data['x_hp'].length; i++) { - const laea = eqToLambertAEA(data['ra'][i] * Math.PI / 180, data['decl'][i] * Math.PI / 180, hemisphere, true) - data['x_laea'][i] = laea[0] - data['y_laea'][i] = laea[1] - } - } - - if (eqUpdated && ('x_moll' in data)) { - for (let i = 0; i < data['x_hp'].length; i++) { - const moll = eqToMollweide(data['ra'][i] * Math.PI / 180, data['decl'][i] * Math.PI / 180, true) - data['x_moll'][i] = moll[0] - data['y_moll'][i] = moll[1] - } - } - - if (hzUpdated && ('x_hz' in data)) { - for (let i = 0; i < data['x_hp'].length; i++) { - const horizonCart = eqToHorizonCart(data['ra'][i] * Math.PI / 180, data['decl'][i] * Math.PI / 180, lat, lst) - data['x_hz'][i] = horizonCart[0] - data['y_hz'][i] = horizonCart[1] - } - } - } else { - multiplyMultiMatrix.setOutput([data_source.data['x_hp'][0].length, data_source.data['x_hp'].length, 3]); - - if ('alt' in data) { - hzUpdated = false - for (let j = 0; j < data['x_hp'][0].length; j++) { - for (let i = 0; i < data['x_hp'].length; i++) { - pointEq = horizonToEq(lat, data['alt'][i][j] * Math.PI / 180, data['az'][i][j] * Math.PI / 180, lst) - const new_ra = pointEq[0] * 180 / Math.PI - const new_decl = pointEq[1] * 180 / Math.PI - if ((data['ra'][i][j] != new_ra) || (data['decl'][i][j] != new_decl)) { - data['ra'][i][j] = pointEq[0] * 180 / Math.PI - data['decl'][i][j] = pointEq[1] * 180 / Math.PI - eqUpdated = True - - let cartCoords = eqToCart(pointEq[0], pointEq[1]) - data['x_hp'][i][j] = cartCoords[0] - data['y_hp'][i][j] = cartCoords[1] - data['z_hp'][i][j] = cartCoords[2] - } - } - } - } - - const coords = applyHealpixRotations(data['x_hp'], data['y_hp'], data['z_hp'], codecl, ra, orient, npoleCoords1) - for (let j = 0; j < data['x_hp'][0].length; j++) { - for (let i = 0; i < data['x_hp'].length; i++) { - // const coords = applyRotations(data['x_hp'][i][j], data['y_hp'][i][j], data['z_hp'][i][j], codecl, ra, orient, npoleCoords1) - data['x_orth'][i][j] = coords[0][i][j] - data['y_orth'][i][j] = coords[1][i][j] - data['z_orth'][i][j] = coords[2][i][j] - } - } - - if (eqUpdated && ('x_laea' in data)) { - for (let j = 0; j < data['x_hp'][0].length; j++) { - for (let i = 0; i < data['x_hp'].length; i++) { - const laea = eqToLambertAEA(data['ra'][i][j] * Math.PI / 180, data['decl'][i][j] * Math.PI / 180, hemisphere, true) - data['x_laea'][i][j] = laea[0] - data['y_laea'][i][j] = laea[1] - } - } - } - - if (eqUpdated && ('x_moll' in data)) { - for (let j = 0; j < data['x_hp'][0].length; j++) { - for (let i = 0; i < data['x_hp'].length; i++) { - const moll = eqToMollweide(data['ra'][i][j] * Math.PI / 180, data['decl'][i][j] * Math.PI / 180, true) - data['x_moll'][i][j] = moll[0] - data['y_moll'][i][j] = moll[1] - } - } - } - - if (hzUpdated && ('x_hz' in data)) { - for (let j = 0; j < data['x_hp'][0].length; j++) { - for (let i = 0; i < data['x_hp'].length; i++) { - const horizonCart = eqToHorizonCart(data['ra'][i][j] * Math.PI / 180, data['decl'][i][j] * Math.PI / 180, lat, lst) - data['x_hz'][i][j] = horizonCart[0] - data['y_hz'][i][j] = horizonCart[1] - } - } - } - } - - if ('in_mjd_window' in data) { - for (let i = 0; i < data['x_hp'].length; i++) { - data['in_mjd_window'][i] = 0.3 - if ('min_mjd' in data) { - if (mjd < data['min_mjd'][i]) { - data['in_mjd_window'][i] = 0.0 - } - } - if ('max_mjd' in data) { - if (mjd > data['max_mjd'][i]) { - data['in_mjd_window'][i] = 0.0 - } - } - } - } - - if (('recent_mjd' in data) && ('min_mjd' in data)) { - for (let i = 0; i < data['x_hp'].length; i++) { - let recent_mjd = 1.0 - (mjd - data['min_mjd'][i]) / data['fade_scale'][i] - if ((recent_mjd < 0) || (recent_mjd > 1)) { - recent_mjd = 0.0 - } - data['recent_mjd'][i] = recent_mjd - } - } - -} - -updateData() - -data_source.change.emit() diff --git a/schedview/munge/__init__.py b/schedview/munge/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/schedview/munge/monkeypatch_rubin_sim.py b/schedview/munge/monkeypatch_rubin_sim.py deleted file mode 100644 index 02be5424..00000000 --- a/schedview/munge/monkeypatch_rubin_sim.py +++ /dev/null @@ -1,523 +0,0 @@ -from io import StringIO -from collections import OrderedDict -from copy import deepcopy - -import pandas as pd -import numpy as np -import healpy as hp - -import rubin_sim -import rubin_sim.scheduler -import rubin_sim.scheduler.surveys -import rubin_sim.scheduler.features.conditions -import rubin_sim.scheduler.basis_functions - - -class CoreScheduler(rubin_sim.scheduler.schedulers.core_scheduler.CoreScheduler): - def get_basis_functions(self, survey_index=None, conditions=None): - """Get basis functions for a specific survey in provided conditions. - - Parameters - ---------- - survey_index : `List` [`int`], optional - A list with two elements: the survey list and the element within - that survey list for which the basis function should be retrieved. - If ``None``, use the latest survey to make an addition to the - queue. - conditions : `rubin_sim.scheduler.features.conditions.Conditions`, optional # noqa W505 - The conditions for which to return the basis functions. - If ``None``, use the conditions associated with this sceduler. - By default None. - - Returns - ------- - basis_funcs : `OrderedDict` ['str`, `rubin_sim.scheduler.basis_functions.basis_functions.Base_basis_function`] # noqa W505 - A dictionary of the basis functions, where the keys are names for - the basis functions and the values are the functions themselves. - """ - if survey_index is None: - survey_index = self.survey_index - - if conditions is None: - conditions = self.conditions - - survey = self.survey_lists[survey_index[0]][survey_index[1]] - basis_funcs = OrderedDict() - for basis_func in survey.basis_functions: - if hasattr(basis_func(conditions), "__len__"): - basis_funcs[basis_func.__class__.__name__] = basis_func - return basis_funcs - - def get_healpix_maps(self, survey_index=None, conditions=None): - """Get the healpix maps for a specific survey, in provided conditions. - - Parameters - ---------- - survey_index : `List` [`int`], optional - A list with two elements: the survey list and the element within - that survey list for which the maps that should be retrieved. - If ``None``, use the latest survey to make an addition to - the queue. - conditions : `rubin_sim.scheduler.features.conditions.Conditions`, optional # noqa W505 - The conditions for the maps to be returned. If ``None``, use - the conditions associated with this sceduler. By default None. - - Returns - ------- - basis_funcs : `OrderedDict` ['str`, `numpy.ndarray`] - A dictionary of the maps, where the keys are names for the maps and - values are the numpy arrays as used by ``healpy``. - """ - if survey_index is None: - survey_index = self.survey_index - - if conditions is None: - conditions = self.conditions - - maps = OrderedDict() - for band in conditions.skybrightness.keys(): - maps[f"{band}_sky"] = deepcopy(conditions.skybrightness[band]) - maps[f"{band}_sky"][maps[f"{band}_sky"] < -1e30] = np.nan - - basis_functions = self.get_basis_functions(survey_index, conditions) - - for basis_func_key in basis_functions.keys(): - label = basis_functions[basis_func_key].label() - maps[label] = basis_functions[basis_func_key](conditions) - - return maps - - def __repr__(self): - if isinstance( - self.pointing2hpindx, rubin_sim.scheduler.utils.utils.hp_in_lsst_fov - ): - camera = "LSST" - elif isinstance( - self.pointing2hpindx, rubin_sim.scheduler.utils.utils.hp_in_comcam_fov - ): - camera = "comcam" - else: - camera = None - - this_repr = f"""{self.__class__.__qualname__}( - surveys={repr(self.survey_lists)}, - camera="{camera}", - nside={repr(self.nside)}, - rotator_limits={repr(self.rotator_limits)}, - survey_index={repr(self.survey_index)}, - log={repr(self.log)} - )""" - return this_repr - - def __str__(self): - if isinstance( - self.pointing2hpindx, rubin_sim.scheduler.utils.utils.hp_in_lsst_fov - ): - camera = "LSST" - elif isinstance( - self.pointing2hpindx, rubin_sim.scheduler.utils.utils.hp_in_comcam_fov - ): - camera = "comcam" - else: - camera = None - - output = StringIO() - print(f"# {self.__class__.__name__} at {hex(id(self))}", file=output) - - misc = pd.Series( - { - "camera": camera, - "nside": self.nside, - "rotator limits": self.rotator_limits, - "survey index": self.survey_index, - "Last chosen": str( - self.survey_lists[self.survey_index[0]][self.survey_index[1]] - ), - } - ) - misc.name = "value" - print(misc.to_markdown(), file=output) - - print("", file=output) - print("## Surveys", file=output) - - if len(self.survey_lists) == 0: - print("Scheduler contains no surveys.", file=output) - - for tier_index, tier_surveys in enumerate(self.survey_lists): - print(file=output) - print(f"### Survey list {tier_index}", file=output) - print(self.surveys_df(tier_index).to_markdown(), file=output) - - print("", file=output) - print(str(self.conditions), file=output) - - print("", file=output) - print("## Queue", file=output) - print( - pd.concat(pd.DataFrame(q) for q in self.queue)[ - ["ID", "flush_by_mjd", "RA", "dec", "filter", "exptime", "note"] - ] - .set_index("ID") - .to_markdown(), - file=output, - ) - - result = output.getvalue() - return result - - def _repr_markdown_(self): - return str(self) - - def surveys_df(self, tier): - surveys = [] - survey_list = self.survey_lists[tier] - for survey_list_elem, survey in enumerate(survey_list): - reward = np.max(survey.reward) if tier <= self.survey_index[0] else None - chosen = (tier == self.survey_index[0]) and ( - survey_list_elem == self.survey_index[1] - ) - surveys.append({"survey": str(survey), "reward": reward, "chosen": chosen}) - - df = pd.DataFrame(surveys).set_index("survey") - return df - - def make_reward_df(self, conditions): - survey_dfs = [] - for index0, survey_list in enumerate(self.survey_lists): - for index1, survey in enumerate(survey_list): - survey_df = survey.make_reward_df(conditions) - survey_df["list_index"] = index0 - survey_df["survey_index"] = index1 - survey_dfs.append(survey_df) - - survey_df = pd.concat(survey_dfs).set_index(["list_index", "survey_index"]) - return survey_df - - -class Conditions(rubin_sim.scheduler.features.conditions.Conditions): - def __repr__(self): - return f"<{self.__class__.__name__} mjd_start='{self.mjd_start}' at {hex(id(self))}>" - - def __str__(self): - output = StringIO() - print(f"{self.__class__.__qualname__} at {hex(id(self))}", file=output) - print("============================", file=output) - print("nside: ", self.nside, file=output) - print("site: ", self.site.name, file=output) - print("exptime: ", self.exptime, file=output) - print("lmst: ", self.lmst, file=output) - print("season_offset: ", self.season_offset, file=output) - print("sun_RA_start: ", self.sun_RA_start, file=output) - print("clouds: ", self.clouds, file=output) - print("current_filter: ", self.current_filter, file=output) - print("mounted_filters: ", self.mounted_filters, file=output) - print("night: ", self.night, file=output) - print("wind_speed: ", self.wind_speed, file=output) - print("wind_direction: ", self.wind_direction, file=output) - print( - "len(scheduled_observations): ", - len(self.scheduled_observations), - file=output, - ) - print("len(queue): ", len(self.queue), file=output) - print("moonPhase: ", self.moonPhase, file=output) - print("bulk_cloud: ", self.bulk_cloud, file=output) - print("targets_of_opportunity: ", self.targets_of_opportunity, file=output) - print("season_modulo: ", self.season_modulo, file=output) - print("season_max_season: ", self.season_max_season, file=output) - print("season_length: ", self.season_length, file=output) - print("season_floor: ", self.season_floor, file=output) - print("cumulative_azimuth_rad: ", self.cumulative_azimuth_rad, file=output) - - positions = [ - { - "name": "sun", - "alt": self.sunAlt, - "az": self.sunAz, - "RA": self.sunRA, - "decl": self.sunDec, - } - ] - positions.append( - { - "name": "moon", - "alt": self.moonAlt, - "az": self.moonAz, - "RA": self.moonRA, - "decl": self.moonDec, - } - ) - for planet_name in ("venus", "mars", "jupiter", "saturn"): - positions.append( - { - "name": planet_name, - "RA": np.asscalar(self.planet_positions[planet_name + "_RA"]), - "decl": np.asscalar(self.planet_positions[planet_name + "_dec"]), - } - ) - positions.append( - { - "name": "telescope", - "alt": self.telAlt, - "az": self.telAz, - "RA": self.telRA, - "decl": self.telDec, - "rot": self.rotTelPos, - } - ) - positions = pd.DataFrame(positions).set_index("name") - print(file=output) - print("Positions (radians)", file=output) - print("-------------------", file=output) - print(positions.to_markdown(), file=output) - - positions_deg = np.degrees(positions) - print(file=output) - print("Positions (degrees)", file=output) - print("-------------------", file=output) - print(positions_deg.to_markdown(), file=output) - - events = ( - "mjd_start", - "mjd", - "sunset", - "sun_n12_setting", - "sun_n18_setting", - "sun_n18_rising", - "sun_n12_rising", - "sunrise", - "moonrise", - "moonset", - "sun_0_setting", - "sun_0_rising", - ) - event_rows = [] - for event in events: - mjd = getattr(self, event) - time = pd.to_datetime(mjd + 2400000.5, unit="D", origin="julian") - event_rows.append({"event": event, "MJD": mjd, "date": time}) - event_df = pd.DataFrame(event_rows).set_index("event").sort_values(by="MJD") - print("", file=output) - print("Events", file=output) - print("------", file=output) - print(event_df.to_markdown(), file=output) - - map_stats = [] - for map_name in ("ra", "dec", "slewtime", "airmass"): - values = getattr(self, map_name) - map_stats.append( - { - "map": map_name, - "nside": hp.npix2nside(len(values)), - "min": np.nanmin(values), - "max": np.nanmax(values), - "median": np.nanmedian(values), - } - ) - - for base_map_name in ("skybrightness", "FWHMeff"): - for band in "ugrizy": - values = getattr(self, base_map_name)[band] - map_name = f"{base_map_name}_{band}" - map_stats.append( - { - "map": map_name, - "nside": hp.npix2nside(len(values)), - "min": np.nanmin(values), - "max": np.nanmax(values), - "median": np.nanmedian(values), - } - ) - maps_df = pd.DataFrame(map_stats).set_index("map") - print("", file=output) - print("Maps", file=output) - print("----", file=output) - print(maps_df.to_markdown(), file=output) - - result = output.getvalue() - return result - - def _repr_markdown_(self): - return str(self) - - -class BaseSurvey(rubin_sim.scheduler.surveys.BaseSurvey): - def __repr__(self): - return f"<{self.__class__.__name__} survey_name='{self.survey_name}' at {hex(id(self))}>" - - def make_reward_df(self, conditions): - feasibility = [] - accum_reward = [] - bf_reward = [] - bf_label = [] - basis_functions = [] - for basis_function in self.basis_functions: - basis_functions.append(basis_function) - test_survey = deepcopy(self) - test_survey.basis_functions = basis_functions - bf_label.append(basis_function.label()) - bf_reward.append(np.nanmax(basis_function(conditions))) - feasibility.append(basis_function.check_feasibility(conditions)) - try: - accum_reward.append( - np.nanmax(test_survey.calc_reward_function(conditions)) - ) - except IndexError: - accum_reward.append(None) - - reward_df = pd.DataFrame( - { - "basis_function": bf_label, - "feasible": feasibility, - "basis_reward": bf_reward, - "accum_reward": accum_reward, - } - ) - return reward_df - - def reward_changes(self, conditions): - reward_values = [] - basis_functions = [] - for basis_function in self.basis_functions: - test_survey = deepcopy(self) - basis_functions.append(basis_function) - test_survey.basis_functions = basis_functions - try: - reward_values.append( - np.nanmax(test_survey.calc_reward_function(conditions)) - ) - except IndexError: - reward_values.append(None) - - bf_names = [bf.__class__.__name__ for bf in self.basis_functions] - return list(zip(bf_names, reward_values)) - - -class BaseMarkovDF_survey(rubin_sim.scheduler.surveys.BaseMarkovDF_survey): - def make_reward_df(self, conditions): - feasibility = [] - accum_reward = [] - bf_reward = [] - bf_label = [] - basis_functions = [] - basis_weights = [] - for (weight, basis_function) in zip(self.basis_weights, self.basis_functions): - basis_functions.append(basis_function) - basis_weights.append(weight) - test_survey = deepcopy(self) - test_survey.basis_functions = basis_functions - test_survey.basis_weights = basis_weights - bf_label.append(basis_function.label()) - bf_reward.append(np.nanmax(basis_function(conditions))) - feasibility.append(basis_function.check_feasibility(conditions)) - try: - accum_reward.append( - np.nanmax(test_survey.calc_reward_function(conditions)) - ) - except IndexError: - accum_reward.append(None) - - reward_df = pd.DataFrame( - { - "basis_function": bf_label, - "feasible": feasibility, - "basis_reward": bf_reward, - "accum_reward": accum_reward, - } - ) - return reward_df - - def reward_changes(self, conditions): - reward_values = [] - - basis_functions = [] - basis_weights = [] - for (weight, basis_function) in zip(self.basis_weights, self.basis_functions): - test_survey = deepcopy(self) - basis_functions.append(basis_function) - test_survey.basis_functions = basis_functions - basis_weights.append(weight) - test_survey.basis_weights = basis_weights - try: - reward_values.append( - np.nanmax(test_survey.calc_reward_function(conditions)) - ) - except IndexError: - reward_values.append(None) - - bf_names = [bf.label() for bf in self.basis_functions] - return list(zip(bf_names, reward_values)) - - -class Deep_drilling_survey(rubin_sim.scheduler.surveys.Deep_drilling_survey): - def __repr__(self): - repr_start = f"<{self.__class__.__name__} survey_name='{self.survey_name}'" - repr_end = f", RA={self.ra}, dec={self.dec} at {hex(id(self))}>" - return repr_start + repr_end - - -class Base_basis_function(rubin_sim.scheduler.basis_functions.Base_basis_function): - def label(self): - label = self.__class__.__name__.replace("_basis_function", "") - return label - - -class Slewtime_basis_function( - rubin_sim.scheduler.basis_functions.Slewtime_basis_function -): - def label(self): - label = f"{self.__class__.__name__.replace('_basis_function', '')} {self.maxtime} {self.filtername}" - return label - - -rubin_sim.scheduler.basis_functions.Slewtime_basis_function.label = ( - Slewtime_basis_function.label -) -rubin_sim.scheduler.basis_functions.Base_basis_function.label = ( - Base_basis_function.label -) - -rubin_sim.scheduler.schedulers.core_scheduler.CoreScheduler.get_basis_functions = ( - CoreScheduler.get_basis_functions -) -rubin_sim.scheduler.schedulers.core_scheduler.CoreScheduler.get_healpix_maps = ( - CoreScheduler.get_healpix_maps -) -rubin_sim.scheduler.schedulers.core_scheduler.CoreScheduler.__repr__ = ( - CoreScheduler.__repr__ -) -rubin_sim.scheduler.schedulers.core_scheduler.CoreScheduler.__str__ = ( - CoreScheduler.__str__ -) -rubin_sim.scheduler.schedulers.core_scheduler.CoreScheduler.surveys_df = ( - CoreScheduler.surveys_df -) -rubin_sim.scheduler.schedulers.core_scheduler.CoreScheduler.make_reward_df = ( - CoreScheduler.make_reward_df -) - -rubin_sim.scheduler.schedulers.core_scheduler.CoreScheduler._repr_markdown_ = ( - CoreScheduler._repr_markdown_ -) - -rubin_sim.scheduler.surveys.BaseSurvey.__repr__ = BaseSurvey.__repr__ - -rubin_sim.scheduler.surveys.Deep_drilling_survey.__repr__ = ( - Deep_drilling_survey.__repr__ -) - -rubin_sim.scheduler.features.conditions.Conditions.__str__ = Conditions.__str__ -rubin_sim.scheduler.features.conditions.Conditions._repr_markdown_ = ( - Conditions._repr_markdown_ -) - -rubin_sim.scheduler.surveys.BaseMarkovDF_survey.reward_changes = ( - BaseMarkovDF_survey.reward_changes -) -rubin_sim.scheduler.surveys.BaseSurvey.reward_changes = BaseSurvey.reward_changes - -rubin_sim.scheduler.surveys.BaseSurvey.make_reward_df = BaseSurvey.make_reward_df -rubin_sim.scheduler.surveys.BaseMarkovDF_survey.make_reward_df = ( - BaseMarkovDF_survey.make_reward_df -) diff --git a/schedview/param.py b/schedview/param.py new file mode 100644 index 00000000..dc60de22 --- /dev/null +++ b/schedview/param.py @@ -0,0 +1,88 @@ +# Subclasses of param.Parameter for use in schedview. + +import param +import pandas as pd + + +class Series(param.Parameter): + """A pandas.Series parameter.""" + + def __init__(self, default=None, allow_None=False, **kwargs): + super().__init__(default=default, allow_None=allow_None, **kwargs) + self.allow_None = default is None or allow_None + self._validate(default) + + def _validate_value(self, val, allow_None): + if allow_None and val is None: + return + + if not isinstance(val, pd.Series): + raise ValueError( + f"Parameter {self.name} only takes a pandas.Series, " + f"not value of type {type(val)}." + ) + + def _validate(self, val): + self._validate_value(val, self.allow_None) + + +class DataFrame(param.Parameter): + """A pandas.DataFrame parameter. + + Parameters + ---------- + `columns`: `list` [`str`] or `dict` ['str', 'type'] + The columns of the DataFrame. If a dictionary, the keys are the column + names and the values. If a list, it contains the column names. + If None, any set of columns is accepted. + `allow_empty`: `bool` + Whether to allow a DataFrame with no rows. + """ + + __slots__ = ["columns", "allow_empty"] + + def __init__( + self, default=None, columns=None, allow_empty=True, allow_None=False, **kwargs + ): + super().__init__(default=default, allow_None=allow_None, **kwargs) + self.columns = columns + self.allow_empty = allow_empty + self.allow_None = default is None or allow_None + self._validate(default) + + def _validate_value(self, val, allow_None): + if allow_None and val is None: + return + if not isinstance(val, pd.DataFrame): + raise ValueError( + f"DataFarme parameter {self.name} only takes a pandas.DataFrame, " + f"not value of type {type(val)}." + ) + + if not self.allow_empty and len(val) == 0: + raise ValueError( + f"DataFrame parameter {self.name} must have at least one row." + ) + + # If the DataFrame is empty, do not check columns or column types. + if self.columns is None or len(val) == 0: + return + + for column in self.columns: + if column not in val.columns: + raise ValueError( + f"DataFrame parameter {self.name} must have column {column}." + ) + + try: + required_type = self.columns[column] + if not isinstance(val[column].iloc[0], required_type): + raise ValueError( + f"Column {column} of {self.name} must have type {required_type}," + f" but has type {type(val[column].iloc[0])}" + ) + except TypeError: + pass + + def _validate(self, val): + self._validate_value(val, self.allow_None) diff --git a/schedview/plot/readjs.py b/schedview/plot/readjs.py deleted file mode 100644 index c9c368d0..00000000 --- a/schedview/plot/readjs.py +++ /dev/null @@ -1,35 +0,0 @@ -import importlib.resources - - -def read_javascript(fname): - """Read javascript source code from the current package. - - Parameters - ---------- - fname : `str` - The name of the file from which to load js source code. - - Return - ------ - js_code : `str` - The loaded source code. - """ - root_package = __package__.split(".")[0] - - try: - js_path = importlib.resources.files(root_package).joinpath("js", fname) - with importlib.resources.as_file(js_path) as js_file_path: - with open(js_file_path, "r") as js_io: - js_code = js_io.read() - except AttributeError as e: - # If we are using an older version of importlib, we need to do - # this instead: - if e.args[0] != "module 'importlib.resources' has no attribute 'files'": - raise e - - with importlib.resources.path(root_package, ".") as root_path: - full_name = root_path.joinpath("js", fname) - with open(full_name, "r") as js_io: - js_code = js_io.read() - - return js_code diff --git a/schedview/plot/scheduler.py b/schedview/plot/scheduler.py index 73ee93b4..6723a319 100644 --- a/schedview/plot/scheduler.py +++ b/schedview/plot/scheduler.py @@ -332,11 +332,6 @@ def _set_scheduler(self, scheduler): LOGGER.debug("Setting the scheduler") self._scheduler = scheduler - # FIXME The pickle used for testing does not include several - # required methods of the Scheduler class, so add them. - if "get_basis_functions" not in dir(self.scheduler): - import schedview.munge.monkeypatch_rubin_sim # noqa F401 - self.survey_index[0] = self.scheduler.survey_index[0] self.survey_index[1] = self.scheduler.survey_index[1] @@ -452,7 +447,6 @@ def make_sphere_map( decorate=True, horizon_graticules=False, ): - if "hover_tool" not in self.bokeh_models: self.bokeh_models["hover_tool"] = bokeh.models.HoverTool( renderers=[], tooltips=self.tooltips @@ -470,7 +464,9 @@ def make_sphere_map( if "healpix" in self.data_sources: sphere_map.add_healpix( - self.data_sources["healpix"], cmap=self.healpix_cmap, nside=self.nside + self.data_sources["healpix"], + cmap=self.healpix_cmap, + nside=self.nside, ) else: sphere_map.add_healpix(self.healpix_values, nside=self.nside) @@ -819,7 +815,6 @@ def update_hovertool_bokeh_model(self): data = self.data_sources["healpix"].data for data_key in data.keys(): if not isinstance(data[data_key][0], collections.abc.Sequence): - if data_key == "center_ra": label = "RA" elif data_key == "center_decl": @@ -862,7 +857,7 @@ def update_reward_table_bokeh_model(self): any_bad_urls = False for doc_url in reward_df["doc_url"].values: - if "http" not in doc_url: + if doc_url is None or "http" not in doc_url: any_bad_urls = True break diff --git a/schedview/plot/visitmap.py b/schedview/plot/visitmap.py index b3886ec0..0f60c117 100644 --- a/schedview/plot/visitmap.py +++ b/schedview/plot/visitmap.py @@ -458,8 +458,9 @@ def create_visit_skymaps( scheduler : `CoreScheduler` or `str` The scheduler from which to extract the footprint, or the name of a file from which such a scheduler should be loaded. - night_date : `astropy.time.Time` - A time during the night to plot + night_date : `datetime.date` + The calendar date of the evening of the night for which + to plot the visits. observatory : `ModelObservatory`, optional Provides the location of the observatory, used to compute night start and end times. diff --git a/schedview/plot/visits.py b/schedview/plot/visits.py index fe901671..239333b3 100644 --- a/schedview/plot/visits.py +++ b/schedview/plot/visits.py @@ -36,8 +36,8 @@ def create_visit_explorer( visits : `str` or `pandas.DataFrame` One row per visit, as created by `schedview.collect.opsim.read_opsim`, or the name of a file from which such visits should be loaded. - night_date : `astropy.time.Time` - A time during the night to plot + night_date : `datetime.date` + The calendar date in the evening local time. observatory : `ModelObservatory`, optional Provides the location of the observatory, used to compute night start and end times. diff --git a/schedview/version.py b/schedview/version.py deleted file mode 100644 index 60568d37..00000000 --- a/schedview/version.py +++ /dev/null @@ -1,3 +0,0 @@ -# Generated by setuptools_scm -__all__ = ["__version__"] -__version__ = "0.2.0a2.dev28+g4584bf5.d20221216" diff --git a/tests/test_maf_metrics.py b/tests/test_maf_metrics.py index bd61dda7..1d51cec8 100644 --- a/tests/test_maf_metrics.py +++ b/tests/test_maf_metrics.py @@ -1,7 +1,8 @@ from tempfile import TemporaryDirectory -from astropy.time import Time +import datetime from matplotlib.figure import Figure +import astropy import rubin_sim from rubin_sim import maf @@ -10,9 +11,11 @@ from schedview.plot.maf import create_sample_maf_metric_plot OPSIM_OUTPUT_FNAME = rubin_sim.data.get_baseline() -NIGHT = Time("2023-10-04", scale="utc") +NIGHT = datetime.date(2023, 10, 4) OBSERVATORY = ModelObservatory() +astropy.utils.iers.conf.iers_degraded_accuracy = "warn" + def test_compute_sample_metric_bundle_group(): with TemporaryDirectory() as data_dir: diff --git a/tests/test_readjs.py b/tests/test_readjs.py deleted file mode 100644 index 9f82e962..00000000 --- a/tests/test_readjs.py +++ /dev/null @@ -1,15 +0,0 @@ -import unittest -import schedview.plot.readjs - -JS_FNAME = "update_map.js" - - -class Test_update_js(unittest.TestCase): - def test_update_js(self): - js_code = schedview.plot.readjs.read_javascript(JS_FNAME) - self.assertGreater(len(js_code), 10) - self.assertIsInstance(js_code, str) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_scheduler.py b/tests/test_scheduler.py index f2fa7d8e..07e809e7 100644 --- a/tests/test_scheduler.py +++ b/tests/test_scheduler.py @@ -1,5 +1,6 @@ import unittest import healpy as hp +import astropy.utils.iers from astropy.time import TimeDelta from schedview.plot.scheduler import ( SchedulerDisplay, @@ -12,6 +13,8 @@ NSIDE = 8 +astropy.utils.iers.conf.iers_degraded_accuracy = "warn" + class test_SchedulerDisplay(unittest.TestCase): def test_scheduler_display(self): diff --git a/util/sample_data/README.md b/util/sample_data/README.md new file mode 100644 index 00000000..ca681b35 --- /dev/null +++ b/util/sample_data/README.md @@ -0,0 +1,16 @@ +# Generation of sample data + +The program here generates sample data for demonstrating schedview +applications. + +It can be run directly: + +``` +python make_sample_test_data.py +``` + +There is a `--help` option to describe optional parameters. + +The primary use for it is to generated update sample data for use in +`${SCHEDVIEW_DIR}/schedview/data`. + diff --git a/util/sample_data/make_sample_test_data.py b/util/sample_data/make_sample_test_data.py new file mode 100644 index 00000000..0df8ead3 --- /dev/null +++ b/util/sample_data/make_sample_test_data.py @@ -0,0 +1,136 @@ +import warnings +import numpy as np +import datetime +import lzma +import pickle +import argparse + +from astropy.time import Time +from rubin_sim.scheduler.example import example_scheduler +from rubin_sim.scheduler import sim_runner +from rubin_sim.scheduler.model_observatory import ModelObservatory +from rubin_sim.scheduler.utils import SchemaConverter + +# Several dependencies throw prodigious instances of (benign) warnings. +# Suppress them to avoid poluting the executed notebook. + +warnings.filterwarnings( + "ignore", + module="astropy.time", + message="Numerical value without unit or explicit format passed to TimeDelta, assuming days", +) +warnings.filterwarnings( + "ignore", + module="pandas", + message="In a future version of pandas, a length 1 tuple will be returned when iterating over a groupby with a grouper equal to a list of length 1. Don't supply a list with a single grouper to avoid this warning.", +) +warnings.filterwarnings( + "ignore", + module="healpy", + message="divide by zero encountered in divide", +) +warnings.filterwarnings( + "ignore", + module="healpy", + message="invalid value encountered in multiply", +) +warnings.filterwarnings( + "ignore", + module="holoviews", + message="Discarding nonzero nanoseconds in conversion.", +) +warnings.filterwarnings( + "ignore", + module="rubin_sim", + message="invalid value encountered in arcsin", +) +warnings.filterwarnings( + "ignore", + module="rubin_sim", + message="All-NaN slice encountered", +) + + +def make_sample_test_data(): + parser = argparse.ArgumentParser( + description="Generate sample test data for testing schedview." + ) + parser.add_argument( + "--opsim_output_fname", + type=str, + default="sample_opsim.db", + help="Filename for the opsim output.", + ) + parser.add_argument( + "--scheduler_fname", + type=str, + default="sample_scheduler.pickle.xz", + help="Filename for the scheduler pickle file.", + ) + parser.add_argument( + "--rewards_fname", + type=str, + default="sample_rewards.h5", + help="Filename for the rewards file.", + ) + parser.add_argument( + "--date", + type=str, + default="2023-12-22", + help="Date of the night to simulate (YYYY-MM-DD).", + ) + args = parser.parse_args() + + opsim_output_fname = args.opsim_output_fname + scheduler_fname = args.scheduler_fname + rewards_fname = args.rewards_fname + evening_iso8601 = args.date + + # Set the start date, scheduler, and observatory for the night: + + observatory = ModelObservatory() + + # Set `evening_mjd` to the integer calendar MJD of the local calendar day on which sunset falls on the night of interest. + evening_mjd = Time(evening_iso8601).mjd + + # 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. + # Instead, find the sunset and sunrise of the night we want using the almanac, and use these to determine our start time and duration. + + # If the date represents the local calendar date at sunset, we need to shift by the longitude in units of days + this_night = ( + np.floor( + observatory.almanac.sunsets["sunset"] + observatory.site.longitude / 360 + ) + == evening_mjd + ) + + mjd_start = observatory.almanac.sunsets[this_night]["sun_n12_setting"][0] + mjd_end = observatory.almanac.sunsets[this_night]["sunrise"][0] + + night_duration = mjd_end - mjd_start + + observatory = ModelObservatory(mjd_start=mjd_start) + + scheduler = example_scheduler(mjd_start=mjd_start) + scheduler.keep_rewards = True + + observatory, scheduler, observations, reward_df, obs_rewards = sim_runner( + observatory, + scheduler, + mjd_start=mjd_start, + survey_length=night_duration, + record_rewards=True, + ) + + SchemaConverter().obs2opsim(observations, filename=opsim_output_fname) + + with lzma.open(scheduler_fname, "wb", format=lzma.FORMAT_XZ) as pio: + sched_cond_tuple = (scheduler, scheduler.conditions) + pickle.dump(sched_cond_tuple, pio) + + reward_df.to_hdf(rewards_fname, "reward_df") + obs_rewards.to_hdf(rewards_fname, "obs_rewards") + + +if __name__ == "__main__": + make_sample_test_data()