Extending / Customizing PyGNOME

PyGNOME is a system built of standardized components. The Model’s job is to coordinate each of the components into a fully working model, but the details are all determined at run time according to what components the model has been configured to use.

As a result you can write a new component and plug it in to the rest of the model without having to alter any of the model or other code.

The multiple PyGNOME APIs

The gnome package is designed to work when driven by scripts, as well as the engine behind the WebGNOME system. This means that there are a couple of related APIs for each GnomeObject:

The scripting API

This is how an object is created / manipulated in python scripts. When making new objects, you can use whatever you like, but it’s probably a good idea to make the API similar to other existing objects.

The model API

These are the methods and attributes the object needs to have in order to work with the gnome.Model system. For the most part, the core methods will be defined in a Base Class (e.g. the gnome.movers.mover.Mover class), so if you subclass from that, you only need to implement the ones you need.

The Web / Save file API

WebGNOME use a JSON API to configure the PyGNOME system. A very similar JSON format is used to save model configurations in “save files”, which are zip files with the model configuration and data required to reproduce a model configuration. You can develop and test your new component without using this system, but if you want your component to be usable with WebGNOME or save files, then this API must be defined. This is done by defining a Schema that specifies what attributes need to be saved and can be updated for a given object. See: Overview for the details.

Movers

A mover is a component that “moves” the elements in the model. PyGNOME supports having multiple movers that are all combined via linear superposition. There are some details, but the key method a Mover needs to provide is get_move():

def get_move(self, sc, time_step, model_time_datetime):
    """
    Compute the move in (long, lat, z) space. It returns the delta move
    for each element of the spill as a numpy array of size
    (number_elements X 3) and dtype = gnome.basic_types.world_point_type

    Base class returns an array of numpy.nan for delta to indicate the
    get_move is not implemented yet.

    Each class derived from Mover object must implement it's own get_move

    :param sc: an instance of gnome.spill_container.SpillContainer class
    :param time_step: time step in seconds
    :param model_time_datetime: current model time as datetime object

    All movers must implement get_move().
    """
    positions = sc['positions']

    delta = np.zeros_like(positions)
    delta[:] = np.nan

    return delta

Note that get_move() doesn’t alter the positions of the elements. Rather it returns a “delta” – the amount of movement in (longitude, latitude, vertical) meters units for the provided time step. This is so that the model can apply the “delta” from each mover and the result won’t change with the order in which they are applied. Essentially, the delta values from all the movers are added together, and then added to the positions array.

sc is a SpillContainer which contains all the arrays of data associated with the elements. For the most part, for a mover, the “positions” array is the important one, but if movement is affected by other properties of the elements, that data will be stored in the spill_container. The SpillContainer is a complex object that manages all the data associated with the elements. But it presents a “Mapping” interface (like a Python dict) for easy access to the data associated with the elements – for example: sc['postions'] will return a 3xN numpy array with the positions of the elements. sc['mass'] wil return an array with the mass of each element, etc. Which arrays are assocaed with the elements depends on what components have been added to the model.

the get_move() method will also have access to the time step length (in seconds), and the model time at the beginning of this time step.

Writing a custom mover

[To be filled in]

Here’s how to do this.

And here’s some example code doing this:

from gnome.model import Model
model = Model()

That doesn’t do much. You can find out more in gnome.movers

Weatherers

Weatherers are objects that alter the elements. The name comes from the oil weathering code shipped with PyGNOME, but they can be used for any process that changes the properties of the elements.

The Base class for weatherers is gnome.weatherers.core.Weatherer

The core method each weatherer needs is:

weatherers update the data arrays in the spill container, sc.

NOTE: this is different than movers – as each weatherer updates the actual data associated with the elements, the result will be different depending on what order they are run. This order is controlled by sort_order in gnome/weatherers/__init__.py

Making your own weatherer

And here’s how to do this…

Map

The Model has to have a Map at all times. After moving and weathering the elements, the map is responsible for the interaction between the elements and the shoreline, as well as the ocean bottom and water surface. (Bottom and water surface are only relevant for 3-d simulations).

It will determine whether elements have impacted the shoreline, or gone off the map, and can also refloat any elements that were previously beached.

The base map object: gnome.GnomeMap represents a “water world” – no land anywhere, and unlimited map bounds. But it also provides a base class with the full API. To create another map object, you derive from GnomeMap, and override the methods that you want to implement in a different way. In addition, it should have a few attributes used by the model:

Key Attributes of Maps

map_bounds: The polygon bounding the map if any elements are outside the map bounds, they are removed from the simulation.

spillable_area: The PolygonSet bounding the spillable_area. Either a PolygonSet object or a list of lists from which a polygon set can be created. Each element in the list is a list of points defining a polygon.

land_polys: The PolygonSet holding the land polygons. These are only used for display. Either a PolygonSet object or a list of lists from which a polygon set can be created. Each element in the list is a list of points defining a polygon.

Each of these defaults to the whole world with no land.

Key methods

The key methods to override include:

__init__: Whatever you need to initialize your map object

refloat_elements(self, spill_container, time_step):
    """
    This method performs the re-float logic -- changing the element
    status flag, and moving the element to the last known water position

    :param spill_container: current SpillContainer
    :type spill_container:  :class:`gnome.spill_container.SpillContainer`
    """