Write a driver

In a nutshell a driver is a collection of code that loads and visualises a particular data type, e.g. observation, model forecasts, user feedback etc. To play nicely with other components a driver must implement certain interfaces, a.k.a classes with specifically named methods. This guide walks through the interfaces and provides a rough guide on how best to structure your code.

At present drivers are Python modules placed in forest/drivers directory that implement a Dataset class. We’ll see later how a Dataset can be used to navigate and visualise its data.

A minimal driver would be the following;

"""Hypothetical module forest/drivers/minimal.py"""

class Dataset:
    def __init__(self, **kwargs):
        pass

To invoke this driver a user would either specify it as --file-type minimal on the command line or place it in a config file.

files:
  - label: Minimal driver
    file_type: minimal

But this driver is not very useful, it doesn’t speak to data, support navigation or add anything to the map. Let’s fix it.

To allow a user to navigate a dataset we need a navigator() method that returns a Navigator instance. You may be wondering what the Navigator interface is, any class that has four methods valid_times(), initial_times(), variables() and pressures() is a navigator. At the moment these are the four basic dimensions, in future a navigator may be able to define the dimensionality of its underlying data or even custom user interfaces to explore it.

"""Driver with a minimal Navigator"""

class Dataset:
    ...
    def navigator(self):
        return Navigator()

class Navigator:
    def variables(self, *args, **kwargs):
        return ["relative_humidity"]

    def initial_times(self, *args, **kwargs):
        return [datetime(2020, 1, 1)]

    def valid_times(self, *args, **kwargs):
        return [datetime(2020, 1, 1), datetime(2020, 1, 1, 3), ...]

    def pressures(self, *args, **kwargs):
        return [1000, 750, ...]

This interface is a little clunky, but for now it provides enough information to populate dropdown menus.

To add a visualisation to the map the map_view() is used to return an instance that implements the MapView interface.

class Dataset:
    ...
    def map_view(self):
        return MapView()

Again, a map view is an interface that supports adding glyphs to a figure and updating when the application state changes. It does this by implementing add_figure(figure) and render(state) methods. For example to plot circles on a map.

class MapView:
    def __init__(self):
        self.source = bokeh.models.ColumnDataSource({
            "x": [],
            "y": []
        })

    def add_figure(self, figure):
        return figure.circle(x="x", y="y", source=self.source)

    def render(state):
        self.source.data = {
             "x": [1, 2, 3],
             "y": [1, 2, 3],
        }

While it is nice to plot circles on a map, having access to general purpose scripting and a full application state is the real benefit of defining a MapView. The possibilites at this point are endless.

Thus concludes our walk through implementing a driver. It’s not a perfect design but hopefully it is enough to get started.