From 3558b071eee9e95f9d6e4e84a69c4ed0f9092284 Mon Sep 17 00:00:00 2001 From: Eman Ali Date: Mon, 3 Jun 2024 15:49:16 +1000 Subject: [PATCH 01/18] restructure the dashboard in seperate modules --- .../app/scheduler_dashboard/constants.py | 39 + .../lfa_scheduler_snapshot_dashboard.py | 67 + ...restricted_scheduler_snapshot_dashboard.py | 32 + .../scheduler_dashboard.py | 1490 +---------------- .../scheduler_snapshot_dashboard.py | 1352 +++++++++++++++ schedview/app/scheduler_dashboard/utils.py | 36 + 6 files changed, 1546 insertions(+), 1470 deletions(-) create mode 100644 schedview/app/scheduler_dashboard/constants.py create mode 100644 schedview/app/scheduler_dashboard/lfa_scheduler_snapshot_dashboard.py create mode 100644 schedview/app/scheduler_dashboard/restricted_scheduler_snapshot_dashboard.py create mode 100644 schedview/app/scheduler_dashboard/scheduler_snapshot_dashboard.py diff --git a/schedview/app/scheduler_dashboard/constants.py b/schedview/app/scheduler_dashboard/constants.py new file mode 100644 index 00000000..f70a672b --- /dev/null +++ b/schedview/app/scheduler_dashboard/constants.py @@ -0,0 +1,39 @@ +import importlib.resources + +import bokeh +from astropy.time import Time + +# Change styles using CSS variables. +h1_stylesheet = """ +:host { + --mono-font: Helvetica; + color: white; + font-size: 16pt; + font-weight: 500; +} +""" +h2_stylesheet = """ +:host { + --mono-font: Helvetica; + color: white; + font-size: 14pt; + font-weight: 300; +} +""" +h3_stylesheet = """ +:host { + --mono-font: Helvetica; + color: white; + font-size: 13pt; + font-weight: 300; +} +""" + +DEFAULT_CURRENT_TIME = Time.now() +DEFAULT_TIMEZONE = "UTC" # "America/Santiago" +LOGO = "/schedview-snapshot/assets/lsst_white_logo.png" +COLOR_PALETTES = [color for color in bokeh.palettes.__palettes__ if "256" in color] +DEFAULT_COLOR_PALETTE = "Viridis256" +DEFAULT_NSIDE = 16 +PACKAGE_DATA_DIR = importlib.resources.files("schedview.data").as_posix() +LFA_DATA_DIR = "s3://rubin:" diff --git a/schedview/app/scheduler_dashboard/lfa_scheduler_snapshot_dashboard.py b/schedview/app/scheduler_dashboard/lfa_scheduler_snapshot_dashboard.py new file mode 100644 index 00000000..4ad214be --- /dev/null +++ b/schedview/app/scheduler_dashboard/lfa_scheduler_snapshot_dashboard.py @@ -0,0 +1,67 @@ +from datetime import datetime +from zoneinfo import ZoneInfo + +import param +from astropy.time import Time +from pandas import Timestamp + +from schedview.app.scheduler_dashboard.constants import DEFAULT_TIMEZONE +from schedview.app.scheduler_dashboard.scheduler_snapshot_dashboard import SchedulerSnapshotDashboard +from schedview.app.scheduler_dashboard.utils import query_night_schedulers + + +class LFASchedulerSnapshotDashboard(SchedulerSnapshotDashboard): + """A Parametrized container for parameters, data, and panel objects for the + scheduler dashboard working in LFA mode where data files are loaded from + a certain S3 bucket. + """ + + scheduler_fname_doc = """Recent pickles from LFA + """ + + # precedence is used to make sure fields are displayed + # in the right order regardless of the dashboard mode + scheduler_fname = param.Selector( + default="", + objects=[], + doc=scheduler_fname_doc, + precedence=3, + ) + + pickles_date = param.Date( + default=datetime.now(), + label="Snapshot selection cutoff date and time", + doc="Show snapshots that are recent as of this date and time in the scheduler snapshot file dropdown", + precedence=1, + ) + + telescope = param.Selector( + default=None, objects={"All": None, "Simonyi": 1, "Auxtel": 2}, doc="Source Telescope", precedence=2 + ) + + # Summary widget and Reward widget heights are different in this mode + # because there are more data loading parameters + _summary_widget_height = 310 + _reward_widget_height = 350 + + def __init__(self): + super().__init__() + + async def query_schedulers(self, selected_time, selected_tel): + """Query snapshots that have a timestamp between the start of the + night and selected datetime and generated by selected telescope + """ + selected_time = Time( + Timestamp( + selected_time, + tzinfo=ZoneInfo(DEFAULT_TIMEZONE), + ) + ) + self.show_loading_indicator = True + self._debugging_message = "Starting retrieving snapshots" + self.logger.debug("Starting retrieving snapshots") + scheduler_urls = await query_night_schedulers(selected_time, selected_tel) + self.logger.debug("Finished retrieving snapshots") + self._debugging_message = "Finished retrieving snapshots" + self.show_loading_indicator = False + return scheduler_urls diff --git a/schedview/app/scheduler_dashboard/restricted_scheduler_snapshot_dashboard.py b/schedview/app/scheduler_dashboard/restricted_scheduler_snapshot_dashboard.py new file mode 100644 index 00000000..c89f6964 --- /dev/null +++ b/schedview/app/scheduler_dashboard/restricted_scheduler_snapshot_dashboard.py @@ -0,0 +1,32 @@ +import schedview +import schedview.param +from schedview.app.scheduler_dashboard.constants import PACKAGE_DATA_DIR +from schedview.app.scheduler_dashboard.scheduler_snapshot_dashboard import SchedulerSnapshotDashboard + + +class RestrictedSchedulerSnapshotDashboard(SchedulerSnapshotDashboard): + """A Parametrized container for parameters, data, and panel objects for the + scheduler dashboard working in restricted more where data files can only + be loaded from a certain data directory that is set through constructor. + """ + + # Param parameters that are modifiable by user actions. + 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_scheduler.scheduler.schedulers.CoreScheduler, or a tuple of the form + (scheduler, conditions), where scheduler is an instance of a subclass of + rubin_scheduler.scheduler.schedulers.CoreScheduler, and conditions is an + instance of rubin_scheduler.scheduler.conditions.Conditions. + """ + scheduler_fname = schedview.param.FileSelectorWithEmptyOption( + path=f"{PACKAGE_DATA_DIR}/*scheduler*.p*", + doc=scheduler_fname_doc, + default=None, + allow_None=True, + ) + + def __init__(self, data_dir=None): + super().__init__() + + if data_dir is not None: + self.param["scheduler_fname"].update(path=f"{data_dir}/*scheduler*.p*") diff --git a/schedview/app/scheduler_dashboard/scheduler_dashboard.py b/schedview/app/scheduler_dashboard/scheduler_dashboard.py index 99beda3e..8b2481c0 100644 --- a/schedview/app/scheduler_dashboard/scheduler_dashboard.py +++ b/schedview/app/scheduler_dashboard/scheduler_dashboard.py @@ -25,54 +25,39 @@ import argparse import importlib.resources -import logging import os -import traceback # Filter the astropy warnings swamping the terminal import warnings -from datetime import datetime from glob import glob -from zoneinfo import ZoneInfo -import bokeh -import numpy as np import panel as pn -import param -import rubin_scheduler.site_models -from astropy.time import Time from astropy.utils.exceptions import AstropyWarning -from bokeh.models import ColorBar, LinearColorMapper -from bokeh.models.widgets.tables import BooleanFormatter, HTMLTemplateFormatter, NumberFormatter -from lsst.resources import ResourcePath -from pandas import Timestamp from panel.io.loading import start_loading_spinner, stop_loading_spinner -from pytz import timezone -# For the conditions.mjd bugfix -from rubin_scheduler.scheduler.model_observatory import ModelObservatory -from rubin_scheduler.skybrightness_pre.sky_model_pre import SkyModelPre +from schedview.app.scheduler_dashboard.constants import ( + LFA_DATA_DIR, + LOGO, + PACKAGE_DATA_DIR, + h1_stylesheet, + h2_stylesheet, +) +from schedview.app.scheduler_dashboard.lfa_scheduler_snapshot_dashboard import LFASchedulerSnapshotDashboard +from schedview.app.scheduler_dashboard.restricted_scheduler_snapshot_dashboard import ( + RestrictedSchedulerSnapshotDashboard, +) -import schedview -import schedview.collect.scheduler_pickle -import schedview.compute.scheduler -import schedview.compute.survey -import schedview.param -import schedview.plot.survey -from schedview.app.scheduler_dashboard.utils import query_night_schedulers +# import schedview +# import schedview.collect.scheduler_pickle +# import schedview.compute.scheduler +# import schedview.compute.survey +# import schedview.param +# import schedview.plot.survey +from schedview.app.scheduler_dashboard.scheduler_snapshot_dashboard import SchedulerSnapshotDashboard # Filter astropy warning that's filling the terminal with every update. warnings.filterwarnings("ignore", category=AstropyWarning) -DEFAULT_CURRENT_TIME = Time.now() -DEFAULT_TIMEZONE = "UTC" # "America/Santiago" -LOGO = "/schedview-snapshot/assets/lsst_white_logo.png" -COLOR_PALETTES = [color for color in bokeh.palettes.__palettes__ if "256" in color] -DEFAULT_COLOR_PALETTE = "Viridis256" -DEFAULT_NSIDE = 16 -PACKAGE_DATA_DIR = importlib.resources.files("schedview.data").as_posix() -LFA_DATA_DIR = "s3://rubin:" - pn.extension( "tabulator", @@ -80,1445 +65,8 @@ notifications=True, ) -# Change styles using CSS variables. -h1_stylesheet = """ -:host { - --mono-font: Helvetica; - color: white; - font-size: 16pt; - font-weight: 500; -} -""" -h2_stylesheet = """ -:host { - --mono-font: Helvetica; - color: white; - font-size: 14pt; - font-weight: 300; -} -""" -h3_stylesheet = """ -:host { - --mono-font: Helvetica; - color: white; - font-size: 13pt; - font-weight: 300; -} -""" - - -def get_sky_brightness_date_bounds(): - """Load available datetime range from SkyBrightness_Pre files""" - sky_model = SkyModelPre() - min_date = Time(sky_model.mjd_left.min(), format="mjd") - max_date = Time(sky_model.mjd_right.max() - 0.001, format="mjd") - return (min_date, max_date) - - -def url_formatter(dataframe_row, name_column, url_column): - """Format survey name as a HTML href to survey URL (if URL exists). - - Parameters - ---------- - dataframe_row : 'pandas.core.series.Series' - A row of a pandas.core.frame.DataFrame. - - Returns - ------- - survey_name_or_url : 'str' - A HTML href or plain string. - """ - if dataframe_row[url_column] == "": - return dataframe_row[name_column] - else: - return f' \ - ' - - -class SchedulerSnapshotDashboard(param.Parameterized): - """A Parametrized container for parameters, data, and panel objects for the - scheduler dashboard. - """ - - # Param parameters that are modifiable by user actions. - 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_scheduler.scheduler.schedulers.CoreScheduler, or a tuple of the form - (scheduler, conditions), where scheduler is an instance of a subclass of - rubin_scheduler.scheduler.schedulers.CoreScheduler, and conditions is an - instance of rubin_scheduler.scheduler.conditions.Conditions. - """ - - (mjd_min, mjd_max) = get_sky_brightness_date_bounds() - date_bounds = (mjd_min.to_datetime(), mjd_max.to_datetime()) - - scheduler_fname = param.String( - default="", - label="Scheduler snapshot file", - doc=scheduler_fname_doc, - precedence=3, - ) - widget_datetime = param.Date( - default=date_bounds[0], - label="Date and time (UTC)", - doc=f"Select dates between {date_bounds[0]} and {date_bounds[1]}", - bounds=date_bounds, - precedence=4, - ) - url_mjd = param.Number(default=None) - widget_tier = param.Selector( - default="", - objects=[""], - label="Tier", - doc="The label for the first index into the CoreScheduler.survey_lists.", - precedence=5, - ) - survey_map = param.Selector( - default="reward", - objects=["reward"], - doc="Sky brightness maps, non-scalar rewards and survey reward map.", - ) - nside = param.ObjectSelector( - default=DEFAULT_NSIDE, - objects=[2, 4, 8, 16, 32, 64], - label="Map resolution (nside)", - doc="", - ) - - color_palette = param.Selector(default=DEFAULT_COLOR_PALETTE, objects=COLOR_PALETTES, doc="") - summary_widget = param.Parameter(default=None, doc="") - reward_widget = param.Parameter(default=None, doc="") - show_loading_indicator = param.Boolean(default=False) - - # Param parameters (used in depends decoraters and trigger calls). - _publish_summary_widget = param.Parameter(None) - _publish_reward_widget = param.Parameter(None) - _publish_map = param.Parameter(None) - _update_headings = param.Parameter(None) - _debugging_message = param.Parameter(None) - - # Non-Param parameters storing Panel pane objects. - debug_pane = None - dashboard_subtitle_pane = None - summary_table_heading_pane = None - reward_table_heading_pane = None - map_title_pane = None - - # Non-Param internal parameters. - _mjd = None - _tier = None - _survey = 0 - _reward = -1 - _survey_name = "" - _reward_name = "" - _map_name = "" - _scheduler = None - _conditions = None - _reward_df = None - _scheduler_summary_df = None - _survey_maps = None - _survey_reward_df = None - _sky_map_base = None - _debug_string = "" - _display_reward = False - _display_dashboard_data = False - _do_not_trigger_update = True - _summary_widget_height = 220 - _reward_widget_height = 400 - - def __init__(self, **params): - super().__init__(**params) - self.config_logger() - - def config_logger(self, logger_name="schedule-snapshot"): - """Configure the logger. - - Parameters - ---------- - logger_name : `str` - The name of the logger. - """ - - self.logger = logging.getLogger(logger_name) - self.logger.setLevel(logging.DEBUG) - - log_stream_handler = None - if self.logger.hasHandlers(): - for handler in self.logger.handlers: - if isinstance(handler, logging.StreamHandler): - log_stream_handler = handler - - if log_stream_handler is None: - log_stream_handler = logging.StreamHandler() - - log_stream_formatter = logging.Formatter("%(asctime)s: %(message)s") - log_stream_handler.setFormatter(log_stream_formatter) - self.logger.addHandler(log_stream_handler) - - # ------------------------------------------------------------ User actions - - @param.depends("scheduler_fname", watch=True) - def _update_scheduler_fname(self): - """Update the dashboard when a user enters a new filepath/URL.""" - self.logger.debug("UPDATE: scheduler file") - self.show_loading_indicator = True - self.clear_dashboard() - - if not self.read_scheduler(): - self.clear_dashboard() - self.show_loading_indicator = False - return - - # Current fix for _conditions.mjd having different datatypes. - if type(self._conditions._mjd) == np.ndarray: - self._conditions._mjd = self._conditions._mjd[0] - - # Get mjd from pickle and set widget and URL to match. - self._do_not_trigger_update = True - self.url_mjd = self._conditions._mjd - self.widget_datetime = Time(self._conditions._mjd, format="mjd").to_datetime() - self._do_not_trigger_update = False - - if not self.make_scheduler_summary_df(): - self.clear_dashboard() - self.show_loading_indicator = False - return - - self.create_summary_widget() - self.param.trigger("_publish_summary_widget") - - self._do_not_trigger_update = True - self.summary_widget.selection = [0] - self._do_not_trigger_update = False - - self.compute_survey_maps() - self.survey_map = self.param["survey_map"].objects[-1] - self._map_name = self.survey_map.split("@")[0].strip() - - self.create_sky_map_base() - self.update_sky_map_with_survey_map() - self.param.trigger("_publish_map") - - self.make_reward_df() - self.create_reward_widget() - self.param.trigger("_publish_reward_widget") - - self._display_dashboard_data = True - self._display_reward = False - self.param.trigger("_update_headings") - - self.show_loading_indicator = False - - @param.depends("widget_datetime", watch=True) - def _update_mjd_from_picker(self): - """Update the dashboard when a datetime is input in widget.""" - if self._do_not_trigger_update: - return - - self.logger.debug("UPDATE: mjd from date-picker") - self.show_loading_indicator = True - self.clear_dashboard() - - self._do_not_trigger_update = True - self.url_mjd = Time( - Timestamp( - self.widget_datetime, - tzinfo=ZoneInfo(DEFAULT_TIMEZONE), - ) - ).mjd - self._do_not_trigger_update = False - self._mjd = self.url_mjd - - if not self.update_conditions(): - self.clear_dashboard() - self.show_loading_indicator = False - return - - if not self.make_scheduler_summary_df(): - self.clear_dashboard() - self.show_loading_indicator = False - return - - if self.summary_widget is None: - self.create_summary_widget() - else: - self.update_summary_widget_data() - self.param.trigger("_publish_summary_widget") - - self._do_not_trigger_update = True - self.summary_widget.selection = [0] - self._do_not_trigger_update = False - - self.compute_survey_maps() - self.survey_map = self.param["survey_map"].objects[-1] - self._map_name = self.survey_map.split("@")[0].strip() - - self.create_sky_map_base() - self.update_sky_map_with_survey_map() - self.param.trigger("_publish_map") - - self.make_reward_df() - self.create_reward_widget() - self.param.trigger("_publish_reward_widget") - - self._display_dashboard_data = True - self._display_reward = False - self.param.trigger("_update_headings") - - self.show_loading_indicator = False - - @param.depends("url_mjd", watch=True) - def _update_mjd_from_url(self): - """Update the dashboard when an mjd is input in URL.""" - if self._do_not_trigger_update: - return - - self.logger.debug("UPDATE: mjd from url") - self.show_loading_indicator = True - self.clear_dashboard() - - self._do_not_trigger_update = True - self.widget_datetime = Time(self.url_mjd, format="mjd").to_datetime() - self._do_not_trigger_update = False - self._mjd = self.url_mjd - - if not self.update_conditions(): - self.clear_dashboard() - self.show_loading_indicator = False - return - - if not self.make_scheduler_summary_df(): - self.clear_dashboard() - self.show_loading_indicator = False - return - - if self.summary_widget is None: - self.create_summary_widget() - else: - self.update_summary_widget_data() - self.param.trigger("_publish_summary_widget") - - self._do_not_trigger_update = True - self.summary_widget.selection = [0] - self._do_not_trigger_update = False - - self.compute_survey_maps() - self.survey_map = self.param["survey_map"].objects[-1] - self._map_name = self.survey_map.split("@")[0].strip() - - self.create_sky_map_base() - self.update_sky_map_with_survey_map() - self.param.trigger("_publish_map") - - self.make_reward_df() - self.create_reward_widget() - self.param.trigger("_publish_reward_widget") - - self._display_dashboard_data = True - self._display_reward = False - self.param.trigger("_update_headings") - - self.show_loading_indicator = False - - @param.depends("widget_tier", watch=True) - def _update_tier(self): - """Update the dashboard when a user chooses a new tier.""" - if not self._display_dashboard_data: - return - self.logger.debug("UPDATE: tier") - self._tier = self.widget_tier - self._survey = 0 - self._survey_name = self._scheduler_summary_df[ - self._scheduler_summary_df["tier"] == self._tier - ].reset_index()["survey"][self._survey] - - if self.summary_widget is None: - self.create_summary_widget() - else: - self.update_summary_widget_data() - self.param.trigger("_publish_summary_widget") - - self.compute_survey_maps() - self._do_not_trigger_update = True - self.survey_map = self.param["survey_map"].objects[-1] - self._map_name = self.survey_map.split("@")[0].strip() - self.summary_widget.selection = [0] - self._do_not_trigger_update = False - - self.make_reward_df() - if self.reward_widget is None: - self.create_reward_widget() - else: - self.update_reward_widget_data() - self.param.trigger("_publish_reward_widget") - - self.create_sky_map_base() - self.update_sky_map_with_survey_map() - self.param.trigger("_publish_map") - - self._display_reward = False - self.param.trigger("_update_headings") - - @param.depends("summary_widget.selection", watch=True) - def _update_survey(self): - """Update the dashboard when a user selects a survey.""" - if self.summary_widget.selection == [] or self._do_not_trigger_update: - return - - self.logger.debug("UPDATE: survey") - self._survey = self.summary_widget.selection[0] - self._survey_name = self._scheduler_summary_df[ - self._scheduler_summary_df["tier"] == self._tier - ].reset_index()["survey"][self._survey] - - self.compute_survey_maps() - self._do_not_trigger_update = True - self.survey_map = self.param["survey_map"].objects[-1] - self._map_name = self.survey_map.split("@")[0].strip() - self._do_not_trigger_update = False - - self.make_reward_df() - if self.reward_widget is None: - self.create_reward_widget() - else: - self.update_reward_widget_data() - self.param.trigger("_publish_reward_widget") - - self.create_sky_map_base() - self.update_sky_map_with_survey_map() - self.param.trigger("_publish_map") - - self._display_reward = False - self.param.trigger("_update_headings") - - @param.depends("reward_widget.selection", watch=True) - def _update_reward(self): - """Update the dashboard when a user selects a reward.""" - # Do not run code if no selection or when update flag is True. - if self.reward_widget.selection == [] or self._do_not_trigger_update: - return - - self.logger.debug("UPDATE: reward") - self._reward = self.reward_widget.selection[0] - self._reward_name = self._survey_reward_df["basis_function"][self._reward] - - # If reward is in survey maps, update survey maps drop-down. - if any(self._reward_name in key for key in self._survey_maps): - self._do_not_trigger_update = True - self.survey_map = list(key for key in self._survey_maps if self._reward_name in key)[0] - self._map_name = self.survey_map.split("@")[0].strip() - self._do_not_trigger_update = False - else: - self.survey_map = "" - - self.update_sky_map_with_reward() - self.param.trigger("_publish_map") - - self._display_reward = True - self.param.trigger("_update_headings") - - @param.depends("survey_map", watch=True) - def _update_survey_map(self): - """Update the dashboard when a user chooses a new survey map.""" - # Don't run code during initial load or when updating tier or survey. - if not self._display_dashboard_data or self._do_not_trigger_update: - return - - # If user selects null map, do nothing. - if self.survey_map == "": - return - - self.logger.debug("UPDATE: survey map") - # If selection is a reward map, reflect in reward table. - self._do_not_trigger_update = True - self._map_name = self.survey_map.split("@")[0].strip() - if any(self._survey_reward_df["basis_function"].isin([self._map_name])): - index = self._survey_reward_df["basis_function"].index[ - self._survey_reward_df["basis_function"].tolist().index(self._map_name) - ] - self.reward_widget.selection = [index] - elif self.reward_widget is not None: - self.reward_widget.selection = [] - self._do_not_trigger_update = False - - self.update_sky_map_with_survey_map() - self.param.trigger("_publish_map") - - self._display_reward = False - self.param.trigger("_update_headings") - - @param.depends("nside", watch=True) - def _update_nside(self): - """Update the dashboard when a user chooses a new nside.""" - # Don't run code during initial load. - if not self._display_dashboard_data: - return - - self.logger.debug("UPDATE: nside") - self.compute_survey_maps() - - self.create_sky_map_base() - self.update_sky_map_with_survey_map() - self.param.trigger("_publish_map") - - @param.depends("color_palette", watch=True) - def _update_color_palette(self): - """Update the dashboard when a user chooses a new color palette.""" - self.logger.debug("UPDATE: color palette") - if self._display_reward: - self.update_sky_map_with_reward() - else: - self.update_sky_map_with_survey_map() - self.param.trigger("_publish_map") - - # ------------------------------------------------------- Internal workings - - def clear_dashboard(self): - """Clear the dashboard for a new pickle or a new date.""" - self._debugging_message = "Starting to clear dashboard." - - self.summary_widget = None - self._survey_reward_df = None - self._sky_map_base = None - self._display_dashboard_data = False - self.reward_widget = None - - self.param.trigger("_publish_summary_widget") - self.param.trigger("_publish_reward_widget") - self.param.trigger("_publish_map") - self.param.trigger("_update_headings") - - self.param["widget_tier"].objects = [""] - self.param["survey_map"].objects = [""] - - self.widget_tier = "" - self.survey_map = "" - - self._tier = "" - self._survey = 0 - self._reward = -1 - - self._debugging_message = "Finished clearing dashboard." - - def read_scheduler(self): - """Load the scheduler and conditions objects from pickle file. - - Returns - ------- - success : 'bool' - Record of success or failure of reading scheduler from file/URL. - """ - try: - self._debugging_message = "Starting to load scheduler." - pn.state.notifications.info("Scheduler loading...", duration=0) - - os.environ["LSST_DISABLE_BUCKET_VALIDATION"] = "1" - scheduler_resource_path = ResourcePath(self.scheduler_fname) - scheduler_resource_path.use_threads = False - with scheduler_resource_path.as_local() as local_scheduler_resource: - (scheduler, conditions) = schedview.collect.scheduler_pickle.read_scheduler( - local_scheduler_resource.ospath - ) - - self._scheduler = scheduler - self._conditions = conditions - - self._debugging_message = "Finished loading scheduler." - pn.state.notifications.clear() - pn.state.notifications.success("Scheduler pickle loaded successfully!") - - return True - - except Exception: - tb = traceback.format_exc(limit=-1) - self._debugging_message = f"Cannot load scheduler from {self.scheduler_fname}: \n{tb}" - pn.state.notifications.clear() - pn.state.notifications.error(f"Cannot load scheduler from {self.scheduler_fname}", duration=0) - - self._scheduler = None - self._conditions = None - - return False - - def update_conditions(self): - """Update Conditions object. - - Returns - ------- - success : 'bool' - Record of success of Conditions update. - """ - if self._conditions is None: - self._debugging_message = "Cannot update Conditions object as no Conditions object is loaded." - - return False - - try: - self._debugging_message = "Starting to update Conditions object." - pn.state.notifications.info("Updating Conditions object...", duration=0) - - # self._conditions.mjd = self._mjd - - # Use instance of ModelObservatory until Conditions - # setting bug is fixed. - if ( - not hasattr(self, "_model_observatory") - or self._model_observatory.nside != self._scheduler.nside - ): - # Get weather conditions from pickle. - wind_data = rubin_scheduler.site_models.ConstantWindData( - wind_speed=self._conditions.wind_speed, - wind_direction=self._conditions.wind_direction, - ) - # Set seeing to fiducial site seeing. - seeing_data = rubin_scheduler.site_models.ConstantSeeingData(0.69) - # Create new MO instance. - self._model_observatory = ModelObservatory( - nside=self._scheduler.nside, - init_load_length=1, - wind_data=wind_data, - seeing_data=seeing_data, - ) - - self._model_observatory.mjd = self._mjd - self._conditions = self._model_observatory.return_conditions() - self._scheduler.update_conditions(self._conditions) - - self._debugging_message = "Finished updating Conditions object." - pn.state.notifications.clear() - pn.state.notifications.success("Conditions object updated successfully") - - return True - - except Exception: - tb = traceback.format_exc(limit=-1) - self._debugging_message = f"Conditions object unable to be updated: \n{tb}" - pn.state.notifications.clear() - pn.state.notifications.error("Conditions object unable to be updated!", duration=0) - - return False - - def make_scheduler_summary_df(self): - """Make the reward and scheduler summary dataframes. - - Returns - ------- - success : 'bool' - Record of success of dataframe construction. - """ - if self._scheduler is None: - self._debugging_message = "Cannot update survey reward table as no pickle is loaded." - - return False - - try: - self._debugging_message = "Starting to make scheduler summary dataframe." - pn.state.notifications.info("Making scheduler summary dataframe...", duration=0) - - self._reward_df = self._scheduler.make_reward_df(self._conditions) - scheduler_summary_df = schedview.compute.scheduler.make_scheduler_summary_df( - self._scheduler, - self._conditions, - self._reward_df, - ) - - # Duplicate column and apply URL formatting to one of the columns. - scheduler_summary_df["survey"] = scheduler_summary_df.loc[:, "survey_name_with_id"] - scheduler_summary_df["survey_name_with_id"] = scheduler_summary_df.apply( - url_formatter, - axis=1, - args=("survey_name_with_id", "survey_url"), - ) - self._scheduler_summary_df = scheduler_summary_df - - tiers = self._scheduler_summary_df.tier.unique().tolist() - self.param["widget_tier"].objects = tiers - self.widget_tier = tiers[0] - self._tier = tiers[0] - self._survey = 0 - self._survey_name = self._scheduler_summary_df[ - self._scheduler_summary_df["tier"] == self._tier - ].reset_index()["survey"][self._survey] - - self._debugging_message = "Finished making scheduler summary dataframe." - pn.state.notifications.clear() - pn.state.notifications.success("Scheduler summary dataframe updated successfully") - - return True - - except Exception: - tb = traceback.format_exc(limit=-1) - self._debugging_message = f"Scheduler summary dataframe unable to be updated: \n{tb}" - pn.state.notifications.clear() - pn.state.notifications.error("Scheduler summary dataframe unable to be updated!", duration=0) - self._scheduler_summary_df = None - - return False - - def create_summary_widget(self): - """Create Tabulator widget with scheduler summary dataframe.""" - if self._scheduler_summary_df is None: - return - - self._debugging_message = "Starting to create summary widget." - tabulator_formatter = {"survey_name_with_id": HTMLTemplateFormatter(template="<%= value %>")} - columns = [ - "survey_index", - "survey", - "reward", - "survey_name_with_id", - "tier", - "survey_url", - ] - titles = { - "survey_index": "Index", - "survey": "Survey", - "reward": "Reward", - "survey_name_with_id": "Docs", - } - widths = { - "survey_index": "10%", - "survey": "48%", - "reward": "30%", - "survey_name_with_id": "10%", - } - text_align = { - "survey_index": "left", - "survey": "left", - "reward": "right", - "survey_name_with_id": "center", - } - summary_widget = pn.widgets.Tabulator( - self._scheduler_summary_df[self._scheduler_summary_df["tier"] == self._tier][columns], - titles=titles, - widths=widths, - text_align=text_align, - sortable={"survey_name_with_id": False}, - show_index=False, - formatters=tabulator_formatter, - disabled=True, - selectable=1, - hidden_columns=["tier", "survey_url"], - sizing_mode="stretch_width", - height=self._summary_widget_height, - ) - self.summary_widget = summary_widget - self._debugging_message = "Finished making summary widget." - - def update_summary_widget_data(self): - """Update data for survey Tabulator widget.""" - self._debugging_message = "Starting to update summary widget." - columns = [ - "survey_index", - "survey", - "reward", - "survey_name_with_id", - "tier", - "survey_url", - ] - self.summary_widget._update_data( - self._scheduler_summary_df[self._scheduler_summary_df["tier"] == self._tier][columns] - ) - self._debugging_message = "Finished updating summary widget." - - @param.depends("_publish_summary_widget") - def publish_summary_widget(self): - """Publish the summary Tabulator widget - to be displayed on the dashboard. - - Returns - ------- - widget: 'panel.widgets.Tabulator' - Table of scheduler summary data. - """ - if self.summary_widget is None: - return "No summary available." - else: - self._debugging_message = "Publishing summary widget." - return self.summary_widget - - def compute_survey_maps(self): - """Compute survey maps and update drop-down selection.""" - if self._scheduler is None: - self._debugging_message = "Cannot compute survey maps as no scheduler loaded." - return - if self._scheduler_summary_df is None: - self._debugging_message = "Cannot compute survey maps as no scheduler summary made." - return - try: - self._debugging_message = "Starting to compute survey maps." - self._survey_maps = schedview.compute.survey.compute_maps( - self._scheduler.survey_lists[int(self._tier[-1])][self._survey], - self._conditions, - np.int64(self.nside), - ) - self.param["survey_map"].objects = [""] + list(self._survey_maps.keys()) - self._debugging_message = "Finished computing survey maps." - - except Exception: - self._debugging_message = f"Cannot compute survey maps: \n{traceback.format_exc(limit=-1)}" - pn.state.notifications.error("Cannot compute survey maps!", duration=0) - - def make_reward_df(self): - """Make the summary dataframe.""" - if self._scheduler is None: - self._debugging_message = "Cannot make summary dataframe as no scheduler loaded." - return - if self._scheduler_summary_df is None: - self._debugging_message = "Cannot make summary dataframe as no scheduler summary made." - return - try: - self._debugging_message = "Starting to make reward dataframe." - # Survey has rewards. - if self._reward_df.index.isin([(int(self._tier[-1]), self._survey)]).any(): - survey_reward_df = schedview.compute.survey.make_survey_reward_df( - self._scheduler.survey_lists[int(self._tier[-1])][self._survey], - self._conditions, - self._reward_df.loc[[(int(self._tier[-1]), self._survey)], :], - ) - # Create accumulation order column. - survey_reward_df["accum_order"] = range(len(survey_reward_df)) - # Duplicate column and apply - # URL formatting to one of the columns. - survey_reward_df["basis_function_href"] = survey_reward_df.loc[:, "basis_function"] - survey_reward_df["basis_function_href"] = survey_reward_df.apply( - url_formatter, - axis=1, - args=("basis_function_href", "doc_url"), - ) - self._survey_reward_df = survey_reward_df - self._debugging_message = "Finished making reward dataframe." - else: - self._survey_reward_df = None - self._debugging_message = "No reward dataframe made; survey has no rewards." - - except Exception: - tb = traceback.format_exc(limit=-1) - self._debugging_message = f"Cannot make survey reward dataframe: \n{tb}" - pn.state.notifications.error("Cannot make survey reward dataframe!", duration=0) - - def create_reward_widget(self): - """Create Tabulator widget with survey reward dataframe.""" - if self._survey_reward_df is None: - return - - self._debugging_message = "Starting to create reward widget." - tabulator_formatter = { - "basis_function_href": HTMLTemplateFormatter(template="<%= value %>"), - "feasible": BooleanFormatter(), - "basis_area": NumberFormatter(format="0.00"), - "accum_area": NumberFormatter(format="0.00"), - "max_basis_reward": NumberFormatter(format="0.000"), - "max_accum_reward": NumberFormatter(format="0.000"), - "basis_weight": NumberFormatter(format="0.0"), - } - columns = [ - "basis_function", - "basis_function_href", - "feasible", - "max_basis_reward", - "basis_area", - "basis_weight", - "accum_order", - "max_accum_reward", - "accum_area", - "doc_url", - ] - titles = { - "basis_function": "Basis Function", - "basis_function_href": "Docs", - "feasible": "Feasible", - "max_basis_reward": "Max. Reward", - "basis_area": "Area (deg2)", - "basis_weight": "Weight", - "accum_order": "Accum. Order", - "max_accum_reward": "Max. Accum. Reward", - "accum_area": "Accum. Area (deg2)", - } - text_align = { - "basis_function": "left", - "basis_function_href": "center", - "feasible": "center", - "max_basis_reward": "right", - "basis_area": "right", - "basis_weight": "right", - "accum_order": "right", - "max_accum_reward": "right", - "accum_area": "right", - } - sortable = { - "feasible": False, - "basis_function_href": False, - } - widths = { - "basis_function": "14%", - "basis_function_href": "5%", - "feasible": "6%", - "max_basis_reward": "10%", - "basis_area": "12%", - "basis_weight": "8%", - "accum_order": "13%", - "max_accum_reward": "15%", - "accum_area": "15%", - } - - reward_widget = pn.widgets.Tabulator( - self._survey_reward_df[columns], - titles=titles, - text_align=text_align, - sortable=sortable, - show_index=False, - formatters=tabulator_formatter, - disabled=True, - frozen_columns=["basis_function"], - hidden_columns=["doc_url"], - selectable=1, - height=self._reward_widget_height, - widths=widths, - ) - self.reward_widget = reward_widget - self._debugging_message = "Finished making reward widget." - - def update_reward_widget_data(self): - """Update Reward Tabulator widget data.""" - if self._survey_reward_df is None: - return - - self._debugging_message = "Starting to update reward widget data." - self.reward_widget.selection = [] - columns = [ - "basis_function", - "basis_function_href", - "feasible", - "max_basis_reward", - "basis_area", - "basis_weight", - "accum_order", - "max_accum_reward", - "accum_area", - "doc_url", - ] - self.reward_widget._update_data(self._survey_reward_df[columns]) - self._debugging_message = "Finished updating reward widget data." - - @param.depends("_publish_reward_widget") - def publish_reward_widget(self): - """Return the reward Tabulator widget for display. - - Returns - ------- - widget: 'panel.widgets.Tabulator' - Table of reward data for selected survey. - """ - if self._survey_reward_df is None: - return "No rewards available." - else: - self._debugging_message = "Publishing reward widget." - return self.reward_widget - - def create_sky_map_base(self): - """Create a base plot with a dummy map.""" - if self._survey_maps is None: - self._debugging_message = "Cannot create sky map as no survey maps made." - return - - try: - self._debugging_message = "Starting to create sky map base." - # Make a dummy map that is 1.0 for all healpixels - # that might have data. - self._survey_maps["above_horizon"] = np.where(self._conditions.alt > 0, 1.0, np.nan) - self._sky_map_base = schedview.plot.survey.map_survey_healpix( - self._conditions.mjd, - self._survey_maps, - "above_horizon", - np.int64(self.nside), - conditions=self._conditions, - survey=self._scheduler.survey_lists[int(self._tier[-1])][self._survey], - ) - self._sky_map_base.plot.toolbar.tools[-1].tooltips.remove(("above_horizon", "@above_horizon")) - - color_bar = ColorBar( - color_mapper=LinearColorMapper(palette=self.color_palette, low=0, high=1), - label_standoff=10, - location=(0, 0), - ) - self._sky_map_base.plot.add_layout(color_bar, "below") - self._sky_map_base.plot.below[1].visible = False - self._sky_map_base.plot.toolbar.autohide = True # show toolbar only when mouseover plot - self._sky_map_base.plot.title.text = "" # remove 'Horizon' title - self._sky_map_base.plot.legend.propagate_hover = True # hover tool works over in-plot legend - self._sky_map_base.plot.legend.title = "Key" - self._sky_map_base.plot.legend.title_text_font_style = "bold" - self._sky_map_base.plot.legend.border_line_color = "#048b8c" - self._sky_map_base.plot.legend.border_line_width = 3 - self._sky_map_base.plot.legend.border_line_alpha = 1 - self._sky_map_base.plot.legend.label_standoff = 10 # gap between images and text - self._sky_map_base.plot.legend.padding = 15 # space around inside edge - self._sky_map_base.plot.legend.title_standoff = 10 # space between title and items - self._sky_map_base.plot.legend.click_policy = "hide" # hide elements when clicked - self._sky_map_base.plot.add_layout(self._sky_map_base.plot.legend[0], "right") - self._sky_map_base.plot.right[0].location = "center_right" - - self._debugging_message = "Finished creating sky map base." - - except Exception: - self._debugging_message = f"Cannot create sky map base: \n{traceback.format_exc(limit=-1)}" - pn.state.notifications.error("Cannot create sky map base!", duration=0) - - def update_sky_map_with_survey_map(self): - """Update base plot with healpixel data from selected survey map. - - Notes - ----- - There are three possible update cases: - - Case 1: Selection is a reward map. - - Case 2: Selection is a survey map and is all NaNs. - - Case 3: Selection is a survey map and is not all NaNs. - """ - if self._sky_map_base is None: - self._debugging_message = "Cannot update sky map with survey map as no base map loaded." - return - - try: - self._debugging_message = "Starting to update sky map with survey map." - hpix_renderer = self._sky_map_base.plot.select(name="hpix_renderer")[0] - hpix_data_source = self._sky_map_base.plot.select(name="hpix_ds")[0] - - # CASE 1: Selection is a reward map. - if self.survey_map not in ["u_sky", "g_sky", "r_sky", "i_sky", "z_sky", "y_sky", "reward"]: - reward_underscored = self._map_name.replace(" ", "_") - reward_survey_key = list(key for key in self._survey_maps if self._map_name in key)[0] - reward_bokeh_key = list(key for key in hpix_data_source.data if reward_underscored in key)[0] - - min_good_value = np.nanmin(self._survey_maps[reward_survey_key]) - max_good_value = np.nanmax(self._survey_maps[reward_survey_key]) - - if min_good_value == max_good_value: - min_good_value -= 1 - max_good_value += 1 - - hpix_renderer.glyph.fill_color = bokeh.transform.linear_cmap( - field_name=reward_bokeh_key, - palette=self.color_palette, - low=min_good_value, - high=max_good_value, - nan_color="white", - ) - self._sky_map_base.plot.below[1].visible = True - self._sky_map_base.plot.below[1].color_mapper.palette = self.color_palette - self._sky_map_base.plot.below[1].color_mapper.low = min_good_value - self._sky_map_base.plot.below[1].color_mapper.high = max_good_value - - # CASE 2: Selection is a survey map and is all NaNs. - elif np.isnan(self._survey_maps[self.survey_map]).all(): - hpix_renderer.glyph.fill_color = bokeh.transform.linear_cmap( - field_name=self.survey_map, - palette=self.color_palette, - low=-1, - high=1, - nan_color="white", - ) - self._sky_map_base.plot.below[1].visible = False - - # CASE 3: Selection is a survey map and is not all NaNs. - else: - min_good_value = np.nanmin(self._survey_maps[self.survey_map]) - max_good_value = np.nanmax(self._survey_maps[self.survey_map]) - - if min_good_value == max_good_value: - min_good_value -= 1 - max_good_value += 1 - - hpix_renderer.glyph.fill_color = bokeh.transform.linear_cmap( - field_name=self.survey_map, - palette=self.color_palette, - low=min_good_value, - high=max_good_value, - nan_color="white", - ) - self._sky_map_base.plot.below[1].visible = True - self._sky_map_base.plot.below[1].color_mapper.palette = self.color_palette - self._sky_map_base.plot.below[1].color_mapper.low = min_good_value - self._sky_map_base.plot.below[1].color_mapper.high = max_good_value - hpix_renderer.glyph.line_color = hpix_renderer.glyph.fill_color - self._sky_map_base.update() - self._debugging_message = "Finished updating sky map with survey map." - - except Exception: - self._debugging_message = f"Cannot update sky map: \n{traceback.format_exc(limit=-1)}" - pn.state.notifications.error("Cannot update sky map!", duration=0) - - def update_sky_map_with_reward(self): - """Update base plot with healpixel data from selected survey map. - - Notes - ----- - There are three possible update cases: - - Case 1: Reward is not scalar. - - Case 2: Reward is scalar and finite. - - Case 3: Reward is -Inf. - """ - if self._sky_map_base is None: - self._debugging_message = "Cannot update sky map with reward as no base map is loaded." - return - - try: - self._debugging_message = "Starting to update sky map with reward." - hpix_renderer = self._sky_map_base.plot.select(name="hpix_renderer")[0] - hpix_data_source = self._sky_map_base.plot.select(name="hpix_ds")[0] - - reward_underscored = self._reward_name.replace(" ", "_") - max_basis_reward = self._survey_reward_df.loc[self._reward, :]["max_basis_reward"] - - # CASE 1: Reward is not scalar. - if any(self._reward_name in key for key in self._survey_maps): - reward_survey_key = list(key for key in self._survey_maps if self._reward_name in key)[0] - reward_bokeh_key = list(key for key in hpix_data_source.data if reward_underscored in key)[0] - - min_good_value = np.nanmin(self._survey_maps[reward_survey_key]) - max_good_value = np.nanmax(self._survey_maps[reward_survey_key]) - - if min_good_value == max_good_value: - min_good_value -= 1 - max_good_value += 1 - - # Modify existing bokeh object. - hpix_renderer.glyph.fill_color = bokeh.transform.linear_cmap( - field_name=reward_bokeh_key, - palette=self.color_palette, - low=min_good_value, - high=max_good_value, - nan_color="white", - ) - self._sky_map_base.plot.below[1].visible = True - self._sky_map_base.plot.below[1].color_mapper.palette = self.color_palette - self._sky_map_base.plot.below[1].color_mapper.low = min_good_value - self._sky_map_base.plot.below[1].color_mapper.high = max_good_value - - # CASE 2: Reward is scalar and finite. - elif max_basis_reward != -np.inf: - # Create array populated with scalar values - # where sky brightness map is not NaN. - scalar_array = hpix_data_source.data["u_sky"].copy() - scalar_array[~np.isnan(hpix_data_source.data["u_sky"])] = max_basis_reward - hpix_data_source.data[reward_underscored] = scalar_array - - hpix_renderer.glyph.fill_color = bokeh.transform.linear_cmap( - field_name=reward_underscored, - palette=self.color_palette, - low=max_basis_reward - 1, - high=max_basis_reward + 1, - nan_color="white", - ) - self._sky_map_base.plot.below[1].visible = True - self._sky_map_base.plot.below[1].color_mapper.palette = self.color_palette - self._sky_map_base.plot.below[1].color_mapper.low = max_basis_reward - 1 - self._sky_map_base.plot.below[1].color_mapper.high = max_basis_reward + 1 - - # CASE 3: Reward is -Inf. - else: - hpix_renderer.glyph.fill_color = bokeh.transform.linear_cmap( - field_name=self._reward_name, - palette="Greys256", - low=-1, - high=1, - nan_color="white", - ) - self._sky_map_base.plot.below[1].visible = False - hpix_renderer.glyph.line_color = hpix_renderer.glyph.fill_color - self._sky_map_base.update() - self._debugging_message = "Finished updating sky map with reward." - - except Exception: - self._debugging_message = f"Cannot update sky map: \n{traceback.format_exc(limit=-1)}" - pn.state.notifications.error("Cannot update sky map!", duration=0) - - @param.depends("_publish_map") - def publish_sky_map(self): - """Return the Bokeh plot for display. - - Returns - ------- - sky_map : 'bokeh.models.layouts.Column' - Map of survey map or reward map as a Bokeh plot. - """ - if self._conditions is None: - return "No scheduler loaded." - - elif self._survey_maps is None: - return "No surveys are loaded." - - elif self._sky_map_base is None: - return "No map loaded." - - else: - self._debugging_message = "Publishing sky map." - return self._sky_map_base.figure - - @param.depends("_debugging_message") - def _debugging_messages(self): - """Construct a debugging pane to display error messages. - - Returns - ------- - debugging_messages : 'panel.pane.Str' - A list of debugging messages ordered by newest message first. - """ - if self._debugging_message is None: - return None - - timestamp = datetime.now(timezone("America/Santiago")).strftime("%Y-%m-%d %H:%M:%S") - self._debug_string = f"\n {timestamp} - {self._debugging_message}" + self._debug_string - - # Send messages to stderr. - self.logger.debug(self._debugging_message) - - if self.debug_pane is None: - self.debug_pane = pn.pane.Str( - self._debug_string, - height=200, - styles={ - "font-size": "9pt", - "color": "black", - "overflow": "scroll", - "background": "#EDEDED", - }, - ) - else: - self.debug_pane.object = self._debug_string - return self.debug_pane - - # ------------------------------------------------------ Dashboard titles - - def generate_dashboard_subtitle(self): - """Select the dashboard subtitle string based on whether whether a - survey map or reward map is being displayed. - - Returns - ------- - subtitle : 'str' - Lists the tier and survey, and either the survey or reward map. - """ - if not self._display_dashboard_data: - return "" - maps = ["u_sky", "g_sky", "r_sky", "i_sky", "z_sky", "y_sky", "reward"] - if not self._display_reward and self.survey_map in maps: - return f"\nTier {self._tier[-1]} - Survey {self._survey} - Map {self._map_name}" - elif not self._display_reward and self.survey_map not in maps: - return f"\nTier {self._tier[-1]} - Survey {self._survey} - Reward {self._map_name}" - else: - return f"\nTier {self._tier[-1]} - Survey {self._survey} - Reward {self._reward_name}" - - def generate_summary_table_heading(self): - """Select the summary table heading based on whether data is being - displayed or not. - - Returns - ------- - heading : 'str' - Lists the tier if data is displayed; else just a general title. - """ - if not self._display_dashboard_data: - return "Scheduler summary" - else: - return f"Scheduler summary for tier {self._tier[-1]}" - - def generate_reward_table_heading(self): - """Select the reward table heading based on whether data is - being displayed or not. - - Returns - ------- - heading : 'str' - Lists the survey name if data is displayed; else a general title. - """ - if not self._display_dashboard_data: - return "Basis functions & rewards" - else: - return f"Basis functions & rewards for survey {self._survey_name}" - - def generate_map_heading(self): - """Select the map heading based on whether a survey or reward map - is being displayed. - - Returns - ------- - heading : 'str' - Lists the survey name and either the survey map name or reward - name if data is being displayed; else a general title. - """ - if not self._display_dashboard_data: - return "Map" - maps = ["u_sky", "g_sky", "r_sky", "i_sky", "z_sky", "y_sky", "reward"] - if not self._display_reward and self.survey_map in maps: - return f"Survey {self._survey_name}\nMap: {self._map_name}" - elif not self._display_reward and self.survey_map not in maps: - return f"Survey {self._survey_name}\nReward: {self._map_name}" - else: - return f"Survey {self._survey_name}\nReward: {self._reward_name}" - - @param.depends("_update_headings") - def dashboard_subtitle(self): - """Load subtitle data and create/update - a String pane to display subtitle. - - Returns - ------- - title : 'panel.pane.Str' - A panel String pane to display as the dashboard's subtitle. - """ - title_string = self.generate_dashboard_subtitle() - if self.dashboard_subtitle_pane is None: - self.dashboard_subtitle_pane = pn.pane.Str( - title_string, - height=20, - stylesheets=[h2_stylesheet], - ) - else: - self.dashboard_subtitle_pane.object = title_string - return self.dashboard_subtitle_pane - - @param.depends("_update_headings") - def summary_table_heading(self): - """Load heading data and create/update - a String pane to display heading. - - Returns - ------- - title : 'panel.pane.Str' - A panel String pane to display as the survey table's heading. - """ - title_string = self.generate_summary_table_heading() - if self.summary_table_heading_pane is None: - self.summary_table_heading_pane = pn.pane.Str( - title_string, - stylesheets=[h3_stylesheet], - ) - else: - self.summary_table_heading_pane.object = title_string - return self.summary_table_heading_pane - - @param.depends("_update_headings") - def reward_table_heading(self): - """Load title data and create/update a String pane to display heading. - - Returns - ------- - title : 'panel.pane.Str' - A panel String pane to display as the reward table heading. - """ - title_string = self.generate_reward_table_heading() - if self.reward_table_heading_pane is None: - self.reward_table_heading_pane = pn.pane.Str( - title_string, - stylesheets=[h3_stylesheet], - ) - else: - self.reward_table_heading_pane.object = title_string - return self.reward_table_heading_pane - - @param.depends("_update_headings") - def map_title(self): - """Load title data and create/update a String pane to display heading. - - Returns - ------- - title : 'panel.pane.Str' - A panel String pane to display as the map heading. - """ - title_string = self.generate_map_heading() - if self.map_title_pane is None: - self.map_title_pane = pn.pane.Str( - title_string, - stylesheets=[h3_stylesheet], - ) - else: - self.map_title_pane.object = title_string - return self.map_title_pane - - -class RestrictedSchedulerSnapshotDashboard(SchedulerSnapshotDashboard): - """A Parametrized container for parameters, data, and panel objects for the - scheduler dashboard. - """ - - # Param parameters that are modifiable by user actions. - 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_scheduler.scheduler.schedulers.CoreScheduler, or a tuple of the form - (scheduler, conditions), where scheduler is an instance of a subclass of - rubin_scheduler.scheduler.schedulers.CoreScheduler, and conditions is an - instance of rubin_scheduler.scheduler.conditions.Conditions. - """ - scheduler_fname = schedview.param.FileSelectorWithEmptyOption( - path=f"{PACKAGE_DATA_DIR}/*scheduler*.p*", - doc=scheduler_fname_doc, - default=None, - allow_None=True, - ) - - def __init__(self, data_dir=None): - super().__init__() - - if data_dir is not None: - self.param["scheduler_fname"].update(path=f"{data_dir}/*scheduler*.p*") - - -class LFASchedulerSnapshotDashboard(SchedulerSnapshotDashboard): - """A Parametrized container for parameters, data, and panel objects for the - scheduler dashboard. - """ - - scheduler_fname_doc = """Recent pickles from LFA - """ - - scheduler_fname = param.Selector( - default="", - objects=[], - doc=scheduler_fname_doc, - precedence=3, - ) - - pickles_date = param.Date( - default=datetime.now(), - label="Snapshot selection cutoff date and time", - doc="Show snapshots that are recent as of this date and time in the scheduler snapshot file dropdown", - precedence=1, - ) - - telescope = param.Selector( - default=None, objects={"All": None, "Simonyi": 1, "Auxtel": 2}, doc="Source Telescope", precedence=2 - ) - - _summary_widget_height = 310 - _reward_widget_height = 350 - - def __init__(self): - super().__init__() - - async def query_schedulers(self, selected_time, selected_tel): - """Query snapshots that have a timestamp between the start of the - night and selected datetime and generated by selected telescope - """ - selected_time = Time( - Timestamp( - selected_time, - tzinfo=ZoneInfo(DEFAULT_TIMEZONE), - ) - ) - self.show_loading_indicator = True - self._debugging_message = "Starting retrieving snapshots" - self.logger.debug("Starting retrieving snapshots") - scheduler_urls = await query_night_schedulers(selected_time, selected_tel) - self.logger.debug("Finished retrieving snapshots") - self._debugging_message = "Finished retrieving snapshots" - self.show_loading_indicator = False - return scheduler_urls - # ------------------------------------------------------------ Create dashboard - - def scheduler_app(date_time=None, scheduler_pickle=None, **kwargs): """Create a dashboard with grids of Param parameters, Tabulator widgets, and Bokeh plots. @@ -1541,6 +89,7 @@ def scheduler_app(date_time=None, scheduler_pickle=None, **kwargs): max_height=1000, ).servable() + # Bools for different dashboard modes from_urls = False data_dir = None from_lfa = False @@ -1558,6 +107,7 @@ def scheduler_app(date_time=None, scheduler_pickle=None, **kwargs): del kwargs["lfa"] scheduler = None + # Suggestion: move these dynamic widgets to class definition? data_loading_widgets = {} # data loading parameters in both restricted and URL modes data_loading_parameters = ["scheduler_fname", "widget_datetime", "widget_tier"] diff --git a/schedview/app/scheduler_dashboard/scheduler_snapshot_dashboard.py b/schedview/app/scheduler_dashboard/scheduler_snapshot_dashboard.py new file mode 100644 index 00000000..e5a54407 --- /dev/null +++ b/schedview/app/scheduler_dashboard/scheduler_snapshot_dashboard.py @@ -0,0 +1,1352 @@ +import logging +import os +import traceback + +# Filter the astropy warnings swamping the terminal +from datetime import datetime +from zoneinfo import ZoneInfo + +import bokeh +import numpy as np +import panel as pn +import param +import rubin_scheduler.site_models +from astropy.time import Time +from bokeh.models import ColorBar, LinearColorMapper +from bokeh.models.widgets.tables import BooleanFormatter, HTMLTemplateFormatter, NumberFormatter +from lsst.resources import ResourcePath +from pandas import Timestamp +from pytz import timezone + +# For the conditions.mjd bugfix +from rubin_scheduler.scheduler.model_observatory import ModelObservatory + +import schedview +import schedview.collect.scheduler_pickle +import schedview.compute.scheduler +import schedview.compute.survey +import schedview.param +import schedview.plot.survey +from schedview.app.scheduler_dashboard.constants import ( + COLOR_PALETTES, + DEFAULT_COLOR_PALETTE, + DEFAULT_NSIDE, + DEFAULT_TIMEZONE, + h2_stylesheet, + h3_stylesheet, +) +from schedview.app.scheduler_dashboard.utils import get_sky_brightness_date_bounds, url_formatter + + +class SchedulerSnapshotDashboard(param.Parameterized): + """A Parametrized container for parameters, data, and panel objects for the + scheduler dashboard working in flexible (insecure mode) where data files + are loaded from any path or URL. + """ + + # Param parameters that are modifiable by user actions. + 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_scheduler.scheduler.schedulers.CoreScheduler, or a tuple of the form + (scheduler, conditions), where scheduler is an instance of a subclass of + rubin_scheduler.scheduler.schedulers.CoreScheduler, and conditions is an + instance of rubin_scheduler.scheduler.conditions.Conditions. + """ + + # Get available skybrightness_pre date range + (mjd_min, mjd_max) = get_sky_brightness_date_bounds() + # convert astropy times to iso format to be + # used as bounds for datetime parameter + date_bounds = (mjd_min.to_datetime(), mjd_max.to_datetime()) + + scheduler_fname = param.String( + default="", + label="Scheduler snapshot file", + doc=scheduler_fname_doc, + precedence=3, + ) + widget_datetime = param.Date( + default=date_bounds[0], + label="Date and time (UTC)", + doc=f"Select dates between {date_bounds[0]} and {date_bounds[1]}", + bounds=date_bounds, + precedence=4, + ) + # mjd date passed as a url path parameter when + # dashboard operates in --data_from_urls mode + url_mjd = param.Number(default=None) + widget_tier = param.Selector( + default="", + objects=[""], + label="Tier", + doc="The label for the first index into the CoreScheduler.survey_lists.", + precedence=5, + ) + survey_map = param.Selector( + default="reward", + objects=["reward"], + doc="Sky brightness maps, non-scalar rewards and survey reward map.", + ) + nside = param.ObjectSelector( + default=DEFAULT_NSIDE, + objects=[2, 4, 8, 16, 32, 64], + label="Map resolution (nside)", + doc="", + ) + + color_palette = param.Selector(default=DEFAULT_COLOR_PALETTE, objects=COLOR_PALETTES, doc="") + summary_widget = param.Parameter(default=None, doc="") + reward_widget = param.Parameter(default=None, doc="") + show_loading_indicator = param.Boolean(default=False) + + # Param parameters (used in depends decoraters and trigger calls). + _publish_summary_widget = param.Parameter(None) + _publish_reward_widget = param.Parameter(None) + _publish_map = param.Parameter(None) + _update_headings = param.Parameter(None) + _debugging_message = param.Parameter(None) + + # Non-Param parameters storing Panel pane objects. + debug_pane = None + dashboard_subtitle_pane = None + summary_table_heading_pane = None + reward_table_heading_pane = None + map_title_pane = None + + # Non-Param internal parameters. + _mjd = None + _tier = None + _survey = 0 + _reward = -1 + _survey_name = "" + _reward_name = "" + _map_name = "" + _scheduler = None + _conditions = None + _reward_df = None + _scheduler_summary_df = None + _survey_maps = None + _survey_reward_df = None + _sky_map_base = None + _debug_string = "" + _display_reward = False + _display_dashboard_data = False + # Suggestion: describe how updating parameters works + _do_not_trigger_update = True + _summary_widget_height = 220 + _reward_widget_height = 400 + + def __init__(self, **params): + super().__init__(**params) + self.config_logger() + + def config_logger(self, logger_name="schedule-snapshot"): + """Configure the logger. + + Parameters + ---------- + logger_name : `str` + The name of the logger. + """ + + self.logger = logging.getLogger(logger_name) + self.logger.setLevel(logging.DEBUG) + + log_stream_handler = None + if self.logger.hasHandlers(): + for handler in self.logger.handlers: + if isinstance(handler, logging.StreamHandler): + log_stream_handler = handler + + if log_stream_handler is None: + log_stream_handler = logging.StreamHandler() + + log_stream_formatter = logging.Formatter("%(asctime)s: %(message)s") + log_stream_handler.setFormatter(log_stream_formatter) + self.logger.addHandler(log_stream_handler) + + # ------------------------------------------------------------ User actions + + @param.depends("scheduler_fname", watch=True) + def _update_scheduler_fname(self): + """Update the dashboard when a user enters a new filepath/URL.""" + self.logger.debug("UPDATE: scheduler file") + self.show_loading_indicator = True + self.clear_dashboard() + + if not self.read_scheduler(): + self.clear_dashboard() + self.show_loading_indicator = False + return + + # Current fix for _conditions.mjd having different datatypes. + if type(self._conditions._mjd) == np.ndarray: + self._conditions._mjd = self._conditions._mjd[0] + + # Get mjd from pickle and set widget and URL to match. + self._do_not_trigger_update = True + self.url_mjd = self._conditions._mjd + self.widget_datetime = Time(self._conditions._mjd, format="mjd").to_datetime() + self._do_not_trigger_update = False + + if not self.make_scheduler_summary_df(): + self.clear_dashboard() + self.show_loading_indicator = False + return + + self.create_summary_widget() + self.param.trigger("_publish_summary_widget") + + self._do_not_trigger_update = True + self.summary_widget.selection = [0] + self._do_not_trigger_update = False + + self.compute_survey_maps() + self.survey_map = self.param["survey_map"].objects[-1] + self._map_name = self.survey_map.split("@")[0].strip() + + self.create_sky_map_base() + self.update_sky_map_with_survey_map() + self.param.trigger("_publish_map") + + self.make_reward_df() + self.create_reward_widget() + self.param.trigger("_publish_reward_widget") + + self._display_dashboard_data = True + self._display_reward = False + self.param.trigger("_update_headings") + + self.show_loading_indicator = False + + @param.depends("widget_datetime", watch=True) + def _update_mjd_from_picker(self): + """Update the dashboard when a datetime is input in widget.""" + if self._do_not_trigger_update: + return + + self.logger.debug("UPDATE: mjd from date-picker") + self.show_loading_indicator = True + self.clear_dashboard() + + self._do_not_trigger_update = True + self.url_mjd = Time( + Timestamp( + self.widget_datetime, + tzinfo=ZoneInfo(DEFAULT_TIMEZONE), + ) + ).mjd + self._do_not_trigger_update = False + self._mjd = self.url_mjd + + if not self.update_conditions(): + self.clear_dashboard() + self.show_loading_indicator = False + return + + if not self.make_scheduler_summary_df(): + self.clear_dashboard() + self.show_loading_indicator = False + return + + if self.summary_widget is None: + self.create_summary_widget() + else: + self.update_summary_widget_data() + self.param.trigger("_publish_summary_widget") + + self._do_not_trigger_update = True + self.summary_widget.selection = [0] + self._do_not_trigger_update = False + + self.compute_survey_maps() + self.survey_map = self.param["survey_map"].objects[-1] + self._map_name = self.survey_map.split("@")[0].strip() + + self.create_sky_map_base() + self.update_sky_map_with_survey_map() + self.param.trigger("_publish_map") + + self.make_reward_df() + self.create_reward_widget() + self.param.trigger("_publish_reward_widget") + + self._display_dashboard_data = True + self._display_reward = False + self.param.trigger("_update_headings") + + self.show_loading_indicator = False + + @param.depends("url_mjd", watch=True) + def _update_mjd_from_url(self): + """Update the dashboard when an mjd is input in URL.""" + if self._do_not_trigger_update: + return + + self.logger.debug("UPDATE: mjd from url") + self.show_loading_indicator = True + self.clear_dashboard() + + self._do_not_trigger_update = True + # set Date Time widget to the mjd value set in URL + self.widget_datetime = Time(self.url_mjd, format="mjd").to_datetime() + self._do_not_trigger_update = False + # set the mjd value that's used across the dashboard + self._mjd = self.url_mjd + + if not self.update_conditions(): + self.clear_dashboard() + self.show_loading_indicator = False + return + + if not self.make_scheduler_summary_df(): + self.clear_dashboard() + self.show_loading_indicator = False + return + + if self.summary_widget is None: + self.create_summary_widget() + else: + self.update_summary_widget_data() + self.param.trigger("_publish_summary_widget") + + self._do_not_trigger_update = True + self.summary_widget.selection = [0] + self._do_not_trigger_update = False + + # Suggestion: Should this fragment be separated into a function? + # Looks like it's repeated in other parts of the dashboard + self.compute_survey_maps() + self.survey_map = self.param["survey_map"].objects[-1] + self._map_name = self.survey_map.split("@")[0].strip() + + self.create_sky_map_base() + self.update_sky_map_with_survey_map() + self.param.trigger("_publish_map") + + self.make_reward_df() + self.create_reward_widget() + self.param.trigger("_publish_reward_widget") + + self._display_dashboard_data = True + self._display_reward = False + self.param.trigger("_update_headings") + + self.show_loading_indicator = False + # --------------------------------End of fragment + + @param.depends("widget_tier", watch=True) + def _update_tier(self): + """Update the dashboard when a user chooses a new tier.""" + if not self._display_dashboard_data: + return + self.logger.debug("UPDATE: tier") + self._tier = self.widget_tier + self._survey = 0 + self._survey_name = self._scheduler_summary_df[ + self._scheduler_summary_df["tier"] == self._tier + ].reset_index()["survey"][self._survey] + + if self.summary_widget is None: + self.create_summary_widget() + else: + self.update_summary_widget_data() + self.param.trigger("_publish_summary_widget") + + self.compute_survey_maps() + self._do_not_trigger_update = True + self.survey_map = self.param["survey_map"].objects[-1] + self._map_name = self.survey_map.split("@")[0].strip() + self.summary_widget.selection = [0] + self._do_not_trigger_update = False + + self.make_reward_df() + if self.reward_widget is None: + self.create_reward_widget() + else: + self.update_reward_widget_data() + self.param.trigger("_publish_reward_widget") + + self.create_sky_map_base() + self.update_sky_map_with_survey_map() + self.param.trigger("_publish_map") + + self._display_reward = False + self.param.trigger("_update_headings") + + @param.depends("summary_widget.selection", watch=True) + def _update_survey(self): + """Update the dashboard when a user selects a survey.""" + if self.summary_widget.selection == [] or self._do_not_trigger_update: + return + + self.logger.debug("UPDATE: survey") + self._survey = self.summary_widget.selection[0] + self._survey_name = self._scheduler_summary_df[ + self._scheduler_summary_df["tier"] == self._tier + ].reset_index()["survey"][self._survey] + + self.compute_survey_maps() + self._do_not_trigger_update = True + self.survey_map = self.param["survey_map"].objects[-1] + self._map_name = self.survey_map.split("@")[0].strip() + self._do_not_trigger_update = False + + self.make_reward_df() + if self.reward_widget is None: + self.create_reward_widget() + else: + self.update_reward_widget_data() + self.param.trigger("_publish_reward_widget") + + self.create_sky_map_base() + self.update_sky_map_with_survey_map() + self.param.trigger("_publish_map") + + self._display_reward = False + self.param.trigger("_update_headings") + + @param.depends("reward_widget.selection", watch=True) + def _update_reward(self): + """Update the dashboard when a user selects a reward.""" + # Do not run code if no selection or when update flag is True. + if self.reward_widget.selection == [] or self._do_not_trigger_update: + return + + self.logger.debug("UPDATE: reward") + self._reward = self.reward_widget.selection[0] + self._reward_name = self._survey_reward_df["basis_function"][self._reward] + + # If reward is in survey maps, update survey maps drop-down. + if any(self._reward_name in key for key in self._survey_maps): + self._do_not_trigger_update = True + self.survey_map = list(key for key in self._survey_maps if self._reward_name in key)[0] + self._map_name = self.survey_map.split("@")[0].strip() + self._do_not_trigger_update = False + else: + self.survey_map = "" + + self.update_sky_map_with_reward() + self.param.trigger("_publish_map") + + self._display_reward = True + self.param.trigger("_update_headings") + + @param.depends("survey_map", watch=True) + def _update_survey_map(self): + """Update the dashboard when a user chooses a new survey map.""" + # Don't run code during initial load or when updating tier or survey. + if not self._display_dashboard_data or self._do_not_trigger_update: + return + + # If user selects null map, do nothing. + if self.survey_map == "": + return + + self.logger.debug("UPDATE: survey map") + # If selection is a reward map, reflect in reward table. + self._do_not_trigger_update = True + self._map_name = self.survey_map.split("@")[0].strip() + if any(self._survey_reward_df["basis_function"].isin([self._map_name])): + index = self._survey_reward_df["basis_function"].index[ + self._survey_reward_df["basis_function"].tolist().index(self._map_name) + ] + self.reward_widget.selection = [index] + elif self.reward_widget is not None: + self.reward_widget.selection = [] + self._do_not_trigger_update = False + + self.update_sky_map_with_survey_map() + self.param.trigger("_publish_map") + + self._display_reward = False + self.param.trigger("_update_headings") + + @param.depends("nside", watch=True) + def _update_nside(self): + """Update the dashboard when a user chooses a new nside.""" + # Don't run code during initial load. + if not self._display_dashboard_data: + return + + self.logger.debug("UPDATE: nside") + self.compute_survey_maps() + + self.create_sky_map_base() + self.update_sky_map_with_survey_map() + self.param.trigger("_publish_map") + + @param.depends("color_palette", watch=True) + def _update_color_palette(self): + """Update the dashboard when a user chooses a new color palette.""" + self.logger.debug("UPDATE: color palette") + if self._display_reward: + self.update_sky_map_with_reward() + else: + self.update_sky_map_with_survey_map() + self.param.trigger("_publish_map") + + # ------------------------------------------------------- Internal workings + + def clear_dashboard(self): + """Clear the dashboard for a new pickle or a new date.""" + self._debugging_message = "Starting to clear dashboard." + + self.summary_widget = None + self._survey_reward_df = None + self._sky_map_base = None + self._display_dashboard_data = False + self.reward_widget = None + + self.param.trigger("_publish_summary_widget") + self.param.trigger("_publish_reward_widget") + self.param.trigger("_publish_map") + self.param.trigger("_update_headings") + + self.param["widget_tier"].objects = [""] + self.param["survey_map"].objects = [""] + + self.widget_tier = "" + self.survey_map = "" + + self._tier = "" + self._survey = 0 + self._reward = -1 + + self._debugging_message = "Finished clearing dashboard." + + def read_scheduler(self): + """Load the scheduler and conditions objects from pickle file. + + Returns + ------- + success : 'bool' + Record of success or failure of reading scheduler from file/URL. + """ + try: + self._debugging_message = "Starting to load scheduler." + pn.state.notifications.info("Scheduler loading...", duration=0) + + os.environ["LSST_DISABLE_BUCKET_VALIDATION"] = "1" + scheduler_resource_path = ResourcePath(self.scheduler_fname) + scheduler_resource_path.use_threads = False + with scheduler_resource_path.as_local() as local_scheduler_resource: + (scheduler, conditions) = schedview.collect.scheduler_pickle.read_scheduler( + local_scheduler_resource.ospath + ) + + self._scheduler = scheduler + self._conditions = conditions + + self._debugging_message = "Finished loading scheduler." + pn.state.notifications.clear() + pn.state.notifications.success("Scheduler pickle loaded successfully!") + + return True + + except Exception: + tb = traceback.format_exc(limit=-1) + self._debugging_message = f"Cannot load scheduler from {self.scheduler_fname}: \n{tb}" + pn.state.notifications.clear() + pn.state.notifications.error(f"Cannot load scheduler from {self.scheduler_fname}", duration=0) + + self._scheduler = None + self._conditions = None + + return False + + def update_conditions(self): + """Update Conditions object. + + Returns + ------- + success : 'bool' + Record of success of Conditions update. + """ + if self._conditions is None: + self._debugging_message = "Cannot update Conditions object as no Conditions object is loaded." + + return False + + try: + self._debugging_message = "Starting to update Conditions object." + pn.state.notifications.info("Updating Conditions object...", duration=0) + + # self._conditions.mjd = self._mjd + + # Use instance of ModelObservatory until Conditions + # setting bug is fixed. + if ( + not hasattr(self, "_model_observatory") + or self._model_observatory.nside != self._scheduler.nside + ): + # Get weather conditions from pickle. + wind_data = rubin_scheduler.site_models.ConstantWindData( + wind_speed=self._conditions.wind_speed, + wind_direction=self._conditions.wind_direction, + ) + # Set seeing to fiducial site seeing. + seeing_data = rubin_scheduler.site_models.ConstantSeeingData(0.69) + # Create new MO instance. + self._model_observatory = ModelObservatory( + nside=self._scheduler.nside, + init_load_length=1, + wind_data=wind_data, + seeing_data=seeing_data, + ) + + self._model_observatory.mjd = self._mjd + self._conditions = self._model_observatory.return_conditions() + self._scheduler.update_conditions(self._conditions) + + self._debugging_message = "Finished updating Conditions object." + pn.state.notifications.clear() + pn.state.notifications.success("Conditions object updated successfully") + + return True + + except Exception: + tb = traceback.format_exc(limit=-1) + self._debugging_message = f"Conditions object unable to be updated: \n{tb}" + pn.state.notifications.clear() + pn.state.notifications.error("Conditions object unable to be updated!", duration=0) + + return False + + def make_scheduler_summary_df(self): + """Make the reward and scheduler summary dataframes. + + Returns + ------- + success : 'bool' + Record of success of dataframe construction. + """ + if self._scheduler is None: + self._debugging_message = "Cannot update survey reward table as no pickle is loaded." + + return False + + try: + self._debugging_message = "Starting to make scheduler summary dataframe." + pn.state.notifications.info("Making scheduler summary dataframe...", duration=0) + + self._reward_df = self._scheduler.make_reward_df(self._conditions) + scheduler_summary_df = schedview.compute.scheduler.make_scheduler_summary_df( + self._scheduler, + self._conditions, + self._reward_df, + ) + + # Duplicate column and apply URL formatting to one of the columns. + scheduler_summary_df["survey"] = scheduler_summary_df.loc[:, "survey_name_with_id"] + scheduler_summary_df["survey_name_with_id"] = scheduler_summary_df.apply( + url_formatter, + axis=1, + args=("survey_name_with_id", "survey_url"), + ) + self._scheduler_summary_df = scheduler_summary_df + + tiers = self._scheduler_summary_df.tier.unique().tolist() + self.param["widget_tier"].objects = tiers + self.widget_tier = tiers[0] + self._tier = tiers[0] + self._survey = 0 + self._survey_name = self._scheduler_summary_df[ + self._scheduler_summary_df["tier"] == self._tier + ].reset_index()["survey"][self._survey] + + self._debugging_message = "Finished making scheduler summary dataframe." + pn.state.notifications.clear() + pn.state.notifications.success("Scheduler summary dataframe updated successfully") + + return True + + except Exception: + tb = traceback.format_exc(limit=-1) + self._debugging_message = f"Scheduler summary dataframe unable to be updated: \n{tb}" + pn.state.notifications.clear() + pn.state.notifications.error("Scheduler summary dataframe unable to be updated!", duration=0) + self._scheduler_summary_df = None + + return False + + def create_summary_widget(self): + """Create Tabulator widget with scheduler summary dataframe.""" + if self._scheduler_summary_df is None: + return + + self._debugging_message = "Starting to create summary widget." + tabulator_formatter = {"survey_name_with_id": HTMLTemplateFormatter(template="<%= value %>")} + columns = [ + "survey_index", + "survey", + "reward", + "survey_name_with_id", + "tier", + "survey_url", + ] + titles = { + "survey_index": "Index", + "survey": "Survey", + "reward": "Reward", + "survey_name_with_id": "Docs", + } + widths = { + "survey_index": "10%", + "survey": "48%", + "reward": "30%", + "survey_name_with_id": "10%", + } + text_align = { + "survey_index": "left", + "survey": "left", + "reward": "right", + "survey_name_with_id": "center", + } + summary_widget = pn.widgets.Tabulator( + self._scheduler_summary_df[self._scheduler_summary_df["tier"] == self._tier][columns], + titles=titles, + widths=widths, + text_align=text_align, + sortable={"survey_name_with_id": False}, + show_index=False, + formatters=tabulator_formatter, + disabled=True, + selectable=1, + hidden_columns=["tier", "survey_url"], + sizing_mode="stretch_width", + height=self._summary_widget_height, + ) + self.summary_widget = summary_widget + self._debugging_message = "Finished making summary widget." + + def update_summary_widget_data(self): + """Update data for survey Tabulator widget.""" + self._debugging_message = "Starting to update summary widget." + columns = [ + "survey_index", + "survey", + "reward", + "survey_name_with_id", + "tier", + "survey_url", + ] + self.summary_widget._update_data( + self._scheduler_summary_df[self._scheduler_summary_df["tier"] == self._tier][columns] + ) + self._debugging_message = "Finished updating summary widget." + + @param.depends("_publish_summary_widget") + def publish_summary_widget(self): + """Publish the summary Tabulator widget + to be displayed on the dashboard. + + Returns + ------- + widget: 'panel.widgets.Tabulator' + Table of scheduler summary data. + """ + if self.summary_widget is None: + return "No summary available." + else: + self._debugging_message = "Publishing summary widget." + return self.summary_widget + + def compute_survey_maps(self): + """Compute survey maps and update drop-down selection.""" + if self._scheduler is None: + self._debugging_message = "Cannot compute survey maps as no scheduler loaded." + return + if self._scheduler_summary_df is None: + self._debugging_message = "Cannot compute survey maps as no scheduler summary made." + return + try: + self._debugging_message = "Starting to compute survey maps." + self._survey_maps = schedview.compute.survey.compute_maps( + self._scheduler.survey_lists[int(self._tier[-1])][self._survey], + self._conditions, + np.int64(self.nside), + ) + self.param["survey_map"].objects = [""] + list(self._survey_maps.keys()) + self._debugging_message = "Finished computing survey maps." + + except Exception: + self._debugging_message = f"Cannot compute survey maps: \n{traceback.format_exc(limit=-1)}" + pn.state.notifications.error("Cannot compute survey maps!", duration=0) + + def make_reward_df(self): + """Make the summary dataframe.""" + if self._scheduler is None: + self._debugging_message = "Cannot make summary dataframe as no scheduler loaded." + return + if self._scheduler_summary_df is None: + self._debugging_message = "Cannot make summary dataframe as no scheduler summary made." + return + try: + self._debugging_message = "Starting to make reward dataframe." + # Survey has rewards. + if self._reward_df.index.isin([(int(self._tier[-1]), self._survey)]).any(): + survey_reward_df = schedview.compute.survey.make_survey_reward_df( + self._scheduler.survey_lists[int(self._tier[-1])][self._survey], + self._conditions, + self._reward_df.loc[[(int(self._tier[-1]), self._survey)], :], + ) + # Create accumulation order column. + survey_reward_df["accum_order"] = range(len(survey_reward_df)) + # Duplicate column and apply + # URL formatting to one of the columns. + survey_reward_df["basis_function_href"] = survey_reward_df.loc[:, "basis_function"] + survey_reward_df["basis_function_href"] = survey_reward_df.apply( + url_formatter, + axis=1, + args=("basis_function_href", "doc_url"), + ) + self._survey_reward_df = survey_reward_df + self._debugging_message = "Finished making reward dataframe." + else: + self._survey_reward_df = None + self._debugging_message = "No reward dataframe made; survey has no rewards." + + except Exception: + tb = traceback.format_exc(limit=-1) + self._debugging_message = f"Cannot make survey reward dataframe: \n{tb}" + pn.state.notifications.error("Cannot make survey reward dataframe!", duration=0) + + def create_reward_widget(self): + """Create Tabulator widget with survey reward dataframe.""" + if self._survey_reward_df is None: + return + + self._debugging_message = "Starting to create reward widget." + tabulator_formatter = { + "basis_function_href": HTMLTemplateFormatter(template="<%= value %>"), + "feasible": BooleanFormatter(), + "basis_area": NumberFormatter(format="0.00"), + "accum_area": NumberFormatter(format="0.00"), + "max_basis_reward": NumberFormatter(format="0.000"), + "max_accum_reward": NumberFormatter(format="0.000"), + "basis_weight": NumberFormatter(format="0.0"), + } + columns = [ + "basis_function", + "basis_function_href", + "feasible", + "max_basis_reward", + "basis_area", + "basis_weight", + "accum_order", + "max_accum_reward", + "accum_area", + "doc_url", + ] + titles = { + "basis_function": "Basis Function", + "basis_function_href": "Docs", + "feasible": "Feasible", + "max_basis_reward": "Max. Reward", + "basis_area": "Area (deg2)", + "basis_weight": "Weight", + "accum_order": "Accum. Order", + "max_accum_reward": "Max. Accum. Reward", + "accum_area": "Accum. Area (deg2)", + } + text_align = { + "basis_function": "left", + "basis_function_href": "center", + "feasible": "center", + "max_basis_reward": "right", + "basis_area": "right", + "basis_weight": "right", + "accum_order": "right", + "max_accum_reward": "right", + "accum_area": "right", + } + sortable = { + "feasible": False, + "basis_function_href": False, + } + widths = { + "basis_function": "14%", + "basis_function_href": "5%", + "feasible": "6%", + "max_basis_reward": "10%", + "basis_area": "12%", + "basis_weight": "8%", + "accum_order": "13%", + "max_accum_reward": "15%", + "accum_area": "15%", + } + + reward_widget = pn.widgets.Tabulator( + self._survey_reward_df[columns], + titles=titles, + text_align=text_align, + sortable=sortable, + show_index=False, + formatters=tabulator_formatter, + disabled=True, + frozen_columns=["basis_function"], + hidden_columns=["doc_url"], + selectable=1, + height=self._reward_widget_height, + widths=widths, + ) + self.reward_widget = reward_widget + self._debugging_message = "Finished making reward widget." + + def update_reward_widget_data(self): + """Update Reward Tabulator widget data.""" + if self._survey_reward_df is None: + return + + self._debugging_message = "Starting to update reward widget data." + self.reward_widget.selection = [] + columns = [ + "basis_function", + "basis_function_href", + "feasible", + "max_basis_reward", + "basis_area", + "basis_weight", + "accum_order", + "max_accum_reward", + "accum_area", + "doc_url", + ] + self.reward_widget._update_data(self._survey_reward_df[columns]) + self._debugging_message = "Finished updating reward widget data." + + @param.depends("_publish_reward_widget") + def publish_reward_widget(self): + """Return the reward Tabulator widget for display. + + Returns + ------- + widget: 'panel.widgets.Tabulator' + Table of reward data for selected survey. + """ + if self._survey_reward_df is None: + return "No rewards available." + else: + self._debugging_message = "Publishing reward widget." + return self.reward_widget + + def create_sky_map_base(self): + """Create a base plot with a dummy map.""" + if self._survey_maps is None: + self._debugging_message = "Cannot create sky map as no survey maps made." + return + + try: + self._debugging_message = "Starting to create sky map base." + # Make a dummy map that is 1.0 for all healpixels + # that might have data. + self._survey_maps["above_horizon"] = np.where(self._conditions.alt > 0, 1.0, np.nan) + self._sky_map_base = schedview.plot.survey.map_survey_healpix( + self._conditions.mjd, + self._survey_maps, + "above_horizon", + np.int64(self.nside), + conditions=self._conditions, + survey=self._scheduler.survey_lists[int(self._tier[-1])][self._survey], + ) + self._sky_map_base.plot.toolbar.tools[-1].tooltips.remove(("above_horizon", "@above_horizon")) + + color_bar = ColorBar( + color_mapper=LinearColorMapper(palette=self.color_palette, low=0, high=1), + label_standoff=10, + location=(0, 0), + ) + self._sky_map_base.plot.add_layout(color_bar, "below") + self._sky_map_base.plot.below[1].visible = False + self._sky_map_base.plot.toolbar.autohide = True # show toolbar only when mouseover plot + self._sky_map_base.plot.title.text = "" # remove 'Horizon' title + self._sky_map_base.plot.legend.propagate_hover = True # hover tool works over in-plot legend + self._sky_map_base.plot.legend.title = "Key" + self._sky_map_base.plot.legend.title_text_font_style = "bold" + self._sky_map_base.plot.legend.border_line_color = "#048b8c" + self._sky_map_base.plot.legend.border_line_width = 3 + self._sky_map_base.plot.legend.border_line_alpha = 1 + self._sky_map_base.plot.legend.label_standoff = 10 # gap between images and text + self._sky_map_base.plot.legend.padding = 15 # space around inside edge + self._sky_map_base.plot.legend.title_standoff = 10 # space between title and items + self._sky_map_base.plot.legend.click_policy = "hide" # hide elements when clicked + self._sky_map_base.plot.add_layout(self._sky_map_base.plot.legend[0], "right") + self._sky_map_base.plot.right[0].location = "center_right" + + self._debugging_message = "Finished creating sky map base." + + except Exception: + self._debugging_message = f"Cannot create sky map base: \n{traceback.format_exc(limit=-1)}" + pn.state.notifications.error("Cannot create sky map base!", duration=0) + + def update_sky_map_with_survey_map(self): + """Update base plot with healpixel data from selected survey map. + + Notes + ----- + There are three possible update cases: + - Case 1: Selection is a reward map. + - Case 2: Selection is a survey map and is all NaNs. + - Case 3: Selection is a survey map and is not all NaNs. + """ + if self._sky_map_base is None: + self._debugging_message = "Cannot update sky map with survey map as no base map loaded." + return + + try: + self._debugging_message = "Starting to update sky map with survey map." + hpix_renderer = self._sky_map_base.plot.select(name="hpix_renderer")[0] + hpix_data_source = self._sky_map_base.plot.select(name="hpix_ds")[0] + + # CASE 1: Selection is a reward map. + if self.survey_map not in ["u_sky", "g_sky", "r_sky", "i_sky", "z_sky", "y_sky", "reward"]: + reward_underscored = self._map_name.replace(" ", "_") + reward_survey_key = list(key for key in self._survey_maps if self._map_name in key)[0] + reward_bokeh_key = list(key for key in hpix_data_source.data if reward_underscored in key)[0] + + min_good_value = np.nanmin(self._survey_maps[reward_survey_key]) + max_good_value = np.nanmax(self._survey_maps[reward_survey_key]) + + if min_good_value == max_good_value: + min_good_value -= 1 + max_good_value += 1 + + hpix_renderer.glyph.fill_color = bokeh.transform.linear_cmap( + field_name=reward_bokeh_key, + palette=self.color_palette, + low=min_good_value, + high=max_good_value, + nan_color="white", + ) + self._sky_map_base.plot.below[1].visible = True + self._sky_map_base.plot.below[1].color_mapper.palette = self.color_palette + self._sky_map_base.plot.below[1].color_mapper.low = min_good_value + self._sky_map_base.plot.below[1].color_mapper.high = max_good_value + + # CASE 2: Selection is a survey map and is all NaNs. + elif np.isnan(self._survey_maps[self.survey_map]).all(): + hpix_renderer.glyph.fill_color = bokeh.transform.linear_cmap( + field_name=self.survey_map, + palette=self.color_palette, + low=-1, + high=1, + nan_color="white", + ) + self._sky_map_base.plot.below[1].visible = False + + # CASE 3: Selection is a survey map and is not all NaNs. + else: + min_good_value = np.nanmin(self._survey_maps[self.survey_map]) + max_good_value = np.nanmax(self._survey_maps[self.survey_map]) + + if min_good_value == max_good_value: + min_good_value -= 1 + max_good_value += 1 + + hpix_renderer.glyph.fill_color = bokeh.transform.linear_cmap( + field_name=self.survey_map, + palette=self.color_palette, + low=min_good_value, + high=max_good_value, + nan_color="white", + ) + self._sky_map_base.plot.below[1].visible = True + self._sky_map_base.plot.below[1].color_mapper.palette = self.color_palette + self._sky_map_base.plot.below[1].color_mapper.low = min_good_value + self._sky_map_base.plot.below[1].color_mapper.high = max_good_value + hpix_renderer.glyph.line_color = hpix_renderer.glyph.fill_color + self._sky_map_base.update() + self._debugging_message = "Finished updating sky map with survey map." + + except Exception: + self._debugging_message = f"Cannot update sky map: \n{traceback.format_exc(limit=-1)}" + pn.state.notifications.error("Cannot update sky map!", duration=0) + + def update_sky_map_with_reward(self): + """Update base plot with healpixel data from selected survey map. + + Notes + ----- + There are three possible update cases: + - Case 1: Reward is not scalar. + - Case 2: Reward is scalar and finite. + - Case 3: Reward is -Inf. + """ + if self._sky_map_base is None: + self._debugging_message = "Cannot update sky map with reward as no base map is loaded." + return + + try: + self._debugging_message = "Starting to update sky map with reward." + hpix_renderer = self._sky_map_base.plot.select(name="hpix_renderer")[0] + hpix_data_source = self._sky_map_base.plot.select(name="hpix_ds")[0] + + reward_underscored = self._reward_name.replace(" ", "_") + max_basis_reward = self._survey_reward_df.loc[self._reward, :]["max_basis_reward"] + + # CASE 1: Reward is not scalar. + if any(self._reward_name in key for key in self._survey_maps): + reward_survey_key = list(key for key in self._survey_maps if self._reward_name in key)[0] + reward_bokeh_key = list(key for key in hpix_data_source.data if reward_underscored in key)[0] + + min_good_value = np.nanmin(self._survey_maps[reward_survey_key]) + max_good_value = np.nanmax(self._survey_maps[reward_survey_key]) + + if min_good_value == max_good_value: + min_good_value -= 1 + max_good_value += 1 + + # Modify existing bokeh object. + hpix_renderer.glyph.fill_color = bokeh.transform.linear_cmap( + field_name=reward_bokeh_key, + palette=self.color_palette, + low=min_good_value, + high=max_good_value, + nan_color="white", + ) + self._sky_map_base.plot.below[1].visible = True + self._sky_map_base.plot.below[1].color_mapper.palette = self.color_palette + self._sky_map_base.plot.below[1].color_mapper.low = min_good_value + self._sky_map_base.plot.below[1].color_mapper.high = max_good_value + + # CASE 2: Reward is scalar and finite. + elif max_basis_reward != -np.inf: + # Create array populated with scalar values + # where sky brightness map is not NaN. + scalar_array = hpix_data_source.data["u_sky"].copy() + scalar_array[~np.isnan(hpix_data_source.data["u_sky"])] = max_basis_reward + hpix_data_source.data[reward_underscored] = scalar_array + + hpix_renderer.glyph.fill_color = bokeh.transform.linear_cmap( + field_name=reward_underscored, + palette=self.color_palette, + low=max_basis_reward - 1, + high=max_basis_reward + 1, + nan_color="white", + ) + self._sky_map_base.plot.below[1].visible = True + self._sky_map_base.plot.below[1].color_mapper.palette = self.color_palette + self._sky_map_base.plot.below[1].color_mapper.low = max_basis_reward - 1 + self._sky_map_base.plot.below[1].color_mapper.high = max_basis_reward + 1 + + # CASE 3: Reward is -Inf. + else: + hpix_renderer.glyph.fill_color = bokeh.transform.linear_cmap( + field_name=self._reward_name, + palette="Greys256", + low=-1, + high=1, + nan_color="white", + ) + self._sky_map_base.plot.below[1].visible = False + hpix_renderer.glyph.line_color = hpix_renderer.glyph.fill_color + self._sky_map_base.update() + self._debugging_message = "Finished updating sky map with reward." + + except Exception: + self._debugging_message = f"Cannot update sky map: \n{traceback.format_exc(limit=-1)}" + pn.state.notifications.error("Cannot update sky map!", duration=0) + + @param.depends("_publish_map") + def publish_sky_map(self): + """Return the Bokeh plot for display. + + Returns + ------- + sky_map : 'bokeh.models.layouts.Column' + Map of survey map or reward map as a Bokeh plot. + """ + if self._conditions is None: + return "No scheduler loaded." + + elif self._survey_maps is None: + return "No surveys are loaded." + + elif self._sky_map_base is None: + return "No map loaded." + + else: + self._debugging_message = "Publishing sky map." + return self._sky_map_base.figure + + @param.depends("_debugging_message") + def _debugging_messages(self): + """Construct a debugging pane to display error messages. + + Returns + ------- + debugging_messages : 'panel.pane.Str' + A list of debugging messages ordered by newest message first. + """ + if self._debugging_message is None: + return None + + timestamp = datetime.now(timezone("America/Santiago")).strftime("%Y-%m-%d %H:%M:%S") + self._debug_string = f"\n {timestamp} - {self._debugging_message}" + self._debug_string + + # Send messages to stderr. + self.logger.debug(self._debugging_message) + + if self.debug_pane is None: + self.debug_pane = pn.pane.Str( + self._debug_string, + height=200, + styles={ + "font-size": "9pt", + "color": "black", + "overflow": "scroll", + "background": "#EDEDED", + }, + ) + else: + self.debug_pane.object = self._debug_string + return self.debug_pane + + # ------------------------------------------------------ Dashboard titles + + def generate_dashboard_subtitle(self): + """Select the dashboard subtitle string based on whether whether a + survey map or reward map is being displayed. + + Returns + ------- + subtitle : 'str' + Lists the tier and survey, and either the survey or reward map. + """ + if not self._display_dashboard_data: + return "" + maps = ["u_sky", "g_sky", "r_sky", "i_sky", "z_sky", "y_sky", "reward"] + if not self._display_reward and self.survey_map in maps: + return f"\nTier {self._tier[-1]} - Survey {self._survey} - Map {self._map_name}" + elif not self._display_reward and self.survey_map not in maps: + return f"\nTier {self._tier[-1]} - Survey {self._survey} - Reward {self._map_name}" + else: + return f"\nTier {self._tier[-1]} - Survey {self._survey} - Reward {self._reward_name}" + + def generate_summary_table_heading(self): + """Select the summary table heading based on whether data is being + displayed or not. + + Returns + ------- + heading : 'str' + Lists the tier if data is displayed; else just a general title. + """ + if not self._display_dashboard_data: + return "Scheduler summary" + else: + return f"Scheduler summary for tier {self._tier[-1]}" + + def generate_reward_table_heading(self): + """Select the reward table heading based on whether data is + being displayed or not. + + Returns + ------- + heading : 'str' + Lists the survey name if data is displayed; else a general title. + """ + if not self._display_dashboard_data: + return "Basis functions & rewards" + else: + return f"Basis functions & rewards for survey {self._survey_name}" + + def generate_map_heading(self): + """Select the map heading based on whether a survey or reward map + is being displayed. + + Returns + ------- + heading : 'str' + Lists the survey name and either the survey map name or reward + name if data is being displayed; else a general title. + """ + if not self._display_dashboard_data: + return "Map" + maps = ["u_sky", "g_sky", "r_sky", "i_sky", "z_sky", "y_sky", "reward"] + if not self._display_reward and self.survey_map in maps: + return f"Survey {self._survey_name}\nMap: {self._map_name}" + elif not self._display_reward and self.survey_map not in maps: + return f"Survey {self._survey_name}\nReward: {self._map_name}" + else: + return f"Survey {self._survey_name}\nReward: {self._reward_name}" + + @param.depends("_update_headings") + def dashboard_subtitle(self): + """Load subtitle data and create/update + a String pane to display subtitle. + + Returns + ------- + title : 'panel.pane.Str' + A panel String pane to display as the dashboard's subtitle. + """ + title_string = self.generate_dashboard_subtitle() + if self.dashboard_subtitle_pane is None: + self.dashboard_subtitle_pane = pn.pane.Str( + title_string, + height=20, + stylesheets=[h2_stylesheet], + ) + else: + self.dashboard_subtitle_pane.object = title_string + return self.dashboard_subtitle_pane + + @param.depends("_update_headings") + def summary_table_heading(self): + """Load heading data and create/update + a String pane to display heading. + + Returns + ------- + title : 'panel.pane.Str' + A panel String pane to display as the survey table's heading. + """ + title_string = self.generate_summary_table_heading() + if self.summary_table_heading_pane is None: + self.summary_table_heading_pane = pn.pane.Str( + title_string, + stylesheets=[h3_stylesheet], + ) + else: + self.summary_table_heading_pane.object = title_string + return self.summary_table_heading_pane + + @param.depends("_update_headings") + def reward_table_heading(self): + """Load title data and create/update a String pane to display heading. + + Returns + ------- + title : 'panel.pane.Str' + A panel String pane to display as the reward table heading. + """ + title_string = self.generate_reward_table_heading() + if self.reward_table_heading_pane is None: + self.reward_table_heading_pane = pn.pane.Str( + title_string, + stylesheets=[h3_stylesheet], + ) + else: + self.reward_table_heading_pane.object = title_string + return self.reward_table_heading_pane + + @param.depends("_update_headings") + def map_title(self): + """Load title data and create/update a String pane to display heading. + + Returns + ------- + title : 'panel.pane.Str' + A panel String pane to display as the map heading. + """ + title_string = self.generate_map_heading() + if self.map_title_pane is None: + self.map_title_pane = pn.pane.Str( + title_string, + stylesheets=[h3_stylesheet], + ) + else: + self.map_title_pane.object = title_string + return self.map_title_pane diff --git a/schedview/app/scheduler_dashboard/utils.py b/schedview/app/scheduler_dashboard/utils.py index a3fa114e..bcbe2ce0 100644 --- a/schedview/app/scheduler_dashboard/utils.py +++ b/schedview/app/scheduler_dashboard/utils.py @@ -5,6 +5,7 @@ from astropy.time import Time, TimeDelta from lsst.resources import ResourcePath from lsst_efd_client import EfdClient +from rubin_scheduler.skybrightness_pre.sky_model_pre import SkyModelPre LOCAL_ROOT_URI = {"usdf": "s3://rubin:", "summit": "https://s3.cp.lsst.org/"} @@ -128,3 +129,38 @@ def mock_schedulers_df(): df = df.sort_index(ascending=False) df["url"] = df["url"].apply(lambda x: localize_scheduler_url(x)) return df["url"] + + +# Suggestion: Move to a utils module +def get_sky_brightness_date_bounds(): + """Load available datetime range from SkyBrightness_Pre files. + + Returns + ------- + (min_date, max_date): tuple[astropy.time.Time, astropy.time.Time] + """ + sky_model = SkyModelPre() + min_date = Time(sky_model.mjd_left.min(), format="mjd") + max_date = Time(sky_model.mjd_right.max() - 0.001, format="mjd") + return (min_date, max_date) + + +# Suggestion: Move to a utils module +def url_formatter(dataframe_row, name_column, url_column): + """Format survey name as a HTML href to survey URL (if URL exists). + + Parameters + ---------- + dataframe_row : 'pandas.core.series.Series' + A row of a pandas.core.frame.DataFrame. + + Returns + ------- + survey_name_or_url : 'str' + A HTML href or plain string. + """ + if dataframe_row[url_column] == "": + return dataframe_row[name_column] + else: + return f' \ + ' From ad608bcbb5610705b429be3498b9916b078ccb61 Mon Sep 17 00:00:00 2001 From: Eman Ali Date: Mon, 3 Jun 2024 15:52:18 +1000 Subject: [PATCH 02/18] remove commented imports --- schedview/app/scheduler_dashboard/scheduler_dashboard.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/schedview/app/scheduler_dashboard/scheduler_dashboard.py b/schedview/app/scheduler_dashboard/scheduler_dashboard.py index 8b2481c0..33c52ae2 100644 --- a/schedview/app/scheduler_dashboard/scheduler_dashboard.py +++ b/schedview/app/scheduler_dashboard/scheduler_dashboard.py @@ -46,13 +46,6 @@ from schedview.app.scheduler_dashboard.restricted_scheduler_snapshot_dashboard import ( RestrictedSchedulerSnapshotDashboard, ) - -# import schedview -# import schedview.collect.scheduler_pickle -# import schedview.compute.scheduler -# import schedview.compute.survey -# import schedview.param -# import schedview.plot.survey from schedview.app.scheduler_dashboard.scheduler_snapshot_dashboard import SchedulerSnapshotDashboard # Filter astropy warning that's filling the terminal with every update. From aa5fcfa5a9b656d5376d0236e6140dacbc5e8cae Mon Sep 17 00:00:00 2001 From: Eman Ali Date: Thu, 6 Jun 2024 13:29:25 +1000 Subject: [PATCH 03/18] move dynamic widgets to class definition --- .../lfa_scheduler_snapshot_dashboard.py | 17 ++++ ...restricted_scheduler_snapshot_dashboard.py | 6 ++ .../scheduler_dashboard.py | 99 +++++-------------- .../scheduler_snapshot_dashboard.py | 18 ++++ 4 files changed, 68 insertions(+), 72 deletions(-) diff --git a/schedview/app/scheduler_dashboard/lfa_scheduler_snapshot_dashboard.py b/schedview/app/scheduler_dashboard/lfa_scheduler_snapshot_dashboard.py index 4ad214be..0df10fcd 100644 --- a/schedview/app/scheduler_dashboard/lfa_scheduler_snapshot_dashboard.py +++ b/schedview/app/scheduler_dashboard/lfa_scheduler_snapshot_dashboard.py @@ -1,6 +1,7 @@ from datetime import datetime from zoneinfo import ZoneInfo +import panel as pn import param from astropy.time import Time from pandas import Timestamp @@ -44,6 +45,22 @@ class LFASchedulerSnapshotDashboard(SchedulerSnapshotDashboard): _summary_widget_height = 310 _reward_widget_height = 350 + data_loading_parameters = [ + "scheduler_fname", + "pickles_date", + "telescope", + "widget_datetime", + "widget_tier", + ] + # set specific widget props for data loading parameters + # in LFA mode + data_loading_widgets = { + "pickles_date": pn.widgets.DatetimePicker, + "widget_datetime": pn.widgets.DatetimePicker, + } + # set the data loading parameter section height in LFA mode + data_params_grid_height = 42 + def __init__(self): super().__init__() diff --git a/schedview/app/scheduler_dashboard/restricted_scheduler_snapshot_dashboard.py b/schedview/app/scheduler_dashboard/restricted_scheduler_snapshot_dashboard.py index c89f6964..8eca5eaf 100644 --- a/schedview/app/scheduler_dashboard/restricted_scheduler_snapshot_dashboard.py +++ b/schedview/app/scheduler_dashboard/restricted_scheduler_snapshot_dashboard.py @@ -1,3 +1,5 @@ +import panel as pn + import schedview import schedview.param from schedview.app.scheduler_dashboard.constants import PACKAGE_DATA_DIR @@ -25,6 +27,10 @@ class RestrictedSchedulerSnapshotDashboard(SchedulerSnapshotDashboard): allow_None=True, ) + data_loading_widgets = { + "widget_datetime": pn.widgets.DatetimePicker, + } + def __init__(self, data_dir=None): super().__init__() diff --git a/schedview/app/scheduler_dashboard/scheduler_dashboard.py b/schedview/app/scheduler_dashboard/scheduler_dashboard.py index 33c52ae2..359f6773 100644 --- a/schedview/app/scheduler_dashboard/scheduler_dashboard.py +++ b/schedview/app/scheduler_dashboard/scheduler_dashboard.py @@ -100,15 +100,7 @@ def scheduler_app(date_time=None, scheduler_pickle=None, **kwargs): del kwargs["lfa"] scheduler = None - # Suggestion: move these dynamic widgets to class definition? - data_loading_widgets = {} - # data loading parameters in both restricted and URL modes - data_loading_parameters = ["scheduler_fname", "widget_datetime", "widget_tier"] - # set the data loading parameter section height in both - # restricted and URL modes - # this will be used to adjust the layout of other sections - # in the grid - data_params_grid_height = 30 + # Accept pickle files from url or any path. if from_urls: scheduler = SchedulerSnapshotDashboard() @@ -130,58 +122,17 @@ def scheduler_app(date_time=None, scheduler_pickle=None, **kwargs): "url_mjd": "mjd", }, ) - # set specific widget props for data loading parameters - # in URL and restricted modes - data_loading_widgets = { - "scheduler_fname": { - "placeholder": "filepath or URL of pickle", - }, - "widget_datetime": pn.widgets.DatetimePicker, - } + # Load pickles from S3 bucket elif from_lfa: scheduler = LFASchedulerSnapshotDashboard() - # data loading parameters in LFA mode - data_loading_parameters = [ - "scheduler_fname", - "pickles_date", - "telescope", - "widget_datetime", - "widget_tier", - ] - # set specific widget props for data loading parameters - # in LFA mode - data_loading_widgets = { - "pickles_date": pn.widgets.DatetimePicker, - "widget_datetime": pn.widgets.DatetimePicker, - } - # set the data loading parameter section height in LFA mode - data_params_grid_height = 42 - - @pn.depends( - selected_time=scheduler.param.pickles_date, selected_tel=scheduler.param.telescope, watch=True - ) - async def get_scheduler_list(selected_time, selected_tel): - pn.state.notifications.clear() - pn.state.notifications.info("Loading snapshots...") - os.environ["LSST_DISABLE_BUCKET_VALIDATION"] = "1" - # add an empty option at index 0 to be the default - # selection upon loading snapshot list - schedulers = [""] - schedulers[1:] = await scheduler.query_schedulers(selected_time, selected_tel) - scheduler.param["scheduler_fname"].objects = schedulers - scheduler.clear_dashboard() - if len(schedulers) > 1: - pn.state.notifications.success("Snapshots loaded!!") - else: - pn.state.notifications.info("No snapshots found for selected night!!", duration=0) # Restrict files to data_directory. else: scheduler = RestrictedSchedulerSnapshotDashboard(data_dir=data_dir) - data_loading_widgets = { - "widget_datetime": pn.widgets.DatetimePicker, - } + # data_loading_widgets = { + # "widget_datetime": pn.widgets.DatetimePicker, + # } # Show dashboard as busy when scheduler.show_loading_spinner is True. @pn.depends(loading=scheduler.param.show_loading_indicator, watch=True) @@ -238,16 +189,18 @@ def handle_reload_pickle(event): ) # Parameter inputs (pickle, widget_datetime, tier) # as well as pickles date and telescope when running in LFA - sched_app[8:data_params_grid_height, 0:21] = pn.Param( + sched_app[8 : scheduler.data_params_grid_height, 0:21] = pn.Param( scheduler, - parameters=data_loading_parameters, - widgets=data_loading_widgets, + parameters=scheduler.data_loading_parameters, + widgets=scheduler.data_loading_widgets, name="Select pickle file, date and tier.", ) # Reset button. - sched_app[data_params_grid_height : data_params_grid_height + 6, 3:15] = pn.Row(reset_button) + sched_app[scheduler.data_params_grid_height : scheduler.data_params_grid_height + 6, 3:15] = pn.Row( + reset_button + ) # Survey rewards table and header. - sched_app[8 : data_params_grid_height + 6, 21:67] = pn.Row( + sched_app[8 : scheduler.data_params_grid_height + 6, 21:67] = pn.Row( pn.Spacer(width=10), pn.Column( pn.Spacer(height=10), @@ -260,7 +213,7 @@ def handle_reload_pickle(event): pn.Spacer(width=10), ) # Reward table and header. - sched_app[data_params_grid_height + 6 : data_params_grid_height + 45, 0:67] = pn.Row( + sched_app[scheduler.data_params_grid_height + 6 : scheduler.data_params_grid_height + 45, 0:67] = pn.Row( pn.Spacer(width=10), pn.Column( pn.Spacer(height=10), @@ -273,7 +226,7 @@ def handle_reload_pickle(event): pn.Spacer(width=10), ) # Map display and header. - sched_app[8 : data_params_grid_height + 25, 67:100] = pn.Column( + sched_app[8 : scheduler.data_params_grid_height + 25, 67:100] = pn.Column( pn.Spacer(height=10), pn.Row( scheduler.map_title, @@ -282,19 +235,21 @@ def handle_reload_pickle(event): pn.param.ParamMethod(scheduler.publish_sky_map, loading_indicator=True), ) # Map display parameters (map, nside, color palette). - sched_app[data_params_grid_height + 32 : data_params_grid_height + 45, 67:100] = pn.Param( - scheduler, - widgets={ - "survey_map": {"type": pn.widgets.Select, "width": 250}, - "nside": {"type": pn.widgets.Select, "width": 150}, - "color_palette": {"type": pn.widgets.Select, "width": 100}, - }, - parameters=["survey_map", "nside", "color_palette"], - show_name=False, - default_layout=pn.Row, + sched_app[scheduler.data_params_grid_height + 32 : scheduler.data_params_grid_height + 45, 67:100] = ( + pn.Param( + scheduler, + widgets={ + "survey_map": {"type": pn.widgets.Select, "width": 250}, + "nside": {"type": pn.widgets.Select, "width": 150}, + "color_palette": {"type": pn.widgets.Select, "width": 100}, + }, + parameters=["survey_map", "nside", "color_palette"], + show_name=False, + default_layout=pn.Row, + ) ) # Debugging collapsable card. - sched_app[data_params_grid_height + 45 : data_params_grid_height + 52, :] = pn.Card( + sched_app[scheduler.data_params_grid_height + 45 : scheduler.data_params_grid_height + 52, :] = pn.Card( scheduler._debugging_messages, header=pn.pane.Str("Debugging", stylesheets=[h2_stylesheet]), header_color="white", diff --git a/schedview/app/scheduler_dashboard/scheduler_snapshot_dashboard.py b/schedview/app/scheduler_dashboard/scheduler_snapshot_dashboard.py index e5a54407..6d34618d 100644 --- a/schedview/app/scheduler_dashboard/scheduler_snapshot_dashboard.py +++ b/schedview/app/scheduler_dashboard/scheduler_snapshot_dashboard.py @@ -136,6 +136,24 @@ class SchedulerSnapshotDashboard(param.Parameterized): _summary_widget_height = 220 _reward_widget_height = 400 + # data loading parameters in both restricted and URL modes + data_loading_parameters = ["scheduler_fname", "widget_datetime", "widget_tier"] + # set the data loading parameter section height in both + # restricted and URL modes + # this will be used to adjust the layout of other sections + # in the grid + + data_params_grid_height = 30 + + # set specific widget props for data loading parameters + # in URL and restricted modes + data_loading_widgets = { + "scheduler_fname": { + "placeholder": "filepath or URL of pickle", + }, + "widget_datetime": pn.widgets.DatetimePicker, + } + def __init__(self, **params): super().__init__(**params) self.config_logger() From f880c2fb74957bd7b3bd898cff2e67ffe47a3299 Mon Sep 17 00:00:00 2001 From: Eman Ali Date: Thu, 6 Jun 2024 13:51:23 +1000 Subject: [PATCH 04/18] fix test --- tests/test_scheduler_dashboard.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/tests/test_scheduler_dashboard.py b/tests/test_scheduler_dashboard.py index 59386183..17393489 100644 --- a/tests/test_scheduler_dashboard.py +++ b/tests/test_scheduler_dashboard.py @@ -20,11 +20,8 @@ from rubin_scheduler.scheduler.schedulers.core_scheduler import CoreScheduler import schedview -from schedview.app.scheduler_dashboard.scheduler_dashboard import ( - SchedulerSnapshotDashboard, - get_sky_brightness_date_bounds, - scheduler_app, -) +from schedview.app.scheduler_dashboard.scheduler_dashboard import SchedulerSnapshotDashboard, scheduler_app +from schedview.app.scheduler_dashboard.utils import get_sky_brightness_date_bounds # Schedview methods from schedview.compute.scheduler import make_scheduler_summary_df From 1694620df541049ee30a1990e212beab648e561a Mon Sep 17 00:00:00 2001 From: Eman Ali Date: Thu, 13 Jun 2024 14:43:47 +1000 Subject: [PATCH 05/18] rename dashboard filenames to be less confusing --- schedview/app/scheduler_dashboard/__init__.py | 2 +- .../scheduler_dashboard/lfa_scheduler_snapshot_dashboard.py | 4 +++- .../restricted_scheduler_snapshot_dashboard.py | 4 +++- .../{scheduler_dashboard.py => scheduler_dashboard_app.py} | 4 +++- ...board.py => unrestricted_scheduler_snapshot_dashboard.py} | 0 tests/test_scheduler_dashboard.py | 5 ++++- 6 files changed, 14 insertions(+), 5 deletions(-) rename schedview/app/scheduler_dashboard/{scheduler_dashboard.py => scheduler_dashboard_app.py} (98%) rename schedview/app/scheduler_dashboard/{scheduler_snapshot_dashboard.py => unrestricted_scheduler_snapshot_dashboard.py} (100%) diff --git a/schedview/app/scheduler_dashboard/__init__.py b/schedview/app/scheduler_dashboard/__init__.py index 4051677c..462752b8 100644 --- a/schedview/app/scheduler_dashboard/__init__.py +++ b/schedview/app/scheduler_dashboard/__init__.py @@ -2,4 +2,4 @@ "scheduler_app", ] -from .scheduler_dashboard import scheduler_app +from .scheduler_dashboard_app import scheduler_app diff --git a/schedview/app/scheduler_dashboard/lfa_scheduler_snapshot_dashboard.py b/schedview/app/scheduler_dashboard/lfa_scheduler_snapshot_dashboard.py index 0df10fcd..f5b66e2c 100644 --- a/schedview/app/scheduler_dashboard/lfa_scheduler_snapshot_dashboard.py +++ b/schedview/app/scheduler_dashboard/lfa_scheduler_snapshot_dashboard.py @@ -7,7 +7,9 @@ from pandas import Timestamp from schedview.app.scheduler_dashboard.constants import DEFAULT_TIMEZONE -from schedview.app.scheduler_dashboard.scheduler_snapshot_dashboard import SchedulerSnapshotDashboard +from schedview.app.scheduler_dashboard.unrestricted_scheduler_snapshot_dashboard import ( + SchedulerSnapshotDashboard, +) from schedview.app.scheduler_dashboard.utils import query_night_schedulers diff --git a/schedview/app/scheduler_dashboard/restricted_scheduler_snapshot_dashboard.py b/schedview/app/scheduler_dashboard/restricted_scheduler_snapshot_dashboard.py index 8eca5eaf..3cc103a3 100644 --- a/schedview/app/scheduler_dashboard/restricted_scheduler_snapshot_dashboard.py +++ b/schedview/app/scheduler_dashboard/restricted_scheduler_snapshot_dashboard.py @@ -3,7 +3,9 @@ import schedview import schedview.param from schedview.app.scheduler_dashboard.constants import PACKAGE_DATA_DIR -from schedview.app.scheduler_dashboard.scheduler_snapshot_dashboard import SchedulerSnapshotDashboard +from schedview.app.scheduler_dashboard.unrestricted_scheduler_snapshot_dashboard import ( + SchedulerSnapshotDashboard, +) class RestrictedSchedulerSnapshotDashboard(SchedulerSnapshotDashboard): diff --git a/schedview/app/scheduler_dashboard/scheduler_dashboard.py b/schedview/app/scheduler_dashboard/scheduler_dashboard_app.py similarity index 98% rename from schedview/app/scheduler_dashboard/scheduler_dashboard.py rename to schedview/app/scheduler_dashboard/scheduler_dashboard_app.py index 359f6773..18e24cef 100644 --- a/schedview/app/scheduler_dashboard/scheduler_dashboard.py +++ b/schedview/app/scheduler_dashboard/scheduler_dashboard_app.py @@ -46,7 +46,9 @@ from schedview.app.scheduler_dashboard.restricted_scheduler_snapshot_dashboard import ( RestrictedSchedulerSnapshotDashboard, ) -from schedview.app.scheduler_dashboard.scheduler_snapshot_dashboard import SchedulerSnapshotDashboard +from schedview.app.scheduler_dashboard.unrestricted_scheduler_snapshot_dashboard import ( + SchedulerSnapshotDashboard, +) # Filter astropy warning that's filling the terminal with every update. warnings.filterwarnings("ignore", category=AstropyWarning) diff --git a/schedview/app/scheduler_dashboard/scheduler_snapshot_dashboard.py b/schedview/app/scheduler_dashboard/unrestricted_scheduler_snapshot_dashboard.py similarity index 100% rename from schedview/app/scheduler_dashboard/scheduler_snapshot_dashboard.py rename to schedview/app/scheduler_dashboard/unrestricted_scheduler_snapshot_dashboard.py diff --git a/tests/test_scheduler_dashboard.py b/tests/test_scheduler_dashboard.py index 17393489..1a151e8f 100644 --- a/tests/test_scheduler_dashboard.py +++ b/tests/test_scheduler_dashboard.py @@ -20,7 +20,10 @@ from rubin_scheduler.scheduler.schedulers.core_scheduler import CoreScheduler import schedview -from schedview.app.scheduler_dashboard.scheduler_dashboard import SchedulerSnapshotDashboard, scheduler_app +from schedview.app.scheduler_dashboard.scheduler_dashboard_app import ( + SchedulerSnapshotDashboard, + scheduler_app, +) from schedview.app.scheduler_dashboard.utils import get_sky_brightness_date_bounds # Schedview methods From 7060efe114be36d9e8a3f413ca9f436e5e3be9a0 Mon Sep 17 00:00:00 2001 From: Eman Ali Date: Thu, 13 Jun 2024 15:09:46 +1000 Subject: [PATCH 06/18] update docs --- docs/usage.rst | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/usage.rst b/docs/usage.rst index 5e78e393..27a02f46 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -8,7 +8,7 @@ Begin by activating the conda environment:: $ conda activate schedview -There are two ways to start the dashboard, depending on what you want to use +There are three ways to start the dashboard, depending on what you want to use as the source of data. One way is for users to enter arbitrary URLs or file paths from which to load the data. **This is insecure,** because users can point the dashboard to malicious @@ -23,6 +23,13 @@ load data from a pre-specified directory on the host running the dashboard:: In either case, the app will then give you the URL at which you can find the app. +Finally, if the dashboard is running at the USDF or another LFA facility, data can +be loaded from an S3 bucket that is already preset in the dashboard. The dashboard +will retrieve a list of snapshots for a selected night. + +To start the dashbaord in LFA mode:: + $ scheduler_dashboard --lfa + Running ``prenight`` -------------------- From 4b6e9db0267d6e5fbf5b0d6d1b44ddf042b130bd Mon Sep 17 00:00:00 2001 From: alserene <118706797+alserene@users.noreply.github.com> Date: Tue, 18 Jun 2024 11:15:27 +1000 Subject: [PATCH 07/18] Update usage.rst. --- docs/usage.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/usage.rst b/docs/usage.rst index 27a02f46..83ce9278 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -21,15 +21,16 @@ load data from a pre-specified directory on the host running the dashboard:: $ scheduler_dashboard --data_dir /where/the/snapshot/pickles/are -In either case, the app will then give you the URL at which you can find the app. - Finally, if the dashboard is running at the USDF or another LFA facility, data can be loaded from an S3 bucket that is already preset in the dashboard. The dashboard will retrieve a list of snapshots for a selected night. To start the dashbaord in LFA mode:: + $ scheduler_dashboard --lfa +In each case, the app will then give you the URL at which you can find the app. + Running ``prenight`` -------------------- From efd5be4ebd5c00e58290c8c2008879207b64c871 Mon Sep 17 00:00:00 2001 From: alserene Date: Mon, 17 Jun 2024 18:24:46 -0700 Subject: [PATCH 08/18] Remove unneccessary comment. --- .../restricted_scheduler_snapshot_dashboard.py | 1 - 1 file changed, 1 deletion(-) diff --git a/schedview/app/scheduler_dashboard/restricted_scheduler_snapshot_dashboard.py b/schedview/app/scheduler_dashboard/restricted_scheduler_snapshot_dashboard.py index 3cc103a3..a27de867 100644 --- a/schedview/app/scheduler_dashboard/restricted_scheduler_snapshot_dashboard.py +++ b/schedview/app/scheduler_dashboard/restricted_scheduler_snapshot_dashboard.py @@ -14,7 +14,6 @@ class RestrictedSchedulerSnapshotDashboard(SchedulerSnapshotDashboard): be loaded from a certain data directory that is set through constructor. """ - # Param parameters that are modifiable by user actions. 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_scheduler.scheduler.schedulers.CoreScheduler, or a tuple of the form From e276e1464ce41edd756b42bd652a2b2337e29c12 Mon Sep 17 00:00:00 2001 From: alserene Date: Mon, 17 Jun 2024 18:41:50 -0700 Subject: [PATCH 09/18] Comments are complte sentences. --- .../lfa_scheduler_snapshot_dashboard.py | 12 ++--- .../scheduler_dashboard_app.py | 16 +++--- ...restricted_scheduler_snapshot_dashboard.py | 51 +++++++++---------- 3 files changed, 39 insertions(+), 40 deletions(-) diff --git a/schedview/app/scheduler_dashboard/lfa_scheduler_snapshot_dashboard.py b/schedview/app/scheduler_dashboard/lfa_scheduler_snapshot_dashboard.py index f5b66e2c..f9a5771a 100644 --- a/schedview/app/scheduler_dashboard/lfa_scheduler_snapshot_dashboard.py +++ b/schedview/app/scheduler_dashboard/lfa_scheduler_snapshot_dashboard.py @@ -22,8 +22,8 @@ class LFASchedulerSnapshotDashboard(SchedulerSnapshotDashboard): scheduler_fname_doc = """Recent pickles from LFA """ - # precedence is used to make sure fields are displayed - # in the right order regardless of the dashboard mode + # Precedence is used to make sure fields are displayed + # in the right order regardless of the dashboard mode. scheduler_fname = param.Selector( default="", objects=[], @@ -43,7 +43,7 @@ class LFASchedulerSnapshotDashboard(SchedulerSnapshotDashboard): ) # Summary widget and Reward widget heights are different in this mode - # because there are more data loading parameters + # because there are more data loading parameters. _summary_widget_height = 310 _reward_widget_height = 350 @@ -54,13 +54,13 @@ class LFASchedulerSnapshotDashboard(SchedulerSnapshotDashboard): "widget_datetime", "widget_tier", ] - # set specific widget props for data loading parameters - # in LFA mode + # Set specific widget props for data loading parameters + # in LFA mode. data_loading_widgets = { "pickles_date": pn.widgets.DatetimePicker, "widget_datetime": pn.widgets.DatetimePicker, } - # set the data loading parameter section height in LFA mode + # Set the data loading parameter section height in LFA mode. data_params_grid_height = 42 def __init__(self): diff --git a/schedview/app/scheduler_dashboard/scheduler_dashboard_app.py b/schedview/app/scheduler_dashboard/scheduler_dashboard_app.py index 18e24cef..44206df0 100644 --- a/schedview/app/scheduler_dashboard/scheduler_dashboard_app.py +++ b/schedview/app/scheduler_dashboard/scheduler_dashboard_app.py @@ -27,7 +27,7 @@ import importlib.resources import os -# Filter the astropy warnings swamping the terminal +# Filter the astropy warnings swamping the terminal. import warnings from glob import glob @@ -61,7 +61,7 @@ ) -# ------------------------------------------------------------ Create dashboard +# ------------------------------------------------------------ Create dashboard. def scheduler_app(date_time=None, scheduler_pickle=None, **kwargs): """Create a dashboard with grids of Param parameters, Tabulator widgets, and Bokeh plots. @@ -84,7 +84,7 @@ def scheduler_app(date_time=None, scheduler_pickle=None, **kwargs): max_height=1000, ).servable() - # Bools for different dashboard modes + # Bools for different dashboard modes. from_urls = False data_dir = None from_lfa = False @@ -106,8 +106,8 @@ def scheduler_app(date_time=None, scheduler_pickle=None, **kwargs): # Accept pickle files from url or any path. if from_urls: scheduler = SchedulerSnapshotDashboard() - # read pickle and time if provided to the function in a notebook - # it will be overriden if the dashboard runs in an app + # Read pickle and time if provided to the function in a notebook. + # It will be overriden if the dashboard runs in an app. if date_time is not None: scheduler.widget_datetime = date_time @@ -125,7 +125,7 @@ def scheduler_app(date_time=None, scheduler_pickle=None, **kwargs): }, ) - # Load pickles from S3 bucket + # Load pickles from S3 bucket. elif from_lfa: scheduler = LFASchedulerSnapshotDashboard() @@ -167,7 +167,7 @@ def handle_reload_pickle(event): # Set function trigger. reset_button.on_click(handle_reload_pickle) - # ------------------------------------------------------ Dashboard layout + # ------------------------------------------------------ Dashboard layout. # Dashboard title. sched_app[0:8, :] = pn.Row( pn.Column( @@ -190,7 +190,7 @@ def handle_reload_pickle(event): styles={"background": "#048b8c"}, ) # Parameter inputs (pickle, widget_datetime, tier) - # as well as pickles date and telescope when running in LFA + # as well as pickles date and telescope when running in LFA. sched_app[8 : scheduler.data_params_grid_height, 0:21] = pn.Param( scheduler, parameters=scheduler.data_loading_parameters, diff --git a/schedview/app/scheduler_dashboard/unrestricted_scheduler_snapshot_dashboard.py b/schedview/app/scheduler_dashboard/unrestricted_scheduler_snapshot_dashboard.py index 6d34618d..d3805a9a 100644 --- a/schedview/app/scheduler_dashboard/unrestricted_scheduler_snapshot_dashboard.py +++ b/schedview/app/scheduler_dashboard/unrestricted_scheduler_snapshot_dashboard.py @@ -2,7 +2,7 @@ import os import traceback -# Filter the astropy warnings swamping the terminal +# Filter the astropy warnings swamping the terminal. from datetime import datetime from zoneinfo import ZoneInfo @@ -18,7 +18,7 @@ from pandas import Timestamp from pytz import timezone -# For the conditions.mjd bugfix +# For the conditions.mjd bugfix. from rubin_scheduler.scheduler.model_observatory import ModelObservatory import schedview @@ -53,10 +53,10 @@ class SchedulerSnapshotDashboard(param.Parameterized): instance of rubin_scheduler.scheduler.conditions.Conditions. """ - # Get available skybrightness_pre date range + # Get available skybrightness_pre date range. (mjd_min, mjd_max) = get_sky_brightness_date_bounds() - # convert astropy times to iso format to be - # used as bounds for datetime parameter + # Convert astropy times to iso format to be + # used as bounds for datetime parameter. date_bounds = (mjd_min.to_datetime(), mjd_max.to_datetime()) scheduler_fname = param.String( @@ -73,7 +73,7 @@ class SchedulerSnapshotDashboard(param.Parameterized): precedence=4, ) # mjd date passed as a url path parameter when - # dashboard operates in --data_from_urls mode + # dashboard operates in --data_from_urls mode. url_mjd = param.Number(default=None) widget_tier = param.Selector( default="", @@ -136,17 +136,16 @@ class SchedulerSnapshotDashboard(param.Parameterized): _summary_widget_height = 220 _reward_widget_height = 400 - # data loading parameters in both restricted and URL modes + # Data loading parameters in both restricted and URL modes. data_loading_parameters = ["scheduler_fname", "widget_datetime", "widget_tier"] - # set the data loading parameter section height in both - # restricted and URL modes - # this will be used to adjust the layout of other sections - # in the grid - + # Set the data loading parameter section height in both + # restricted and URL modes. + # This will be used to adjust the layout of other sections + # in the grid. data_params_grid_height = 30 - # set specific widget props for data loading parameters - # in URL and restricted modes + # Set specific widget properties for data loading parameters + # in URL and restricted modes. data_loading_widgets = { "scheduler_fname": { "placeholder": "filepath or URL of pickle", @@ -183,7 +182,7 @@ def config_logger(self, logger_name="schedule-snapshot"): log_stream_handler.setFormatter(log_stream_formatter) self.logger.addHandler(log_stream_handler) - # ------------------------------------------------------------ User actions + # ------------------------------------------------------------ User actions. @param.depends("scheduler_fname", watch=True) def _update_scheduler_fname(self): @@ -306,10 +305,10 @@ def _update_mjd_from_url(self): self.clear_dashboard() self._do_not_trigger_update = True - # set Date Time widget to the mjd value set in URL + # Set Date Time widget to the mjd value set in URL. self.widget_datetime = Time(self.url_mjd, format="mjd").to_datetime() self._do_not_trigger_update = False - # set the mjd value that's used across the dashboard + # Set the mjd value that's used across the dashboard. self._mjd = self.url_mjd if not self.update_conditions(): @@ -504,7 +503,7 @@ def _update_color_palette(self): self.update_sky_map_with_survey_map() self.param.trigger("_publish_map") - # ------------------------------------------------------- Internal workings + # ------------------------------------------------------- Internal workings. def clear_dashboard(self): """Clear the dashboard for a new pickle or a new date.""" @@ -977,18 +976,18 @@ def create_sky_map_base(self): ) self._sky_map_base.plot.add_layout(color_bar, "below") self._sky_map_base.plot.below[1].visible = False - self._sky_map_base.plot.toolbar.autohide = True # show toolbar only when mouseover plot - self._sky_map_base.plot.title.text = "" # remove 'Horizon' title - self._sky_map_base.plot.legend.propagate_hover = True # hover tool works over in-plot legend + self._sky_map_base.plot.toolbar.autohide = True # Show toolbar only when mouseover plot. + self._sky_map_base.plot.title.text = "" # Remove 'Horizon' title. + self._sky_map_base.plot.legend.propagate_hover = True # Hover tool works over in-plot legend. self._sky_map_base.plot.legend.title = "Key" self._sky_map_base.plot.legend.title_text_font_style = "bold" self._sky_map_base.plot.legend.border_line_color = "#048b8c" self._sky_map_base.plot.legend.border_line_width = 3 self._sky_map_base.plot.legend.border_line_alpha = 1 - self._sky_map_base.plot.legend.label_standoff = 10 # gap between images and text - self._sky_map_base.plot.legend.padding = 15 # space around inside edge - self._sky_map_base.plot.legend.title_standoff = 10 # space between title and items - self._sky_map_base.plot.legend.click_policy = "hide" # hide elements when clicked + self._sky_map_base.plot.legend.label_standoff = 10 # Gap between images and text. + self._sky_map_base.plot.legend.padding = 15 # Space around inside edge. + self._sky_map_base.plot.legend.title_standoff = 10 # Space between title and items. + self._sky_map_base.plot.legend.click_policy = "hide" # Hide elements when clicked. self._sky_map_base.plot.add_layout(self._sky_map_base.plot.legend[0], "right") self._sky_map_base.plot.right[0].location = "center_right" @@ -1221,7 +1220,7 @@ def _debugging_messages(self): self.debug_pane.object = self._debug_string return self.debug_pane - # ------------------------------------------------------ Dashboard titles + # ------------------------------------------------------ Dashboard titles. def generate_dashboard_subtitle(self): """Select the dashboard subtitle string based on whether whether a From 66ae3569127b35541e96cbb4aeb2287cd1613beb Mon Sep 17 00:00:00 2001 From: alserene Date: Mon, 17 Jun 2024 18:43:33 -0700 Subject: [PATCH 10/18] Move comment to relevant code. --- .../unrestricted_scheduler_snapshot_dashboard.py | 1 + 1 file changed, 1 insertion(+) diff --git a/schedview/app/scheduler_dashboard/unrestricted_scheduler_snapshot_dashboard.py b/schedview/app/scheduler_dashboard/unrestricted_scheduler_snapshot_dashboard.py index d3805a9a..3d25b96b 100644 --- a/schedview/app/scheduler_dashboard/unrestricted_scheduler_snapshot_dashboard.py +++ b/schedview/app/scheduler_dashboard/unrestricted_scheduler_snapshot_dashboard.py @@ -59,6 +59,7 @@ class SchedulerSnapshotDashboard(param.Parameterized): # used as bounds for datetime parameter. date_bounds = (mjd_min.to_datetime(), mjd_max.to_datetime()) + # Param parameters that are modifiable by user actions. scheduler_fname = param.String( default="", label="Scheduler snapshot file", From 2074d5674246b801a39d43089ca87a3c831fd239 Mon Sep 17 00:00:00 2001 From: alserene Date: Mon, 17 Jun 2024 22:40:15 -0700 Subject: [PATCH 11/18] Correct spelling. --- schedview/app/scheduler_dashboard/scheduler_dashboard_app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/schedview/app/scheduler_dashboard/scheduler_dashboard_app.py b/schedview/app/scheduler_dashboard/scheduler_dashboard_app.py index 44206df0..2dde2200 100644 --- a/schedview/app/scheduler_dashboard/scheduler_dashboard_app.py +++ b/schedview/app/scheduler_dashboard/scheduler_dashboard_app.py @@ -107,7 +107,7 @@ def scheduler_app(date_time=None, scheduler_pickle=None, **kwargs): if from_urls: scheduler = SchedulerSnapshotDashboard() # Read pickle and time if provided to the function in a notebook. - # It will be overriden if the dashboard runs in an app. + # It will be overridden if the dashboard runs in an app. if date_time is not None: scheduler.widget_datetime = date_time From d3dc28ac684b65fd8d1b448578bf3720b061ac3d Mon Sep 17 00:00:00 2001 From: alserene Date: Mon, 17 Jun 2024 22:40:41 -0700 Subject: [PATCH 12/18] Correct reference to table name. --- schedview/app/scheduler_dashboard/scheduler_dashboard_app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/schedview/app/scheduler_dashboard/scheduler_dashboard_app.py b/schedview/app/scheduler_dashboard/scheduler_dashboard_app.py index 2dde2200..664f22fa 100644 --- a/schedview/app/scheduler_dashboard/scheduler_dashboard_app.py +++ b/schedview/app/scheduler_dashboard/scheduler_dashboard_app.py @@ -201,7 +201,7 @@ def handle_reload_pickle(event): sched_app[scheduler.data_params_grid_height : scheduler.data_params_grid_height + 6, 3:15] = pn.Row( reset_button ) - # Survey rewards table and header. + # Summary table and header. sched_app[8 : scheduler.data_params_grid_height + 6, 21:67] = pn.Row( pn.Spacer(width=10), pn.Column( From 3d9ad06a22bc2441c734348321fa15e2ba5426b8 Mon Sep 17 00:00:00 2001 From: alserene Date: Mon, 17 Jun 2024 22:41:42 -0700 Subject: [PATCH 13/18] Add comments regarding Param function updates. --- ...restricted_scheduler_snapshot_dashboard.py | 33 ++++++++++++++++--- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/schedview/app/scheduler_dashboard/unrestricted_scheduler_snapshot_dashboard.py b/schedview/app/scheduler_dashboard/unrestricted_scheduler_snapshot_dashboard.py index 3d25b96b..ace53b97 100644 --- a/schedview/app/scheduler_dashboard/unrestricted_scheduler_snapshot_dashboard.py +++ b/schedview/app/scheduler_dashboard/unrestricted_scheduler_snapshot_dashboard.py @@ -44,7 +44,6 @@ class SchedulerSnapshotDashboard(param.Parameterized): are loaded from any path or URL. """ - # Param parameters that are modifiable by user actions. 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_scheduler.scheduler.schedulers.CoreScheduler, or a tuple of the form @@ -94,7 +93,6 @@ class SchedulerSnapshotDashboard(param.Parameterized): label="Map resolution (nside)", doc="", ) - color_palette = param.Selector(default=DEFAULT_COLOR_PALETTE, objects=COLOR_PALETTES, doc="") summary_widget = param.Parameter(default=None, doc="") reward_widget = param.Parameter(default=None, doc="") @@ -132,8 +130,11 @@ class SchedulerSnapshotDashboard(param.Parameterized): _debug_string = "" _display_reward = False _display_dashboard_data = False - # Suggestion: describe how updating parameters works + + # This boolean variable prevents unwanted triggering + # of Param functions when updating watched variables. _do_not_trigger_update = True + _summary_widget_height = 220 _reward_widget_height = 400 @@ -184,6 +185,13 @@ def config_logger(self, logger_name="schedule-snapshot"): self.logger.addHandler(log_stream_handler) # ------------------------------------------------------------ User actions. + # The following set of functions are triggered when the user + # updates one of the Parameters via the UI. The parameter + # being "watched" is specified in the function decorators. + # + # These "watcher" functions call the relevant "internal + # workings" functions (next section) to correctly update + # the data and UI according to the user's behaviour. @param.depends("scheduler_fname", watch=True) def _update_scheduler_fname(self): @@ -219,6 +227,7 @@ def _update_scheduler_fname(self): self.summary_widget.selection = [0] self._do_not_trigger_update = False + # Suggestion: here the fragment is used again self.compute_survey_maps() self.survey_map = self.param["survey_map"].objects[-1] self._map_name = self.survey_map.split("@")[0].strip() @@ -236,6 +245,7 @@ def _update_scheduler_fname(self): self.param.trigger("_update_headings") self.show_loading_indicator = False + # --------------------------------End of fragment @param.depends("widget_datetime", watch=True) def _update_mjd_from_picker(self): @@ -277,6 +287,7 @@ def _update_mjd_from_picker(self): self.summary_widget.selection = [0] self._do_not_trigger_update = False + # Suggestion: here the fragment is used again self.compute_survey_maps() self.survey_map = self.param["survey_map"].objects[-1] self._map_name = self.survey_map.split("@")[0].strip() @@ -294,6 +305,7 @@ def _update_mjd_from_picker(self): self.param.trigger("_update_headings") self.show_loading_indicator = False + # --------------------------------End of fragment @param.depends("url_mjd", watch=True) def _update_mjd_from_url(self): @@ -333,7 +345,7 @@ def _update_mjd_from_url(self): self._do_not_trigger_update = False # Suggestion: Should this fragment be separated into a function? - # Looks like it's repeated in other parts of the dashboard + # it is used 3x in functions. self.compute_survey_maps() self.survey_map = self.param["survey_map"].objects[-1] self._map_name = self.survey_map.split("@")[0].strip() @@ -505,6 +517,16 @@ def _update_color_palette(self): self.param.trigger("_publish_map") # ------------------------------------------------------- Internal workings. + # The following set of functions are called by the above + # "watcher" functions. These functions are responsible + # for correctly updating particular aspects of the + # dashboard when the user makes a change in the UI. + # + # They are all self-contained functions (they do not call + # other functions). + # + # The functions with decorators are those that are + # returning objects to be displayed in the UI. def clear_dashboard(self): """Clear the dashboard for a new pickle or a new date.""" @@ -1222,6 +1244,9 @@ def _debugging_messages(self): return self.debug_pane # ------------------------------------------------------ Dashboard titles. + # The following set of functions are responsible for + # generating and updating the various text headings + # throughout the dashboard. def generate_dashboard_subtitle(self): """Select the dashboard subtitle string based on whether whether a From c1b462de21ef52ee49babb6b56a9e43dcf8a935f Mon Sep 17 00:00:00 2001 From: alserene Date: Mon, 17 Jun 2024 23:06:04 -0700 Subject: [PATCH 14/18] Add docstrings to functions. --- .../scheduler_dashboard_app.py | 35 ++++++++++++++++--- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/schedview/app/scheduler_dashboard/scheduler_dashboard_app.py b/schedview/app/scheduler_dashboard/scheduler_dashboard_app.py index 664f22fa..1028bf95 100644 --- a/schedview/app/scheduler_dashboard/scheduler_dashboard_app.py +++ b/schedview/app/scheduler_dashboard/scheduler_dashboard_app.py @@ -68,14 +68,14 @@ def scheduler_app(date_time=None, scheduler_pickle=None, **kwargs): Parameters ---------- - widget_datetime : 'datetime' or 'date', optional + widget_datetime : `datetime` or `date`, optional The date/datetime of interest. The default is None. - scheduler_pickle : 'str', optional + scheduler_pickle : `str`, optional A filepath or URL for the scheduler pickle. The default is None. Returns ------- - sched_app : 'panel.layout.grid.GridSpec' + sched_app : `panel.layout.grid.GridSpec` The dashboard. """ # Initialize the dashboard layout. @@ -139,6 +139,14 @@ def scheduler_app(date_time=None, scheduler_pickle=None, **kwargs): # Show dashboard as busy when scheduler.show_loading_spinner is True. @pn.depends(loading=scheduler.param.show_loading_indicator, watch=True) def update_loading(loading): + """Update the dashboard's loading indicator based on the + 'show_loading_indicator' parameter. + + Parameters + ---------- + loading : `bool` + Indicates whether the loading indicator should be shown. + """ if loading: scheduler.logger.debug("DASHBOARD START LOADING") start_loading_spinner(sched_app) @@ -156,6 +164,13 @@ def update_loading(loading): # Reset dashboard to loading conditions. def handle_reload_pickle(event): + """Reset the dashboard to its initial loading conditions. + + Parameters + ---------- + event : `pn.widgets.Button` + The Button widget instance that triggered the function call. + """ scheduler.logger.debug("RELOAD PICKLE") scheduler.nside = 16 scheduler.color_palette = "Viridis256" @@ -264,8 +279,7 @@ def handle_reload_pickle(event): def parse_arguments(): - """ - Parse commandline arguments to read data directory if provided + """Parse commandline arguments to read data directory if provided. """ parser = argparse.ArgumentParser(description="On-the-fly Rubin Scheduler dashboard") default_data_dir = f"{LFA_DATA_DIR}/*" if os.path.exists(LFA_DATA_DIR) else PACKAGE_DATA_DIR @@ -304,6 +318,17 @@ def parse_arguments(): def main(): + """Start the scheduler dashboard server. + + Parse command-line arguments, set up the scheduler application, + and serve it using Panel (pn). + + Notes + ----- + Use environment variable 'SCHEDULER_SNAPSHOT_DASHBOARD_PORT' for port + configuration. Default to port 8888 if not set. + """ + print("Starting scheduler dashboard.") commandline_args = parse_arguments() From e253a5062910743d3e17c1cbf57434e9e6dc2b40 Mon Sep 17 00:00:00 2001 From: alserene Date: Mon, 17 Jun 2024 23:10:26 -0700 Subject: [PATCH 15/18] Remove space. --- schedview/app/scheduler_dashboard/scheduler_dashboard_app.py | 1 - 1 file changed, 1 deletion(-) diff --git a/schedview/app/scheduler_dashboard/scheduler_dashboard_app.py b/schedview/app/scheduler_dashboard/scheduler_dashboard_app.py index 1028bf95..808f1026 100644 --- a/schedview/app/scheduler_dashboard/scheduler_dashboard_app.py +++ b/schedview/app/scheduler_dashboard/scheduler_dashboard_app.py @@ -328,7 +328,6 @@ def main(): Use environment variable 'SCHEDULER_SNAPSHOT_DASHBOARD_PORT' for port configuration. Default to port 8888 if not set. """ - print("Starting scheduler dashboard.") commandline_args = parse_arguments() From cac188fab787e0b7109615ae2f991f188bfd6354 Mon Sep 17 00:00:00 2001 From: alserene Date: Mon, 17 Jun 2024 23:39:02 -0700 Subject: [PATCH 16/18] Amend docstrings to be inline with LSST dev docs. --- ...restricted_scheduler_snapshot_dashboard.py | 54 ++++++++++--------- 1 file changed, 28 insertions(+), 26 deletions(-) diff --git a/schedview/app/scheduler_dashboard/unrestricted_scheduler_snapshot_dashboard.py b/schedview/app/scheduler_dashboard/unrestricted_scheduler_snapshot_dashboard.py index ace53b97..db8774e6 100644 --- a/schedview/app/scheduler_dashboard/unrestricted_scheduler_snapshot_dashboard.py +++ b/schedview/app/scheduler_dashboard/unrestricted_scheduler_snapshot_dashboard.py @@ -560,7 +560,7 @@ def read_scheduler(self): Returns ------- - success : 'bool' + success : `bool` Record of success or failure of reading scheduler from file/URL. """ try: @@ -600,7 +600,7 @@ def update_conditions(self): Returns ------- - success : 'bool' + success : `bool` Record of success of Conditions update. """ if self._conditions is None: @@ -658,7 +658,7 @@ def make_scheduler_summary_df(self): Returns ------- - success : 'bool' + success : `bool` Record of success of dataframe construction. """ if self._scheduler is None: @@ -761,7 +761,7 @@ def create_summary_widget(self): self._debugging_message = "Finished making summary widget." def update_summary_widget_data(self): - """Update data for survey Tabulator widget.""" + """Update data for summary Tabulator widget.""" self._debugging_message = "Starting to update summary widget." columns = [ "survey_index", @@ -783,7 +783,7 @@ def publish_summary_widget(self): Returns ------- - widget: 'panel.widgets.Tabulator' + widget: `panel.widgets.Tabulator` Table of scheduler summary data. """ if self.summary_widget is None: @@ -815,12 +815,12 @@ def compute_survey_maps(self): pn.state.notifications.error("Cannot compute survey maps!", duration=0) def make_reward_df(self): - """Make the summary dataframe.""" + """Make the survey reward dataframe.""" if self._scheduler is None: - self._debugging_message = "Cannot make summary dataframe as no scheduler loaded." + self._debugging_message = "Cannot make reward dataframe as no scheduler loaded." return if self._scheduler_summary_df is None: - self._debugging_message = "Cannot make summary dataframe as no scheduler summary made." + self._debugging_message = "Cannot make reward dataframe as no scheduler summary made." return try: self._debugging_message = "Starting to make reward dataframe." @@ -935,7 +935,7 @@ def create_reward_widget(self): self._debugging_message = "Finished making reward widget." def update_reward_widget_data(self): - """Update Reward Tabulator widget data.""" + """Update survey reward Tabulator widget data.""" if self._survey_reward_df is None: return @@ -962,7 +962,7 @@ def publish_reward_widget(self): Returns ------- - widget: 'panel.widgets.Tabulator' + widget: `panel.widgets.Tabulator` Table of reward data for selected survey. """ if self._survey_reward_df is None: @@ -1194,7 +1194,7 @@ def publish_sky_map(self): Returns ------- - sky_map : 'bokeh.models.layouts.Column' + sky_map : `bokeh.models.layouts.Column` Map of survey map or reward map as a Bokeh plot. """ if self._conditions is None: @@ -1216,7 +1216,7 @@ def _debugging_messages(self): Returns ------- - debugging_messages : 'panel.pane.Str' + debugging_messages : `panel.pane.Str` A list of debugging messages ordered by newest message first. """ if self._debugging_message is None: @@ -1249,13 +1249,13 @@ def _debugging_messages(self): # throughout the dashboard. def generate_dashboard_subtitle(self): - """Select the dashboard subtitle string based on whether whether a + """Select the dashboard subtitle string based on whether a survey map or reward map is being displayed. Returns ------- - subtitle : 'str' - Lists the tier and survey, and either the survey or reward map. + subtitle : `str` + Heading text. Include tier and survey, and either the survey or reward map. """ if not self._display_dashboard_data: return "" @@ -1273,8 +1273,9 @@ def generate_summary_table_heading(self): Returns ------- - heading : 'str' - Lists the tier if data is displayed; else just a general title. + heading : `str` + Heading text. If data is diplayed, include current tier; + else, a general heading. """ if not self._display_dashboard_data: return "Scheduler summary" @@ -1287,8 +1288,9 @@ def generate_reward_table_heading(self): Returns ------- - heading : 'str' - Lists the survey name if data is displayed; else a general title. + heading : `str` + Heading text. If data is diplayed, include current survey; + else, a general heading. """ if not self._display_dashboard_data: return "Basis functions & rewards" @@ -1301,9 +1303,9 @@ def generate_map_heading(self): Returns ------- - heading : 'str' - Lists the survey name and either the survey map name or reward - name if data is being displayed; else a general title. + heading : `str` + Heading text. If data is displayed, include current survey and + either the survey map or reward name; else, a general heading. """ if not self._display_dashboard_data: return "Map" @@ -1322,7 +1324,7 @@ def dashboard_subtitle(self): Returns ------- - title : 'panel.pane.Str' + title : `panel.pane.Str` A panel String pane to display as the dashboard's subtitle. """ title_string = self.generate_dashboard_subtitle() @@ -1343,7 +1345,7 @@ def summary_table_heading(self): Returns ------- - title : 'panel.pane.Str' + title : `panel.pane.Str` A panel String pane to display as the survey table's heading. """ title_string = self.generate_summary_table_heading() @@ -1362,7 +1364,7 @@ def reward_table_heading(self): Returns ------- - title : 'panel.pane.Str' + title : `panel.pane.Str` A panel String pane to display as the reward table heading. """ title_string = self.generate_reward_table_heading() @@ -1381,7 +1383,7 @@ def map_title(self): Returns ------- - title : 'panel.pane.Str' + title : `panel.pane.Str` A panel String pane to display as the map heading. """ title_string = self.generate_map_heading() From 552c38521a097c5d8a0c94bda9e916312317f710 Mon Sep 17 00:00:00 2001 From: alserene Date: Tue, 18 Jun 2024 16:48:45 +1000 Subject: [PATCH 17/18] Remove comments. --- tests/test_scheduler_dashboard.py | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/tests/test_scheduler_dashboard.py b/tests/test_scheduler_dashboard.py index 1a151e8f..5ec0def1 100644 --- a/tests/test_scheduler_dashboard.py +++ b/tests/test_scheduler_dashboard.py @@ -30,33 +30,6 @@ from schedview.compute.scheduler import make_scheduler_summary_df from schedview.compute.survey import compute_maps -""" -Tests I usually perform: - - 1. Load valid pickle. - 2. Choose date. - 3. Choose tier. - 4. Select survey. - 5. Select basis function (array). - 6. Select basis function (finite scalar). - 7. Select basis function (-inf scalar). - 8. Choose survey map (basis function). - 9. Choose survey map (sky brightness). - 10. Choose nside. - 11. Choose color palette. - 12. Check tooltip reflects selection data. - 13. Choose invalid date. - 14. Choose invalid pickle. - 15. Load two pickles, one after the other. - - -Notes - - - Unit tests are not supposed to rely on each other. - - Unit tests are run in alphabetical order. - -""" - TEST_PICKLE = str(importlib.resources.files(schedview).joinpath("data", "sample_scheduler.pickle.xz")) MJD_START = get_sky_brightness_date_bounds()[0] TEST_DATE = Time(MJD_START + 0.2, format="mjd").datetime From 218b8370f4f394cb62aad697f75bf86b73f1f186 Mon Sep 17 00:00:00 2001 From: alserene Date: Tue, 18 Jun 2024 16:52:46 +1000 Subject: [PATCH 18/18] Fix line lengths. --- .../app/scheduler_dashboard/scheduler_dashboard_app.py | 5 ++--- .../unrestricted_scheduler_snapshot_dashboard.py | 9 +++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/schedview/app/scheduler_dashboard/scheduler_dashboard_app.py b/schedview/app/scheduler_dashboard/scheduler_dashboard_app.py index 808f1026..eebd51a8 100644 --- a/schedview/app/scheduler_dashboard/scheduler_dashboard_app.py +++ b/schedview/app/scheduler_dashboard/scheduler_dashboard_app.py @@ -61,7 +61,7 @@ ) -# ------------------------------------------------------------ Create dashboard. +# ----------------------------------------------------------- Create dashboard. def scheduler_app(date_time=None, scheduler_pickle=None, **kwargs): """Create a dashboard with grids of Param parameters, Tabulator widgets, and Bokeh plots. @@ -279,8 +279,7 @@ def handle_reload_pickle(event): def parse_arguments(): - """Parse commandline arguments to read data directory if provided. - """ + """Parse commandline arguments to read data directory if provided.""" parser = argparse.ArgumentParser(description="On-the-fly Rubin Scheduler dashboard") default_data_dir = f"{LFA_DATA_DIR}/*" if os.path.exists(LFA_DATA_DIR) else PACKAGE_DATA_DIR diff --git a/schedview/app/scheduler_dashboard/unrestricted_scheduler_snapshot_dashboard.py b/schedview/app/scheduler_dashboard/unrestricted_scheduler_snapshot_dashboard.py index db8774e6..a4526e35 100644 --- a/schedview/app/scheduler_dashboard/unrestricted_scheduler_snapshot_dashboard.py +++ b/schedview/app/scheduler_dashboard/unrestricted_scheduler_snapshot_dashboard.py @@ -184,7 +184,7 @@ def config_logger(self, logger_name="schedule-snapshot"): log_stream_handler.setFormatter(log_stream_formatter) self.logger.addHandler(log_stream_handler) - # ------------------------------------------------------------ User actions. + # ----------------------------------------------------------- User actions. # The following set of functions are triggered when the user # updates one of the Parameters via the UI. The parameter # being "watched" is specified in the function decorators. @@ -516,7 +516,7 @@ def _update_color_palette(self): self.update_sky_map_with_survey_map() self.param.trigger("_publish_map") - # ------------------------------------------------------- Internal workings. + # ------------------------------------------------------ Internal workings. # The following set of functions are called by the above # "watcher" functions. These functions are responsible # for correctly updating particular aspects of the @@ -1243,7 +1243,7 @@ def _debugging_messages(self): self.debug_pane.object = self._debug_string return self.debug_pane - # ------------------------------------------------------ Dashboard titles. + # ----------------------------------------------------- Dashboard titles. # The following set of functions are responsible for # generating and updating the various text headings # throughout the dashboard. @@ -1255,7 +1255,8 @@ def generate_dashboard_subtitle(self): Returns ------- subtitle : `str` - Heading text. Include tier and survey, and either the survey or reward map. + Heading text. Include tier and survey, and either the + survey or reward map. """ if not self._display_dashboard_data: return ""