Design

Zephyr is written to make heavy use of object-oriented programming techniques, including class multiple inheritance. The majority of programming constructs in the Zephyr implementation are subclasses of AttributeMapper, which automatically handles configuring new objects based on a global systemConfig dictionary (i.e., a key/value store).1

The interrelation between different packages (i.e., sub-modules) and object classes in Zephyr is easiest to understand visually. The following graphs were designed using pylint's pyreverse command, and can be recreated by running

make graphs

in the base directory of Zephyr's source code.

Submodule structure and design goals

This graph shows the linkages between Zephyr's submodules, including which submodules depend on others. It is important to note that zephyr.backend is intended to be minimally dependent on non-standard external packages, to enable solvers to be easily embedded in existing code. The zephyr.middleware layer is dependent on features in SimPEG and other libraries to enable waveform inversion.

Click to expand

The zephyr.frontend layer is designed to encompass user-facing features, including the command-line interface.

Class inheritance graph

This graph shows the interrelation between the object classes in Zephyr. The dependencies for zephyr.backend are largely internal, whereas several features of zephyr.middleware depend on SimPEG.

Click to expand

Object hierarchy and AttributeMapper

An AttributeMapper subclass defines a dictionary initMap, which includes keys for mappable inputs expected from the systemConfig parameter. The class definition takes the form:

class BaseModelDependent(AttributeMapper):
    '''
    AttributeMapper subclass that implements model-dependent properties,
    such as grid coordinates and free-surface conditions.
    '''

    initMap = {
    #   Argument        Required    Rename as ...   Store as type
        'nx':           (True,      None,           np.int64),
        'ny':           (False,     None,           np.int64),
        'nz':           (True,      None,           np.int64),
        'xorig':        (False,     '_xorig',       np.float64),
        'yorig':        (False,     '_xorig',       np.float64),
        'zorig':        (False,     '_zorig',       np.float64),
        'dx':           (False,     '_dx',          np.float64),
        'dy':           (False,     '_dx',          np.float64),
        'dz':           (False,     '_dz',          np.float64),
        'freeSurf':     (False,     '_freeSurf',    tuple),
    }
    ...

NB: Complex numpy arguments are handled specially: the real part of the value is kept and the imaginary part is discarded when they are typecast to a float.


  1. In fact, most of the assembly of objects is handled by zephyr.backend.meta.AMMetaClass, which is the metaclass for AttributeMapper and its descendants. All of this happens before the call to AttributeMapper.__init__. This enables the initMap class attribute to be merged automatically and inherit keys from the subclass's bases.