Source code for parts.dashboard

import numpy as np

import threading

from bokeh                               import events
from bokeh.colors                        import RGB
from bokeh.server.server                 import Server
from bokeh.application                   import Application
from bokeh.application.handlers.function import FunctionHandler
from bokeh.plotting                      import figure, ColumnDataSource, curdoc
from bokeh.models.widgets                import Div, Tabs, Panel, Select, CheckboxGroup, \
    CheckboxButtonGroup, RadioButtonGroup
from bokeh.models.glyphs                 import Image
from bokeh.models                        import Range1d, LinearColorMapper, ColorBar, \
    BasicTicker, PrintfTickFormatter, FuncTickFormatter, Rect
from bokeh.models.callbacks              import CustomJS
from bokeh.layouts                       import column, row
from bokeh.palettes                      import RdBu, Inferno

from parts.sensor import ActiveSensor, PassiveSensor


[docs]class BokehServer(): """ Handles the singleton Bokeh server used to serve visualisations if no jupyter notebook is open. """ def __init__(self): self.server = None def __del__(self): if not self.server is None: self.server.io_loop.stop() self.server.stop() def start(self, application, port = 5000): if not self.server is None: self.server.io_loop.stop() self.server.stop() self.server = Server(application, port = port) self.server.io_loop.start()
standalone_server = BokehServer() ################################################################################ # Profile plot ################################################################################
[docs]class ProfilePlot: """ A profile plot displays the height distribution of an atmospheric quantity with respect to either altitude or pressure. """ def __init__(self, z, p, title, name, width = 500): def update_y_variable(attrname, old, new): if self.y_axis_quantity.active == 0: self.y = self.z self.y_range = Range1d(start = self.z[0], end = self.z[-1]) if self.y_axis_quantity.active == 1: self.y = self.p self.y_range = Range1d(start = self.p[-1], end = self.p[0]) for s in self.sources: s.data["y"] = self.y fig = curdoc().get_model_by_name(self.name).children[0] fig.y_range = self.y_range def update_x_scale(attrname, old, new): if self.x_axis_scale.active == 1: for s in self.sources: s.data["x"] = np.log10(s.data["x_"]) else: for s in self.sources: s.data["x"] = s.data["x_"] def update_y_scale(attrname, old, new): if self.y_axis_scale.active == 1: for s in self.sources: s.data["y"] = np.log10(self.y) else: for s in self.sources: s.data["y"] = self.y self.z = np.copy(z) self.p = np.copy(p) self.y = z self.quantities = [] self.name = name self.x_axis_scale = RadioButtonGroup(labels = ["linear", "log"], active = 0) self.x_axis_scale.on_change("active", update_x_scale) self.y_axis_scale = RadioButtonGroup(labels = ["linear", "log"], active = 0) self.y_axis_scale.on_change("active", update_y_scale) self.y_axis_quantity = RadioButtonGroup(labels = ["altitude", "pressure"], active = 0) self.y_axis_quantity.on_change("active", update_y_variable) self.sources = [] self.fig = figure(title = title, width = width, height = 500) def add_quantity(self, x, line_kwargs = {}): self.sources += [ColumnDataSource(data = dict(x = x, x_ = x, y = self.y))] self.fig.line("x", "y", source = self.sources[-1], **line_kwargs) @property def doc(self): d1 = Div(text = "y-axis variable:", width = 200) d2 = Div(text = "x-axis scale:", width = 200) d3 = Div(text = "y-axis scale:", width = 200) return column(self.fig, row(d1, self.y_axis_quantity), row(d2, self.x_axis_scale), row(d3, self.y_axis_scale), name = self.name)
class RetrievalResultPlot: def __init__(self, z, p, retrieval): self.z = z self.p = p y = z self.retrieval = retrieval self.retrieval_quantities = retrieval.retrieval_quantities self.figures = dict([(rq, ProfilePlot(self.z, self.p, rq.name, "rq_plot_" + rq.name, width = 400)) for rq in self.retrieval_quantities]) self.status = dict([(rq, True) for rq in self.retrieval_quantities]) self.sources_x = dict([(rq, []) for rq in self.retrieval_quantities]) self.sources_xa = dict([(rq, []) for rq in self.retrieval_quantities]) self.sources_x0 = dict([(rq, []) for rq in self.retrieval_quantities]) self.lines_x = dict([(rq, []) for rq in self.retrieval_quantities]) self.lines_xa = dict([(rq, []) for rq in self.retrieval_quantities]) self.lines_x0 = dict([(rq, []) for rq in self.retrieval_quantities]) # # Get retrieval results as list. # if type(self.retrieval.results) is list: results = self.retrieval.results else: results = [self.retrieval.results] self.colors = Inferno[len(results) + 2][1:-1] # # Add plots for each retrieval quantity. # for i, rr in enumerate(self.retrieval.results): for rq in self.retrieval_quantities: xa = rr.get_result(rq, attribute = "xa") x = rr.get_result(rq, attribute = "x") x0 = rr.get_result(rq, attribute = "x0") if xa is None: continue self.sources_x[rq] += [ColumnDataSource(data = dict(x = x, y = y))] self.sources_xa[rq] += [ColumnDataSource(data = dict(x = xa, y = y))] self.sources_x0[rq] += [ColumnDataSource(data = dict(x = x0, y = y))] fig = self.figures[rq] fig.add_quantity(x, line_kwargs = dict(line_color = self.colors[i], line_width = 2)) if i == 0: fig.add_quantity(xa, line_kwargs = dict(line_dash = "dashed", line_color = self.colors[i], line_width = 2)) #fig.add_quantity(x0, line_kwargs = dict(line_dash = "dashdot", # line_color = self.colors[i], # line_width = 2)) self.plots = row(*[self.figures[k].doc for k in self.figures], name = "retrieval_quantities") def update_plots(attrname, old, new): state = self.checks.active plots = curdoc().get_model_by_name('retrieval_quantities') print(state) for i, rq in enumerate(self.retrieval_quantities): if i in state: if self.status[rq] == True: continue else: self.status[rq] = True plots.children.append(self.figures[rq].doc) else: if self.status[rq] == False: continue else: fig = curdoc().get_model_by_name("rq_plot_" + rq.name) self.status[rq] = False plots.children.remove(fig) print(plots.children) print(self.status) # # Checkbox button to hide plots # labels = [rq.name for rq in self.retrieval.retrieval_quantities] active = list(range(len(labels))) self.checks = CheckboxButtonGroup(labels = labels, active = active) self.checks.on_change("active", update_plots) def make_doc(self): title = Div(text = "<h2>Retrieved quantities</h2>") c = column(title, self.checks, self.plots) return c class AVKPlot: def __init__(self, simulation): self.simulation = simulation self.sources = {} if not type(simulation.retrieval.results) is list: result = simulation.retrieval.results else: result = simulation.retrieval.results[-1] self.figures = {} self.images = {} rqs = simulation.retrieval.retrieval_quantities self.panels = [self._make_avk_plot(result, rq) for rq in rqs] def _make_avk_plot(self, result, rq): avk = result.get_avk(rq) x = np.arange(avk.shape[0]) xx, yy = np.meshgrid(x, x) self.sources[rq] = ColumnDataSource(data = dict(x = xx.ravel(), y = yy.ravel(), z = avk.ravel())) self.figures[rq] = figure(x_range = (x[0] - 0.5, x[-1] + 0.5), y_range = (x[0] - 0.5, x[-1] + 0.5), tools = ["box_zoom", "box_select", "reset"], width = 600, aspect_scale = 1.0, toolbar_location = "above") colors = RdBu[11] clim_high = max(avk.max(), -avk.min()) clim_low = min(-avk.max(), avk.min()) mapper = LinearColorMapper(palette= colors, low = clim_low, high= clim_high) self.images[rq] = self.figures[rq].rect(source = self.sources[rq], x = "x", y = "y", fill_color = {'field' : 'z', 'transform' : mapper}, line_color = None, width = 1, height = 1) #marker_source = ColumnDataSource(data = dict(x = np.array([x[0]]), # y = np.array([x[0]]))) #marker = Rect(x = "x", # y = "y", # fill_color = None, # line_color = RGB(0.0, 0.0, 0.0), # width = x[-1] - x[0], # height = 1) #self.images[rq].add_glyph(marker_source, rect) #axis = self.figures[rq].xaxis #axis.major_label_overrides = dict(zip(axis.ticker, # z[axis.ticker])) #axis = self.figures[rq].yaxis #axis.major_label_overrides = dict(zip(axis.ticker, # z[axis.ticker])) def update(attr, old, new): print(old) print(new) print(self.sources[rq].selected.indices) # # The colorbar # color_bar = ColorBar(color_mapper = mapper, #major_label_text_font_size ="5pt", ticker = BasicTicker(desired_num_ticks = len(colors)), formatter = PrintfTickFormatter(format="%.2f"), label_standoff = 6, border_line_color=None, location=(0, 0)) self.figures[rq].add_layout(color_bar, 'right') # # Single altitude # source = ColumnDataSource(data = dict(x = x, y = avk[0])) fig = figure(tools = ["box_zoom", "box_select", "reset"], width = 400, y_range = [clim_low, clim_high], toolbar_location = "above") line = fig.line(x = "x", y = "y", source = source) # # Plot callback # cb = CustomJS(args = dict(source_avk = self.sources[rq], #source_marker = marker_source, source = source), code = """ var x = Number(cb_obj["x"]); var y = Number(cb_obj["y"]); console.log(x, y); var i = Math.floor(y); var j = i + 1; var n = source.data["x"].length; source.data["y"] = []; for (var k = 0; k < n; k++) { source.data["y"].push(source_avk.data["z"][i * n + k]); } source.change.emit(); console.log(source.data["x"].length); console.log(source.data["y"].length); """) self.figures[rq].js_on_event(events.Tap, cb) r = row(self.figures[rq], fig) return Panel(child = r, title = rq.name, width = 500) def make_doc(self): title = Div(text = "<h2>Averaging kernels</h2>") t = Tabs(tabs = self.panels) return column(title, t) class ObservationPlot: def __init__(self, sensor): self.sensor = sensor self.observations = [] def make_active_sensor_plot(self): fig = figure(title = self.sensor.name, width = 500) z = self.sensor.range_bins x = self.sensor.y z = np.ravel(0.5 * (z[1:] + z[:-1])) if self.observations == []: observations = [sensor.y] else: observations = self.observations for o in observations: x = o.reshape(z.size, -1) y = z // 1e3 fig.line(x = x, y = o) fig.circle(x = x, y = o) fig.xaxis.axis_label = "Radar reflectivity [{0}]"\ .format(self.sensor.iy_unit) fig.yaxis.axis_label = "Altitude [km]" return fig def make_passive_sensor_plot(self): fig = figure(title = self.sensor.name, width = 500) x = self.sensor.f_grid y = self.sensor.y for o in self.observations: fig.line(x = x, y = o) fig.circle(x = x, y = o) return fig def add_observation(self, o): self.observations += [o] def make_doc(self): if isinstance(self.sensor, ActiveSensor): return self.make_active_sensor_plot() else: return self.make_passive_sensor_plot() ################################################################################ # Atmosphere ################################################################################ def plot_temperature(simulation, data_provider): fig = figure(title = "Temperature", width = 500) t = simulation.workspace.t_field.value.ravel() z = simulation.workspace.z_field.value.ravel() / 1e3 fig.line(x = t, y = z) fig.circle(x = t, y = z) fig.xaxis.axis_label = "Temperature [K]" fig.yaxis.axis_label = "Altitude [km]" return fig def plot_absorption_species(simulation, data_provider): fig = figure(title = "Absorption species", width = 500) for a in simulation.atmosphere.absorbers: i = a._wsv_index x = simulation.workspace.vmr_field.value[i, :, :, :].ravel() z = simulation.workspace.z_field.value.ravel() / 1e3 fig.line(x = x, y = z) fig.circle(x = x, y = z) fig.xaxis.axis_label = "VMR [mol/m^3]" fig.yaxis.axis_label = "Altitude [km]" return fig def plot_scattering_species(simulation, data_provider): pass def make_atmosphere_panel(simulation, data_provider): c = column(plot_temperature(simulation, data_provider), plot_absorption_species(simulation, data_provider)) return Panel(child = c, title = "Atmosphere", width = 600) ################################################################################ # Retrieval ################################################################################ def plot_temperature(simulation, data_provider): fig = figure(title = "Temperature", width = 500) t = simulation.workspace.t_field.value.ravel() z = simulation.workspace.z_field.value.ravel() / 1e3 fig.line(x = t, y = z) fig.circle(x = t, y = z) fig.xaxis.axis_label = "Temperature [K]" fig.yaxis.axis_label = "Altitude [km]" return fig def plot_absorption_species(simulation, data_provider): fig = figure(title = "Absorption species", width = 500) for a in simulation.atmosphere.absorbers: i = a._wsv_index x = simulation.workspace.vmr_field.value[i, :, :, :].ravel() z = simulation.workspace.z_field.value.ravel() / 1e3 fig.line(x = x, y = z) fig.circle(x = x, y = z) fig.xaxis.axis_label = "VMR [mol/m^3]" fig.yaxis.axis_label = "Altitude [km]" return fig def plot_scattering_species(simulation, data_provider): pass def make_atmosphere_panel(simulation, data_provider): c = column(plot_temperature(simulation, data_provider), plot_absorption_species(simulation, data_provider)) return Panel(child = c, title = "Atmosphere", width = 600) ################################################################################ # Sensor ################################################################################ def make_sensor_plot(sensor): fig = figure(title = sensor.name, width = 500) if isinstance(sensor, ActiveSensor): z = sensor.range_bins x = sensor.y z = 0.5 * (z[1:] + z[:-1]) for i in range(sensor.y.shape[1]): x = sensor.y[:, i] y = z.ravel() // 1e3 fig.line(x = x, y = y) fig.circle(x = x, y = y) fig.xaxis.axis_label = "Radar reflectivity [{0}]"\ .format(sensor.iy_unit) fig.yaxis.axis_label = "Altitude [km]" if isinstance(sensor, PassiveSensor): x = sensor.f_grid y = sensor.y for i in range(sensor.y.shape[1]): fig.line(x = x, y = y[:, i]) fig.circle(x = x, y = y[:, i]) return fig def make_sensor_panel(sensors): c = column(*[make_sensor_plot(s) for s in sensors], sizing_mode = "fixed") return Panel(child = c, title = "Measurements", width = 600) def make_retrieval_panel(simulation): z = np.copy(simulation.workspace.z_field.value.ravel()) p = np.copy(simulation.workspace.p_grid.value.ravel()) retrieval_result_plot = RetrievalResultPlot(z, p, simulation.retrieval) r = retrieval_result_plot.make_doc() if type(simulation.retrieval.results) is list: results = simulation.retrieval.results else: results = [simulation.retrieval.results] if all([not r.avk is None for r in simulation.retrieval.results]): avks = AVKPlot(simulation) r.children += avks.make_doc().children return Panel(child = r, title = "Retrieval", width = 600) observation_plots = {} for s in simulation.sensors: observation_plots[s] = ObservationPlot(s) if type(simulation.retrieval.results) is list: plots = [] for r in simulation.retrieval.results: for s in r.sensors: i, j = r.sensor_indices[s.name] observation_plots[s].add_observation(r.y[i : j]) print(r.y[i:j]) observation_plots[s].add_observation(r.yf[i : j]) print(r.yf[i:j]) plot = ProfilePlot(z, p) for q in r.retrieval_quantities: x = r.get_result(q) plot.add_quantity(x, r.name, dict(line_dash = "solid", line_width = 3)) xa = r.get_result(q, "xa") plot.add_quantity(xa, r.name, dict(line_dash = "dashed")) x0 = r.get_result(q, "x0") if not all(xa == x0): plot.add_quantity(x0, r.name, dict(line_dash = "dotted")) plots += [plot] else: plot = ProfilePlot(z, p) for q in r.retrieval_quantities: x = r.get_result(q) plot.add(x, r.name, "blue") plots = [plot] title = Div(text = "<h2>Retrieved quantities</h2>") doms = [title] + [p.make_doc() for p in plots] title = Div(text = "<h2>Fitted measurements</h2>") doms += [title] + [observation_plots[s].make_doc() for s in simulation.sensors] c = column(*doms) return Panel(child = c, title = "Retrieval", width = 600) def make_document_factory(simulation): def make_document(doc): doc.title = "parts dashboard" sensor_p = make_sensor_panel(simulation.sensors) atmosphere_p = make_atmosphere_panel(simulation, simulation.data_provider) retrieval_p = make_retrieval_panel(simulation) t = Tabs(tabs = [sensor_p, atmosphere_p, retrieval_p]) doc.add_root(t) return make_document def dashboard(simulation): apps = {'/': Application(FunctionHandler(make_document_factory(simulation)))} standalone_server.start(apps, port = 8880)