modelutil: Programming Structure and Logic of Models

Overview

This package provides the utilities needed to define models and submodels that are interchangeable yet simple. There are three design principles used to accomplish this.

First, model variables (e.g. zonal wind, precipitation, sensible heat flux) are managed using the Field, State, and StateSet classes, including enough metadata to enable one to figure out what exactly is being stored and passed. A State object is a collection of Field objects, and a StateSet object is a collection of State objects. See this descriptive overview of these three classes or the class doctrings for details.

Second, each model package and subpackage has a module interface_meta which defines a class InterfaceMeta. This class gives the metadata for Field variables that are consistently defined among all modules at that level and subpackages immediately below that level that pass information in and out. As an example consider the package seaice which defines a series of sea-ice models. Instances of InterfaceMeta defined in module seaice.interface_meta apply to modules in seaice and any variables that get passed in or out of the seaice.semtner model (as defined in the file seaice/semtner/__init__.py). The Field, State, and StateSet classes each contain a meta_ok_to_interface method that accepts a single InterfaceMeta as an argument which tests whether the object contains Field variables that meet the InterfaceMeta metadata definition. More information on meta_ok_to_interface and InterfaceMeta can be found in the overview description of the Field, State, and StateSet classes.

Lastly, all models defined by the framework specified by this package are objects structured the same way. They are all subclasses of the GenericModel class which defines all methods and attributes common to all models. Objects of the GenericModel class have the following characteristics:

  • Are subclasses of the StateSet class. All the methods of the StateSet class are available to model objects.
  • Are instantiated with a single positional argument, a StateSet object. All the attributes (public and private, found in __dict__) of the StateSet object are copied (by reference) as attributes of the model.

and have the following additional public methods:

Method Description
add_tend1([args], mode="replace") Adds args "tendency" Field objects to the model data container (args can be empty). If keyword mode equals "replace", args replaces any previously existing values, if there is a "tend1" State in the model data container. If keyword mode equals "sum_with_previous", args are summed with any previously existing values, if there is a "tend1" State in the model data container. Though a user-written overloaded tend1 method can call this method any number of times, it must call it at least once.
asStateSet() Returns the model object as a StateSet class object by reference. This is often used to pass the model output state to another model.
setup() Additional tasks to perform on object instantiation. The __init__ method in GenericModel does two things: it sets (by reference) all the attributes of the input StateSet variable to attributes of the model object, and it executes setup. Thus, setup enables subclasses of GenericModel to add additional functionality to the instantiation method without overloading __init__.
step1() Calculates the state at the next timestep. This is expressed as a State object with id of "tp1" which consists of adding the State object with id of "tend1" added to "t0" State object.
step1_return() Calculates the state at the next timestep, shifts all State objects one timestep back, and deletes the earliest timestep. Thus, after this method is executed, the State with id of "t0" in the model is the newly calculated timestep.
tend1() Calculates the "tendency" of model prognostic variables (actually the tendency multiplied by one timestep, and thus the value to add to the current state to increment one timestep forward; has id of "tend1"). This method is the meat of the model, and is described in further detail in the subsection below.

In other words, all model objects are just StateSet objects with additional methods. Remember, in a StateSet (and thus GenericModel model) object all State objects have a unique id attribute, which is the reference key in the StateSet or GenericModel object.

New models are created by defining a new class that is a subclass of GenericModel. In that new class the tend1 method is overloaded; this is the only method or attribute that must be written in this new subclass. Optionally, if the new model has to do additional tasks on object instantiation, the setup method can be overloaded with those tasks (don't overload the __init__ method); usually these tests would be error checks and data initialization.

Because StateSet objects are passed as input and returned as output for all models, they can be nested in any order. Thus, there is no rigid distinction between "upper-level" and "lower-level" models. As long as the Field objects making up the StateSet interface return true for the meta_ok_to_interface method, those fields are interchangeable as input/output for all models in this package. More often than not, however, the model will be used "one-way"; meta_ok_to_interface will often return false in those cases.

The tend1 Method

If you're only interested in the final results of model integration, you will probably not need to use the tend1 method, outside of its automatic inclusion in the step1_return method. However, the tend1 method is useful if you're only interested in the "tendencies" or if you wish to calculate a number of "tendencies" in the same time step with the same initial conditions before completing the integration.

The structure of the code in this method should include the following and be in the following order:

  1. Calculate "tendencies" as Field objects. These Field objects should have attribute extra_meta.istend1 equal to True.
  2. Add these Field objects to the model data container as a State object with id "tend1" using the add_tend1 method. Usually this method call is the final line in the tend1 method.

Using the add_tend1 method automatically creates an appropriate "tend1" State object in the model's data container, if it doesn't already exist. If the "tend1" State object already exists, the add_tend1 method adds the newly calculated "tendencies" (if the Field objects share the same ids, overwriting if the mode argument equals "replace" and summing with any previous value if the mode argument equals "sum_with_previous"). An example of a case to use mode equal to "replace" would be if that model should be the only place that calculates that variable. An example of a case to use mode equal to "sum_with_previous" would be if a number of models each contribute a part of the overall "tendency" in that Field variable (and the total "tendency" is the sum of the contributions).

Most of the time the list of Field objects in the add_tend1 arguments list will be local variables calculated in the tend1 method. For models where all the tendencies in the tend1 method are calculated by calling the tend1 methods of submodels, there will likely be no locally calculated "tendencies" to add. In those cases, there's nothing to add to the "tend1" State so add_tend1 should be called with no arguments.

See the pydoc documentation for the GenericModel class for additional information.

Examples

Here is some pseudo-code defining a "leaf" model class (i.e. a model that does not call any submodels) using GenericModel, integrating it 100 timesteps, and returning the result as a new StateSet:

class LeafModel(GenericModel):
    def setup(self):
        [... add optional extra initialization code ...]
    def tend1(self):
        [... add lines to calculate "tendency" tend_Field ...]
        self.add_tend1( tend_Field )

[... define Field variables ...]
[... define State variables s1, s2 ...]

set = StateSet(s1, s2)
model = LeafModel(set)
for i in range(100):  model.step1_return()
newset = model.asStateSet()

Here we define a "branch" model class (i.e. a model that does call submodels) that calls the LeafModel tend1 to calculate "tendencies". We integrate the model 100 timesteps and returning the result as a new StateSet. Note how the use of the "branch" model is identical as the "leaf":

class BranchModel(GenericModel):
    def setup(self):
	  self._leafmodel = LeafModel( self.asStateSet() )
    def tend1(self):
        self._leafmodel.tend1()
        self.add_tend1()

[... define Field variables ...]
[... define State variables s1, s2 ...]

set = StateSet(s1, s2)
model = BranchModel(set)
for i in range(100):  model.step1_return()
newset = model.asStateSet()

Finally, here we illustrate how the use of asStateSet makes it easy to change the calling order of models, helping blur the lines (at least in terms of initialization syntax) between "leaf" and "branch" models. Because BranchModel is initialized as a reference to StateSet object set, leaf_model will also reference set:

[... define Field variables ...]
[... define State variables s1, s2 ...]

set = StateSet(s1, s2)
branch_model = BranchModel(set)
leaf_model = LeafModel(branch_model.asStateSet())
for i in range(100):  leaf_model.step1_return()
newset = leaf_model.asStateSet()

Acknowledgements: Thanks to Michael Tobis for the idea of structuring models in a recursive manner.

Return to modelutil Reference Manual Index.
Return to modelutil Home Page.

Valid HTML 4.01! Valid CSS! Updated: January 5, 2005 by Johnny Lin <email address>. License.