diff --git a/schedview/app/metric_maps/main.py b/schedview/app/metric_maps/main.py index 08b3cdc9..38bf1e89 100644 --- a/schedview/app/metric_maps/main.py +++ b/schedview/app/metric_maps/main.py @@ -1,105 +1,5 @@ -import bokeh.plotting +from schedview.app.metric_maps.metric_maps import add_metric_app +import bokeh -from rubin_sim import maf - -from schedview.plot.SphereMap import ArmillarySphere, Planisphere, MollweideMap -from schedview.collect.stars import load_bright_stars -from schedview.collect import get_metric_path - - -def make_metric_figure(metric_values_fname=None, nside=32, mag_limit_slider=True): - """Create a figure showing multiple projections of a set of a MAF metric. - - Parameters - ---------- - metric_values_fname : `str`, optional - Name of file from which to load metric values, as saved by MAF - in a saved metric bundle. If it is None, the look for the - file name in the ``METRIC_FNAME`` environment varable. By default None - nside : `int`, optional - Healpix nside to use for display, by default 8 - mag_limit_slider : `bool`, optional - Show the mag limit slider for stars?, by default True - - Returns - ------- - fig : `bokeh.models.layouts.LayoutDOM` - A bokeh figure that can be displayed in a notebook (e.g. with - ``bokeh.io.show``) or used to create a bokeh app. - - Notes - ----- - If ``mag_limit_slider`` is ``True``, it creates a magnitude limit - slider for the stars. This is implemented as a python callback, and - so is only operational in full bokeh app, not standalone output. - """ - - if metric_values_fname is None: - metric_values_fname = get_metric_path() - - healpy_values = maf.MetricBundle.load(metric_values_fname).metricValues - - star_data = load_bright_stars().loc[:, ["name", "ra", "decl", "Vmag"]] - star_data["glyph_size"] = 15 - (15.0 / 3.5) * star_data["Vmag"] - star_data.query("glyph_size>0", inplace=True) - - arm = ArmillarySphere() - hp_ds, cmap, _ = arm.add_healpix(healpy_values, nside=nside) - hz = arm.add_horizon() - zd70 = arm.add_horizon(zd=70, line_kwargs={"color": "red", "line_width": 2}) - star_ds = arm.add_stars( - star_data, mag_limit_slider=mag_limit_slider, star_kwargs={"color": "black"} - ) - arm.decorate() - - pla = Planisphere() - pla.add_healpix(hp_ds, cmap=cmap, nside=nside) - pla.add_horizon(data_source=hz) - pla.add_horizon( - zd=60, data_source=zd70, line_kwargs={"color": "red", "line_width": 2} - ) - pla.add_stars( - star_data, - data_source=star_ds, - mag_limit_slider=False, - star_kwargs={"color": "black"}, - ) - pla.decorate() - - mol_plot = bokeh.plotting.figure(plot_width=512, plot_height=256, match_aspect=True) - mol = MollweideMap(plot=mol_plot) - mol.add_healpix(hp_ds, cmap=cmap, nside=nside) - mol.add_horizon(data_source=hz) - mol.add_horizon( - zd=70, data_source=zd70, line_kwargs={"color": "red", "line_width": 2} - ) - mol.add_stars( - star_data, - data_source=star_ds, - mag_limit_slider=False, - star_kwargs={"color": "black"}, - ) - mol.decorate() - - figure = bokeh.layouts.row( - bokeh.layouts.column(mol.plot, *arm.sliders.values()), arm.plot, pla.plot - ) - - return figure - - -def add_metric_app(doc): - """Add a metric figure to a bokeh document - - Parameters - ---------- - doc : `bokeh.document.document.Document` - The bokeh document to which to add the figure. - """ - figure = make_metric_figure() - doc.add_root(figure) - - -if __name__.startswith("bokeh_app_"): - doc = bokeh.plotting.curdoc() - add_metric_app(doc) +doc = bokeh.plotting.curdoc() +add_metric_app(doc) diff --git a/schedview/app/metric_maps/metric_maps.py b/schedview/app/metric_maps/metric_maps.py new file mode 100644 index 00000000..08b3cdc9 --- /dev/null +++ b/schedview/app/metric_maps/metric_maps.py @@ -0,0 +1,105 @@ +import bokeh.plotting + +from rubin_sim import maf + +from schedview.plot.SphereMap import ArmillarySphere, Planisphere, MollweideMap +from schedview.collect.stars import load_bright_stars +from schedview.collect import get_metric_path + + +def make_metric_figure(metric_values_fname=None, nside=32, mag_limit_slider=True): + """Create a figure showing multiple projections of a set of a MAF metric. + + Parameters + ---------- + metric_values_fname : `str`, optional + Name of file from which to load metric values, as saved by MAF + in a saved metric bundle. If it is None, the look for the + file name in the ``METRIC_FNAME`` environment varable. By default None + nside : `int`, optional + Healpix nside to use for display, by default 8 + mag_limit_slider : `bool`, optional + Show the mag limit slider for stars?, by default True + + Returns + ------- + fig : `bokeh.models.layouts.LayoutDOM` + A bokeh figure that can be displayed in a notebook (e.g. with + ``bokeh.io.show``) or used to create a bokeh app. + + Notes + ----- + If ``mag_limit_slider`` is ``True``, it creates a magnitude limit + slider for the stars. This is implemented as a python callback, and + so is only operational in full bokeh app, not standalone output. + """ + + if metric_values_fname is None: + metric_values_fname = get_metric_path() + + healpy_values = maf.MetricBundle.load(metric_values_fname).metricValues + + star_data = load_bright_stars().loc[:, ["name", "ra", "decl", "Vmag"]] + star_data["glyph_size"] = 15 - (15.0 / 3.5) * star_data["Vmag"] + star_data.query("glyph_size>0", inplace=True) + + arm = ArmillarySphere() + hp_ds, cmap, _ = arm.add_healpix(healpy_values, nside=nside) + hz = arm.add_horizon() + zd70 = arm.add_horizon(zd=70, line_kwargs={"color": "red", "line_width": 2}) + star_ds = arm.add_stars( + star_data, mag_limit_slider=mag_limit_slider, star_kwargs={"color": "black"} + ) + arm.decorate() + + pla = Planisphere() + pla.add_healpix(hp_ds, cmap=cmap, nside=nside) + pla.add_horizon(data_source=hz) + pla.add_horizon( + zd=60, data_source=zd70, line_kwargs={"color": "red", "line_width": 2} + ) + pla.add_stars( + star_data, + data_source=star_ds, + mag_limit_slider=False, + star_kwargs={"color": "black"}, + ) + pla.decorate() + + mol_plot = bokeh.plotting.figure(plot_width=512, plot_height=256, match_aspect=True) + mol = MollweideMap(plot=mol_plot) + mol.add_healpix(hp_ds, cmap=cmap, nside=nside) + mol.add_horizon(data_source=hz) + mol.add_horizon( + zd=70, data_source=zd70, line_kwargs={"color": "red", "line_width": 2} + ) + mol.add_stars( + star_data, + data_source=star_ds, + mag_limit_slider=False, + star_kwargs={"color": "black"}, + ) + mol.decorate() + + figure = bokeh.layouts.row( + bokeh.layouts.column(mol.plot, *arm.sliders.values()), arm.plot, pla.plot + ) + + return figure + + +def add_metric_app(doc): + """Add a metric figure to a bokeh document + + Parameters + ---------- + doc : `bokeh.document.document.Document` + The bokeh document to which to add the figure. + """ + figure = make_metric_figure() + doc.add_root(figure) + + +if __name__.startswith("bokeh_app_"): + doc = bokeh.plotting.curdoc() + add_metric_app(doc) diff --git a/schedview/app/sched_maps/main.py b/schedview/app/sched_maps/main.py index 9946039a..9b58e34f 100644 --- a/schedview/app/sched_maps/main.py +++ b/schedview/app/sched_maps/main.py @@ -1,554 +1,5 @@ -import bokeh.plotting -from astropy.time import Time +from schedview.app.sched_maps.sched_maps import add_scheduler_map_app +import bokeh -import pandas as pd -import bokeh.models -import bokeh.core.properties - -from schedview.plot.SphereMap import ( - ArmillarySphere, - HorizonMap, - Planisphere, - MollweideMap, -) - -from schedview.collect import sample_pickle - -from schedview.plot.scheduler import SchedulerDisplay -from schedview.collect import read_scheduler -from schedview.plot.scheduler import LOGGER, DEFAULT_NSIDE - - -class SchedulerDisplayApp(SchedulerDisplay): - include_mollweide = False - - def make_pickle_entry_box(self): - """Make the entry box for a file name from which to load state.""" - file_input_box = bokeh.models.TextInput( - value=sample_pickle("auxtel59628.pickle.gz") + " ", - title="Pickle path:", - ) - - def switch_pickle(attrname, old, new): - def do_switch_pickle(): - LOGGER.info(f"Loading {new}.") - try: - # load updates the survey & conditions, which updates - # bokeh models... - self.load(new) - except Exception as e: - LOGGER.warning(f"Failed to load file {new}: {e}") - pass - - LOGGER.debug(f"Finished loading {new}") - - # If we do not have access to the document, this won't - # do anything and is unnecessary, but that's okay. - self.enable_controls() - - if file_input_box.document is None: - # If we don't have access to the document, we can't disable - # the controls, so just do it. - do_switch_pickle() - else: - # disable the controls, and ask the document to do the update - # on the following event look tick. - self.disable_controls() - file_input_box.document.add_next_tick_callback(do_switch_pickle) - - file_input_box.on_change("value", switch_pickle) - self.bokeh_models["file_input_box"] = file_input_box - - def _set_scheduler(self, scheduler): - LOGGER.info("Setting scheduler") - super()._set_scheduler(scheduler) - self.update_tier_selector() - self.update_reward_summary_table_bokeh_model() - - def _set_conditions(self, conditions): - super()._set_conditions(conditions) - self.update_healpix_bokeh_model() - self.update_reward_table_bokeh_model() - self.update_hovertool_bokeh_model() - self.update_telescope_marker_bokeh_model() - self.update_moon_marker_bokeh_model() - self.update_sun_marker_bokeh_model() - self.update_survey_marker_bokeh_model() - self.update_chosen_survey_bokeh_model() - self.update_mjd_slider_bokeh_model() - self.update_time_input_box_bokeh_model() - - def make_time_selector(self): - """Create the time selector slider bokeh model.""" - time_selector = bokeh.models.Slider( - title="MJD", - start=self.mjd - 1, - end=self.mjd + 1, - value=self.mjd, - step=1.0 / 1440, - ) - - def switch_time(attrname, old, new): - if time_selector.document is None: - # If we don't have access to the document, we can't disable - # the controls, so don't try. - self.mjd = new - else: - # To disable controls as the time is being updated, we need to - # separate the callback so it happens in two event loop ticks: - # the first tick disables the controls, the next one - # actually updates the MJD and then re-enables the controls. - def do_switch_time(): - self.mjd = new - self.enable_controls() - - self.disable_controls() - time_selector.document.add_next_tick_callback(do_switch_time) - - time_selector.on_change("value_throttled", switch_time) - self.bokeh_models["time_selector"] = time_selector - self.update_time_selector_bokeh_model() - - def update_time_selector_bokeh_model(self): - """Update the time selector limits and value to match the date.""" - if "time_selector" in self.bokeh_models: - self.bokeh_models["time_selector"].start = self.conditions.sun_n12_setting - self.bokeh_models["time_selector"].end = self.conditions.sun_n12_rising - self.bokeh_models["time_selector"].value = self.conditions.mjd - - def add_mjd_slider_callback(self): - """Create the mjd slider bokeh model.""" - mjd_slider = self.bokeh_models["mjd_slider"] - - def switch_time(attrname, old, new): - if mjd_slider.document is None: - # If we don't have access to the document, we can't disable - # the controls, so don't try. - self.mjd = new - else: - # To disable controls as the time is being updated, we need to - # separate the callback so it happens in two event loop ticks: - # the first tick disables the controls, the next one - # actually updates the MJD and then re-enables the controls. - def do_switch_time(): - self.mjd = new - self.enable_controls() - - self.disable_controls() - mjd_slider.document.add_next_tick_callback(do_switch_time) - - mjd_slider.on_change("value_throttled", switch_time) - self.update_time_selector_bokeh_model() - - def update_mjd_slider_bokeh_model(self): - """Update the time selector limits and value to match the date.""" - if "mjd_slider" in self.bokeh_models: - self.bokeh_models["mjd_slider"].start = self.conditions.sun_n12_setting - self.bokeh_models["mjd_slider"].end = self.conditions.sun_n12_rising - self.bokeh_models["mjd_slider"].value = self.conditions.mjd - - def make_time_input_box(self): - """Create the time entry box bokeh model.""" - time_input_box = bokeh.models.TextInput(title="Date and time (UTC):") - self.bokeh_models["time_input_box"] = time_input_box - self.update_time_input_box_bokeh_model() - - def switch_time(attrname, old, new): - new_mjd = pd.to_datetime(new, utc=True).to_julian_date() - 2400000.5 - - if time_input_box.document is None: - # If we don't have access to the document, we can't disable - # the controls, so don't try. - self.mjd = new_mjd - else: - # To disable controls as the time is being updated, we need to - # separate the callback so it happens in two event loop ticks: - # the first tick disables the controls, the next one - # actually updates the MJD and then re-enables the controls. - def do_switch_time(): - self.mjd = new_mjd - self.enable_controls() - - self.disable_controls() - time_input_box.document.add_next_tick_callback(do_switch_time) - - time_input_box.on_change("value", switch_time) - - def update_time_input_box_bokeh_model(self): - """Update the time selector limits and value to match the date.""" - if "time_input_box" in self.bokeh_models: - iso_time = Time(self.mjd, format="mjd", scale="utc").iso - self.bokeh_models["time_input_box"].value = iso_time - - def make_tier_selector(self): - """Create the tier selector bokeh model.""" - tier_selector = bokeh.models.Select(value=None, options=[None]) - - def switch_tier(attrname, old, new): - self.select_tier(new) - - tier_selector.on_change("value", switch_tier) - self.bokeh_models["tier_selector"] = tier_selector - self.update_tier_selector() - - def update_tier_selector(self): - """Update tier selector to represent tiers for the current survey.""" - if "tier_selector" in self.bokeh_models: - options = self.tier_names - self.bokeh_models["tier_selector"].options = options - self.bokeh_models["tier_selector"].value = options[self.survey_index[0]] - - def select_tier(self, tier): - """Set the tier being displayed.""" - super().select_tier(tier) - self.update_survey_selector() - - def make_survey_selector(self): - """Create the survey selector bokeh model.""" - survey_selector = bokeh.models.Select(value=None, options=[None]) - - def switch_survey(attrname, old, new): - self.select_survey(new) - - survey_selector.on_change("value", switch_survey) - self.bokeh_models["survey_selector"] = survey_selector - - def update_survey_selector(self): - """Uptade the survey selector to the current scheduler and tier.""" - if "survey_selector" in self.bokeh_models: - options = [ - self._unique_survey_name([self.survey_index[0], s]) - for s in range(len(self.scheduler.survey_lists[self.survey_index[0]])) - ] - self.bokeh_models["survey_selector"].options = options - self.bokeh_models["survey_selector"].value = options[self.survey_index[1]] - - def select_survey(self, survey): - """Set the tier being displayed.""" - super().select_survey(survey) - # Note that updating the value selector triggers the - # callback, which updates the maps themselves - self.update_value_selector() - self.update_reward_table_bokeh_model() - self.update_hovertool_bokeh_model() - self.update_survey_marker_bokeh_model() - - def make_value_selector(self): - """Create the bokeh model to select which value to show in maps.""" - value_selector = bokeh.models.Select(value=None, options=[None]) - - def switch_value(attrname, old, new): - self.select_value(new) - - value_selector.on_change("value", switch_value) - self.bokeh_models["value_selector"] = value_selector - - def update_value_selector(self): - """Update the value selector bokeh model to show available options.""" - if "value_selector" in self.bokeh_models: - self.bokeh_models["value_selector"].options = self.map_keys - if self.map_key in self.map_keys: - self.bokeh_models["value_selector"].value = self.map_key - elif self.init_key in self.map_keys: - self.bokeh_models["value_selector"].value = self.init_key - else: - self.bokeh_models["value_selector"].value = self.map_keys[-1] - - def select_value(self, map_key): - """Set the tier being displayed.""" - super().select_value(map_key) - self.update_healpix_bokeh_model() - - def update_time_display_bokeh_model(self): - """Update the value of the displayed time.""" - if "mjd" in self.sliders: - self.update_mjd_slider_bokeh_model() - - if "time_input_box" in self.bokeh_models: - self.update_time_input_box_bokeh_model() - - def update_displayed_value_metadata_bokeh_model(self): - self.update_tier_selector() - - def _select_survey_from_summary_table(self, attr, old, new): - LOGGER.debug("Called select_survey_from_summary_table") - selected_index = new[0] - tier_name = self.data_sources["reward_summary_table"].data["tier"][ - selected_index - ] - survey_name = self.data_sources["reward_summary_table"].data["survey_name"][ - selected_index - ] - - # Update the selectors, and this will run - # the callbacks to do all the updates - self.bokeh_models["tier_selector"].value = tier_name - self.bokeh_models["survey_selector"].value = survey_name - - def make_reward_summary_table(self): - super().make_reward_summary_table() - - self.data_sources["reward_summary_table"].selected.on_change( - "indices", - self._select_survey_from_summary_table, - ) - - def disable_controls(self): - """Disable all controls. - - Intended to be used while plot elements are updating, and the - control therefore do not do what the user probably intends. - """ - LOGGER.info("Disabling controls") - for model in self.bokeh_models.values(): - try: - model.disabled = True - except AttributeError: - pass - - def enable_controls(self): - """Enable all controls.""" - LOGGER.info("Enabling controls") - for model in self.bokeh_models.values(): - try: - model.disabled = False - except AttributeError: - pass - - def make_figure(self): - """Create a bokeh figures showing sky maps for scheduler behavior. - - Returns - ------- - fig : `bokeh.models.layouts.LayoutDOM` - A bokeh figure that can be displayed in a notebook (e.g. with - ``bokeh.io.show``) or used to create a bokeh app. - """ - self.make_sphere_map( - "altaz", - HorizonMap, - "Alt Az", - plot_width=512, - plot_height=512, - decorate=False, - horizon_graticules=True, - ) - - self.bokeh_models["key"] = bokeh.models.Div(text=self.key_markup) - - self.bokeh_models["reward_table_title"] = bokeh.models.Div( - text="

Basis functions for displayed survey

" - ) - self.make_reward_table() - - self.bokeh_models["reward_summary_table_title"] = bokeh.models.Div( - text="

Rewards for all survey schedulers

" - ) - self.make_reward_summary_table() - self.make_chosen_survey() - self.make_value_selector() - self.make_survey_selector() - self.make_tier_selector() - self.make_pickle_entry_box() - - controls = [self.bokeh_models["file_input_box"]] - - if self.observatory is not None: - self.make_time_input_box() - controls.append(self.bokeh_models["time_input_box"]) - - controls += [ - self.bokeh_models["tier_selector"], - self.bokeh_models["survey_selector"], - self.bokeh_models["value_selector"], - ] - - figure = bokeh.layouts.column( - self.bokeh_models["key"], - self.bokeh_models["altaz"], - *controls, - self.bokeh_models["chosen_survey"], - self.bokeh_models["reward_table_title"], - self.bokeh_models["reward_table"], - self.bokeh_models["reward_summary_table_title"], - self.bokeh_models["reward_summary_table"], - ) - - return figure - - def make_figure_with_many_projections(self): - """Create a bokeh figures showing sky maps for scheduler behavior. - - Returns - ------- - fig : `bokeh.models.layouts.LayoutDOM` - A bokeh figure that can be displayed in a notebook (e.g. with - ``bokeh.io.show``) or used to create a bokeh app. - """ - self.make_sphere_map( - "armillary_sphere", - ArmillarySphere, - "Armillary Sphere", - plot_width=512, - plot_height=512, - decorate=True, - ) - self.bokeh_models["alt_slider"] = self.sphere_maps["armillary_sphere"].sliders[ - "alt" - ] - self.bokeh_models["az_slider"] = self.sphere_maps["armillary_sphere"].sliders[ - "az" - ] - self.bokeh_models["mjd_slider"] = self.sphere_maps["armillary_sphere"].sliders[ - "mjd" - ] - # self.bokeh_models["mjd_slider"].visible = False - self.make_sphere_map( - "planisphere", - Planisphere, - "Planisphere", - plot_width=512, - plot_height=512, - decorate=True, - ) - self.make_sphere_map( - "altaz", - HorizonMap, - "Alt Az", - plot_width=512, - plot_height=512, - decorate=False, - horizon_graticules=True, - ) - - if self.include_mollweide: - self.make_sphere_map( - "mollweide", - MollweideMap, - "Mollweide", - plot_width=512, - plot_height=512, - decorate=True, - ) - - self.bokeh_models["key"] = bokeh.models.Div(text=self.key_markup) - - self.bokeh_models["reward_table_title"] = bokeh.models.Div( - text="

Basis functions for displayed survey

" - ) - self.make_reward_table() - - self.bokeh_models["reward_summary_table_title"] = bokeh.models.Div( - text="

Rewards for all survey schedulers

" - ) - self.make_reward_summary_table() - self.make_chosen_survey() - self.make_value_selector() - self.make_survey_selector() - self.make_tier_selector() - self.make_pickle_entry_box() - - # slider was created by SphereMap - self.add_mjd_slider_callback() - - arm_controls = [ - self.bokeh_models["alt_slider"], - self.bokeh_models["az_slider"], - ] - - controls = [self.bokeh_models["file_input_box"]] - - if self.observatory is not None: - self.make_time_input_box() - controls.append(self.bokeh_models["time_input_box"]) - - controls += [ - self.bokeh_models["mjd_slider"], - self.bokeh_models["tier_selector"], - self.bokeh_models["survey_selector"], - self.bokeh_models["value_selector"], - ] - - if self.include_mollweide: - map_column = bokeh.layouts.column( - self.bokeh_models["altaz"], - self.bokeh_models["planisphere"], - self.bokeh_models["armillary_sphere"], - *arm_controls, - self.bokeh_models["mollweide"], - ) - else: - map_column = bokeh.layouts.column( - self.bokeh_models["altaz"], - self.bokeh_models["planisphere"], - self.bokeh_models["armillary_sphere"], - *arm_controls, - ) - - figure = bokeh.layouts.row( - bokeh.layouts.column( - self.bokeh_models["key"], - *controls, - self.bokeh_models["chosen_survey"], - self.bokeh_models["reward_table_title"], - self.bokeh_models["reward_table"], - self.bokeh_models["reward_summary_table_title"], - self.bokeh_models["reward_summary_table"], - ), - map_column, - ) - - return figure - - -def make_scheduler_map_figure( - scheduler_pickle_fname="baseline22_start.pickle.gz", - init_key="AvoidDirectWind", - nside=DEFAULT_NSIDE, -): - """Create a set of bekeh figures showing sky maps for scheduler behavior. - - Parameters - ---------- - scheduler_pickle_fname : `str`, optional - File from which to load the scheduler state. If set to none, look for - the file name in the ``SCHED_PICKLE`` environment variable. - By default None - init_key : `str`, optional - Name of the initial map to show, by default 'AvoidDirectWind' - nside : int, optional - Healpix nside to use for display, by default 32 - - Returns - ------- - fig : `bokeh.models.layouts.LayoutDOM` - A bokeh figure that can be displayed in a notebook (e.g. with - ``bokeh.io.show``) or used to create a bokeh app. - """ - if scheduler_pickle_fname is None: - scheduler_map = SchedulerDisplayApp(nside=nside) - else: - scheduler, conditions = read_scheduler(sample_pickle(scheduler_pickle_fname)) - scheduler.update_conditions(conditions) - scheduler_map = SchedulerDisplayApp(nside=nside, scheduler=scheduler) - - figure = scheduler_map.make_figure() - - return figure - - -def add_scheduler_map_app(doc): - """Add a scheduler map figure to a bokeh document - - Parameters - ---------- - doc : `bokeh.document.document.Document` - The bokeh document to which to add the figure. - """ - figure = make_scheduler_map_figure() - doc.add_root(figure) - - -if __name__.startswith("bokeh_app_"): - doc = bokeh.plotting.curdoc() - add_scheduler_map_app(doc) +doc = bokeh.plotting.curdoc() +add_scheduler_map_app(doc) diff --git a/schedview/app/sched_maps/sched_maps.py b/schedview/app/sched_maps/sched_maps.py new file mode 100644 index 00000000..9946039a --- /dev/null +++ b/schedview/app/sched_maps/sched_maps.py @@ -0,0 +1,554 @@ +import bokeh.plotting +from astropy.time import Time + +import pandas as pd +import bokeh.models +import bokeh.core.properties + +from schedview.plot.SphereMap import ( + ArmillarySphere, + HorizonMap, + Planisphere, + MollweideMap, +) + +from schedview.collect import sample_pickle + +from schedview.plot.scheduler import SchedulerDisplay +from schedview.collect import read_scheduler +from schedview.plot.scheduler import LOGGER, DEFAULT_NSIDE + + +class SchedulerDisplayApp(SchedulerDisplay): + include_mollweide = False + + def make_pickle_entry_box(self): + """Make the entry box for a file name from which to load state.""" + file_input_box = bokeh.models.TextInput( + value=sample_pickle("auxtel59628.pickle.gz") + " ", + title="Pickle path:", + ) + + def switch_pickle(attrname, old, new): + def do_switch_pickle(): + LOGGER.info(f"Loading {new}.") + try: + # load updates the survey & conditions, which updates + # bokeh models... + self.load(new) + except Exception as e: + LOGGER.warning(f"Failed to load file {new}: {e}") + pass + + LOGGER.debug(f"Finished loading {new}") + + # If we do not have access to the document, this won't + # do anything and is unnecessary, but that's okay. + self.enable_controls() + + if file_input_box.document is None: + # If we don't have access to the document, we can't disable + # the controls, so just do it. + do_switch_pickle() + else: + # disable the controls, and ask the document to do the update + # on the following event look tick. + self.disable_controls() + file_input_box.document.add_next_tick_callback(do_switch_pickle) + + file_input_box.on_change("value", switch_pickle) + self.bokeh_models["file_input_box"] = file_input_box + + def _set_scheduler(self, scheduler): + LOGGER.info("Setting scheduler") + super()._set_scheduler(scheduler) + self.update_tier_selector() + self.update_reward_summary_table_bokeh_model() + + def _set_conditions(self, conditions): + super()._set_conditions(conditions) + self.update_healpix_bokeh_model() + self.update_reward_table_bokeh_model() + self.update_hovertool_bokeh_model() + self.update_telescope_marker_bokeh_model() + self.update_moon_marker_bokeh_model() + self.update_sun_marker_bokeh_model() + self.update_survey_marker_bokeh_model() + self.update_chosen_survey_bokeh_model() + self.update_mjd_slider_bokeh_model() + self.update_time_input_box_bokeh_model() + + def make_time_selector(self): + """Create the time selector slider bokeh model.""" + time_selector = bokeh.models.Slider( + title="MJD", + start=self.mjd - 1, + end=self.mjd + 1, + value=self.mjd, + step=1.0 / 1440, + ) + + def switch_time(attrname, old, new): + if time_selector.document is None: + # If we don't have access to the document, we can't disable + # the controls, so don't try. + self.mjd = new + else: + # To disable controls as the time is being updated, we need to + # separate the callback so it happens in two event loop ticks: + # the first tick disables the controls, the next one + # actually updates the MJD and then re-enables the controls. + def do_switch_time(): + self.mjd = new + self.enable_controls() + + self.disable_controls() + time_selector.document.add_next_tick_callback(do_switch_time) + + time_selector.on_change("value_throttled", switch_time) + self.bokeh_models["time_selector"] = time_selector + self.update_time_selector_bokeh_model() + + def update_time_selector_bokeh_model(self): + """Update the time selector limits and value to match the date.""" + if "time_selector" in self.bokeh_models: + self.bokeh_models["time_selector"].start = self.conditions.sun_n12_setting + self.bokeh_models["time_selector"].end = self.conditions.sun_n12_rising + self.bokeh_models["time_selector"].value = self.conditions.mjd + + def add_mjd_slider_callback(self): + """Create the mjd slider bokeh model.""" + mjd_slider = self.bokeh_models["mjd_slider"] + + def switch_time(attrname, old, new): + if mjd_slider.document is None: + # If we don't have access to the document, we can't disable + # the controls, so don't try. + self.mjd = new + else: + # To disable controls as the time is being updated, we need to + # separate the callback so it happens in two event loop ticks: + # the first tick disables the controls, the next one + # actually updates the MJD and then re-enables the controls. + def do_switch_time(): + self.mjd = new + self.enable_controls() + + self.disable_controls() + mjd_slider.document.add_next_tick_callback(do_switch_time) + + mjd_slider.on_change("value_throttled", switch_time) + self.update_time_selector_bokeh_model() + + def update_mjd_slider_bokeh_model(self): + """Update the time selector limits and value to match the date.""" + if "mjd_slider" in self.bokeh_models: + self.bokeh_models["mjd_slider"].start = self.conditions.sun_n12_setting + self.bokeh_models["mjd_slider"].end = self.conditions.sun_n12_rising + self.bokeh_models["mjd_slider"].value = self.conditions.mjd + + def make_time_input_box(self): + """Create the time entry box bokeh model.""" + time_input_box = bokeh.models.TextInput(title="Date and time (UTC):") + self.bokeh_models["time_input_box"] = time_input_box + self.update_time_input_box_bokeh_model() + + def switch_time(attrname, old, new): + new_mjd = pd.to_datetime(new, utc=True).to_julian_date() - 2400000.5 + + if time_input_box.document is None: + # If we don't have access to the document, we can't disable + # the controls, so don't try. + self.mjd = new_mjd + else: + # To disable controls as the time is being updated, we need to + # separate the callback so it happens in two event loop ticks: + # the first tick disables the controls, the next one + # actually updates the MJD and then re-enables the controls. + def do_switch_time(): + self.mjd = new_mjd + self.enable_controls() + + self.disable_controls() + time_input_box.document.add_next_tick_callback(do_switch_time) + + time_input_box.on_change("value", switch_time) + + def update_time_input_box_bokeh_model(self): + """Update the time selector limits and value to match the date.""" + if "time_input_box" in self.bokeh_models: + iso_time = Time(self.mjd, format="mjd", scale="utc").iso + self.bokeh_models["time_input_box"].value = iso_time + + def make_tier_selector(self): + """Create the tier selector bokeh model.""" + tier_selector = bokeh.models.Select(value=None, options=[None]) + + def switch_tier(attrname, old, new): + self.select_tier(new) + + tier_selector.on_change("value", switch_tier) + self.bokeh_models["tier_selector"] = tier_selector + self.update_tier_selector() + + def update_tier_selector(self): + """Update tier selector to represent tiers for the current survey.""" + if "tier_selector" in self.bokeh_models: + options = self.tier_names + self.bokeh_models["tier_selector"].options = options + self.bokeh_models["tier_selector"].value = options[self.survey_index[0]] + + def select_tier(self, tier): + """Set the tier being displayed.""" + super().select_tier(tier) + self.update_survey_selector() + + def make_survey_selector(self): + """Create the survey selector bokeh model.""" + survey_selector = bokeh.models.Select(value=None, options=[None]) + + def switch_survey(attrname, old, new): + self.select_survey(new) + + survey_selector.on_change("value", switch_survey) + self.bokeh_models["survey_selector"] = survey_selector + + def update_survey_selector(self): + """Uptade the survey selector to the current scheduler and tier.""" + if "survey_selector" in self.bokeh_models: + options = [ + self._unique_survey_name([self.survey_index[0], s]) + for s in range(len(self.scheduler.survey_lists[self.survey_index[0]])) + ] + self.bokeh_models["survey_selector"].options = options + self.bokeh_models["survey_selector"].value = options[self.survey_index[1]] + + def select_survey(self, survey): + """Set the tier being displayed.""" + super().select_survey(survey) + # Note that updating the value selector triggers the + # callback, which updates the maps themselves + self.update_value_selector() + self.update_reward_table_bokeh_model() + self.update_hovertool_bokeh_model() + self.update_survey_marker_bokeh_model() + + def make_value_selector(self): + """Create the bokeh model to select which value to show in maps.""" + value_selector = bokeh.models.Select(value=None, options=[None]) + + def switch_value(attrname, old, new): + self.select_value(new) + + value_selector.on_change("value", switch_value) + self.bokeh_models["value_selector"] = value_selector + + def update_value_selector(self): + """Update the value selector bokeh model to show available options.""" + if "value_selector" in self.bokeh_models: + self.bokeh_models["value_selector"].options = self.map_keys + if self.map_key in self.map_keys: + self.bokeh_models["value_selector"].value = self.map_key + elif self.init_key in self.map_keys: + self.bokeh_models["value_selector"].value = self.init_key + else: + self.bokeh_models["value_selector"].value = self.map_keys[-1] + + def select_value(self, map_key): + """Set the tier being displayed.""" + super().select_value(map_key) + self.update_healpix_bokeh_model() + + def update_time_display_bokeh_model(self): + """Update the value of the displayed time.""" + if "mjd" in self.sliders: + self.update_mjd_slider_bokeh_model() + + if "time_input_box" in self.bokeh_models: + self.update_time_input_box_bokeh_model() + + def update_displayed_value_metadata_bokeh_model(self): + self.update_tier_selector() + + def _select_survey_from_summary_table(self, attr, old, new): + LOGGER.debug("Called select_survey_from_summary_table") + selected_index = new[0] + tier_name = self.data_sources["reward_summary_table"].data["tier"][ + selected_index + ] + survey_name = self.data_sources["reward_summary_table"].data["survey_name"][ + selected_index + ] + + # Update the selectors, and this will run + # the callbacks to do all the updates + self.bokeh_models["tier_selector"].value = tier_name + self.bokeh_models["survey_selector"].value = survey_name + + def make_reward_summary_table(self): + super().make_reward_summary_table() + + self.data_sources["reward_summary_table"].selected.on_change( + "indices", + self._select_survey_from_summary_table, + ) + + def disable_controls(self): + """Disable all controls. + + Intended to be used while plot elements are updating, and the + control therefore do not do what the user probably intends. + """ + LOGGER.info("Disabling controls") + for model in self.bokeh_models.values(): + try: + model.disabled = True + except AttributeError: + pass + + def enable_controls(self): + """Enable all controls.""" + LOGGER.info("Enabling controls") + for model in self.bokeh_models.values(): + try: + model.disabled = False + except AttributeError: + pass + + def make_figure(self): + """Create a bokeh figures showing sky maps for scheduler behavior. + + Returns + ------- + fig : `bokeh.models.layouts.LayoutDOM` + A bokeh figure that can be displayed in a notebook (e.g. with + ``bokeh.io.show``) or used to create a bokeh app. + """ + self.make_sphere_map( + "altaz", + HorizonMap, + "Alt Az", + plot_width=512, + plot_height=512, + decorate=False, + horizon_graticules=True, + ) + + self.bokeh_models["key"] = bokeh.models.Div(text=self.key_markup) + + self.bokeh_models["reward_table_title"] = bokeh.models.Div( + text="

Basis functions for displayed survey

" + ) + self.make_reward_table() + + self.bokeh_models["reward_summary_table_title"] = bokeh.models.Div( + text="

Rewards for all survey schedulers

" + ) + self.make_reward_summary_table() + self.make_chosen_survey() + self.make_value_selector() + self.make_survey_selector() + self.make_tier_selector() + self.make_pickle_entry_box() + + controls = [self.bokeh_models["file_input_box"]] + + if self.observatory is not None: + self.make_time_input_box() + controls.append(self.bokeh_models["time_input_box"]) + + controls += [ + self.bokeh_models["tier_selector"], + self.bokeh_models["survey_selector"], + self.bokeh_models["value_selector"], + ] + + figure = bokeh.layouts.column( + self.bokeh_models["key"], + self.bokeh_models["altaz"], + *controls, + self.bokeh_models["chosen_survey"], + self.bokeh_models["reward_table_title"], + self.bokeh_models["reward_table"], + self.bokeh_models["reward_summary_table_title"], + self.bokeh_models["reward_summary_table"], + ) + + return figure + + def make_figure_with_many_projections(self): + """Create a bokeh figures showing sky maps for scheduler behavior. + + Returns + ------- + fig : `bokeh.models.layouts.LayoutDOM` + A bokeh figure that can be displayed in a notebook (e.g. with + ``bokeh.io.show``) or used to create a bokeh app. + """ + self.make_sphere_map( + "armillary_sphere", + ArmillarySphere, + "Armillary Sphere", + plot_width=512, + plot_height=512, + decorate=True, + ) + self.bokeh_models["alt_slider"] = self.sphere_maps["armillary_sphere"].sliders[ + "alt" + ] + self.bokeh_models["az_slider"] = self.sphere_maps["armillary_sphere"].sliders[ + "az" + ] + self.bokeh_models["mjd_slider"] = self.sphere_maps["armillary_sphere"].sliders[ + "mjd" + ] + # self.bokeh_models["mjd_slider"].visible = False + self.make_sphere_map( + "planisphere", + Planisphere, + "Planisphere", + plot_width=512, + plot_height=512, + decorate=True, + ) + self.make_sphere_map( + "altaz", + HorizonMap, + "Alt Az", + plot_width=512, + plot_height=512, + decorate=False, + horizon_graticules=True, + ) + + if self.include_mollweide: + self.make_sphere_map( + "mollweide", + MollweideMap, + "Mollweide", + plot_width=512, + plot_height=512, + decorate=True, + ) + + self.bokeh_models["key"] = bokeh.models.Div(text=self.key_markup) + + self.bokeh_models["reward_table_title"] = bokeh.models.Div( + text="

Basis functions for displayed survey

" + ) + self.make_reward_table() + + self.bokeh_models["reward_summary_table_title"] = bokeh.models.Div( + text="

Rewards for all survey schedulers

" + ) + self.make_reward_summary_table() + self.make_chosen_survey() + self.make_value_selector() + self.make_survey_selector() + self.make_tier_selector() + self.make_pickle_entry_box() + + # slider was created by SphereMap + self.add_mjd_slider_callback() + + arm_controls = [ + self.bokeh_models["alt_slider"], + self.bokeh_models["az_slider"], + ] + + controls = [self.bokeh_models["file_input_box"]] + + if self.observatory is not None: + self.make_time_input_box() + controls.append(self.bokeh_models["time_input_box"]) + + controls += [ + self.bokeh_models["mjd_slider"], + self.bokeh_models["tier_selector"], + self.bokeh_models["survey_selector"], + self.bokeh_models["value_selector"], + ] + + if self.include_mollweide: + map_column = bokeh.layouts.column( + self.bokeh_models["altaz"], + self.bokeh_models["planisphere"], + self.bokeh_models["armillary_sphere"], + *arm_controls, + self.bokeh_models["mollweide"], + ) + else: + map_column = bokeh.layouts.column( + self.bokeh_models["altaz"], + self.bokeh_models["planisphere"], + self.bokeh_models["armillary_sphere"], + *arm_controls, + ) + + figure = bokeh.layouts.row( + bokeh.layouts.column( + self.bokeh_models["key"], + *controls, + self.bokeh_models["chosen_survey"], + self.bokeh_models["reward_table_title"], + self.bokeh_models["reward_table"], + self.bokeh_models["reward_summary_table_title"], + self.bokeh_models["reward_summary_table"], + ), + map_column, + ) + + return figure + + +def make_scheduler_map_figure( + scheduler_pickle_fname="baseline22_start.pickle.gz", + init_key="AvoidDirectWind", + nside=DEFAULT_NSIDE, +): + """Create a set of bekeh figures showing sky maps for scheduler behavior. + + Parameters + ---------- + scheduler_pickle_fname : `str`, optional + File from which to load the scheduler state. If set to none, look for + the file name in the ``SCHED_PICKLE`` environment variable. + By default None + init_key : `str`, optional + Name of the initial map to show, by default 'AvoidDirectWind' + nside : int, optional + Healpix nside to use for display, by default 32 + + Returns + ------- + fig : `bokeh.models.layouts.LayoutDOM` + A bokeh figure that can be displayed in a notebook (e.g. with + ``bokeh.io.show``) or used to create a bokeh app. + """ + if scheduler_pickle_fname is None: + scheduler_map = SchedulerDisplayApp(nside=nside) + else: + scheduler, conditions = read_scheduler(sample_pickle(scheduler_pickle_fname)) + scheduler.update_conditions(conditions) + scheduler_map = SchedulerDisplayApp(nside=nside, scheduler=scheduler) + + figure = scheduler_map.make_figure() + + return figure + + +def add_scheduler_map_app(doc): + """Add a scheduler map figure to a bokeh document + + Parameters + ---------- + doc : `bokeh.document.document.Document` + The bokeh document to which to add the figure. + """ + figure = make_scheduler_map_figure() + doc.add_root(figure) + + +if __name__.startswith("bokeh_app_"): + doc = bokeh.plotting.curdoc() + add_scheduler_map_app(doc) diff --git a/schedview/app/start.py b/schedview/app/start.py new file mode 100644 index 00000000..528bbbc0 --- /dev/null +++ b/schedview/app/start.py @@ -0,0 +1,26 @@ +import bokeh.command.bootstrap +from pathlib import Path + + +def start_app(app_name): + """Start a bokeh app. + + Parameters + ---------- + app_name : `str` + The name of the bokeh app (and submodule of schedview.app) + + """ + base_dir = Path(__file__).resolve().parent + app_dir = Path(base_dir, app_name).as_posix() + bokeh.command.bootstrap.main(["bokeh", "serve", app_dir]) + + +def sched_maps(): + """Start the sched_maps app.""" + start_app("sched_maps") + + +def metric_maps(): + """Start the metric_maps app.""" + start_app("metric_maps")