Source code for schedview.plot.nightly

"""Plots that summarize a night's visits and other parameters."""

import bokeh
import bokeh.models
import colorcet
import numpy as np

from .visitmap import BAND_COLORS

DEFAULT_EVENT_LABELS = {
    "sunset": "sunset",
    "sun_n12_setting": "sun alt=-12" + "\u00B0",
    "sun_n18_setting": "sun alt=-18" + "\u00B0",
    "sun_n18_rising": "sun alt=-18" + "\u00B0",
    "sun_n12_rising": "sun alt=-12" + "\u00B0",
    "sunrise": "sunrise",
    "moonrise": "moonrise",
    "moonset": "moonset",
    "night_middle": None,
}

DEFAULT_EVENT_COLORS = {
    "sunset": "darkblue",
    "sun_n12_setting": "blue",
    "sun_n18_setting": "lightblue",
    "sun_n18_rising": "lightblue",
    "sun_n12_rising": "blue",
    "sunrise": "darkblue",
    "moonrise": "green",
    "moonset": "green",
    "night_middle": None,
}

BAND_SHAPES = {
    "y": "circle",
    "z": "hex",
    "i": "star",
    "r": "square",
    "g": "diamond",
    "u": "triangle",
}


def _visits_tooltips(visits, weather=False):
    deg = "\u00b0"
    tooltips = [
        (
            "Start time",
            "@start_date{%F %T} UTC (mjd=@observationStartMJD{00000.00}, LST=@observationStartLST"
            + deg
            + ")",
        ),
        ("flush by mjd", "@flush_by_mjd{00000.00}"),
        ("Note", "@note"),
        ("Filter", "@filter"),
        (
            "Field coordinates",
            "RA=@fieldRA" + deg + ", Decl=@fieldDec" + deg + ", Az=@azimuth" + deg + ", Alt=@altitude" + deg,
        ),
        ("Parallactic angle", "@paraAngle" + deg),
        ("Rotator angle", "@rotTelPos" + deg),
        ("Rotator angle (backup)", "@rotTelPos_backup" + deg),
        ("Cumulative telescope azimuth", "@cummTelAz" + deg),
        ("Airmass", "@airmass"),
        ("Moon distance", "@moonDistance" + deg),
        (
            "Moon",
            "RA=@sunRA"
            + deg
            + ", Decl=@sunDec"
            + deg
            + ", Az=@sunAz"
            + deg
            + ", Alt=@sunAlt"
            + deg
            + ", phase=@moonPhase"
            + deg,
        ),
        (
            "Sun",
            "RA=@moonRA"
            + deg
            + ", Decl=@moonDec"
            + deg
            + ", Az=@moonAz"
            + deg
            + ", Alt=@moonAlt"
            + deg
            + ", elong=@solarElong"
            + deg,
        ),
        ("Sky brightness", "@skyBrightness mag arcsec^-2"),
        ("Exposure time", "@visitExposureTime seconds (@numExposures exposures)"),
        ("Visit time", "@visitTime seconds"),
        ("Slew distance", "@slewDistance" + deg),
        ("Slew time", "@slewTime seconds"),
        ("Field ID", "@fieldId"),
        ("Proposal ID", "@proposalId"),
        ("Block ID", "@block_id"),
        ("Scripted ID", "@scripted_id"),
    ]

    if weather:
        tooltips += [
            (
                "Seeing",
                '@seeingFwhm500" (500nm), @seeingFwhmEff" (Eff), @seeingFwhmGeom" (Geom)',
            ),
            ("Cloud", "@cloud"),
            ("5-sigma depth", "@fiveSigmaDepth"),
        ]

    return tooltips


def _add_almanac_events(fig, almanac_events, event_labels, event_colors):
    for event, row in almanac_events.iterrows():
        if event_labels[event] is None:
            continue

        event_marker = bokeh.models.Span(location=row.UTC, dimension="height", line_color=event_colors[event])
        fig.add_layout(event_marker)
        event_label = bokeh.models.Label(
            x=row.UTC,
            y=fig.y_range.start,
            text=" " + event_labels[event],
            angle=90,
            angle_units="deg",
            text_color=event_colors[event],
        )
        fig.add_layout(event_label)


[docs] def plot_airmass_vs_time( visits, almanac_events, band_colors=BAND_COLORS, event_labels=DEFAULT_EVENT_LABELS, event_colors=DEFAULT_EVENT_COLORS, figure=None, ): """Plot airmass vs. time for a set of visits Parameters ---------- visits : `pandas.DataFrame` or `bokeh.models.ColumnDataSource` Dataframe or ColumnDataSource containing visit information. almanac_events : `pandas.DataFrame` Dataframe containing almanac events. band_colors : `dict` Mapping of filter names to colors. Default is `BAND_COLORS`. event_labels : `dict` Mapping of almanac events to labels. Default is `DEFAULT_EVENT_LABELS`. event_colors : `dict` Mapping of almanac events to colors. Default is `DEFAULT_EVENT_COLORS`. figure : `bokeh.plotting.Figure` Bokeh figure object to plot on. If None, a new figure will be created. Returns ------- fig : `bokeh.plotting.Figure` Bokeh figure object """ if figure is None: fig = bokeh.plotting.figure( title="Airmass", x_axis_label="Time (UTC)", y_axis_label="Airmass", frame_width=768, frame_height=512, ) else: fig = figure if isinstance(visits, bokeh.models.ColumnDataSource): visits_ds = visits visits = visits_ds.to_df() else: visits_ds = bokeh.models.ColumnDataSource(visits) filter_color_mapper = bokeh.models.CategoricalColorMapper( factors=tuple(band_colors.keys()), palette=tuple(band_colors.values()), name="filter", ) fig.line("start_date", "airmass", source=visits_ds, color="gray") fig.circle( "start_date", "airmass", source=visits_ds, color={"field": "filter", "transform": filter_color_mapper}, legend_field="filter", name="visit_airmass", ) hover_tool = bokeh.models.HoverTool() hover_tool.renderers = fig.select({"name": "visit_airmass"}) hover_tool.tooltips = _visits_tooltips(visits) hover_tool.formatters = {"@start_date": "datetime"} fig.add_tools(hover_tool) plot_airmass_limit = np.round(np.max(visits.airmass), 1) + 0.1 fig.y_range = bokeh.models.Range1d(plot_airmass_limit, 1) fig.xaxis[0].ticker = bokeh.models.DatetimeTicker() fig.xaxis[0].formatter = bokeh.models.DatetimeTickFormatter(hours="%H:%M") fig.add_layout(fig.legend[0], "left") if almanac_events is not None: _add_almanac_events(fig, almanac_events, event_labels, event_colors) return fig
def _make_airmass_tick_formatter(): """Make a bokeh.models.TickFormatter for airmass values""" js_code = """ const cos_zd = Math.sin(tick * Math.PI / 180.0) const a = 462.46 + 2.8121/(cos_zd**2 + 0.22*cos_zd + 0.01) const airmass = Math.sqrt((a*cos_zd)**2 + 2*a + 1) - a * cos_zd return airmass.toFixed(2) """ return bokeh.models.CustomJSTickFormatter(code=js_code)
[docs] def plot_alt_vs_time( visits, almanac_events, band_shapes=BAND_SHAPES, event_labels=DEFAULT_EVENT_LABELS, event_colors=DEFAULT_EVENT_COLORS, figure=None, ): """Plot airmass vs. time for a set of visits Parameters ---------- visits : `pandas.DataFrame` or `bokeh.models.ColumnDataSource` Dataframe or ColumnDataSource containing visit information. almanac_events : `pandas.DataFrame` Dataframe containing almanac events. band_colors : `dict` Mapping of filter names to colors. Default is `BAND_COLORS`. event_labels : `dict` Mapping of almanac events to labels. Default is `DEFAULT_EVENT_LABELS`. event_colors : `dict` Mapping of almanac events to colors. Default is `DEFAULT_EVENT_COLORS`. figure : `bokeh.plotting.Figure` Bokeh figure object to plot on. If None, a new figure will be created. Returns ------- fig : `bokeh.plotting.Figure` Bokeh figure object """ for time_column in "start_date", "observationStartDatetime64": if isinstance(visits, bokeh.models.ColumnDataSource): if time_column in visits.data: break else: if time_column in visits: break if figure is None: fig = bokeh.plotting.figure( title="Altitude", x_axis_label="Time (UTC)", y_axis_label="Altitude", frame_width=512, frame_height=512, ) else: fig = figure if isinstance(visits, bokeh.models.ColumnDataSource): visits_ds = visits else: visits_ds = bokeh.models.ColumnDataSource(visits) note_values = np.unique(visits_ds.data["note"]) too_many_note_values = len(note_values) > len(colorcet.palette["glasbey"]) if not too_many_note_values: note_color_mapper = bokeh.models.CategoricalColorMapper( factors=note_values, palette=colorcet.palette["glasbey"][: len(note_values)], name="note" ) if "note_and_filter" not in visits_ds.column_names: note_and_filter = tuple( [f"{n} in {f}" for n, f in zip(visits_ds.data["note"], visits_ds.data["filter"])] ) visits_ds.add(note_and_filter, "note_and_filter") filter_marker_mapper = bokeh.models.CategoricalMarkerMapper( factors=tuple(band_shapes.keys()), markers=tuple(band_shapes.values()), name="filter", ) fig.line(time_column, "altitude", source=visits_ds, color="gray") if too_many_note_values: fig.scatter( time_column, "altitude", source=visits_ds, marker={"field": "filter", "transform": filter_marker_mapper}, size=5, legend_field="filter", name="visit_altitude", ) else: fig.scatter( time_column, "altitude", source=visits_ds, color={"field": "note", "transform": note_color_mapper}, marker={"field": "filter", "transform": filter_marker_mapper}, size=5, legend_field="note_and_filter", name="visit_altitude", ) hover_tool = bokeh.models.HoverTool() hover_tool.renderers = fig.select({"name": "visit_altitude"}) hover_tool.tooltips = _visits_tooltips(visits) hover_tool.formatters = {"@start_date": "datetime"} fig.add_tools(hover_tool) fig.y_range = bokeh.models.Range1d(0, 90) fig.xaxis[0].ticker = bokeh.models.DatetimeTicker() fig.xaxis[0].formatter = bokeh.models.DatetimeTickFormatter(hours="%H:%M") fig.add_layout(fig.legend[0], "left") fig.yaxis[0].ticker.desired_num_ticks = 10 fig.extra_y_ranges = {"airmass": fig.y_range} fig.add_layout(bokeh.models.LinearAxis(), "right") fig.yaxis[1].ticker.desired_num_ticks = fig.yaxis[0].ticker.desired_num_ticks fig.yaxis[1].formatter = _make_airmass_tick_formatter() fig.yaxis[1].minor_tick_line_alpha = 0 fig.yaxis[1].axis_label = "Airmass" if almanac_events is not None: _add_almanac_events(fig, almanac_events, event_labels, event_colors) return fig
def _add_alt_graticules(fig, transform, min_alt=0, max_alt=90, alt_step=30, label=True): """Add altitude graticules to a figure""" for alt in np.arange(min_alt, max_alt + alt_step, alt_step): azimuth = np.arange(0, 361, 1) zd = 90 - np.full_like(azimuth, alt) graticule_source = bokeh.models.ColumnDataSource({"azimuth": azimuth, "zd": zd}) fig.line( transform.y, transform.x, source=graticule_source, color="gray", line_width=1, line_alpha=0.5, ) if label and alt < 90: label_source = bokeh.models.ColumnDataSource( {"azimuth": [0], "zd": [90 - alt], "label": [f" {alt}\u00b0"]} ) fig.text( transform.y, transform.x, source=label_source, text="label", text_color="gray", text_font_size={"value": "10px"}, text_align="left", text_baseline="top", ) def _add_az_graticules(fig, transform, min_alt=0, min_az=0, max_az=360, az_step=30, label=True): """Add azimuth graticules to a figure""" for azimuth in np.arange(min_az, max_az + az_step, az_step): zd = [0, 90 - min_alt] azimuths = [azimuth, azimuth] graticule_source = bokeh.models.ColumnDataSource({"azimuth": azimuths, "zd": zd}) fig.line( transform.y, transform.x, source=graticule_source, color="gray", line_width=1, line_alpha=0.5, ) if label: if azimuth % 360 == 0: graticule_label = "N" text_align = "center" text_baseline = "bottom" elif azimuth % 360 == 90: graticule_label = "E " text_align = "right" text_baseline = "middle" elif azimuth % 360 == 180: graticule_label = "S" text_align = "center" text_baseline = "top" elif azimuth % 360 == 270: graticule_label = " W" text_align = "left" text_baseline = "middle" else: graticule_label = f"{azimuth}\u00b0" text_align = "right" if azimuth < 180 else "left" text_baseline = "bottom" if azimuth < 90 or azimuth > 270 else "top" label_source = bokeh.models.ColumnDataSource( {"azimuth": [azimuth], "zd": [90 - min_alt], "label": [graticule_label]} ) fig.text( transform.y, transform.x, source=label_source, text="label", text_color="gray", text_font_size={"value": "10px"}, text_align=text_align, text_baseline=text_baseline, )
[docs] def plot_polar_alt_az(visits, band_shapes=BAND_SHAPES, figure=None, legend=True): """Plot airmass vs. time for a set of visits Parameters ---------- visits : `pandas.DataFrame` or `bokeh.models.ColumnDataSource` Dataframe or ColumnDataSource containing visit information. almanac_events : `pandas.DataFrame` Dataframe containing almanac events. band_colors : `dict` Mapping of filter names to colors. Default is `BAND_COLORS`. figure : `bokeh.plotting.Figure` Bokeh figure object to plot on. If None, a new figure will be created. legend : `bool` Generate a legend. Default is True. Returns ------- fig : `bokeh.plotting.Figure` Bokeh figure object """ if figure is None: fig = bokeh.plotting.figure( title="Horizon Coordinates", x_axis_type=None, y_axis_type=None, frame_width=512, frame_height=512, ) else: fig = figure if isinstance(visits, bokeh.models.ColumnDataSource): visits_ds = visits else: visits_ds = bokeh.models.ColumnDataSource(visits) if "HA" not in visits_ds.column_names: hour_angle = ( (np.array(visits_ds.data["observationStartLST"]) - np.array(visits_ds.data["fieldRA"])) * 24.0 / 360.0 ) hour_angle = np.mod(hour_angle + 12.0, 24) - 12 visits_ds.add(hour_angle, "HA") if "zd" not in visits_ds.column_names: visits_ds.add(90 - np.array(visits_ds.data["altitude"]), "zd") note_values = np.unique(visits_ds.data["note"]) too_many_note_values = len(note_values) > len(colorcet.palette["glasbey"]) if not too_many_note_values: note_color_mapper = bokeh.models.CategoricalColorMapper( factors=note_values, palette=colorcet.palette["glasbey"][: len(note_values)], name="note" ) if "note_and_filter" not in visits_ds.column_names: note_and_filter = tuple( [f"{n} in {f}" for n, f in zip(visits_ds.data["note"], visits_ds.data["filter"])] ) visits_ds.add(note_and_filter, "note_and_filter") filter_marker_mapper = bokeh.models.CategoricalMarkerMapper( factors=tuple(band_shapes.keys()), markers=tuple(band_shapes.values()), name="filter", ) polar_transform = bokeh.models.PolarTransform( angle="azimuth", radius="zd", angle_units="deg", direction="clock" ) fig.line(polar_transform.y, polar_transform.x, source=visits_ds, color="gray") if too_many_note_values: fig.scatter( polar_transform.y, polar_transform.x, source=visits_ds, marker={"field": "filter", "transform": filter_marker_mapper}, size=5, legend_field="filter", name="visit_altitude", ) else: fig.scatter( polar_transform.y, polar_transform.x, source=visits_ds, color={"field": "note", "transform": note_color_mapper}, marker={"field": "filter", "transform": filter_marker_mapper}, size=5, legend_field="note_and_filter", name="visit_altitude", ) _add_alt_graticules(fig, polar_transform) _add_az_graticules(fig, polar_transform) hover_tool = bokeh.models.HoverTool() hover_tool.renderers = fig.select({"name": "visit_altitude"}) hover_tool.tooltips = _visits_tooltips(visits) hover_tool.formatters = {"@start_date": "datetime"} fig.add_tools(hover_tool) if legend: fig.add_layout(fig.legend[0], "left") else: fig.legend.visible = False return fig