Subclassing BaseSolver and FluidSolver

Subclassing FluidSolver

You may want to subclass FluidSolver to have a class with the same equations as FluidSolver, but a different set of default assumptions. The default assumptions are a tuple default_assumptions that may be overridden:

class DryFluidSolver(FluidSolver):
    default_assumptions = ('no liquid water', 'no ice', 'low water vapor',
                           'ideal gas', 'bolton', 'constant g',
                           'constant Lv', 'hydrostatic', 'constant Cp')
    # also set a new solution dictionary so it will not be inherited
    # from FluidSolver
    _solutions = {}

The new class can then be instantiated with, for instance:

>>> solver = DryFluidSolver(p=1e5, T=300.)

Subclassing BaseSolver

atmos.equations has a particular set of equations relevant to atmospheric science. It may be that you want to use a different set of equations, whether because you want more equations than FluidSolver has available or because you want to use a completely different set of equations, say, for oceanic science. In order to do this, you can write a new module in the same style as atmos.equations, and create a Solver that uses your equation module. For details on writing an equations module, see the next section.

Assuming your equations module is called myequations, this is how you would create a solver for it:

import myequations

class MySolver(BaseSolver):
    # This should be set to your equation module
    _equation_module = myequations
    # This contains a list of assumptions from your module that you want
    # to enable by default
    default_assumptions = ('constant density', 'constant g')
    # a solution dictionary is necessary for caching solutions between
    # calls to calculate()
    _solutions = {}

Writing an Equation Module

In order for BaseSolver to propertly interpret your equation module, it needs to follow a certain format specification, which are described below.

quantities dictionary

There must be a variable quantities defined whose keys are the abbreviations of quantities you will use (such as ‘rho’, ‘T’, ‘x’, etc.). The values of the dictionary should be dictionaries with the keys ‘name’ and ‘units’, whose values are a descriptive name of the quantity (such as ‘air density’, ‘air temperature’, or ‘zonal position’), and an udunits-compatible specification of the units of the quantity (such as ‘kg/m^3’, ‘K’, or ‘m’).

Some key requirements to note:

  • Any quantity output by an equation or taken as input for an equation in your module must be present in the quantities dictionary
  • Your equations must always take in quantities with the units specified in this dictionary, and return quantities with the units specified in this dictionary.

Example:

quantities = {
    'AH': {
        'name': 'absolute humidity',
        'units': 'kg/m^3',
    },
    'DSE': {
        'name': 'dry static energy',
        'units': 'J',
    },
    'e': {
        'name': 'water vapor partial pressure',
        'units': 'Pa',
    }
}

assumptions dictionary

There must be a variable assumptions defined whose keys are abbreviations for assumptions you will use (such as ‘constant g’, ‘hydrostatic’, etc.), and values are longer (but still short) descriptions for those assumptions (such as ‘g is constant and equal to 9.81’, ‘the hydrostatic approximation is valid’, etc.). The values are only used when generating documentation for your equations (and when other users are looking at your code to figure out what your assumptions mean).

Example:

assumptions = {
    'hydrostatic': 'hydrostatic balance',
    'constant g': 'g is constant',
    'constant Lv': 'latent heat of vaporization of water is constant',

automatic documentation

Docstrings can be automatically generated for your equation functions using a decorator generated by atmos.decorators.equation_docstring(). The easiest way to use this decorator is to define the following function in your equation module:

def autodoc(**kwargs):
    return equation_docstring(quantities, assumptions, **kwargs)

You do not need to use this generator (or have any documentation) for your equations, but since automatically generating docstrings ensures they are always kept up to date when you update your functions, it is recommended.

writing equation functions

Any function in your module whose name follows the convention “{quantity}_from_*” is considered to be an equation function by BaseSolver. Here is an example equation function (assuming autodoc has been defined as above):

@autodoc(equation=r'qv = \frac{(\frac{Tv}{T} - 1)}{0.608}')
@assumes('no liquid water', 'no ice')
@overridden_by_assumptions('Tv equals T')
def qv_from_Tv_T(Tv, T):
    return (Tv/T - 1.)/0.608

At the least, an equation function must:

  • have a name conforming to “{quantity}_from_*” with a quantity that is present in your quantity dictionary
  • be decorated by atmos.decorators.assumes()
  • take in only variables that are abbreviations present in your quantity dictionary
  • return only one variable, which is the result of the equation

If you choose to decorate with autodoc, you must have the decorator above any other equation decorators so that it can use the metadata they attach to the equation function. autodoc takes in an optional keyword argument equation which is a latex-formatted string of the equation used.

The decorator atmos.decorators.assumes() takes in assumption abbreviation strings which correspond to assumptions made by the equation described in the function. This equation will only be enabled if all of its assumptions are enabled.

The decorator atmos.decorators.overridden_by_assumptions() is optional, and if present takes in assumption abbreviation strings which correspond to assumptions that override the equation described in the function. If any of these assumptions are enabled, this equation will be disabled.