<commit_before>#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# repairbox documentation build configuration file, created by
# sphinx-quickstart on Mon Oct  2 20:16:23 2017.
#
# This file is execfile()d with the current directory set to its
# containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.

# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#

import datetime

# import os
# import sys
# sys.path.insert(0, '/home/chris/git/repairbox/repairbox')

# -- General configuration ------------------------------------------------

# If your documentation needs a minimal Sphinx version, state it here.
#
# needs_sphinx = '1.0'

# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
    'sphinx.ext.autodoc',
    'sphinx.ext.todo',
    'sphinx.ext.napoleon',
    'sphinx.ext.viewcode'
]

# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']

# The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string:
#
# source_suffix = ['.rst', '.md']
source_suffix = '.rst'

# The master toctree document.
master_doc = 'index'

# General information about the project.
project = 'repairbox'
year = datetime.datetime.now().year
copyright = '{}, Christopher Steven Timperley'.format(year)
author = 'Christopher Steven Timperley'

# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = ''
# The full version, including alpha/beta/rc tags.
release = ''

# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
language = 'en'

# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This patterns also effect to html_static_path and html_extra_path
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']

# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'

# If true, `todo` and `todoList` produce output, else they produce nothing.
todo_include_todos = True

# Disable preceding module names
add_module_names = False

# -- Options for HTML output ----------------------------------------------

# The theme to use for HTML and HTML Help pages.  See the documentation for
# a list of builtin themes.
#
html_theme = 'classic'

# Theme options are theme-specific and customize the look and feel of a theme
# further.  For a list of options available for each theme, see the
# documentation.
#
# html_theme_options = {}

# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']

# Custom sidebar templates, must be a dictionary that maps document names
# to template names.
#
# This is required for the alabaster theme
# refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars
# html_sidebars = {
#     '**': [
#         'about.html',
#         'navigation.html',
#         'relations.html',  # needs 'show_related': True theme option to display
#         'searchbox.html',
#         'donate.html',
#     ]
# }

# -- Options for HTMLHelp output ------------------------------------------

# Output file base name for HTML help builder.
htmlhelp_basename = 'repairboxdoc'

# -- Options for LaTeX output ---------------------------------------------

latex_elements = {
    # The paper size ('letterpaper' or 'a4paper').
    #
    # 'papersize': 'letterpaper',

    # The font size ('10pt', '11pt' or '12pt').
    #
    # 'pointsize': '10pt',

    # Additional stuff for the LaTeX preamble.
    #
    # 'preamble': '',

    # Latex figure (float) alignment
    #
    # 'figure_align': 'htbp',
}

# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
#  author, documentclass [howto, manual, or own class]).
latex_documents = [
    (master_doc, 'repairbox.tex', 'repairbox Documentation',
     'Author', 'manual'),
]

# -- Options for manual page output ---------------------------------------

# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
    (master_doc, 'repairbox', 'repairbox Documentation',
     [author], 1)
]

# -- Options for Texinfo output -------------------------------------------

# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
#  dir menu entry, description, category)
texinfo_documents = [
    (master_doc, 'repairbox', 'repairbox Documentation',
     author, 'repairbox', 'One line description of project.',
     'Miscellaneous'),
]

# -- Options for Epub output ----------------------------------------------

# Bibliographic Dublin Core info.
epub_title = project
epub_author = author
epub_publisher = author
epub_copyright = copyright

# The unique identifier of the text. This can be a ISBN number
# or the project homepage.
#
# epub_identifier = ''

# A unique identification for the text.
#
# epub_uid = ''

# A list of files that should not be packed into the epub file.
epub_exclude_files = ['search.html']
<commit_msg>switched sphinx theme<commit_after>#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# repairbox documentation build configuration file, created by
# sphinx-quickstart on Mon Oct  2 20:16:23 2017.
#
# This file is execfile()d with the current directory set to its
# containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.

# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#

import datetime

# import os
# import sys
# sys.path.insert(0, '/home/chris/git/repairbox/repairbox')

# -- General configuration ------------------------------------------------

# If your documentation needs a minimal Sphinx version, state it here.
#
# needs_sphinx = '1.0'

# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
    'sphinx.ext.autodoc',
    'sphinx.ext.todo',
    'sphinx.ext.napoleon',
    'sphinx.ext.viewcode'
]

# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']

# The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string:
#
# source_suffix = ['.rst', '.md']
source_suffix = '.rst'

# The master toctree document.
master_doc = 'index'

# General information about the project.
project = 'repairbox'
year = datetime.datetime.now().year
copyright = '{}, Christopher Steven Timperley'.format(year)
author = 'Christopher Steven Timperley'

# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = ''
# The full version, including alpha/beta/rc tags.
release = ''

# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
language = 'en'

# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This patterns also effect to html_static_path and html_extra_path
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']

# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'

# If true, `todo` and `todoList` produce output, else they produce nothing.
todo_include_todos = True

# Disable preceding module names
add_module_names = False

# -- Options for HTML output ----------------------------------------------

# The theme to use for HTML and HTML Help pages.  See the documentation for
# a list of builtin themes.
#
html_theme = 'alabaster'

# Theme options are theme-specific and customize the look and feel of a theme
# further.  For a list of options available for each theme, see the
# documentation.
#
html_theme_options = {
    'description': 'A Python-based framework for conducting high-quality empirical studies of software artefacts.',
    'fixed_sidebar': True
}

# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']

# Custom sidebar templates, must be a dictionary that maps document names
# to template names.
#
# This is required for the alabaster theme
# refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars
html_sidebars = {
    '**': [
        'about.html',
        'navigation.html',
        'searchbox.html',
    ]
}

# -- Options for HTMLHelp output ------------------------------------------

# Output file base name for HTML help builder.
htmlhelp_basename = 'repairboxdoc'

# -- Options for LaTeX output ---------------------------------------------

latex_elements = {
    # The paper size ('letterpaper' or 'a4paper').
    #
    # 'papersize': 'letterpaper',

    # The font size ('10pt', '11pt' or '12pt').
    #
    # 'pointsize': '10pt',

    # Additional stuff for the LaTeX preamble.
    #
    # 'preamble': '',

    # Latex figure (float) alignment
    #
    # 'figure_align': 'htbp',
}

# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
#  author, documentclass [howto, manual, or own class]).
latex_documents = [
    (master_doc, 'repairbox.tex', 'repairbox Documentation',
     'Author', 'manual'),
]

# -- Options for manual page output ---------------------------------------

# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
    (master_doc, 'repairbox', 'repairbox Documentation',
     [author], 1)
]

# -- Options for Texinfo output -------------------------------------------

# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
#  dir menu entry, description, category)
texinfo_documents = [
    (master_doc, 'repairbox', 'repairbox Documentation',
     author, 'repairbox', 'One line description of project.',
     'Miscellaneous'),
]

# -- Options for Epub output ----------------------------------------------

# Bibliographic Dublin Core info.
epub_title = project
epub_author = author
epub_publisher = author
epub_copyright = copyright

# The unique identifier of the text. This can be a ISBN number
# or the project homepage.
#
# epub_identifier = ''

# A unique identification for the text.
#
# epub_uid = ''

# A list of files that should not be packed into the epub file.
epub_exclude_files = ['search.html']
<|endoftext|>


<commit_before><commit_msg>Utilizing proper inheritance and updating to a modern format.<commit_after><|endoftext|>


<commit_before>
"""
Module for handling experiment parameters and defaults.

This module defines an attribute descriptor for experiment parameters.
An experiment parameter is represented in Topographica as a special
kind of class attribute.  See the Parameter class documentation for
more details.

$Id$
"""
__version__='$Revision$'
from utils import classlist

class Parameter(object):
    """
    An attribute descriptor for declaring Topographica parameters.

    Simulation parameters are represented in Topographica as a special
    kind of class attribute.  Setting a class attribute to be an
    instance of this class causes that attribute of the class and its
    instances to be treated as a Topographica parameter.  This allows
    special behavior, including dynamically generated parameter values
    (using lambdas or generators), and type or range checking at
    assignment time.

    For example, suppose someone has defined a new kind of sheet, that
    has parameters alpha, sigma, and gamma.  He would begin his class
    definition something like this:

    class FooSheet(Sheet):
        alpha = Parameter(default=0.1)
        sigma = Parameter(default=0.0)
        gamma = Parameter(default=1.0)
        ...

    Parameters have several advantages over plain attributes:

    1. They can be set automatically in an instance:  The FooSheet
        __init__ method should call setup_params(self,FooSheet,**args),
        where **args is the dictionary of keyword arguments passed to the
        constructor.  This function will discover all the parameters in
        FooSheet, and set them from the arguments passed in.  E.g., if a
        script does this:

            myfoo = FooSheet(alpha=0.5)

        myfoo.alpha will return 0.5, without the foosheet constructor
        needing special code to set alpha.

    2. They can be dynamic.  If a Parameter is declared as Dynamic, it can
        be set to be a callable object (e.g a function), and getting the
        parameter's value will call that callable.  E.g.  To cause all
        FooSheets to draw their gammas from a gaussian distribution you'd
        write something like:

            from random import gauss
            FooSheet.sigma = Dynamic(lambda:gauss(0.5,0.1))

        If a Dynamic Parameter's value is set to a Python generator or iterator,
        then when the Parameter is accessed, the iterator's .next() method
        is called.  So to get a parameter that cycles through a sequence,
        you could write:
            from itertools import cycle
            FooSheet.sigma = Dynamic(cycle([0.1,0.5,0.9]))

    3. The Parameter descriptor class can be subclassed to provide more
        complex behavior, allowing special types of parameters that, for
        example, require their values to be numbers in certain ranges, or
        read their values from a file or other external source.

    Note: See this HOW-TO document for a good intro to descriptors in
    Python:
        http://users.rcn.com/python/download/Descriptor.htm

    (And the other items on http://www.python.org/doc/newstyle.html)
    """

    __slots__ = ['_name','default','doc','hidden']
    count = 0

    def __init__(self,default=None,doc="",hidden=False):
        """
        Initialize a new parameter.

        Set the name of the parameter to a gensym, and initialize
        the default value.
        """
        self._name = None
        self.doc = doc
        self.hidden=hidden
        Parameter.count += 1
        self.default = default

    def __get__(self,obj,objtype):
        """
        Get a parameter value.  If called on the class, produce the
        default value.  If called on an instance, produce the instance's
        value, if one has been set, otherwise produce the default value.
        """
        if not obj:
            result = self.default
        else:
            result = obj.__dict__.get(self.get_name(obj),self.default)
        return result

    def __set__(self,obj,val):
        """
        Set a parameter value.  If called on a class parameter,
        set the default value, if on an instance, set the value of the
        parameter in the object, where the value is stored in the
        instance's dictionary under the parameter's _name gensym.
        """
        if not obj:
            self.default = val
        else:
            obj.__dict__[self.get_name(obj)] = val

    # CEB:
    # A parameter on its own cannot be pickled.
    # Parameters owned by TopoObject get pickled.
    # In the future, parameters might not get pickled:
    # this should be fixed when pickling is sorted out.

    def __delete__(self,obj):
        """
        Delete a parameter.  Raises an exception.
        """
        raise "Deleting parameters is not allowed."

    def get_name(self,obj):
        """
        Return the name that the specified object has for this parameter.

        The parameter instance itself does not store its name, and cannot
        know what name the object that owns it might have for it.  However,
        it can discover this if the owning object is passed to this function,
        which looks for itself in the owning object and returns the name
        assigned to it.
        """
        if not hasattr(self,'_name') or not self._name:
            class_ = obj.__class__
            for attrib_name in dir(class_):
                desc,desctype = class_.get_param_descriptor(attrib_name)
                if desc is self:
                    self._name = '_%s_param_value'%attrib_name
                    break
        return self._name

    def _get_doc(self):
        return self.doc
    __doc__ = property(_get_doc)

# CEBHACKALERT: at the moment, the path must be relative to Topographica's path.
from os.path import normpath
class Filename(Parameter):
    """
    Filename is a Parameter that takes a string specifying the
    path of a file and stores it in the format of the user's operating system.

    The path must be relative to Topographica's own path.

    To make your code work on all platforms, you should specify paths in UNIX format
    (e.g. examples/ellen_arthur.pgm). You can specify paths in your operating
    system's format, but only code that uses UNIX-style paths will run on all operating
    systems.
    """
    def __init__(self,default='',**params):
        default = normpath(default)
        Parameter.__init__(self,default,**params)

    def __set__(self,obj,val):
        """
        Call Parameter's __set__ with the os-specific path.
        """
        super(Filename,self).__set__(obj,normpath(val))

class Number(Parameter):
    """
    Number is a numeric parameter. Numbers have a default value, and
    bounds.  There are two types of bounds: `bounds' and
    `softbounds'.`bounds' are hard bounds: the parameter must have a
    value within the specified range.  The default bounds are
    (None,None), meaning there are actually no hard bounds.  One or both
    bounds can be set by specifying a value (e.g. bounds=(None,10) means
    there is no lower bound, and an upper bound of 10).

    Bounds are checked when a Number is created or set. Using a default
    value outside the hard bounds, or one that is not numeric, results
    in an exception. It is therefore not possible to create a parameter
    with a default value that is inconsistent with the bounds.

    A separate function set_in_bounds() is provided that will silently
    crop the given value into the legal range, for use in, for instance,
    a GUI.

    `softbounds' are present to indicate the typical range of the
    parameter, but are not enforced. Setting the soft bounds allows, for
    instance, a GUI to know what values to display on sliders for the
    Number.

    Example of creating a Number:
    AB = Number(default=0.5, bounds=(None,10), softbounds=(0,1), doc='Distance from A to B.')
    """
    def __init__(self,default=0.0,bounds=(None,None),softbounds=(None,None),doc="",**params):
        Parameter.__init__(self,default=default,doc=doc,**params)
        self.bounds = bounds
        self._softbounds = softbounds
        self._check_bounds(default)  # only create this number if the default value and bounds are consistent

    def __set__(self,obj,val):
        """
        Set to the given value, raising an exception if out of bounds.
        """
        self._check_bounds(val)
        super(Number,self).__set__(obj,val)

    def set_in_bounds(self,obj,val):
        """
        Set to the given value, but cropped to be within the legal bounds.
        All objects are accepted, and no exceptions will be raised.  See
        crop_to_bounds for details on how cropping is done.
        """
        bounded_val = self.crop_to_bounds(val)
        super(Number,self).__set__(obj,bounded_val)

    def crop_to_bounds(self,val):
        """
        Return the given value cropped to be within the hard bounds
        for this parameter.

        If a numeric value is passed in, check it is within the hard
        bounds. If it is larger than the high bound, return the high
        bound. If it's smaller, return the low bound. In either case, the
        returned value could be None.  If a non-numeric value is passed
        in, set to be the default value (which could be None).  In no
        case is an exception raised; all values are accepted.
        """
        # Currently, values outside the bounds are silently cropped to
        # be inside the bounds; it may be appropriate to add a warning
        # in such cases.
        if (is_number(val)):
            min, max = self.bounds
            if min != None:
                if val < min:
                    return  min

            if max != None:
                if val > max:
                    return max

        else:
            # non-numeric value sent in: reverts to default value
            return  self.default

        return val

    def _check_bounds(self,val):
        """
        Checks that the value is numeric and that it is within the hard
        bounds; if not, an exception is raised.
        """

        # CEB: all the following error messages should probably print out the parameter's name
        # ('x', 'theta', or whatever)
        if not (is_number(val)):
            raise _NumberBoundsException("Parameter " + `self._name` + " (" + `self.__class__` + ") only takes a numeric value.")

        min,max = self.bounds
        if min != None and max != None:
            if not (min <= val <= max):
                raise _NumberBoundsException("Parameter must be between " + `min` + ' and ' + `max` + ' (inclusive).')
        elif min != None:
            if not min <= val:
                raise _NumberBoundsException("Parameter must be at least " + `min` + '.')
        elif max != None:
            if not val <= max:
                raise _NumberBoundsException("Parameter must be at most " + `min` + '.')

    def get_soft_bounds(self):
        """
        For each soft bound (upper and lower), if there is a defined bound (not equal to None)
        then it is returned, otherwise it defaults to the hard bound. The hard bound could still be None.
        """
        hl,hu = self.bounds
        sl,su = self._softbounds

        if (sl==None): l = hl
        else:          l = sl

        if (su==None): u = hu
        else:          u = su

        return (l,u)

class Integer(Number):
    def __set__(self,obj,val):
        if not isinstance(val,int):
            raise "Parameter must be an integer."
        super(Integer,self).__set__(obj,val)

class Magnitude(Number):
    def __init__(self,default=1.0,softbounds=(None,None),doc="",**params):
        Number.__init__(self,default=default,bounds=(0.0,1.0),softbounds=softbounds,doc=doc,**params)

class BooleanParameter(Parameter):
    def __init__(self,default=False,bounds=(0,1),doc="",**params):
        Parameter.__init__(self,default=default,doc=doc,**params)
        self.bounds = bounds

    def __set__(self,obj,val):
        if not isinstance(val,bool):
            raise "BooleanParameter only takes a Boolean value."

        if val != True and val != False:
            raise "BooleanParameter must be True or False"

        super(BooleanParameter,self).__set__(obj,val)

### Should this multiply inherit from Dynamic and Number?
### (Currently mixed together by hand)
class DynamicNumber(Number):
    """
    Dynamic version of Number parameter.

    If set with a callable object, the bounds are checked when the number is
    retrieved (generated), rather than when it is set.
    """
    def __init__(self,default=0.0,bounds=(None,None),softbounds=(None,None),doc="",**params):
        Parameter.__init__(self,default=default,doc=doc,**params)
        self.bounds = bounds
        self._softbounds = softbounds
        self._check_bounds(default)  # only create this number if the default value and bounds are consistent

    def __get__(self,obj,objtype):
        """
        Get a parameter value.  If called on the class, produce the
        default value.  If called on an instance, produce the instance's
        value, if one has been set, otherwise produce the default value.
        """
        if not obj:
            result = produce_value(self.default)
        else:
            result = produce_value(obj.__dict__.get(self.get_name(obj),self.default))
        self._check_bounds(result)
        return result

    def _check_bounds(self,val):
        """
        Except for callable values (assumed to be checked at get time), checks against bounds.

        Non-callable values are checked to be numeric and within the hard
        bounds.  If they are not, an exception is raised.
        """

        if not callable(val):
            super(DynamicNumber,self)._check_bounds(val)

class Dynamic(Parameter):
    def __get__(self,obj,objtype):
        """
        Get a parameter value.  If called on the class, produce the
        default value.  If called on an instance, produce the instance's
        value, if one has been set, otherwise produce the default value.
        """
        if not obj:
            result = produce_value(self.default)
        else:
            result = produce_value(obj.__dict__.get(self.get_name(obj),self.default))
        return result

class Constant(Parameter):
    """Constant Parameter that can be constructed and used but not set."""

    def __set__(self,obj,val):
        """Does not allow set commands."""
        raise "Constant parameter cannot be modified"

### JABALERT! Should this be replaced with simply ValueError?
class _NumberBoundsException(Exception):
    """
    This exception should be raised when there is an attempt
    to create a Number with a default outside its hard bounds.
    """
    def __init__(self, value):
        self.value = value
    def __str__(self):
        return repr(self.value)

def produce_value(value_obj):
    """
    A helper function that produces an actual parameter from a stored
    object.  If the object is callable, call it, else if it's an
    iterator, call its .next(), otherwise return the object.
    """
    if callable(value_obj):
        return value_obj()
    if is_iterator(value_obj):
        return value_obj.next()
    return value_obj

def is_iterator(obj):
    """
    Predicate that returns whether an object is an iterator.
    """
    import types
    return type(obj) == types.GeneratorType or ('__iter__' in dir(obj) and 'next' in dir(obj))

def is_number(obj):
    """
    Predicate that returns whether an object is a number.
    """
    # This may need to be extended to work with FixedPoint values.
    return (isinstance(obj,int) or isinstance(obj,float))

<commit_msg>Added 'precedence' attribute to Parameters. Precedence can be used to specify an ordering for Parameters within a TopoObject, which could be useful e.g. to order them in the GUI.<commit_after>
"""
Module for handling experiment parameters and defaults.

This module defines an attribute descriptor for experiment parameters.
An experiment parameter is represented in Topographica as a special
kind of class attribute.  See the Parameter class documentation for
more details.

$Id$
"""
__version__='$Revision$'
from utils import classlist

# CEBHACKALERT: what does objtype exist for in __get__()? Isn't it unused?

class Parameter(object):
    """
    An attribute descriptor for declaring Topographica parameters.

    Simulation parameters are represented in Topographica as a special
    kind of class attribute.  Setting a class attribute to be an
    instance of this class causes that attribute of the class and its
    instances to be treated as a Topographica parameter.  This allows
    special behavior, including dynamically generated parameter values
    (using lambdas or generators), and type or range checking at
    assignment time.

    For example, suppose someone has defined a new kind of sheet, that
    has parameters alpha, sigma, and gamma.  He would begin his class
    definition something like this:

    class FooSheet(Sheet):
        alpha = Parameter(default=0.1)
        sigma = Parameter(default=0.0)
        gamma = Parameter(default=1.0)
        ...

    Parameters have several advantages over plain attributes:

    1. They can be set automatically in an instance:  The FooSheet
        __init__ method should call setup_params(self,FooSheet,**args),
        where **args is the dictionary of keyword arguments passed to the
        constructor.  This function will discover all the parameters in
        FooSheet, and set them from the arguments passed in.  E.g., if a
        script does this:

            myfoo = FooSheet(alpha=0.5)

        myfoo.alpha will return 0.5, without the foosheet constructor
        needing special code to set alpha.

    2. They can be dynamic.  If a Parameter is declared as Dynamic, it can
        be set to be a callable object (e.g a function), and getting the
        parameter's value will call that callable.  E.g.  To cause all
        FooSheets to draw their gammas from a gaussian distribution you'd
        write something like:

            from random import gauss
            FooSheet.sigma = Dynamic(lambda:gauss(0.5,0.1))

        If a Dynamic Parameter's value is set to a Python generator or iterator,
        then when the Parameter is accessed, the iterator's .next() method
        is called.  So to get a parameter that cycles through a sequence,
        you could write:
            from itertools import cycle
            FooSheet.sigma = Dynamic(cycle([0.1,0.5,0.9]))

    3. The Parameter descriptor class can be subclassed to provide more
        complex behavior, allowing special types of parameters that, for
        example, require their values to be numbers in certain ranges, or
        read their values from a file or other external source.

    Note: See this HOW-TO document for a good intro to descriptors in
    Python:
        http://users.rcn.com/python/download/Descriptor.htm

    (And the other items on http://www.python.org/doc/newstyle.html)
    """

    __slots__ = ['_name','default','doc','hidden', 'precedence']
    count = 0

    def __init__(self,default=None,doc="",hidden=False,precedence=0.5):
        """
        Initialize a new parameter.

        Set the name of the parameter to a gensym, and initialize
        the default value.
        """
        self._name = None
        self.doc = doc
        self.hidden=hidden
        self.precedence = precedence
        Parameter.count += 1
        self.default = default

    def __get__(self,obj,objtype):
        """
        Get a parameter value.  If called on the class, produce the
        default value.  If called on an instance, produce the instance's
        value, if one has been set, otherwise produce the default value.
        """
        if not obj:
            result = self.default
        else:
            result = obj.__dict__.get(self.get_name(obj),self.default)
        return result

    def __set__(self,obj,val):
        """
        Set a parameter value.  If called on a class parameter,
        set the default value, if on an instance, set the value of the
        parameter in the object, where the value is stored in the
        instance's dictionary under the parameter's _name gensym.
        """
        if not obj:
            self.default = val
        else:
            obj.__dict__[self.get_name(obj)] = val

    def __delete__(self,obj):
        """
        Delete a parameter.  Raises an exception.
        """
        raise "Deleting parameters is not allowed."

    def get_name(self,obj):
        """
        Return the name that the specified object has for this parameter.

        The parameter instance itself does not store its name, and cannot
        know what name the object that owns it might have for it.  However,
        it can discover this if the owning object is passed to this function,
        which looks for itself in the owning object and returns the name
        assigned to it.
        """
        if not hasattr(self,'_name') or not self._name:
            class_ = obj.__class__
            for attrib_name in dir(class_):
                desc,desctype = class_.get_param_descriptor(attrib_name)
                if desc is self:
                    self._name = '_%s_param_value'%attrib_name
                    break
        return self._name

    def _get_doc(self):
        return self.doc
    __doc__ = property(_get_doc)

# CEBHACKALERT: at the moment, the path must be relative to Topographica's path.
from os.path import normpath
class Filename(Parameter):
    """
    Filename is a Parameter that takes a string specifying the
    path of a file and stores it in the format of the user's operating system.

    The path must be relative to Topographica's own path.

    To make your code work on all platforms, you should specify paths in UNIX format
    (e.g. examples/ellen_arthur.pgm). You can specify paths in your operating
    system's format, but only code that uses UNIX-style paths will run on all operating
    systems.
    """
    def __init__(self,default='',**params):
        Parameter.__init__(self,normpath(default),**params)

    def __set__(self,obj,val):
        """
        Call Parameter's __set__ with the os-specific path.
        """
        super(Filename,self).__set__(obj,normpath(val))

class Number(Parameter):
    """
    Number is a numeric parameter. Numbers have a default value, and
    bounds.  There are two types of bounds: `bounds' and
    `softbounds'.`bounds' are hard bounds: the parameter must have a
    value within the specified range.  The default bounds are
    (None,None), meaning there are actually no hard bounds.  One or both
    bounds can be set by specifying a value (e.g. bounds=(None,10) means
    there is no lower bound, and an upper bound of 10).

    Bounds are checked when a Number is created or set. Using a default
    value outside the hard bounds, or one that is not numeric, results
    in an exception. It is therefore not possible to create a parameter
    with a default value that is inconsistent with the bounds.

    A separate function set_in_bounds() is provided that will silently
    crop the given value into the legal range, for use in, for instance,
    a GUI.

    `softbounds' are present to indicate the typical range of the
    parameter, but are not enforced. Setting the soft bounds allows, for
    instance, a GUI to know what values to display on sliders for the
    Number.

    Example of creating a Number:
    AB = Number(default=0.5, bounds=(None,10), softbounds=(0,1), doc='Distance from A to B.')
    """
    def __init__(self,default=0.0,bounds=(None,None),softbounds=(None,None),doc="",**params):
        Parameter.__init__(self,default=default,doc=doc,**params)
        self.bounds = bounds
        self._softbounds = softbounds
        self._check_bounds(default)  # only create this number if the default value and bounds are consistent

    def __set__(self,obj,val):
        """
        Set to the given value, raising an exception if out of bounds.
        """
        self._check_bounds(val)
        super(Number,self).__set__(obj,val)

    def set_in_bounds(self,obj,val):
        """
        Set to the given value, but cropped to be within the legal bounds.
        All objects are accepted, and no exceptions will be raised.  See
        crop_to_bounds for details on how cropping is done.
        """
        bounded_val = self.crop_to_bounds(val)
        super(Number,self).__set__(obj,bounded_val)

    def crop_to_bounds(self,val):
        """
        Return the given value cropped to be within the hard bounds
        for this parameter.

        If a numeric value is passed in, check it is within the hard
        bounds. If it is larger than the high bound, return the high
        bound. If it's smaller, return the low bound. In either case, the
        returned value could be None.  If a non-numeric value is passed
        in, set to be the default value (which could be None).  In no
        case is an exception raised; all values are accepted.
        """
        # Currently, values outside the bounds are silently cropped to
        # be inside the bounds; it may be appropriate to add a warning
        # in such cases.
        if (is_number(val)):
            min, max = self.bounds
            if min != None:
                if val < min:
                    return  min

            if max != None:
                if val > max:
                    return max

        else:
            # non-numeric value sent in: reverts to default value
            return  self.default

        return val

    def _check_bounds(self,val):
        """
        Checks that the value is numeric and that it is within the hard
        bounds; if not, an exception is raised.
        """

        # CEB: all the following error messages should probably print out the parameter's name
        # ('x', 'theta', or whatever)
        if not (is_number(val)):
            raise _NumberBoundsException("Parameter " + `self._name` + " (" + `self.__class__` + ") only takes a numeric value.")

        min,max = self.bounds
        if min != None and max != None:
            if not (min <= val <= max):
                raise _NumberBoundsException("Parameter must be between " + `min` + ' and ' + `max` + ' (inclusive).')
        elif min != None:
            if not min <= val:
                raise _NumberBoundsException("Parameter must be at least " + `min` + '.')
        elif max != None:
            if not val <= max:
                raise _NumberBoundsException("Parameter must be at most " + `min` + '.')

    def get_soft_bounds(self):
        """
        For each soft bound (upper and lower), if there is a defined bound (not equal to None)
        then it is returned, otherwise it defaults to the hard bound. The hard bound could still be None.
        """
        hl,hu = self.bounds
        sl,su = self._softbounds

        if (sl==None): l = hl
        else:          l = sl

        if (su==None): u = hu
        else:          u = su

        return (l,u)

class Integer(Number):
    def __set__(self,obj,val):
        if not isinstance(val,int):
            raise "Parameter must be an integer."
        super(Integer,self).__set__(obj,val)

class Magnitude(Number):
    def __init__(self,default=1.0,softbounds=(None,None),doc="",**params):
        Number.__init__(self,default=default,bounds=(0.0,1.0),softbounds=softbounds,doc=doc,**params)

class BooleanParameter(Parameter):
    def __init__(self,default=False,bounds=(0,1),doc="",**params):
        Parameter.__init__(self,default=default,doc=doc,**params)
        self.bounds = bounds

    def __set__(self,obj,val):
        if not isinstance(val,bool):
            raise "BooleanParameter only takes a Boolean value."

        if val != True and val != False:
            raise "BooleanParameter must be True or False"

        super(BooleanParameter,self).__set__(obj,val)

### Should this multiply inherit from Dynamic and Number?
### (Currently mixed together by hand)
class DynamicNumber(Number):
    """
    Dynamic version of Number parameter.

    If set with a callable object, the bounds are checked when the number is
    retrieved (generated), rather than when it is set.
    """
    def __init__(self,default=0.0,bounds=(None,None),softbounds=(None,None),doc="",**params):
        Parameter.__init__(self,default=default,doc=doc,**params)
        self.bounds = bounds
        self._softbounds = softbounds
        self._check_bounds(default)  # only create this number if the default value and bounds are consistent

    def __get__(self,obj,objtype):
        """
        Get a parameter value.  If called on the class, produce the
        default value.  If called on an instance, produce the instance's
        value, if one has been set, otherwise produce the default value.
        """
        if not obj:
            result = produce_value(self.default)
        else:
            result = produce_value(obj.__dict__.get(self.get_name(obj),self.default))
        self._check_bounds(result)
        return result

    def _check_bounds(self,val):
        """
        Except for callable values (assumed to be checked at get time), checks against bounds.

        Non-callable values are checked to be numeric and within the hard
        bounds.  If they are not, an exception is raised.
        """

        if not callable(val):
            super(DynamicNumber,self)._check_bounds(val)

class Dynamic(Parameter):
    def __get__(self,obj,objtype):
        """
        Get a parameter value.  If called on the class, produce the
        default value.  If called on an instance, produce the instance's
        value, if one has been set, otherwise produce the default value.
        """
        if not obj:
            result = produce_value(self.default)
        else:
            result = produce_value(obj.__dict__.get(self.get_name(obj),self.default))
        return result

class Constant(Parameter):
    """Constant Parameter that can be constructed and used but not set."""

    def __set__(self,obj,val):
        """Does not allow set commands."""
        raise "Constant parameter cannot be modified"

### JABALERT! Should this be replaced with simply ValueError?
class _NumberBoundsException(Exception):
    """
    This exception should be raised when there is an attempt
    to create a Number with a default outside its hard bounds.
    """
    def __init__(self, value):
        self.value = value
    def __str__(self):
        return repr(self.value)

def produce_value(value_obj):
    """
    A helper function that produces an actual parameter from a stored
    object.  If the object is callable, call it, else if it's an
    iterator, call its .next(), otherwise return the object.
    """
    if callable(value_obj):
        return value_obj()
    if is_iterator(value_obj):
        return value_obj.next()
    return value_obj

def is_iterator(obj):
    """
    Predicate that returns whether an object is an iterator.
    """
    import types
    return type(obj) == types.GeneratorType or ('__iter__' in dir(obj) and 'next' in dir(obj))

def is_number(obj):
    """
    Predicate that returns whether an object is a number.
    """
    # This may need to be extended to work with FixedPoint values.
    return (isinstance(obj,int) or isinstance(obj,float))

<|endoftext|>


<commit_before>import random
import time
from calendar import timegm
from itertools import chain
from .actions import Attack, BeginTurn, Death, EndTurn, EventListener
from .card import THE_COIN
from .entity import Entity
from .enums import CardType, PlayState, State, Step, Zone
from .managers import GameManager
from .utils import CardList

class GameOver(Exception):
	pass

class BaseGame(Entity):
	type = CardType.GAME
	MAX_MINIONS_ON_FIELD = 7
	Manager = GameManager

	def __init__(self, players):
		self.data = None
		super().__init__()
		self.players = players
		for player in players:
			player.game = self
		self.state = State.INVALID
		self.step = None
		self.next_step = None
		self.turn = 0
		self.current_player = None
		self.auras = []
		self.minions_killed_this_turn = CardList()
		self.no_aura_refresh = False
		self.tick = 0
		self.active_aura_buffs = []

	def __repr__(self):
		return "%s(players=%r)" % (self.__class__.__name__, self.players)

	def __iter__(self):
		return self.all_entities.__iter__()

	@property
	def game(self):
		return self

	@property
	def board(self):
		return CardList(chain(self.players[0].field, self.players[1].field))

	@property
	def decks(self):
		return CardList(chain(self.players[0].deck, self.players[1].deck))

	@property
	def hands(self):
		return CardList(chain(self.players[0].hand, self.players[1].hand))

	@property
	def characters(self):
		return CardList(chain(self.players[0].characters, self.players[1].characters))

	@property
	def all_entities(self):
		return CardList(chain(self.entities, self.hands, self.decks, self.graveyard))

	@property
	def graveyard(self):
		return CardList(chain(self.players[0].graveyard, self.players[1].graveyard))

	@property
	def entities(self):
		return CardList(chain([self], self.players[0].entities, self.players[1].entities))

	@property
	def live_entities(self):
		return CardList(chain(self.players[0].live_entities, self.players[1].live_entities))

	def filter(self, *args, **kwargs):
		return self.all_entities.filter(*args, **kwargs)

	def attack(self, source, target):
		return self.queue_actions(source, [Attack(source, target)])

	def _attack(self):
		"""
		See https://github.com/jleclanche/fireplace/wiki/Combat
		for information on how attacking works
		"""
		attacker = self.proposed_attacker
		defender = self.proposed_defender
		self.proposed_attacker = None
		self.proposed_defender = None
		if attacker.should_exit_combat:
			self.log("Attack has been interrupted.")
			attacker.attacking = False
			defender.defending = False
			return
		# Save the attacker/defender atk values in case they change during the attack
		# (eg. in case of Enrage)
		def_atk = defender.atk
		attacker.hit(defender, attacker.atk)
		if def_atk:
			defender.hit(attacker, def_atk)
		attacker.attacking = False
		defender.defending = False
		attacker.num_attacks += 1

	def _play(self, card):
		"""
		Plays \a card from a Player's hand
		"""
		player = card.controller
		self.log("%s plays %r", player, card)
		cost = card.cost
		if player.temp_mana:
			# The coin, Innervate etc
			cost -= player.temp_mana
			player.temp_mana = max(0, player.temp_mana - card.cost)
		player.used_mana += cost
		player.last_card_played = card
		card.zone = Zone.PLAY

	def check_for_end_game(self):
		"""
		Check if one or more player is currently losing.
		End the game if they are.
		"""
		gameover = False
		for player in self.players:
			if player.playstate == PlayState.LOSING:
				gameover = True

		if gameover:
			if self.players[0].playstate == self.players[1].playstate:
				for player in self.players:
					player.playstate = PlayState.TIED
			else:
				for player in self.players:
					if player.playstate == PlayState.LOSING:
						player.playstate = PlayState.LOST
					else:
						player.playstate = PlayState.WON
			self.state = State.COMPLETE
			raise GameOver("The game has ended.")

	def process_deaths(self):
		actions = []
		for card in self.live_entities:
			if card.to_be_destroyed:
				actions += self._schedule_death(card)

		self.check_for_end_game()

		if actions:
			self.queue_actions(self, actions)

	def _schedule_death(self, card):
		"""
		Prepare a card for its death. Will run any related Death
		trigger attached to the Game object.
		Returns a list of actions to perform during the death sweep.
		"""
		self.logger.debug("Scheduling death for %r", card)
		card.ignore_events = True
		card.zone = Zone.GRAVEYARD
		if card.type == CardType.MINION:
			self.minions_killed_this_turn.append(card)
			card.controller.minions_killed_this_turn += 1
		elif card.type == CardType.HERO:
			card.controller.playstate = PlayState.LOSING

		return [Death(card)]

	def queue_actions(self, source, actions):
		"""
		Queue a list of \a actions for processing from \a source.
		"""
		ret = []
		if not hasattr(actions, "__iter__"):
			actions = (actions, )

		for action in actions:
			if isinstance(action, EventListener):
				# Queuing an EventListener registers it as a one-time event
				# This allows registering events from eg. play actions
				self.log("Registering event listener %r on %r", action, self)
				action.once = True
				# FIXME: Figure out a cleaner way to get the event listener target
				if source.type == CardType.SPELL:
					listener = source.controller
				else:
					listener = source
				listener._events.append(action)
			else:
				ret.append(action.trigger(source))
				self.refresh_auras()

		return ret

	def pick_first_player(self):
		"""
		Picks and returns first player, second player
		In the default implementation, the first player is always
		"Player 0". Use CoinRules to decide it randomly.
		"""
		return self.players[0], self.players[1]

	def refresh_auras(self):
		if self.no_aura_refresh:
			return
		for aura in self.auras:
			aura.update()
		for entity in self.entities:
			if entity.data and hasattr(entity.data.scripts, "update"):
				if not entity.silenced:
					entity.data.scripts.update.trigger(entity)
		for buff in self.active_aura_buffs[:]:
			if buff.tick < self.tick:
				buff.destroy()
		self.tick += 1

	def prepare(self):
		self.players[0].opponent = self.players[1]
		self.players[1].opponent = self.players[0]
		for player in self.players:
			player.zone = Zone.PLAY
			self.manager.new_entity(player)

		for player in self.players:
			player.summon(player.starting_hero)
			for id in player.starting_deck:
				player.card(id, zone=Zone.DECK)
			player.shuffle_deck()
			player.playstate = PlayState.PLAYING
			player.cards_drawn_this_turn = 0

		first, second = self.pick_first_player()
		self.player1 = first
		self.player1.first_player = True
		self.player2 = second
		self.player2.first_player = False
		self.player1.draw(self.player1.start_hand_size - 1)
		self.player2.draw(self.player1.start_hand_size)

	def start(self):
		self.log("Starting game %r", self)
		self.state = State.RUNNING
		self.step = Step.MAIN_BEGIN
		self.zone = Zone.PLAY
		self.prepare()
		self.manager.start_game()
		self.begin_turn(self.player1)

	def end_turn(self):
		return self.queue_actions(self, [EndTurn(self.current_player)])

	def _end_turn(self):
		self.log("%s ends turn %i", self.current_player, self.turn)
		self.manager.step(self.next_step, Step.MAIN_CLEANUP)

		self.current_player.temp_mana = 0
		for character in self.current_player.characters.filter(frozen=True):
			if not character.num_attacks:
				self.log("Freeze fades from %r", character)
				character.frozen = False
		for buff in self.current_player.entities.filter(one_turn_effect=True):
			self.log("Ending One-Turn effect: %r", buff)
			buff.destroy()

		self.manager.step(self.next_step, Step.MAIN_NEXT)
		self.begin_turn(self.current_player.opponent)

	def begin_turn(self, player):
		return self.queue_actions(self, [BeginTurn(player)])

	def _begin_turn(self, player):
		self.manager.step(self.next_step, Step.MAIN_START)
		self.turn += 1
		self.log("%s begins turn %i", player, self.turn)
		self.manager.step(self.next_step, Step.MAIN_ACTION)
		self.current_player = player
		self.minions_killed_this_turn = CardList()

		for p in self.players:
			p.cards_drawn_this_turn = 0

		player.turn_start = timegm(time.gmtime())
		player.cards_played_this_turn = 0
		player.minions_played_this_turn = 0
		player.minions_killed_this_turn = 0
		player.combo = False
		player.max_mana += 1
		player.used_mana = 0
		player.overload_locked = player.overloaded
		player.overloaded = 0
		for entity in player.entities:
			if entity.type != CardType.PLAYER:
				entity.turns_in_play += 1
				if entity.type == CardType.HERO_POWER:
					entity.exhausted = False

		for character in self.characters:
			character.num_attacks = 0

		player.draw()
		self.manager.step(self.next_step, Step.MAIN_END)

class CoinRules:
	"""
	Randomly determines the starting player when the Game starts.
	The second player gets "The Coin" (GAME_005).
	"""
	def pick_first_player(self):
		winner = random.choice(self.players)
		self.log("Tossing the coin... %s wins!", winner)
		return winner, winner.opponent

	def start(self):
		super().start()
		self.log("%s gets The Coin (%s)", self.player2, THE_COIN)
		self.player2.give(THE_COIN)

class MulliganRules:
	"""
	Performs a Mulligan phase when the Game starts.
	Currently just a dummy phase.
	"""

	def start(self):
		from .actions import MulliganChoice

		self.next_step = Step.BEGIN_MULLIGAN
		super().start()

		self.log("Entering mulligan phase")
		self.step, self.next_step = self.next_step, Step.MAIN_READY

		for player in self.players:
			self.queue_actions(self, MulliganChoice(player))

class Game(MulliganRules, CoinRules, BaseGame):
	pass
<commit_msg>Make Game.active_aura_buffs a CardList<commit_after>import random
import time
from calendar import timegm
from itertools import chain
from .actions import Attack, BeginTurn, Death, EndTurn, EventListener
from .card import THE_COIN
from .entity import Entity
from .enums import CardType, PlayState, State, Step, Zone
from .managers import GameManager
from .utils import CardList

class GameOver(Exception):
	pass

class BaseGame(Entity):
	type = CardType.GAME
	MAX_MINIONS_ON_FIELD = 7
	Manager = GameManager

	def __init__(self, players):
		self.data = None
		super().__init__()
		self.players = players
		for player in players:
			player.game = self
		self.state = State.INVALID
		self.step = None
		self.next_step = None
		self.turn = 0
		self.current_player = None
		self.auras = []
		self.minions_killed_this_turn = CardList()
		self.no_aura_refresh = False
		self.tick = 0
		self.active_aura_buffs = CardList()

	def __repr__(self):
		return "%s(players=%r)" % (self.__class__.__name__, self.players)

	def __iter__(self):
		return self.all_entities.__iter__()

	@property
	def game(self):
		return self

	@property
	def board(self):
		return CardList(chain(self.players[0].field, self.players[1].field))

	@property
	def decks(self):
		return CardList(chain(self.players[0].deck, self.players[1].deck))

	@property
	def hands(self):
		return CardList(chain(self.players[0].hand, self.players[1].hand))

	@property
	def characters(self):
		return CardList(chain(self.players[0].characters, self.players[1].characters))

	@property
	def all_entities(self):
		return CardList(chain(self.entities, self.hands, self.decks, self.graveyard))

	@property
	def graveyard(self):
		return CardList(chain(self.players[0].graveyard, self.players[1].graveyard))

	@property
	def entities(self):
		return CardList(chain([self], self.players[0].entities, self.players[1].entities))

	@property
	def live_entities(self):
		return CardList(chain(self.players[0].live_entities, self.players[1].live_entities))

	def filter(self, *args, **kwargs):
		return self.all_entities.filter(*args, **kwargs)

	def attack(self, source, target):
		return self.queue_actions(source, [Attack(source, target)])

	def _attack(self):
		"""
		See https://github.com/jleclanche/fireplace/wiki/Combat
		for information on how attacking works
		"""
		attacker = self.proposed_attacker
		defender = self.proposed_defender
		self.proposed_attacker = None
		self.proposed_defender = None
		if attacker.should_exit_combat:
			self.log("Attack has been interrupted.")
			attacker.attacking = False
			defender.defending = False
			return
		# Save the attacker/defender atk values in case they change during the attack
		# (eg. in case of Enrage)
		def_atk = defender.atk
		attacker.hit(defender, attacker.atk)
		if def_atk:
			defender.hit(attacker, def_atk)
		attacker.attacking = False
		defender.defending = False
		attacker.num_attacks += 1

	def _play(self, card):
		"""
		Plays \a card from a Player's hand
		"""
		player = card.controller
		self.log("%s plays %r", player, card)
		cost = card.cost
		if player.temp_mana:
			# The coin, Innervate etc
			cost -= player.temp_mana
			player.temp_mana = max(0, player.temp_mana - card.cost)
		player.used_mana += cost
		player.last_card_played = card
		card.zone = Zone.PLAY

	def check_for_end_game(self):
		"""
		Check if one or more player is currently losing.
		End the game if they are.
		"""
		gameover = False
		for player in self.players:
			if player.playstate == PlayState.LOSING:
				gameover = True

		if gameover:
			if self.players[0].playstate == self.players[1].playstate:
				for player in self.players:
					player.playstate = PlayState.TIED
			else:
				for player in self.players:
					if player.playstate == PlayState.LOSING:
						player.playstate = PlayState.LOST
					else:
						player.playstate = PlayState.WON
			self.state = State.COMPLETE
			raise GameOver("The game has ended.")

	def process_deaths(self):
		actions = []
		for card in self.live_entities:
			if card.to_be_destroyed:
				actions += self._schedule_death(card)

		self.check_for_end_game()

		if actions:
			self.queue_actions(self, actions)

	def _schedule_death(self, card):
		"""
		Prepare a card for its death. Will run any related Death
		trigger attached to the Game object.
		Returns a list of actions to perform during the death sweep.
		"""
		self.logger.debug("Scheduling death for %r", card)
		card.ignore_events = True
		card.zone = Zone.GRAVEYARD
		if card.type == CardType.MINION:
			self.minions_killed_this_turn.append(card)
			card.controller.minions_killed_this_turn += 1
		elif card.type == CardType.HERO:
			card.controller.playstate = PlayState.LOSING

		return [Death(card)]

	def queue_actions(self, source, actions):
		"""
		Queue a list of \a actions for processing from \a source.
		"""
		ret = []
		if not hasattr(actions, "__iter__"):
			actions = (actions, )

		for action in actions:
			if isinstance(action, EventListener):
				# Queuing an EventListener registers it as a one-time event
				# This allows registering events from eg. play actions
				self.log("Registering event listener %r on %r", action, self)
				action.once = True
				# FIXME: Figure out a cleaner way to get the event listener target
				if source.type == CardType.SPELL:
					listener = source.controller
				else:
					listener = source
				listener._events.append(action)
			else:
				ret.append(action.trigger(source))
				self.refresh_auras()

		return ret

	def pick_first_player(self):
		"""
		Picks and returns first player, second player
		In the default implementation, the first player is always
		"Player 0". Use CoinRules to decide it randomly.
		"""
		return self.players[0], self.players[1]

	def refresh_auras(self):
		if self.no_aura_refresh:
			return
		for aura in self.auras:
			aura.update()
		for entity in self.entities:
			if entity.data and hasattr(entity.data.scripts, "update"):
				if not entity.silenced:
					entity.data.scripts.update.trigger(entity)
		for buff in self.active_aura_buffs[:]:
			if buff.tick < self.tick:
				buff.destroy()
		self.tick += 1

	def prepare(self):
		self.players[0].opponent = self.players[1]
		self.players[1].opponent = self.players[0]
		for player in self.players:
			player.zone = Zone.PLAY
			self.manager.new_entity(player)

		for player in self.players:
			player.summon(player.starting_hero)
			for id in player.starting_deck:
				player.card(id, zone=Zone.DECK)
			player.shuffle_deck()
			player.playstate = PlayState.PLAYING
			player.cards_drawn_this_turn = 0

		first, second = self.pick_first_player()
		self.player1 = first
		self.player1.first_player = True
		self.player2 = second
		self.player2.first_player = False
		self.player1.draw(self.player1.start_hand_size - 1)
		self.player2.draw(self.player1.start_hand_size)

	def start(self):
		self.log("Starting game %r", self)
		self.state = State.RUNNING
		self.step = Step.MAIN_BEGIN
		self.zone = Zone.PLAY
		self.prepare()
		self.manager.start_game()
		self.begin_turn(self.player1)

	def end_turn(self):
		return self.queue_actions(self, [EndTurn(self.current_player)])

	def _end_turn(self):
		self.log("%s ends turn %i", self.current_player, self.turn)
		self.manager.step(self.next_step, Step.MAIN_CLEANUP)

		self.current_player.temp_mana = 0
		for character in self.current_player.characters.filter(frozen=True):
			if not character.num_attacks:
				self.log("Freeze fades from %r", character)
				character.frozen = False
		for buff in self.current_player.entities.filter(one_turn_effect=True):
			self.log("Ending One-Turn effect: %r", buff)
			buff.destroy()

		self.manager.step(self.next_step, Step.MAIN_NEXT)
		self.begin_turn(self.current_player.opponent)

	def begin_turn(self, player):
		return self.queue_actions(self, [BeginTurn(player)])

	def _begin_turn(self, player):
		self.manager.step(self.next_step, Step.MAIN_START)
		self.turn += 1
		self.log("%s begins turn %i", player, self.turn)
		self.manager.step(self.next_step, Step.MAIN_ACTION)
		self.current_player = player
		self.minions_killed_this_turn = CardList()

		for p in self.players:
			p.cards_drawn_this_turn = 0

		player.turn_start = timegm(time.gmtime())
		player.cards_played_this_turn = 0
		player.minions_played_this_turn = 0
		player.minions_killed_this_turn = 0
		player.combo = False
		player.max_mana += 1
		player.used_mana = 0
		player.overload_locked = player.overloaded
		player.overloaded = 0
		for entity in player.entities:
			if entity.type != CardType.PLAYER:
				entity.turns_in_play += 1
				if entity.type == CardType.HERO_POWER:
					entity.exhausted = False

		for character in self.characters:
			character.num_attacks = 0

		player.draw()
		self.manager.step(self.next_step, Step.MAIN_END)

class CoinRules:
	"""
	Randomly determines the starting player when the Game starts.
	The second player gets "The Coin" (GAME_005).
	"""
	def pick_first_player(self):
		winner = random.choice(self.players)
		self.log("Tossing the coin... %s wins!", winner)
		return winner, winner.opponent

	def start(self):
		super().start()
		self.log("%s gets The Coin (%s)", self.player2, THE_COIN)
		self.player2.give(THE_COIN)

class MulliganRules:
	"""
	Performs a Mulligan phase when the Game starts.
	Currently just a dummy phase.
	"""

	def start(self):
		from .actions import MulliganChoice

		self.next_step = Step.BEGIN_MULLIGAN
		super().start()

		self.log("Entering mulligan phase")
		self.step, self.next_step = self.next_step, Step.MAIN_READY

		for player in self.players:
			self.queue_actions(self, MulliganChoice(player))

class Game(MulliganRules, CoinRules, BaseGame):
	pass
<|endoftext|>


<commit_before>import logging
import numpy as np

logger=logging.getLogger(__file__)

class ExponentialDistribution:
    def __init__(self, lam, te):
        self.lam=lam
        self.te=te

    def sample(self, now, rng):
        return now+rng.exponential(self.lam)

    def hazard_integral(self, t0, t1):
        return self.lam*(t1-t0)

    def implicit_hazard_integral(self, xa, t0):
        return t0+xa/self.lam

    def enabling_time(self):
        return self.te

class WeibullDistribution:
    def __init__(self, lam, k, te, shift):
        self.lam=lam
        self.k=k
        self.te=te
        self.delta=shift

    def sample(self, now, rng):
        U=rng.uniform(0, 1)
        l=self.lam
        k=self.k
        d=now - (self.te - self.delta)
        value=0
        if d>0:
            value=l*np.pow(-np.log(1-U) + np.pow(d/l, k), 1/k)-d
        else:
            value=-d+l*np.pow(-np.log(1-U), 1/k)
        return now + value

    def hazard_integral(self, t0, t1):
        return ( np.pow((t1-self.te)/self.lam, self.k)
            -np.pow((t0-self.te)/self.lam, self.k) )

    def implicit_hazard_integral(self, xa, t0):
        return self.te+ self.lam*np.pow(xa+np.pow((t0-self.te)/self.lam,
                self.k), 1/self.k)

    def enabling_time(self):
        return self.te
<commit_msg>Working on uniform<commit_after>import logging
import numpy as np
import scipy.stats

logger=logging.getLogger(__file__)

def anderson_sample_tester(distribution, now, cnt, rng):
    """
    This checks whether hazard_integral and implicit_hazard_integral
    work by using them to sample a distribution.
    """
    samples=np.zeros(cnt)
    maxdiff=0.0
    for i in range(cnt):
        interval=-np.log(rng.uniform(0, 1))
        last_mod=now
        firing_time=distribution.implicit_hazard_integral(interval, last_mod)
        next_fire=firing_time
        stops=np.linspace(now, firing_time, num=rng.randint(0,5))
        for stop in stops[:-1]:
            time_penalty=distribution.hazard_integral(last_mod, stop)
            interval-=time_penalty
            next_fire=distribution.implicit_hazard_integral(interval, stop)
            last_mod=stop
        maxdiff=max(abs(next_fire-firing_time), maxdiff)
        samples[i]=next_fire
    return samples

class ExponentialDistribution(object):
    """
    This represents an exponential distribution.
    .. math::

        F(t) = 1-e^{-\int_0^t \lambda(s) ds}
    """
    def __init__(self, lam, te):
        self.lam=lam
        self.te=te

    def sample(self, now, rng):
        return now+rng.exponential(scale=1.0/self.lam)

    def hazard_integral(self, t0, t1):
        return self.lam*(t1-t0)

    def implicit_hazard_integral(self, xa, t0):
        return t0+xa/self.lam

    def enabling_time(self):
        return self.te

class WeibullDistribution(object):
    """
    This is a Weibull distribution.
    """
    def __init__(self, lam, k, te, shift):
        self.lam=lam
        self.k=k
        self.te=te
        self.delta=shift

    def sample(self, now, rng):
        U=rng.uniform(0, 1)
        l=self.lam
        k=self.k
        d=now - (self.te - self.delta)
        value=0
        if d>0:
            value=l*np.pow(-np.log(1-U) + np.pow(d/l, k), 1/k)+self.te
        else:
            value=l*np.pow(-np.log(1-U), 1/k)+self.te
        return now + value

    def hazard_integral(self, t0, t1):
        return ( np.pow((t1-self.te)/self.lam, self.k)
            -np.pow((t0-self.te)/self.lam, self.k) )

    def implicit_hazard_integral(self, xa, t0):
        return self.te+ self.lam*np.pow(xa+np.pow((t0-self.te)/self.lam,
                self.k), 1/self.k)

    def enabling_time(self):
        return self.te

class GammaDistribution(object):
    """
    This is a gamma distribution with a shape and a rate,
    not a shape and a scale.
    Given a Gamma function,
    .. math::

        \Gamma(t)=\int_0^\infty x^{t-1}e^{-x}dx

    and the (lower) incomplete gamma function,
    .. math::

        \gamma(x;\alpha)=\int_0^x t^{\alpha-1}e^{-t}dt

    the CDF of the gamma distribution is
    .. math::

        F(x;\alpha, \beta)=\frac{\gamma(\alpha, \beta x)}{\Gamma(\alpha)}

    The PDF is
    .. math::

        f(x)=\frac{\beta^{\alpha}}{\Gamma(\alpha)}x^{\alpha-1}e^{-\beta x}

    This is sampled with possible left censoring.
    """
    def __init__(self, alpha, beta, te):
        self.alpha=alpha
        self.beta=beta
        self.te=te

    def sample(self, now, rng):
        """
        Sampling accounts for time shift and uses given random
        number generator.
        """
        U=rng.uniform(low=0, high=1, size=1)
        d=now-self.te
        if d>0:
            cumulative=scipy.stats.gamma.cdf(x=d, a=self.alpha,
                scale=1.0/self.beta, loc=0)
            return scipy.stats.gamma.isf(1-U*(1-cumulative)-cumulative,
                self.alpha, scale=1.0/self.beta, loc=0) + self.te
        else:
            return scipy.stats.gamma.isf(1-U,
                self.alpha, scale=1.0/self.beta, loc=0) + self.te

    def hazard_integral(self, t0, t1):
        """
        Our tools include

         - gammainc(a, x), normalized lower incomplete
         - gammaincinv(a, y), gammainc(a, x)=y
         - gammaincc(a, x), 1-gammainc(a, x)
         - gammainccinv(a, y), gammaincc(a, x)=y
        """
        return np.log(
            (1-scipy.special.gammainc(self.alpha, self.beta*(t0-self.te)))/
            (1-scipy.special.gammainc(self.alpha, self.beta*(t1-self.te)))
            )

    def implicit_hazard_integral(self, xa, t0):
        quad=1-np.exp(-xa)*(1-scipy.special.gammainc(self.alpha,
                self.beta*(t0-self.te)))
        return self.te+scipy.special.gammaincinv(self.alpha, quad)/self.beta

    def enabling_time(self):
        return self.te

class UniformDistribution(object):
    """
    Uniform distribution between a and b, offset by an enabling time te.
    """
    def __init__(self, a, b, te):
        """
        te is an absolute time.
        The earliest firing time is >te+a.
        The latest firing time is <=te+b.
        """
        self.a=a
        self.b=b
        self.te=te

    def sample(self, now, rng):
        """
        Sampling accounts for time shift and uses given random
        number generator.
        """
        if now<=self.te+self.a:
            return rng.uniform(low=self.te+self.a, high=self.te+self.b)
        elif now<=self.te+self.b:
            return rng.uniform(low=now, high=self.te+self.b)
        else:
            return np.float("nan")

    def hazard_integral(self, t0, t1):
        """
        Integrate the hazard, taking into account when the uniform
        interval starts and stops.
        """
        t0e=t0-self.te
        t1e=t1-self.te
        if t0e>self.b:
            return 0
        if t1e<self.a:
            return 0
        low=max(self.a, t0e)
        high=min(self.b, t1e)
        numerator=np.log(1-(t0e-self.a)/(self.b-self.a))
        denominator=np.log((self.b-t1e)/(self.b-self.a))
        return numerator-denominator

    def implicit_hazard_integral(self, xa, t0):
        Ft=1-np.exp(-xa)
        t0e=t0-self.te
        low=max(t0e, self.ta)
        high=min(t0e, self.tb)
        r=self.te+low*(1-Ft) + high*Ft
        return r

    def enabling_time(self):
        return self.te

class EmpiricalDistribution(object):
    """
    This distribution is used to collect samples and then
    compare the estimate of the sampled distribution with
    some known distribution.

    Wikipedia explains this. Kolmogorov-Smirnov test.
    https://en.wikipedia.org/wiki/Kolmogorov%E2%80%93Smirnov_test
    """
    def __init__(self, samples):
        self.samples=samples

    def c_alpha(self):
        """
        The hypothesis is that the two distributions are different.
        The null hypothesis is rejected at level alpha if
        Dnn*sqrt(nn/(n+n))>c_alpha. This returns a table of c_alpha.
        The upshot is that you should look for values from the
        two comparison functions that are less than two.
        """
        return [[0.1, 1.22],
                [0.05, 1.36],
                [0.025, 1.48],
                [0.01, 1.63],
                [0.005, 1.73],
                [0.001, 1.95]]

    def compare_empirical(self, other):
        """
        Compare two empirical distributions using Kolmogorov-Smirnov.
        This returns the Kolmogorov-smirnov goodness-of-fit,
        sqrt(n)*D_n, where D_n is the Kolmogorov-smirnov statistic.
        """
        self.samples.sort()
        other.samples.sort()
        acnt=len(self.samples)
        aidx=-1
        bcnt=len(other.samples)
        bidx=-1
        maxdiff=0.0
        while aidx+1<acnt and bidx+1<bcnt:
            if bidx+1==bcnt:
                v=self.samples[aidx+1]
                while aidx+1!=acnt and self.samples[aidx+1]==v:
                    aidx+=1
                maxdiff=max(maxdiff, abs((aidx+1)/acnt - (bidx+1)/bcnt))
            elif aidx+1==acnt:
                v=other.samples[bidx+1]
                while bidx+1!=bcnt and other.samples[bidx+1]==v:
                    bidx+=1
                maxdiff=max(maxdiff, abs((aidx+1)/acnt - (bidx+1)/bcnt))
            elif self.samples[aidx+1]<other.samples[bidx+1]:
                v=self.samples[aidx+1]
                while aidx+1!=acnt and self.samples[aidx+1]==v:
                    aidx+=1
                maxdiff=max(maxdiff, abs((aidx+1)/acnt - (bidx+1)/bcnt))
            else:
                v=other.samples[bidx+1]
                while aidx+1!=acnt and self.samples[aidx+1]==v:
                    aidx+=1
                while bidx+1!=bcnt and other.samples[bidx+1]==v:
                    bidx+=1
                maxdiff=max(maxdiff, abs((aidx+1)/acnt - (bidx+1)/bcnt))
        return maxdiff*np.sqrt(acnt*bcnt/(acnt+bcnt))

    def compare_theoretical(self, cdf):
        """
        Compare this estimated CDF with a function which returns
        the CDF at a point. Use Kolmogorov-Smirnov to return a
        statistic which should be stable with increasing
        numbers of samples.
        This returns the Kolmogorov-smirnov goodness-of-fit,
        sqrt(n)*D_n, where D_n is the Kolmogorov-smirnov statistic.
        """
        self.samples.sort()
        acnt=len(self.samples)
        aidx=-1
        maxdiff=0.0
        while aidx!=acnt:
            v=self.samples[aidx+1]
            while self.samples[aidx+1]==v:
                aidx+=1
            maxdiff=max(maxdiff,
                abs(cdf(self.samples[aidx])-(aidx+1)/acnt))
        return maxdiff*np.sqrt(acnt)

<|endoftext|>


<commit_before>#!/usr/bin/env python

# Copyright (c) 2016, Simon Carrier, Simon Brodeur
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in the
#    documentation and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its contributors
#    may be used to endorse or promote products derived from this software without
#    specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
# OF SUCH DAMAGE.

import os
import logging
import numpy as np
import rosbag

import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt

from tabulate import tabulate
from optparse import OptionParser

logger = logging.getLogger(__name__)

def getStatisticsFromTimestamps(timestamps, dropThreshold=1.0):

    # Calculate relative times between messages
    timeDiffs = timestamps[1:] - timestamps[:-1]
    mean = np.mean(timeDiffs)
    variance = np.var(timeDiffs)
    if mean > 0.0:
        averageRate = 1.0/mean
    else:
        averageRate = 0.0
    nbMsgs = len(timestamps)
    nbMsgDropped = np.count_nonzero(timeDiffs > mean*(1.0 + dropThreshold))

    return mean, variance, averageRate, nbMsgs, nbMsgDropped

def saveHistogramsToFiles(topicTimestamps, dropThreshold, outputPath):

    outputPath = os.path.abspath(outputPath)
    if not os.path.exists(outputPath):
        logger.info('Creating output directory for histograms: %s' % (outputPath))
        os.makedirs(outputPath)

    for topic, timestamps in topicTimestamps.iteritems():
        name = topic[1:].replace("/", "-")

        # Compute time delays (in msec)
        timeDiffs = (timestamps[1:] - timestamps[:-1]) * 1000.0
        mean = np.mean(timeDiffs)

        fig = plt.figure(figsize=(8,6), facecolor='white')

        plt.title("Histogram for " + name)
        plt.xlabel('Time delay between messages [ms]')
        plt.ylabel('Message count')
        plt.hist(timeDiffs, bins=500, color='k')
        plt.axvline(mean, color='b')
        plt.axvline(mean + mean*dropThreshold, color='r')
        plt.axvline(mean - mean*dropThreshold, color='r')

        filename = os.path.join(outputPath, name + '.png')
        logger.info('Saving histogram figure to file: %s' % (filename))
        plt.savefig(filename, dpi=100)
        plt.close(fig)

def getTabulatedStatistics(topicTimestamps, topicSequenceIds, dropThreshold):

    headers = ["Topic name", "Average Rate [Hz]", "Average Period [ms]",
               "Std Deviation Period [ms]", "Nb of Messages", "Drop ratio [%] (timestamp-based)", "Drop ratio [%] (sequence-based)"]

    # Loop for each topic
    data = []
    for topic, timestamps in topicTimestamps.iteritems():

        # Calculate statistics from timestamps
        mean, variance, averageRate, nbMsgs, nbMsgDropped = getStatisticsFromTimestamps(timestamps, dropThreshold)

        # Calculate statistics from sequence ids
        ids = topicSequenceIds[topic]
        minId, maxId = np.min(ids), np.max(ids)
        nbMsgsSeqId = (maxId + 1) - minId
        diff = np.setdiff1d(ids, np.arange(minId, maxId+1, dtype=np.int))
        nbMsgDroppedSeqId = len(diff)

        # Add to table
        data.append([topic, averageRate, mean*1000.0, np.sqrt(variance)*1000.0, nbMsgs, nbMsgDropped/float(nbMsgs) * 100.0, nbMsgDroppedSeqId/float(nbMsgsSeqId) * 100.0])

    # Sort table by topic name
    data.sort(key=lambda x: x[0])

    return tabulate(data, headers, tablefmt="grid", numalign="right", stralign="left", floatfmt=".2f")

def getAllTopicsMetadata(filename, ignoredTopics, useRosbagTime=False):

    topicTimestamps = dict()
    topicSequenceIds = dict()
    with rosbag.Bag(filename) as bag:
        # Grab list of topics
        topics = bag.get_type_and_topic_info()[1].keys()

        # Generate Topic statisitics objects
        for topic in topics:
            if not topic in ignoredTopics:
                topicTimestamps[topic] = []
                topicSequenceIds[topic] = []

        # Process all messages
        for topic, msg, t in bag.read_messages():
            if topic in topicTimestamps:
                if useRosbagTime:
                    t = t.to_sec()
                else:
                    # Get timestamp from header
                    t = float(msg.header.stamp.to_sec())
                topicTimestamps[topic].append(t)

                id = msg.header.seq
                topicSequenceIds[topic].append(id)

    # Convert to sorted numpy arrays
    # NOTE: sorting is important when using capture time, since they are not guaranteed
    #       to be ordered as in the rosbag.
    for topic, timestamps in topicTimestamps.iteritems():
        topicTimestamps[topic] = np.sort(np.array(timestamps))
    for topic, ids in topicSequenceIds.iteritems():
        topicSequenceIds[topic] = np.sort(np.array(ids))
    return topicTimestamps, topicSequenceIds

def main(args=None):

    parser = OptionParser()
    parser.add_option("-i", "--input", dest="input", default=False,
                      help="Location of Rosbag to analyse")
    parser.add_option("-o", "--output",  dest="output", default="",
                      help="Output stats file")
    parser.add_option("-r", "--ignore-topics", dest="topicRemove", default="",
                      help="Comma separated list of topics to ignore")
    parser.add_option("-c", "--use-rosbag-time",
                      action="store_true", dest="useRosbagTime", default=False,
                      help="Use rosbag time instead of capture time")
    parser.add_option("-g", "--save-histograms",
                      action="store_true", dest="saveHistograms", default=False,
                      help="Specify to save the histograms as images")
    parser.add_option("-t", "--drop-threshold", dest="dropThreshold", type='float', default=1.0,
                      help='Specify the threshold to use for detecting dropped messages')
    (options, args) = parser.parse_args()

    if not options.input:
        parser.error("Please specify input rosbag file with -i ")

    if options.saveHistograms and not options.output:
        parser.error("Please specify an output file with -o when exporting histograms ")

    inputRosbagPath = os.path.abspath(options.input)
    logger.info('Using input rosbag file: %s' % (inputRosbagPath))

    if options.output:
        outputStatsFilePath = os.path.abspath(options.output)
        logger.info('Using output statistics file: %s' % (outputStatsFilePath))

    ignoreList = []
    if options.topicRemove:
        ignoreList = options.topicRemove.split(',')
        logger.info('Ignored topics: %s' % (options.topicRemove))

    # Get timestamps and sequence ids from the rosbag
    topicTimestamps, topicSequenceIds = getAllTopicsMetadata(inputRosbagPath, ignoreList, options.useRosbagTime)

    if options.saveHistograms and options.output:
        outputPath = outputStatsFilePath + '.histograms'
        saveHistogramsToFiles(topicTimestamps, options.dropThreshold, outputPath)

    # Print statistics to string
    str = getTabulatedStatistics(topicTimestamps, topicSequenceIds, options.dropThreshold)

    if not options.output:
        # Just print to stdout
        print str
    else:
        # Print to file
        with open(outputStatsFilePath, 'w') as f:
            f.write(str + '\n')

    logger.info('All done.')

if __name__ == '__main__':
    logging.basicConfig(level=logging.INFO)
    main()
<commit_msg>Added Dropped Msgs over time graph generation option<commit_after>#!/usr/bin/env python

# Copyright (c) 2016, Simon Carrier, Simon Brodeur
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in the
#    documentation and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its contributors
#    may be used to endorse or promote products derived from this software without
#    specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
# OF SUCH DAMAGE.

import os
import logging
import numpy as np
import rosbag

import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt

from tabulate import tabulate
from optparse import OptionParser

logger = logging.getLogger(__name__)

def getStatisticsFromTimestamps(timestamps, dropThreshold=1.0):

    # Calculate relative times between messages
    timeDiffs = timestamps[1:] - timestamps[:-1]
    mean = np.mean(timeDiffs)
    variance = np.var(timeDiffs)
    if mean > 0.0:
        averageRate = 1.0/mean
    else:
        averageRate = 0.0
    nbMsgs = len(timestamps)
    nbMsgDropped = np.count_nonzero(timeDiffs > mean*(1.0 + dropThreshold))

    return mean, variance, averageRate, nbMsgs, nbMsgDropped

def getdroprateGraphOverTime(topicTimestamps, dropThreshold=1.0, windowWidth=10.0):

    firstMessageStamp = -1
    lastMessageStamp = -1
    for topic, timestamps in topicTimestamps.iteritems():
        if (firstMessageStamp > timestamps[0]) or (firstMessageStamp == -1):
            firstMessageStamp = timestamps[0]
        if (lastMessageStamp < timestamps[-1]) or (lastMessageStamp == -1):
            lastMessageStamp = timestamps[-1]
    timeSlices = np.arange(firstMessageStamp, lastMessageStamp, windowWidth)

    totalDropOverTime = np.zeros(np.size(timeSlices))
    for topic, timestamps in topicTimestamps.iteritems():
        for i in range(np.size(timeSlices)-1):
            sWindow = timestamps[(timeSlices[i] < timestamps) & (timestamps < timeSlices[i+1]) ]
            _,_,_,_,dropMsgs = getStatisticsFromTimestamps(sWindow, dropThreshold)
            totalDropOverTime[i] += dropMsgs

    return totalDropOverTime

def saveHistogramsToFiles(topicTimestamps, dropThreshold, outputPath):

    outputPath = os.path.abspath(outputPath)
    if not os.path.exists(outputPath):
        logger.info('Creating output directory for histograms: %s' % (outputPath))
        os.makedirs(outputPath)

    for topic, timestamps in topicTimestamps.iteritems():
        name = topic[1:].replace("/", "-")

        # Compute time delays (in msec)
        timeDiffs = (timestamps[1:] - timestamps[:-1]) * 1000.0
        mean = np.mean(timeDiffs)

        fig = plt.figure(figsize=(8,6), facecolor='white')

        plt.title("Histogram for " + name)
        plt.xlabel('Time delay between messages [ms]')
        plt.ylabel('Message count')
        plt.hist(timeDiffs, bins=500, color='k')
        plt.axvline(mean, color='b')
        plt.axvline(mean + mean*dropThreshold, color='r')
        plt.axvline(mean - mean*dropThreshold, color='r')

        filename = os.path.join(outputPath, name + '.png')
        logger.info('Saving histogram figure to file: %s' % (filename))
        plt.savefig(filename, dpi=100)
        plt.close(fig)

def saveDropMsgsOverTime(topicTimestamps, dropThreshold, outputPath):
    outputPath = os.path.abspath(outputPath)
    if not os.path.exists(outputPath):
        logger.info('Creating output directory for histograms: %s' % (outputPath))
        os.makedirs(outputPath)

    droppedMsgsOT = getdroprateGraphOverTime(topicTimestamps, dropThreshold)

    fig = plt.figure(figsize=(8,6), facecolor='white')

    plt.title("Histogram for Total Dropped messages over time")
    plt.xlabel('Time (10s)')
    plt.ylabel('Messages dropped (all topics)')
    plt.plot(droppedMsgsOT, color='k')

    filename = os.path.join(outputPath, 'msgs_dropped.png')
    logger.info('Saving histogram figure to file: %s' % (filename))
    plt.savefig(filename, dpi=100)
    plt.close(fig)

def getTabulatedStatistics(topicTimestamps, topicSequenceIds, dropThreshold):

    headers = ["Topic name", "Average Rate [Hz]", "Average Period [ms]",
               "Std Deviation Period [ms]", "Nb of Messages", "Drop ratio [%] (timestamp-based)", "Drop ratio [%] (sequence-based)"]

    # Loop for each topic
    data = []
    for topic, timestamps in topicTimestamps.iteritems():

        # Calculate statistics from timestamps
        mean, variance, averageRate, nbMsgs, nbMsgDropped = getStatisticsFromTimestamps(timestamps, dropThreshold)

        # Calculate statistics from sequence ids
        ids = topicSequenceIds[topic]
        minId, maxId = np.min(ids), np.max(ids)
        nbMsgsSeqId = (maxId + 1) - minId
        diff = np.setdiff1d(ids, np.arange(minId, maxId+1, dtype=np.int))
        nbMsgDroppedSeqId = len(diff)

        # Add to table
        data.append([topic, averageRate, mean*1000.0, np.sqrt(variance)*1000.0, nbMsgs, nbMsgDropped/float(nbMsgs) * 100.0, nbMsgDroppedSeqId/float(nbMsgsSeqId) * 100.0])

    # Sort table by topic name
    data.sort(key=lambda x: x[0])

    return tabulate(data, headers, tablefmt="grid", numalign="right", stralign="left", floatfmt=".2f")

def getAllTopicsMetadata(filename, ignoredTopics, useRosbagTime=False):

    topicTimestamps = dict()
    topicSequenceIds = dict()
    with rosbag.Bag(filename) as bag:
        # Grab list of topics
        topics = bag.get_type_and_topic_info()[1].keys()

        # Generate Topic statisitics objects
        for topic in topics:
            if not topic in ignoredTopics:
                topicTimestamps[topic] = []
                topicSequenceIds[topic] = []

        # Process all messages
        for topic, msg, t in bag.read_messages():
            if topic in topicTimestamps:
                if useRosbagTime:
                    t = t.to_sec()
                else:
                    # Get timestamp from header
                    t = float(msg.header.stamp.to_sec())
                topicTimestamps[topic].append(t)

                id = msg.header.seq
                topicSequenceIds[topic].append(id)

    # Convert to sorted numpy arrays
    # NOTE: sorting is important when using capture time, since they are not guaranteed
    #       to be ordered as in the rosbag.
    for topic, timestamps in topicTimestamps.iteritems():
        topicTimestamps[topic] = np.sort(np.array(timestamps))
    for topic, ids in topicSequenceIds.iteritems():
        topicSequenceIds[topic] = np.sort(np.array(ids))
    return topicTimestamps, topicSequenceIds

def main(args=None):

    parser = OptionParser()
    parser.add_option("-i", "--input", dest="input", default=False,
                      help="Location of Rosbag to analyse")
    parser.add_option("-o", "--output",  dest="output", default="",
                      help="Output stats file")
    parser.add_option("-r", "--ignore-topics", dest="topicRemove", default="",
                      help="Comma separated list of topics to ignore")
    parser.add_option("-c", "--use-rosbag-time",
                      action="store_true", dest="useRosbagTime", default=False,
                      help="Use rosbag time instead of capture time")
    parser.add_option("-g", "--save-histograms",
                      action="store_true", dest="saveHistograms", default=False,
                      help="Specify to save the histograms as images")
    parser.add_option("-d", "--save-dropped",
                      action="store_true", dest="saveDropped", default=False,
                      help="Specify to save the dropped messages graph as images")
    parser.add_option("-t", "--drop-threshold", dest="dropThreshold", type='float', default=1.0,
                      help='Specify the threshold to use for detecting dropped messages')
    (options, args) = parser.parse_args()

    if not options.input:
        parser.error("Please specify input rosbag file with -i ")

    if (options.saveHistograms or options.saveDropped)and not options.output:
        parser.error("Please specify an output file with -o when exporting histograms ")

    inputRosbagPath = os.path.abspath(options.input)
    logger.info('Using input rosbag file: %s' % (inputRosbagPath))

    if options.output:
        outputStatsFilePath = os.path.abspath(options.output)
        logger.info('Using output statistics file: %s' % (outputStatsFilePath))

    ignoreList = []
    if options.topicRemove:
        ignoreList = options.topicRemove.split(',')
        logger.info('Ignored topics: %s' % (options.topicRemove))

    # Get timestamps and sequence ids from the rosbag
    topicTimestamps, topicSequenceIds = getAllTopicsMetadata(inputRosbagPath, ignoreList, options.useRosbagTime)

    if options.saveHistograms and options.output:
        outputPath = outputStatsFilePath + '.histograms'
        saveHistogramsToFiles(topicTimestamps, options.dropThreshold, outputPath)

    if options.saveDropped and options.output:
        outputPath = outputStatsFilePath + '.droppedGraph'
        saveDropMsgsOverTime(topicTimestamps, options.dropThreshold, outputPath)

    # Print statistics to string
    str = getTabulatedStatistics(topicTimestamps, topicSequenceIds, options.dropThreshold)

    if not options.output:
        # Just print to stdout
        print str
    else:
        # Print to file
        with open(outputStatsFilePath, 'w') as f:
            f.write(str + '\n')

    logger.info('All done.')

if __name__ == '__main__':
    logging.basicConfig(level=logging.INFO)
    main()
<|endoftext|>