About schedview.plot.visit_skymaps¶
This notebook demonstrates the use of schedview.plot.visit_skymaps.VisitMapBuilder.
To convert this page to an html file: jupyter nbconvert --to html --execute visit_skymaps.ipynb
Imports and notebook setup¶
Imports of general infrastructure¶
import sys
import os
from pathlib import Path
import bokeh.io
import numpy as np
Adjust PYTHONPATH to point to development directories for schedview and uranography, if desired¶
If you want to use the version provided by the current kernel, just leave devel_dir as None.
Otherwise, update devel_dir as appropriate.
devel_dir = None
# devel_dir = Path('/sdf/data/rubin/user/neilsen/devel')
if devel_dir is not None:
for module_name in ("schedview", "uranography", "rubin_sim"):
module_path = devel_dir.joinpath(module_name)
if module_path.exists():
sys.path.insert(0, module_path.as_posix())
Import project specific modules¶
Use autoreload for uranography and schedview to support development.
%load_ext autoreload
%autoreload 1
from rubin_sim.data import get_baseline
from rubin_scheduler.scheduler.utils import get_current_footprint
%aimport uranography.spheremap
%aimport uranography.planisphere
%aimport uranography.armillary
%aimport uranography.camera
%aimport uranography.horizon
%aimport uranography.mollweide
%aimport schedview.plot.visit_skymaps
%aimport schedview.collect
/sdf/data/rubin/user/neilsen/devel/schedview/schedview/clientsite.py:37: UserWarning: Could not determine site from EXTERNAL_INSTANCE_URL .
warn(f"Could not determine site from EXTERNAL_INSTANCE_URL {location}.")
Set basic parameters¶
# Night of the baseline to use for example data.
# For example, 365 is one year into the survey.
night = 365
# healpy nside for maps to be shown.
nside = 64
Prepare notebook for bokeh output¶
Read sample data¶
visits = (
schedview.collect.read_opsim(get_baseline())
.reset_index()
.set_index("night")
.loc[[night, night + 1, night + 2], :]
.reset_index()
.set_index("observationId")
.copy()
)
footprint_depth_by_band, footprint_regions = get_current_footprint(nside)
healpy INFO: Sigma is 254.796540 arcmin (0.074117 rad)
healpy INFO: -> fwhm is 600.000000 arcmin
healpy INFO: Sigma is 0.000000 arcmin (0.000000 rad)
healpy INFO: -> fwhm is 0.000000 arcmin
Minimal example¶
The VisitMapBuilder uses a "fluent method chaining" builder pattern interface: the calling function creates an instance of VisitMapBuilder, followed by a sequence of method calls that set the various desired features and parameters, and finally a call to build which returns an instance of the desired object. In this case, the object returned by build is an instance of bokeh.models.UIElement, a bokeh object that can be rendered with bokeh.io functions to a file, in an application interface or on a web page, or in a jupyter notebook.
builder = (
schedview.plot.visit_skymaps.VisitMapBuilder(
map_classes=[uranography.armillary.ArmillarySphere, uranography.planisphere.Planisphere],
)
.add_graticules()
)
viewable = builder.build()
bokeh.io.show(viewable)
Example with many elements¶
Methods can be called to provide additional data, features and controls:
builder = (
schedview.plot.visit_skymaps.VisitMapBuilder(
visits,
mjd=visits["observationStartMJD"].max(),
map_classes=[uranography.armillary.ArmillarySphere, uranography.planisphere.Planisphere],
figure_kwargs={"match_aspect": True},
)
.add_visit_patches()
.add_footprint_outlines(footprint_regions)
.hide_horizon_sliders()
.make_up_north()
.add_eq_sliders()
.add_graticules()
.add_ecliptic()
.add_galactic_plane()
.add_datetime_slider()
.hide_mjd_slider()
.hide_future_and_other_night_visits()
.highlight_recent_visits()
.add_body("sun", size=15, color="yellow", alpha=1.0)
.add_body("moon", size=15, color="orange", alpha=0.8)
.add_horizon()
.add_horizon(zd=70, color="red")
.add_hovertext()
)
viewable = builder.build()
bokeh.io.show(viewable)
WARNING: Tried to get polar motions for times after IERS data is valid. Defaulting to polar motion from the 50-yr mean for those. This may affect precision at the arcsec level. Please check your astropy.utils.iers.conf.iers_auto_url and point it to a newer version if necessary. [astropy.coordinates.builtin_frames.utils]
astropy WARNING: Tried to get polar motions for times after IERS data is valid. Defaulting to polar motion from the 50-yr mean for those. This may affect precision at the arcsec level. Please check your astropy.utils.iers.conf.iers_auto_url and point it to a newer version if necessary.
WARNING: Tried to get polar motions for times after IERS data is valid. Defaulting to polar motion from the 50-yr mean for those. This may affect precision at the arcsec level. Please check your astropy.utils.iers.conf.iers_auto_url and point it to a newer version if necessary. [astropy.coordinates.builtin_frames.utils]
astropy WARNING: Tried to get polar motions for times after IERS data is valid. Defaulting to polar motion from the 50-yr mean for those. This may affect precision at the arcsec level. Please check your astropy.utils.iers.conf.iers_auto_url and point it to a newer version if necessary.
Sample with custom layout¶
Simple adjustments to the layout can be made by passing a different class in the bokeh.layout module to the layout argument of VisitMapBuilder.build.
More complex layout adjustments can be made by making subclass of VisitMapBuilder to repalce the build method with one that builds whatever layout is desired, for example:
class CustomVisitMapBuilder(schedview.plot.visit_skymaps.VisitMapBuilder):
def build(self) -> bokeh.models.UIElement:
# To have the plots that depend on them update when the
# alt, az, RA, Decl, or MJD sliders are adjusted, the
# `_connect_controls()` method needs to be called in
# the `build` method.
self._connect_controls()
map_column_contents = [bokeh.models.Div(text="<h2>Maps</h2>")]
control_column_contents = [bokeh.models.Div(text="<h2>Controls</h2>")]
# Some controls are associated with multiple plots.
# "shown_control_names" keepn track of which ones have been shown
# already on previous plots so we don't show shared controls
# multiple times.
shown_control_names = set()
for sphere_map in self.spheremaps:
# force_update_time is an invisible div that
# needs to be included somewhere or bad things happen.
map_column_contents.append(sphere_map.force_update_time)
# The .plot member of each SphereMap (or subclass) instance
# holds the instance of bokeh.plotting.figure (a subclass
# of bokeh.models.plots.Plot) that holds the figure itself.
map_column_contents.append(sphere_map.plot)
# The bokeh models with the controls associated
# with a map are in a dictionar in the "controls"
# attribute.
for control_name in sphere_map.visible_control_names:
if control_name not in shown_control_names:
control_column_contents.append(sphere_map.controls[control_name])
shown_control_names.add(control_name)
figure = bokeh.layouts.column(
[
bokeh.models.Div(text="<h2>My custom figure output</h2>"),
bokeh.layouts.row(
[bokeh.layouts.column(map_column_contents), bokeh.layouts.column(control_column_contents)]
),
]
)
return figure
builder = (
CustomVisitMapBuilder(
visits,
mjd=visits["observationStartMJD"].max(),
map_classes=[uranography.planisphere.Planisphere, uranography.armillary.ArmillarySphere],
)
.add_visit_patches()
.add_graticules()
.hide_future_visits()
.add_horizon()
.add_horizon(zd=70, color="red")
)
viewable = builder.build()
bokeh.io.show(viewable)
WARNING: Tried to get polar motions for times after IERS data is valid. Defaulting to polar motion from the 50-yr mean for those. This may affect precision at the arcsec level. Please check your astropy.utils.iers.conf.iers_auto_url and point it to a newer version if necessary. [astropy.coordinates.builtin_frames.utils]
astropy WARNING: Tried to get polar motions for times after IERS data is valid. Defaulting to polar motion from the 50-yr mean for those. This may affect precision at the arcsec level. Please check your astropy.utils.iers.conf.iers_auto_url and point it to a newer version if necessary.
Example with extra points and controls¶
We can extend the VisitMapBuilder with new methods for new types of data or controls with new callbacks either by using uranography calls on the underlying SphereMap instances in the spheremaps attribute, or using the full bokeh API with the plot member of these SphereMap instances.
For example, this example uses the plain bokeh API to add methods to mark the North and South celestial poles, and a toggle button to turn on and off the lables:
import pandas as pd
import bokeh
import bokeh.models
class CustomVisitMapBuilder(schedview.plot.visit_skymaps.VisitMapBuilder):
def add_poles(self):
label_tuples = [('North Celestial Pole', 0.0, 90.0), ('South Celestial Pole', 0.0, -90.0)]
pole_data_source = bokeh.models.ColumnDataSource(pd.DataFrame(label_tuples, columns=['name', 'ra', 'decl']))
# Save the pole bokeh objects to make it
# easier to connect callbacks later.
self.pole_labels = []
# Add new elements to each plot in turn.
for spheremap in self.spheremaps:
# Add markers
spheremap.plot.scatter(
x=spheremap.proj_transform("x", pole_data_source),
y=spheremap.proj_transform("y", pole_data_source),
source=pole_data_source,
fill_color="lightblue",
fill_alpha=0.0,
line_color="gray",
marker="circle_cross",
size=15
)
# Add labels
labels = bokeh.models.LabelSet(
x=spheremap.proj_transform("x", pole_data_source),
y=spheremap.proj_transform("y", pole_data_source),
text="name",
source=pole_data_source,
x_offset=6,
y_offset=6,
text_font_size="10pt",
text_color="black",
background_fill_color="white",
background_fill_alpha=0.6,
border_line_color=None,
)
spheremap.plot.add_layout(labels)
# Save the new labels to the list
# we will update with the callback.
self.pole_labels.append(labels)
# Make sure locations of the markers and labels
# get updated when the user changes the ra/dec or alt/az
# sliders.
spheremap.connect_controls(pole_data_source)
return self
def add_pole_label_toggle(self):
try:
pole_labels = self.pole_labels
except AttributeError:
raise UserError("add_poles must be called before add_pole_label_toggle")
# create a new control
toggle = bokeh.models.Toggle(label="Hide pole labels", button_type="primary", active=True)
# Update the labels when the toggle state is changed.
js_code = """
for (const label of labels) {
label.visible = toggle.active;
}
toggle.label = toggle.active ? "Hide pole labels" : "Show pole labels";
"""
for spheremap in self.spheremaps:
callback = bokeh.models.CustomJS(args=dict(labels=self.pole_labels, toggle=toggle), code=js_code)
toggle.js_on_change("active", callback)
# Add the new toggle to the list of things included
# shown in the layout with the reference map.
self.ref_map.controls['pole_label_toggle'] = toggle
return self
builder = (
CustomVisitMapBuilder(
visits,
mjd=visits["observationStartMJD"].max(),
map_classes=[uranography.armillary.ArmillarySphere, uranography.planisphere.Planisphere],
figure_kwargs={"match_aspect": True},
)
.add_visit_patches()
.add_graticules()
.hide_future_visits()
.add_poles()
.add_pole_label_toggle()
)
viewable = builder.build()
bokeh.io.show(viewable)
Example with customized colors¶
applet_mode = False
Planisphere = uranography.planisphere.Planisphere
ArmillarySphere = uranography.armillary.ArmillarySphere
VisitMapBuilder = schedview.plot.visit_skymaps.VisitMapBuilder
DARK_BAND_COLORS = schedview.plot.colors.LIGHT_PLOT_BAND_COLORS
control_kwargs = {"width": None, "styles": {"color": "#E5E5E5"}}
# Planisphere and ArmillarySphere inherit default_slider_kwargs
# and default_select_kwargs from SphereMap, so setting these
# class attributes on SphereMap take care of both.
uranography.spheremap.SphereMap.default_slider_kwargs = control_kwargs
uranography.spheremap.SphereMap.default_select_kwargs = control_kwargs
nside = 64
footprint_depth_by_band, footprint_regions = get_current_footprint(nside)
maps = [ArmillarySphere, Planisphere] if not applet_mode else [Planisphere]
figure_specs = {"match_aspect": True, "border_fill_color": "#262626", "background_fill_color": "#262626"}
if applet_mode:
figure_specs.update({"width": 340, "height": 220})
tooltips = """
<div style="padding:5px; font-size:12px; line-height:1.2">
<div><strong>Observation ID:</strong> @observationId</div>
<div><strong>Start Timestamp:</strong> @start_timestamp{%F %T} UTC</div>
<div><strong>Band:</strong> @band</div>
<div><strong>RA, Dec:</strong> @fieldRA{0.000}, @fieldDec{0.000}</div>
<div><strong>Observation Reason:</strong> @observation_reason</div>
<div><strong>Science Program:</strong> @science_program</div>
<div><strong>Para Angle:</strong> @paraAngle\u00b0</div>
<div><strong>azimulth, Altitude:</strong> @azimuth\u00b0, @altitude\u00b0</div>
</div>
"""
builder = (
VisitMapBuilder(
visits,
mjd=visits["observationStartMJD"].max(),
map_classes=maps,
visit_fill_colors=DARK_BAND_COLORS,
figure_kwargs=figure_specs,
)
.add_footprint_outlines(footprint_regions, line_width=5)
.add_visit_patches()
.hide_horizon_sliders()
.make_up_north()
.show_up_selector()
.add_eq_sliders()
.add_graticules()
.add_ecliptic()
.add_galactic_plane()
.add_datetime_slider()
.hide_mjd_slider()
.hide_future_and_other_night_visits()
.highlight_recent_visits()
.add_body("sun", size=15, color="yellow", alpha=1.0)
.add_body("moon", size=15, color="orange", alpha=0.8)
.add_horizon(color="#E5E5E5", line_width=5)
.add_horizon(zd=70, color="red", line_width=5)
.add_hovertext(visit_tooltips=tooltips)
)
viewable = builder.build()
bokeh.io.show(viewable)
healpy INFO: Sigma is 254.796540 arcmin (0.074117 rad)
healpy INFO: -> fwhm is 600.000000 arcmin
healpy INFO: Sigma is 0.000000 arcmin (0.000000 rad)
healpy INFO: -> fwhm is 0.000000 arcmin
WARNING: Tried to get polar motions for times after IERS data is valid. Defaulting to polar motion from the 50-yr mean for those. This may affect precision at the arcsec level. Please check your astropy.utils.iers.conf.iers_auto_url and point it to a newer version if necessary. [astropy.coordinates.builtin_frames.utils]
astropy WARNING: Tried to get polar motions for times after IERS data is valid. Defaulting to polar motion from the 50-yr mean for those. This may affect precision at the arcsec level. Please check your astropy.utils.iers.conf.iers_auto_url and point it to a newer version if necessary.
WARNING: Tried to get polar motions for times after IERS data is valid. Defaulting to polar motion from the 50-yr mean for those. This may affect precision at the arcsec level. Please check your astropy.utils.iers.conf.iers_auto_url and point it to a newer version if necessary. [astropy.coordinates.builtin_frames.utils]
astropy WARNING: Tried to get polar motions for times after IERS data is valid. Defaulting to polar motion from the 50-yr mean for those. This may affect precision at the arcsec level. Please check your astropy.utils.iers.conf.iers_auto_url and point it to a newer version if necessary.