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" Field
objects to the model data container (args can be empty). If keywordmode
equals"replace"
, args replaces any previously existing values, if there is a"tend1"
State
in the model data container. If keywordmode
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 overloadedtend1
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 inGenericModel
does two things: it sets (by reference) all the attributes of the inputStateSet
variable to attributes of the model object, and it executessetup
. Thus,setup
enables subclasses ofGenericModel
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 withid
of"tp1"
which consists of adding theState
object withid
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, theState
withid
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.
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 id
s, 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.