Skip to content

Adding New Drivers

Jesse Mapel edited this page Sep 4, 2019 · 16 revisions

The overall goal of a driver is to convert some serialization format to a homogeneous interface so that the formatting functions can create output compatible with their type of sensor model.

Design

The challenge inherent in working with so many different serialization formats comes from handling different file types (e.g. retrieving keys from a PVL label versus an XML label) and handling different file standards (e.g. retrieving keys from a PDS3 PVL label versus an ISIS3 PVL label) compounded by exceptional cases where a particular consumer of a standard had unique requirements which could not satisfied by the standard, so they mutate the standard or implement a combination of standards as a workaround (e.g. Messenger's MDIS camera has a temperature dependent focal length; if reading from a PDS3 label, focal plane temperature must be acquired from the label and function coefficients to derive focal length as a function of temperature is acquired from NAIF SPICE kernels. The keys for focal length temperature and function coefficients are unique to MDIS labels and kernels).

To address this problem, multiple classes are written to match each serialization standard with methods implementing the typical approach for accessing a particular key from a format (e.g. a class that reads keys from PDS3 labels, a class that reads from ISIS3 PVL labels etc.). These classes are predominately a collection of Python properties where each property implements how to access one key required by the base Driver class.

A particular instrument driver is simply a class which combines these generic classes through subclassing. This is normally referred to as mixins. The base classes described in the previous paragraph stand by themselves and are combined to create a concrete driver. If you write a driver for MDIS PDS3 labels, you would subclass Driver, PDS3, and Spice since the required MDIS data is spread across a PDS3 label and Spice kernels. If the instrument requires a unique implementation for any particular property (like our MDIS focal length example in the first paragraph), you simply overwrite that property in your new class.

Typically, any particular driver for any particular instrument only needs to define:

  • sensor_name : The name of the instrument, I.E. MDIS
  • platform_name : The name of the platform the instrument is on, I.E. MESSENGER
  • sensor_model_version : The version number for the sensor model the driver supports
  • isis_naif_keywords : The keywords that get queried from text NAIF SPICE kernels. This is for compatibility with ISIS sensor models.

REMEMBER In exceptional cases, you will need to override properties from the base classes in cases where a particular instrument has unique ways to acquire otherwise common keys.

WARNING: Remember Your Method Resolution Order When inheriting from a combination of classes, be aware of what methods you are inheriting. Method resolution order (MRO) is important when inheriting from classes in cases where two or more of the parent classes define the same method. Otherwise you might end up with strange errors. The TLDR on Python MRO: Methods are prioritized right to left. So class MdisPds3Driver(PDS3, Spice) would override methods in the Spice class in favor of methods in PDS3 in a case where the two have the same method defined.

Attributes and __init__

The multiple inheritance structure of drivers makes setting attributes in __init__ methods problematic. There is no guarantee that a particular mixin's __init__ method will get called when a concrete driver is instantiated. For this reason, the base Driver class accepts a props argument that is a dictionary of attributes. Thus any additional attributes that would normally be set during __init__ required by a particular driver or mix-in to be accessed via self.attribute should instead be passed as an item in the props dictionary and accessed via self._props['attribute'].

Documentation

We use numpy style doc strings for the ALE python module. This means each function and class needs to have a doc string.

The mix-in structure can become hard to parse and the scope of each mix-in is not directly clear from the code. To help with this, whenever a method uses an attribute/property the doc string for that function must state that and define what it expects that attribute/property to be. For example:

@property
def ephemeris_start_time(self):
    """
    The start time for the image in ephemeris seconds past the J2000 epoch.

    The method expects the spacecraft_clock_start_count attribute/property to be defined.
    This must be the SCLK string for the start time of the image.

    Returns
    -------
    float
        The ephemeris seconds past the J2000 epoch at which the image started being captured.
    """
    return spice.scs2e(self.spacecraft_id, self.spacecraft_clock_start_count)

Multiple Inheritance and MRO

The mix-in architecture of the concrete driver classes means we are using multiple inheritance. I.E. concrete driver classes all look like this:

class FooMissionBarInstrumentBazLabelBatData(BazLabel, BatData, Driver):
    """
    Concrete driver for the Bar instrument on the Foo missions when working with Baz labels and Bat data.
    """

In Python, properties and methods are searched for in the defined class and then from left to right in the parent classes. So, in the example above, when you call driver.prop the parent classes are searched for a property called prop in the following order:

  1. FooMissionBarInstrumentBazLabelBatData - The child class is always searched first
  2. BazLabel - This is the left-most parent so it gets searched first
  3. BatData - This is the next parent
  4. Driver - This is the final parent and it gets searched last

As soon as a property called prop is found, this process stops. So, if the BazLabel and Driver classes both have a property called prop, then only the prop from BazLabel will be called because it comes first in MRO.

The most important thing to remember with your concrete driver is to have the Driver class come last in the multiple inheritance order. This, way all of the abstract methods in Driver will be overriden by your mix-ins. If we change our example class to:

class FooMissionBarInstrumentBazLabelBatData(Driver, BazLabel, BatData):
    """
    Concrete driver for the Bar instrument on the Foo missions when working with Baz labels and Bat data.
    """

and call driver.prop, then the prop property from Driver will get called (which is an abstract method). In fact, you won't even be able to get to driver.prop because the following exception will be raised when driver is instantiated:

Can't instantiate abstract class FooMissionBarInstrumentBazLabelBatData with abstract methods props

Conventions

All driver classes are put into ale/drivers/<mission name>_driver.py where <mission name> is the name of the mission that the sensor was a part of.

Other things to keep in mind:

  • When writing a new method in your driver to get label or spice metadata, make sure it isn't already defined in a base class.
  • Only overwrite mixin class method definitions if the instrument driver differs from the generic implementation.
  • If you need to access metadata from the files, try doing it through a method rather than getting directly from the label member variable or spice calls (especially if already defined in a base), this prevents redefinitions and inconsistent values in case of programmer error.
  • If there exists many unique parameters for your instrument driver, think about whether those methods should be factored out into it's own new base class. (e.g. if you're working with a new distortion model or sensor model type that doesn't yet exist as a base class).
  • All ALE drivers use 0 based coordinate systems for image pixels and detector elements. So, the top left corner of an image is (0, 0) and the top left corner of a detector is also (0, 0). Conversions to other coordinate systems are handled in library specific properties such as the isis_naif_keywords property and formatters.