#!/usr/bin/python -tt
#=======================================================================
#                        General Documentation

"""Definition of a generic model (class GenericModel).

   The GenericModel class is the parent class for all models.  The
   difference is that these subclasses of GenericModel overload spe-
   cific customized methods.  An overview of the programming logic 
   and structure of all models and the GenericModel template is gi-
   ven here:

       http://geosci.uchicago.edu/csc/modelutil/doc/man-models.html

   Example of defining a model class using GenericModel, integrating
   it 100 timesteps, and returning the result as a new StateSet:

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

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

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

#-----------------------------------------------------------------------
#                       Additional Documentation
#
# RCS Revision Code:
#   $Id: generic_model.py,v 1.1.1.1 2005/01/13 00:15:42 jlin Exp $
#
# Modification History:
# - 12 Jan 2005:  Original by Johnny Lin, Computation Institute,
#   University of Chicago.  Passed passably reasonable tests.
#
# Notes:
# - Written for Python 2.2.2.
# - Module docstrings can be tested using the doctest module.  To
#   test, execute "python generic_model.py".  Full tests of this
#   class are conducted by subclasses of GenericModel, and usually
#   are written using unittest.
# - See import statements throughout for non-"built-in" packages and
#   modules required.
#
# Copyright (c) 2004-2005 by Johnny Lin.  For licensing, distribution 
# conditions, contact information, and additional documentation see
# the URL http://geosci.uchicago.edu/csc/modelutil/doc/.
#=======================================================================




#---------------- Module General Import and Declarations ---------------

#- If you're importing this module in testing mode, or you're running
#  pydoc on this module via the command line, import user-specific
#  settings to make sure any non-standard libraries are found:

import os, sys
if (__name__ == "__main__") or \
   ("pydoc" in os.path.basename(sys.argv[0])):
    import user
del os, sys


#- Set module version to package version:

import package_version
__version__ = package_version.version
__author__  = package_version.author
__date__    = package_version.date
__credits__ = package_version.credits
del package_version


#- Import statements:

from stateset import StateSet




#--------------------- Class GenericModel:  Header ---------------------

class GenericModel(StateSet):
    """A generic model.

    This is the parent class for all models (thus all models are sub-
    classes of GenericModel) and defines all methods and attributes
    common to all models.

    An object of this class is instantiated with a StateSet as input.
    This class is just a StateSet class with a few extra methods added;
    all StateSet methods and attributes exist and apply in GenericModel.
    The StateSet must have a State object with an id attribute of "t0",
    as it assumes that the "t0" State object is the state at the current 
    timestep, and that the model calculates starting from that point.
    All container methods of this class assume that the StateSet data 
    is the data.  Depending on what methods are applied, this StateSet 
    data is altered/augmented accordingly.
    """




#------------- Class GenericModel:  __init__ Special Method ------------

    def __init__(self, input_StateSet):
        """Instantiation method.
        
        Method argument:
        * input_StateSet:  Input StateSet object.

        In this instantiation method the two things that happen is that 
        all attributes in input_StateSet (listed in __dict__) become 
        attributes of this model, by reference, and the setup method is 
        executed.
        """
        for akey in input_StateSet.__dict__.keys():
            self.__dict__[akey] = input_StateSet.__dict__[akey]
        self.setup()




#---------------- Class GenericModel:  add_tend1 Method ----------------

    def add_tend1(self, *args, **kwds):
        """Adds args "tendency" objects to the model data container.

        A call to this method is generally the final line in the user-
        written overloaded tend1 method.  Note the tend1 method can 
        have more than one call to this method, however.

        Input Positional Argument(s):
        * args:  Field objects to add to the State object in the model 
          data container that has id "tend1".  If such a State object 
          doesn't exist, the State is created.  All the Field objects
          in this list args should have unique ids; an exception is
          thrown if not.

        Input Keyword Argument:
        * kwds:  Dictionary of keyword arguments.  Currently supports
          a single keyword:  mode.  The mode keyword describes whether
          the "tendency" object will be added to the model data contai-
          ner by overwriting any previously existing "tendency" value 
          or summed to any previously existing "tendency" value.  It
          is a string with the following possible values:
          + "replace":  Any previously existing "tendency" values in
            the data container will be overwritten.  This is the de-
            fault value of mode in this method.
          + "sum_with_previous":  Any previously existing "tendency"
            values will be added to the value given in the input Field
            object.

        Most of the time the list of Field objects in args 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 will
        be called with no arguments.
        """
        #- Initialize mode:

        if 'mode' in kwds.keys():  mode = kwds['mode']
        else:                      mode = 'replace'


        #- Check list of arguments does not contain Fields with dupli-
        #  cate ids:

        list_ids = [anarg.id for anarg in args]
        for anid in list_ids:
            if list_ids.count(anid) != 1:
                raise ValueError, "Field list has duplicate ids"


        #- Create increment ("tendency") State object as needed and 
        #  add tendencies:

        if len(args) < 1:
            pass
        else:
            if 'tend1' not in self.keys():
                tend1_State = self['t0'].copymeta()
                tend1_State.id = 'tend1'
                self.add( tend1_State )
                del tend1_State

            if mode == 'replace':
                for anarg in args:
                    self['tend1'].add( anarg )
            elif mode == 'sum_with_previous':
                for anarg in args:
                    if anarg.id in self['tend1'].keys():
                        tmp = self['tend1'][anarg.id] + anarg
                        tmp.replace_all_meta( self['tend1'][anarg.id] )
                        self['tend1'].add( tmp )
                        del tmp
                    else:
                        self['tend1'].add( anarg )
            else:
                raise ValueError, "Bad mode"




#---------------- Class GenericModel:  asStateSet Method ---------------

    def asStateSet(self):
        """Returns model attributes as a StateSet object by reference.

        Only standard StateSet attributes (i.e. those that would be
        initialized on instantiation) are part of the returned State-
        Set object.  Any additional attributes in the model object are 
        not returned.
        """
        output_StateSet = StateSet()
        for akey in output_StateSet.__dict__.keys():
            output_StateSet.__dict__[akey] = self.__dict__[akey] 
        return output_StateSet




#------------------ Class GenericModel:  setup Method ------------------

    def setup(self):
        """Additional tasks to perform on object instantiation.

        This method is executed upon object instantiation.  Its default
        behavior is to do nothing; i.e. here its a stub.  Its use is in
        subclasses of GenericModel where the method will be overloaded
        and customized.
        """
        pass




#------------------ Class GenericModel:  step1 Method ------------------

    def step1(self):
        """Integrate State one timestep forward.

        If the "tend1" id State object does not exist in the model 
        data, method tend1 is called to calculate that "tendency".  
        The current State (i.e. with id attribute "t0") is then inte-
        grated forward one timestep by adding the "tend1" id State 
        object to the "t0" State object to get a new State object 
        with id attribute "tp1", which is then added to the model 
        data.  The "tend1" State object is deleted from the model 
        data.  No other State objects in the model data are altered.
        """
        if 'tend1' not in self.keys():  self.tend1()
        tp1_State = self['t0'].copy()    #- Init. a tp1 State object

        for akey in self['tend1'].keys():         #- Cycle tend1 Fields
            if not self['tend1'][akey].extra_meta.istend1:
                raise ValueError, "Field not a tend1 variable"
            tp1_State[akey] += self['tend1'][akey]
            tp1_State[akey].replace_all_meta( self['tend1'][akey] )
            tp1_State[akey].extra_meta.istend1 = False

        del self['tend1']                #- Delete "tendency" from StateSet
        tp1_State.id = 'tp1'
        self['tp1'] = tp1_State          #- Add tp1 to StateSet




#-------------- Class GenericModel:  step1_return Method ---------------

    def step1_return(self):
        """Integrate one timestep forward and set new timestep to t=0.

        If the State object with id "tp1" does not exist, the method
        calculates the state at the next timestep after "t0" (i.e.
        "tp1"), shifts all State objects one timestep back, deletes 
        the earliest timestep, and deletes any timesteps later than
        "t0".  If the "tp1" object already exists, we assume that
        object is the results of the integration and the integration 
        step is skipped.  The rest of the method tasks are still com-
        pleted, however.

        Thus, after this method is executed, the State with id of "t0" 
        in the model data is the newly calculated timestep.  The pur-
        pose of this method is to put the model in a state ready to be 
        integrated another timestep.
        """
        if 'tp1' not in self.keys():  self.step1()
        self.shift_time_stateid(-1)
        self.del_earliest()
        list_id_num = self.stateid2int()
        if max(list_id_num) > 0:
            for akey in self.keys():
                if akey.startswith('tp'):  del self[akey]




#------------------ Class GenericModel:  tend1 Method ------------------

    def tend1(self):
        """Calculate "tendency" for State for one timestep forward.

        The "tendency" required to increment all time model calculated
        prognostic variables in the current State (i.e. with id attri-
        bute "t0") forward one time step is calculated.  This new State 
        has id attribute "tend1"; only Fields that have an increment 
        are in this new "tend1" State object.  The "tend1" State object 
        is added to the model container.

        The "tendency" calculated here is actually the true tendency 
        multiplied by one timestep; thus it is the value to add to the 
        current state to increment one timestep forward.  In other con-
        texts what is called "'tendency'" here is referred to as "in-
        crement".

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

        (1) Calculate "tendency(ies)" as Field objects.  These Field
            objects should have attribute extra_meta.istend1=True.
        (2) Add these Field objects to the model data container as a
            State object with id "tend1" using the add_tend1 method.  
            This method call is generally the final line in the tend1 
            method.

        The tend1 method is useful if you're only interested in the
        "tendencies" or if you wish to calculate a number of "tenden-
        cies" in the same time step with the same initial conditions
        before completing the integration.

        The use of this method is in subclasses of GenericModel where 
        this method will be overloaded and customized, as this method 
        is the meat of any model.  In GenericModel, this method throws 
        a NotImplementedError exception, which enforces the require-
        ment to overload the method.
        """
        raise NotImplementedError, "Method not implemented"




#-------------------------- Main:  Test Module -------------------------

#- Define additional examples for doctest to use:

__test__ = {'Additional Examples and Tests':
"""
>>> set = StateSet(id='variables')
>>> model = GenericModel(set)
>>> print model.id
variables
"""}


#- Execute doctest if module is run from command line:

if __name__ == "__main__":
    """Test the module.

    Tests the examples in all the module documentation strings, plus 
    __test__.

    Note:  To help ensure that module testing of this file works, the 
    parent directory to the current directory is added to sys.path.
    """
    import doctest, sys, os
    sys.path.append(os.pardir)
    doctest.testmod(sys.modules[__name__])




# ===== end file =====
