modelutil: Programming Structure and Logic of
ModelsThis 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:
StateSet class.
All the methods of the StateSet class are
available to model objects.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" Fieldobjects to the model data container (args can be empty). If keywordmodeequals"replace", args replaces any previously existing values, if there is a"tend1"Statein the model data container. If keywordmodeequals"sum_with_previous", args are summed with any previously existing values, if there is a"tend1"Statein the model data container. Though a user-written overloadedtend1method can call this method any number of times, it must call it at least once.asStateSet()Returns the model object as a StateSetclass 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 inGenericModeldoes two things: it sets (by reference) all the attributes of the inputStateSetvariable to attributes of the model object, and it executessetup. Thus,setupenables subclasses ofGenericModelto add additional functionality to the instantiation method without overloading__init__.step1()Calculates the state at the next timestep. This is expressed as a Stateobject withidof"tp1"which consists of adding theStateobject withidof"tend1"added to"t0"Stateobject.step1_return()Calculates the state at the next timestep, shifts all Stateobjects one timestep back, and deletes the earliest timestep. Thus, after this method is executed, theStatewithidof"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 idof"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.
tend1 MethodIf 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:
Field objects. These Field
objects should have attribute extra_meta.istend1
equal to True.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.
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.