#!/usr/bin/env python
"""
A python module with logging, doctest to serve as template for running
and analyzing a reversible Markov chain. The test chain considered
here is a deterministic counting process.

Metadata:

  - Author:    W.S.Kendall
  - Copyright: opensource (c) W.S.Kendall 2008
  - Date:      2008-07-03
  - URL:       http://www.wilfridkendall.co.uk
  - Generator: 'E:/HOME/WSK/lib/python/generate.py' version 1.18.


Command-line options::

  usage: %prog [options] arguments
    --doctest:             perform doctests;
    --logging=LOGGING:     logging verbosity level.

Notes:
 - values for 'logging' option:
 'DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'
 (decreasing level of verbosity), with 'WARNING' as default.

"""
__RCSID__ = '$Revision: 1.3 $ $Date: 2008/07/03 14:35:59 $'


# \section{Definitions}
# We need the logging module regardless of whether the #logging# option
# is deployed, since #logging.info# calls are scattered through the text.
import itertools, logging, random


#\section{Exceptions}
class Error(Exception):
    """
    Generic exception to be raised by this module.
    """


# \section{Classes}
class Reversible(list):
    """
    Parent class for simulation and analysis of reversible Markov
    chain.  Instantiation allows for keyword parameters 'rng' to
    specify random number generator, 'seed' to specify seed. If not
    over-ridden, the chain in question is a deterministic counting
    process (hence not reversible strictly speaking!)::

     >>> chain = Reversible(seed=1) ; print chain
     []
     >>> chain.equilibrium_draw() ; print chain
     [0]
     >>> chain.evolution(10) ; print chain
     [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
     >>> print chain.reversed_statistics()
     [(9, 8), (8, 7), (7, 6), (6, 5), (5, 4), (4, 3), (3, 2), (2, 1), (1, 0)]
    """
    def __init__(self, rng=None, seed=None):
        if rng is None:
            self.rng = random.Random()
        else:
            self.rng = rng
        if seed is not None:
            self.rng.seed(seed)

    def evolution(self, T):
        """
        Repeat updates till chain has evolved at least 'T' time-steps.
        """
        logging.info('Start evolution from trajectory\n%s' % self)
        while len(self) < T:
            self.update()
            logging.info(' update to %s' % str(self[-1]))

    def reversed_statistics(self):
        """
        Produce list of reversed transitions. Note that there must be
        at least one entry in 'self'::

         >>> chain = Reversible()
         >>> chain.reversed_statistics()
         Traceback (most recent call last):
          ...
         StopIteration
         >>> chain.equilibrium_draw()
         >>> print chain.reversed_statistics()
         []
        """
        a, b = itertools.tee(reversed(self))
        b.next()

        reversed_transitions = list(itertools.izip(a, b))
        logging.info('Reversed transitions list\n%s' % reversed_transitions)
        return reversed_transitions

    def equilibrium_draw(self):
        """
        Placeholder for draw from equilibrium of chain. Result is used
        to initialize chain.
        """
        self[:]=[0]

    def update(self):
        """
        Placeholder for update of chain by single step. The
        accumulated path is stored in 'self'.
        """
        self.append(self[-1]+1)


# \newpage
# \section{Functions}
def main(opt, arguments):
    """
    The action of the module as script should be concentrated here!
    Use option '--logging=INFO' to inspect results.
    """
    logging.info('Entering main function')
    chain = Reversible()
    chain.equilibrium_draw()
    chain.evolution(20)
    chain.reversed_statistics()


def _doctest():
    """
    Doctests.
    """
    logging.info('Commencing doctests')
    import doctest
    doctest.testmod()
    print __RCSID__.replace('$', '').strip()


# \section{Main script}
if __name__ == '__main__':
    import optionparse
    option, args = optionparse.parse(__doc__, version=__RCSID__)

    if option.logging is not None:
        logging.basicConfig(level=logging.__dict__[option.logging])
        logging.info('Logging level %s' % option.logging)

    if option.doctest:
        _doctest()


    # The action of the module as script should be concentrated
    # in the following function, to facilitate profiling.
    main(option, args)


    logging.info('Finishing')
    raise SystemExit


# \newpage \scriptsize
# \begin{multicols}{2}
# \section{History}
# \text{ }
#\\ $Log: reversible.py,v $
#\\ Revision 1.3  2008/07/03 14:35:59  wskendall
#\\ Forgot a reversal in #reversed_statistics# method: this is now
#\\ included.
#\\
#\\ Revision 1.2  2008/07/03 10:49:33  wskendall
#\\ Added doctest for #reversed_statistics# method.
#\\
#\\ Revision 1.1  2008/07/03 10:39:29  wskendall
#\\ Initial revision
#\\
#
# \end{multicols}

'$Id: reversible.py,v 1.3 2008/07/03 14:35:59 wskendall Development wskendall $'
