<commit_before>import copy
import json

class State(object):
    """
    Describes the state of the system at a given moment in time, in terms of
    its internal and external variables.
    """
    @staticmethod
    def fromFile(fn):
        """
        Constructs a system state from a given file, containing a JSON-based
        description of its contents.

        :param  fn  the path to the state description file

        :returns    the corresponding State for that file
        """
        with open(fn, "r") as f:
            jsn = json.load(f)
        print jsn.keys()
        return State.fromJSON(jsn)

    @staticmethod
    def fromJSON(jsn):
        """
        Constructs a description of a state from its JSON description.
        """
        assert ('variables' in jsn)
        assert (isinstance(jsn['variables'], dict))
        return State(jsn['variables'])

    def __init__(self, values):
        """
        Constructs a description of the system state.

        :param  values: a dictionary describing the values of the state
                        variables, indexed by their names.
        """
        self.__values = copy.copy(values)

    def getValues(self):
        """
        Returns the values of each of the state variables as a dictionary,
        indexed by name.
        """
        return copy.copy(self.__values)

    def read(self, variable):
        """
        Returns the value for a given state variable
        """
        return self.__values[variable]

    def dump(self):
        """
        Prints this state to the standard output.
        """
        for variable in self.__values:
            print('Variable: {} - State: {}'.format(variable, self.read(variable)))

    def toJSON(self):
        """
        Returns a JSON description of this state.
        """
        return {
            'variables': copy.copy(self.__values)
        }

    def __str__(self):
        return str(self.toJSON())

    def __repr__(self):
        return str(self)

class ExpectedStateValue(object):

    def __init__(self, value, noise=None):
        """
        Constructs a description of an expected.
        """
        assert (isinstance(noise, float) or isinstance(noise, int) or noise is None)
        assert (noise is None or type(value) == type(noise))

        self.__value = value
        self.__noise = noise

    def isExpected(self, observed):
        if self.__noise is None:
            return self.__value == observed
        else:
            return (self.__value - self.__noise) < observed <(self.__value + self.__noise)

class ExpectedState(object):

    @staticmethod
    def identical(to):
        """
        Returns an ExpectedState object that is identical to a given State.
        """
        expected = {}
        for (name, val) in to.getValues().items():
            expected[name] = ExpectedStateValue(val)
        return ExpectedState(expected)

    def __init__(self, values):
        """
        Constructs a description of an expected state of the system.
        """
        assert (isinstance(values, dict))
        self.__values = values

    def isExpected(self, st):
        for name, expectedValue in self.__values.items():
            if not expectedValue.isExpected(st.read(name)):
                return False

        return True

class StateVariable(object):

    def __init__(self, name, getter):
        """
        Constructs a new state variable

        :param  name:   the name of this variable
        :param  type:   the type of this variable
        :param  getter: a lambda function, used to obtain the value of this variable
        """
        assert(isinstance(self, InternalVariable) or isinstance(self, ExternalVariable))
        self.__name = name
        self.__getter = getter

    """
    Returns the name of this system variable
    """
    def name(self):
        return self.__name

    """
    Inspects the current state of this system variable
    """
    def read(self):
        return self.__getter()

class InternalVariable(StateVariable):
    """
    Internal variables describe the internal state of a given system.
    (i.e., they represent a system's knowledge of itself and its surroundings.)
    A user-provided lambda function is used to inspect the value of the state
    variable at any given time.
    """
    pass

class ExternalVariable(StateVariable):
    """
    TODO
    """
    pass

class Environment(object):
    @staticmethod
    def fromFile(fn):
        """
        Constructs a system environment from a given file, containing a JSON-based
        description of its contents.

        :param  fn  the path to the state description file

        :returns    the corresponding en for that file
        """
        with open(fn, "r") as f:
            jsn = json.load(f)
        return Environment.fromJSON(jsn)

    @staticmethod
    def fromJSON(jsn):
        """
        Constructs a description of an environment from its JSON description.
        """
        assert ('constants' in jsn)
        assert (isinstance(jsn['constants'], dict))
        return Environment(jsn['constants'])

    """
    Holds a description of an environment in which a mission should be conducted.
    """

    def __init__(self, values):
        """
        Constructs a description of a mission environment.

        :param  values: a dictionary of environment constant values, indexed
                        by the name of those constants.
        """
        self.__values = copy.copy(values)

    def read(self, variable):
        """
        Returns the value of a given environment constant.
        """
        return self.__values[variable]

    def toJSON(self):
        """
        Returns this environment description as a JSON object (i.e., a dict)
        """
        return {
            'constants': copy.copy(self.__values)
        }

class Estimator(object):
    """
    Estimates the expected state of a given variable after the execution of an
    action.
    """

    def __init__(self, variable, func, noiseFunc = None):
        """
        Constructs an estimator for a given state variable.

        :param  variable    the name of the (estimated) state variable
        :param  func        a lambda function responsible for calculating \
                            the expected value of the associated variable \
                            after the execution of an action.
        :param  noiseFunc   an (optional) lambda function responsible for \
                            calculating the permitted amount of noise in the \
                            expected value produced by this estimator
        """
        assert (isinstance(variable, str) and not variable is None)
        assert (callable(func))
        assert (callable(noiseFunc) or noiseFunc is None)

        self.__variable = variable
        self.__func = func
        self.__noiseFunc = noiseFunc

    def getVariableName(self):
        """
        Returns the name of the variable that is being estimated.
        """
        return self.__variable

    def computeExpectedValue(self, action, state, environment):
        """
        Computes the expected value for the variable associated with this estimator,
        within a given state and environment.

        :param    action        action used to calculate the expected state.
        :param    state         the state of the system immediately prior to
                                performing the given action.
        :param    environment   the environment in which the action takes place.

        :returns  an ExpectedStateValue object.
        """
        # TODO: sample a random amount of noise
        value = self.__func(action, state, environment)
        noise = None
        if self.__noiseFunc:
            noise = self.__noiseFunc(action, state, environment)
        return ExpectedStateValue(value, noise)

class FixedEstimator(Estimator):
    """
    A fixed estimator is one that always assigns a fixed value to its \
    associated state variable, rather than computing a value based on the \
    (action, state, environment) context.
    """

    def __init__(self, variable, value):
        super(FixedEstimator, self).__init__(variable, lambda action, state, env: value)
<commit_msg>added noise to StateVariable<commit_after>import copy
import json

class State(object):
    """
    Describes the state of the system at a given moment in time, in terms of
    its internal and external variables.
    """
    @staticmethod
    def fromFile(fn):
        """
        Constructs a system state from a given file, containing a JSON-based
        description of its contents.

        :param  fn  the path to the state description file

        :returns    the corresponding State for that file
        """
        with open(fn, "r") as f:
            jsn = json.load(f)
        print jsn.keys()
        return State.fromJSON(jsn)

    @staticmethod
    def fromJSON(jsn):
        """
        Constructs a description of a state from its JSON description.
        """
        assert ('variables' in jsn)
        assert (isinstance(jsn['variables'], dict))
        return State(jsn['variables'])

    def __init__(self, values):
        """
        Constructs a description of the system state.

        :param  values: a dictionary describing the values of the state
                        variables, indexed by their names.
        """
        self.__values = copy.copy(values)

    def getValues(self):
        """
        Returns the values of each of the state variables as a dictionary,
        indexed by name.
        """
        return copy.copy(self.__values)

    def read(self, variable):
        """
        Returns the value for a given state variable
        """
        return self.__values[variable]

    def dump(self):
        """
        Prints this state to the standard output.
        """
        for variable in self.__values:
            print('Variable: {} - State: {}'.format(variable, self.read(variable)))

    def toJSON(self):
        """
        Returns a JSON description of this state.
        """
        return {
            'variables': copy.copy(self.__values)
        }

    def __str__(self):
        return str(self.toJSON())

    def __repr__(self):
        return str(self)

class ExpectedStateValue(object):

    def __init__(self, value, noise=None):
        """
        Constructs a description of an expected.
        """
        assert (isinstance(noise, float) or isinstance(noise, int) or noise is None)
        assert (noise is None or type(value) == type(noise))

        self.__value = value
        self.__noise = noise

    def isExpected(self, observed):
        if self.__noise is None:
            return self.__value == observed
        else:
            return (self.__value - self.__noise) < observed <(self.__value + self.__noise)

class ExpectedState(object):

    @staticmethod
    def identical(to):
        """
        Returns an ExpectedState object that is identical to a given State.
        """
        expected = {}
        for (name, val) in to.getValues().items():
            expected[name] = ExpectedStateValue(val)
        return ExpectedState(expected)

    def __init__(self, values):
        """
        Constructs a description of an expected state of the system.
        """
        assert (isinstance(values, dict))
        self.__values = values

    def isExpected(self, st):
        for name, expectedValue in self.__values.items():
            if not expectedValue.isExpected(st.read(name)):
                return False

        return True

class StateVariable(object):

    def __init__(self, name, getter, noise=None):
        """
        Constructs a new state variable

        :param  name:   the name of this variable
        :param  type:   the type of this variable
        :param  getter: a lambda function, used to obtain the value of this variable
        :param  noise:  the inherent level of noise when measuring this variable
        """
        assert (isinstance(self, InternalVariable) or isinstance(self, ExternalVariable))
        assert (noise is None or type(noise) in [float, int])
        assert (noise is None or noise >= 0)

        self.__name = name
        self.__getter = getter
        self.__noise = noise

    """
    Returns true if there is inherent noise in the measurement of this variable.
    """
    def isNoisy(self):
        return self.__noise is not None

    """
    Returns the inherent level of noise that is to be expected when measuring
    this variable. If no noise is expected, None is returned.
    """
    def getNoise(self):
        return self.__noise

    """
    Returns the name of this system variable
    """
    def name(self):
        return self.__name

    """
    Inspects the current state of this system variable
    """
    def read(self):
        return self.__getter()

class InternalVariable(StateVariable):
    """
    Internal variables describe the internal state of a given system.
    (i.e., they represent a system's knowledge of itself and its surroundings.)
    A user-provided lambda function is used to inspect the value of the state
    variable at any given time.
    """
    pass

class ExternalVariable(StateVariable):
    """
    TODO
    """
    pass

class Environment(object):
    @staticmethod
    def fromFile(fn):
        """
        Constructs a system environment from a given file, containing a JSON-based
        description of its contents.

        :param  fn  the path to the state description file

        :returns    the corresponding en for that file
        """
        with open(fn, "r") as f:
            jsn = json.load(f)
        return Environment.fromJSON(jsn)

    @staticmethod
    def fromJSON(jsn):
        """
        Constructs a description of an environment from its JSON description.
        """
        assert ('constants' in jsn)
        assert (isinstance(jsn['constants'], dict))
        return Environment(jsn['constants'])

    """
    Holds a description of an environment in which a mission should be conducted.
    """

    def __init__(self, values):
        """
        Constructs a description of a mission environment.

        :param  values: a dictionary of environment constant values, indexed
                        by the name of those constants.
        """
        self.__values = copy.copy(values)

    def read(self, variable):
        """
        Returns the value of a given environment constant.
        """
        return self.__values[variable]

    def toJSON(self):
        """
        Returns this environment description as a JSON object (i.e., a dict)
        """
        return {
            'constants': copy.copy(self.__values)
        }

class Estimator(object):
    """
    Estimates the expected state of a given variable after the execution of an
    action.
    """

    def __init__(self, variable, func, noiseFunc = None):
        """
        Constructs an estimator for a given state variable.

        :param  variable    the name of the (estimated) state variable
        :param  func        a lambda function responsible for calculating \
                            the expected value of the associated variable \
                            after the execution of an action.
        :param  noiseFunc   an (optional) lambda function responsible for \
                            calculating the permitted amount of noise in the \
                            expected value produced by this estimator
        """
        assert (isinstance(variable, str) and not variable is None)
        assert (callable(func))
        assert (callable(noiseFunc) or noiseFunc is None)

        self.__variable = variable
        self.__func = func
        self.__noiseFunc = noiseFunc

    def getVariableName(self):
        """
        Returns the name of the variable that is being estimated.
        """
        return self.__variable

    def computeExpectedValue(self, action, state, environment):
        """
        Computes the expected value for the variable associated with this estimator,
        within a given state and environment.

        :param    action        action used to calculate the expected state.
        :param    state         the state of the system immediately prior to
                                performing the given action.
        :param    environment   the environment in which the action takes place.

        :returns  an ExpectedStateValue object.
        """
        # TODO: sample a random amount of noise
        value = self.__func(action, state, environment)
        noise = None
        if self.__noiseFunc:
            noise = self.__noiseFunc(action, state, environment)
        return ExpectedStateValue(value, noise)

class FixedEstimator(Estimator):
    """
    A fixed estimator is one that always assigns a fixed value to its \
    associated state variable, rather than computing a value based on the \
    (action, state, environment) context.
    """

    def __init__(self, variable, value):
        super(FixedEstimator, self).__init__(variable, lambda action, state, env: value)
<|endoftext|>


<commit_before>import matplotlib as mpl
import matplotlib.pyplot as plt
import numpy as np
import sys
import textwrap

import plotting_util as u

def barchart(data, error=None, legend=[], label_length=15, label_clip=-1, title=None, xlabel=None, ylabel=None,
    xtick_rotation=-45, xtick_labels=None, ybound=None, group_width=0.7, color=None, alpha=0.5):
    # data should be a list of lists: each sub-list is a series in the data, and each item in the sublists is a bar
    # based on: http://matplotlib.org/examples/api/barchart_demo.html
    fig, ax = plt.subplots()
    try:
        a = data[0][0]
    except (TypeError, IndexError):
        data = [data]
    pN = len(data[0])
    dlen = len(data)
    bar_width = group_width/dlen
    xt = np.arange(pN)  # the x locations for the groups
    if color:
        if type(color) is tuple or type(color) is list:
            if len(color) < dlen:
                raise RuntimeError("If you specify a list of colors, the length must match the number of series in data")
        elif type(color) is str:
            cmap = color
            color = u.mp_get_cmap(dlen, cmap=cmap)
        else:
            color = [color]*dlen # kluge but it works...
    else:
        color = u.mp_get_cmap(dlen)
    rects = []
    for i in range(len(data)):
        if error:
            rects.append(ax.bar(xt+bar_width*i, data[i], bar_width, color=color(i), alpha=alpha, yerr=error[i]))
        else:
            rects.append(ax.bar(xt+bar_width*i, data[i], bar_width, color=color(i), alpha=alpha))

    # add some text for labels, title and axes ticks
    if xlabel:
        ax.set_xlabel(xlabel)
    if ylabel:
        ax.set_ylabel(ylabel)
    if title:
        ax.set_title(title, fontsize="xx-large", y=1.04)

    mb = None
    for i in data:
        lm = max(i)
        if mb is None:
            mb = lm
        elif lm > mb:
            mb = lm
    ax.set_xbound(lower=xt[0]+group_width/2-0.5, upper=xt[-1]+group_width/2+0.5)
    if ybound:
        ax.set_ybound(lower=ybound[0], upper=ybound[1]*1.07)
    else:
        ax.set_ybound(upper=mb*1.07)
    ax.set_xticks(xt + group_width/2)
    ax.autoscale(enable=False, axis="x", tight=True)

    if xtick_labels:
        names = xtick_labels
        labels_clipped = [u.label_clipper(text,label_clip) for text in names]
        labels = [textwrap.fill(text,label_length) for text in labels_clipped]
        bot_off = (-np.sin(np.radians(xtick_rotation))) * (label_length * 0.016)
        fig.subplots_adjust(bottom=bot_off)
    else:
        labels = [""]*dlen
    ax.set_xticklabels(labels, rotation=xtick_rotation)

    if dlen > 1:
        lr = []
        for i in rects:
            lr.append(i[0])
        llen = len(legend)
        if llen < dlen:
            for i in range(llen-1, dlen):
                legend.append("Series {0}".format(i+1))
        lmax = 0
        for i in legend:
            li = len(i)
            if li > lmax:
                lmax = li
        ax.legend(lr, legend, bbox_to_anchor=(1,1), bbox_transform=fig.transFigure)
        fig.subplots_adjust(left=0.05, right=1-(0.016*li+0.05))

    for i in rects:
        u.mp_autolabel(i, ax)

    plt.show(block=False)
    return (fig, ax)

def plot_mc(data, legend=[], label_length=15, label_clip=-1, title=None, xlabel=None, ylabel=None,
    xtick_rotation=-45, xtick_labels=None, group_width=0.7, color=None, alpha=0.5):
    # data should be of format: {'keys': ['1', '2', '3', '4', '5'], 'names': ['One', 'Two', 'Three', 'Four', 'Five'], 'bars': [5, 13, 1, 1, 0]}
    # based on: http://matplotlib.org/examples/api/barchart_demo.html
    fig, ax = plt.subplots()
    if type(data) is tuple or type(data) is list:
        pass
    else:
        data = [data]
    pN = len(data[0]["bars"])
    dlen = len(data)
    bar_width = group_width/dlen
    xt = np.arange(pN)  # the x locations for the groups
    if color:
        if type(color) is tuple or type(color) is list:
            if len(color) < dlen:
                raise RuntimeError("If you specify a list of colors, the length must match the number of series in data")
        elif type(color) is str:
            cmap = color
            color = u.mp_get_cmap(dlen, cmap=cmap)
        else:
            color = [color]*dlen # kluge but it works...
    else:
        color = u.mp_get_cmap(dlen)
    rects = []
    for i in range(len(data)):
        rects.append(ax.bar(xt+bar_width*i, data[i]["bars"], bar_width, color=color(i), alpha=alpha))

    if xtick_labels:
        names = xtick_labels
    else:
        names = data[0]["names"]
    labels_clipped = [u.label_clipper(text,label_clip) for text in names]
    labels = [textwrap.fill(text,label_length) for text in labels_clipped]

    # add some text for labels, title and axes ticks
    if xlabel:
        ax.set_xlabel(xlabel)
    if ylabel:
        ax.set_ylabel(ylabel)
    if title:
        ax.set_title(title, fontsize="xx-large", y=1.04)
    ax.set_xticks(xt + group_width/2)
    ax.set_xbound(lower=xt[0]+group_width/2-0.5, upper=xt[-1]+group_width/2+0.5)
    mb = None
    for i in data:
        lm = max(i['bars'])
        if mb is None:
            mb = lm
        elif lm > mb:
            mb = lm
    ax.set_ybound(upper=mb*1.07)
    ax.set_xticklabels(labels, rotation=xtick_rotation)
    ax.autoscale(enable=False, axis="x", tight=True)
    bot_off = (-np.sin(np.radians(xtick_rotation))) * (label_length * 0.016)
    fig.subplots_adjust(bottom=bot_off)

    if dlen > 1:
        lr = []
        for i in rects:
            lr.append(i[0])
        llen = len(legend)
        if llen < dlen:
            for i in range(llen-1, dlen):
                legend.append("Series {0}".format(i+1))
        lmax = 0
        for i in legend:
            li = len(i)
            if li > lmax:
                lmax = li
        ax.legend(lr, legend, bbox_to_anchor=(1,1), bbox_transform=fig.transFigure)
        fig.subplots_adjust(left=0.05, right=1-(0.016*li+0.05))

    for i in rects:
        u.mp_autolabel(i, ax)

    plt.show(block=False)
    return (fig, ax)

def scatter_mc(data, c=None, dp=15, title=None, xlabel=None, ylabel=None,
    xlabel_length=15, xlabel_clip=-1, ylabel_length=10, ylabel_clip=-1, xtick_rotation=-45, xtick_labels=None, ytick_labels=None):
    freqs = []
    pairs = []
    for p in data['pairs']:
        found = False
        for i in range(len(freqs)):
            if pairs[i][0] == p[0] and pairs[i][1] == p[1]:
                found = True
                if len(p) > 2:
                    freqs[i] += p[2]
                else:
                    freqs[i] += 1
                break
        if not found:
            pairs.append((p[0],p[1]))
            if len(p) > 2:
                freqs.append(p[2])
            else:
                freqs.append(1)
    x,y = zip(*pairs)
    area = np.pi * (dp * np.array(freqs))**2
    fig, ax = plt.subplots()
    xt = list(map(int,data["keys1"]))
    yt = list(map(int,data["keys2"]))
    ax.set_xticks(xt)
    ax.set_yticks(yt)
    ax.set_xbound(lower=xt[0]-0.5, upper=xt[-1]+0.5)
    ax.set_ybound(lower=yt[0]-0.5, upper=yt[-1]+0.5)
    if xtick_labels:
        names1 = xtick_labels
    else:
        names1 = data['names1']
    labels1_clipped = [u.label_clipper(text,xlabel_clip) for text in names1]
    labels1 = [textwrap.fill(text,xlabel_length) for text in labels1_clipped]
    if ytick_labels:
        names2 = ytick_labels
    else:
        names2 = data['names2']
    labels2_clipped = [u.label_clipper(text,ylabel_clip) for text in names2]
    labels2 = [textwrap.fill(text,ylabel_length) for text in labels2_clipped]
    ax.set_xticklabels(labels1)
    ax.set_yticklabels(labels2)
    ax.autoscale(enable=False, axis="both", tight=True)
    if xlabel:
        ax.set_xlabel(xlabel)
    if ylabel:
        ax.set_ylabel(ylabel)
        plt.subplots_adjust(left=0.15)
    if title:
        ax.set_title(title)
    xy = list(zip(x,y))
    for l in range(len(xy)):
        if freqs[l] > 0:
            plt.annotate(freqs[l], xy = xy[l], xytext = (-4, -4), textcoords = 'offset points')
    plt.scatter(x, y, c=c, s=area, alpha=0.5)
    plt.show(block=False)<commit_msg>Added deprication message<commit_after>import matplotlib as mpl
import matplotlib.pyplot as plt
import numpy as np
import sys
import textwrap

import plotting_util as u

sys.stderr.write("Oldplotting was depricated in favor of the new Pandas interface.  Please use the new interface instead")

def barchart(data, error=None, legend=[], label_length=15, label_clip=-1, title=None, xlabel=None, ylabel=None,
    xtick_rotation=-45, xtick_labels=None, ybound=None, group_width=0.7, color=None, alpha=0.5):
    # data should be a list of lists: each sub-list is a series in the data, and each item in the sublists is a bar
    # based on: http://matplotlib.org/examples/api/barchart_demo.html
    fig, ax = plt.subplots()
    try:
        a = data[0][0]
    except (TypeError, IndexError):
        data = [data]
    pN = len(data[0])
    dlen = len(data)
    bar_width = group_width/dlen
    xt = np.arange(pN)  # the x locations for the groups
    if color:
        if type(color) is tuple or type(color) is list:
            if len(color) < dlen:
                raise RuntimeError("If you specify a list of colors, the length must match the number of series in data")
        elif type(color) is str:
            cmap = color
            color = u.mp_get_cmap(dlen, cmap=cmap)
        else:
            color = [color]*dlen # kluge but it works...
    else:
        color = u.mp_get_cmap(dlen)
    rects = []
    for i in range(len(data)):
        if error:
            rects.append(ax.bar(xt+bar_width*i, data[i], bar_width, color=color(i), alpha=alpha, yerr=error[i]))
        else:
            rects.append(ax.bar(xt+bar_width*i, data[i], bar_width, color=color(i), alpha=alpha))

    # add some text for labels, title and axes ticks
    if xlabel:
        ax.set_xlabel(xlabel)
    if ylabel:
        ax.set_ylabel(ylabel)
    if title:
        ax.set_title(title, fontsize="xx-large", y=1.04)

    mb = None
    for i in data:
        lm = max(i)
        if mb is None:
            mb = lm
        elif lm > mb:
            mb = lm
    ax.set_xbound(lower=xt[0]+group_width/2-0.5, upper=xt[-1]+group_width/2+0.5)
    if ybound:
        ax.set_ybound(lower=ybound[0], upper=ybound[1]*1.07)
    else:
        ax.set_ybound(upper=mb*1.07)
    ax.set_xticks(xt + group_width/2)
    ax.autoscale(enable=False, axis="x", tight=True)

    if xtick_labels:
        names = xtick_labels
        labels_clipped = [u.label_clipper(text,label_clip) for text in names]
        labels = [textwrap.fill(text,label_length) for text in labels_clipped]
        bot_off = (-np.sin(np.radians(xtick_rotation))) * (label_length * 0.016)
        fig.subplots_adjust(bottom=bot_off)
    else:
        labels = [""]*dlen
    ax.set_xticklabels(labels, rotation=xtick_rotation)

    if dlen > 1:
        lr = []
        for i in rects:
            lr.append(i[0])
        llen = len(legend)
        if llen < dlen:
            for i in range(llen-1, dlen):
                legend.append("Series {0}".format(i+1))
        lmax = 0
        for i in legend:
            li = len(i)
            if li > lmax:
                lmax = li
        ax.legend(lr, legend, bbox_to_anchor=(1,1), bbox_transform=fig.transFigure)
        fig.subplots_adjust(left=0.05, right=1-(0.016*li+0.05))

    for i in rects:
        u.mp_autolabel(i, ax)

    plt.show(block=False)
    return (fig, ax)

def plot_mc(data, legend=[], label_length=15, label_clip=-1, title=None, xlabel=None, ylabel=None,
    xtick_rotation=-45, xtick_labels=None, group_width=0.7, color=None, alpha=0.5):
    # data should be of format: {'keys': ['1', '2', '3', '4', '5'], 'names': ['One', 'Two', 'Three', 'Four', 'Five'], 'bars': [5, 13, 1, 1, 0]}
    # based on: http://matplotlib.org/examples/api/barchart_demo.html
    fig, ax = plt.subplots()
    if type(data) is tuple or type(data) is list:
        pass
    else:
        data = [data]
    pN = len(data[0]["bars"])
    dlen = len(data)
    bar_width = group_width/dlen
    xt = np.arange(pN)  # the x locations for the groups
    if color:
        if type(color) is tuple or type(color) is list:
            if len(color) < dlen:
                raise RuntimeError("If you specify a list of colors, the length must match the number of series in data")
        elif type(color) is str:
            cmap = color
            color = u.mp_get_cmap(dlen, cmap=cmap)
        else:
            color = [color]*dlen # kluge but it works...
    else:
        color = u.mp_get_cmap(dlen)
    rects = []
    for i in range(len(data)):
        rects.append(ax.bar(xt+bar_width*i, data[i]["bars"], bar_width, color=color(i), alpha=alpha))

    if xtick_labels:
        names = xtick_labels
    else:
        names = data[0]["names"]
    labels_clipped = [u.label_clipper(text,label_clip) for text in names]
    labels = [textwrap.fill(text,label_length) for text in labels_clipped]

    # add some text for labels, title and axes ticks
    if xlabel:
        ax.set_xlabel(xlabel)
    if ylabel:
        ax.set_ylabel(ylabel)
    if title:
        ax.set_title(title, fontsize="xx-large", y=1.04)
    ax.set_xticks(xt + group_width/2)
    ax.set_xbound(lower=xt[0]+group_width/2-0.5, upper=xt[-1]+group_width/2+0.5)
    mb = None
    for i in data:
        lm = max(i['bars'])
        if mb is None:
            mb = lm
        elif lm > mb:
            mb = lm
    ax.set_ybound(upper=mb*1.07)
    ax.set_xticklabels(labels, rotation=xtick_rotation)
    ax.autoscale(enable=False, axis="x", tight=True)
    bot_off = (-np.sin(np.radians(xtick_rotation))) * (label_length * 0.016)
    fig.subplots_adjust(bottom=bot_off)

    if dlen > 1:
        lr = []
        for i in rects:
            lr.append(i[0])
        llen = len(legend)
        if llen < dlen:
            for i in range(llen-1, dlen):
                legend.append("Series {0}".format(i+1))
        lmax = 0
        for i in legend:
            li = len(i)
            if li > lmax:
                lmax = li
        ax.legend(lr, legend, bbox_to_anchor=(1,1), bbox_transform=fig.transFigure)
        fig.subplots_adjust(left=0.05, right=1-(0.016*li+0.05))

    for i in rects:
        u.mp_autolabel(i, ax)

    plt.show(block=False)
    return (fig, ax)

def scatter_mc(data, c=None, dp=15, title=None, xlabel=None, ylabel=None,
    xlabel_length=15, xlabel_clip=-1, ylabel_length=10, ylabel_clip=-1, xtick_rotation=-45, xtick_labels=None, ytick_labels=None):
    freqs = []
    pairs = []
    for p in data['pairs']:
        found = False
        for i in range(len(freqs)):
            if pairs[i][0] == p[0] and pairs[i][1] == p[1]:
                found = True
                if len(p) > 2:
                    freqs[i] += p[2]
                else:
                    freqs[i] += 1
                break
        if not found:
            pairs.append((p[0],p[1]))
            if len(p) > 2:
                freqs.append(p[2])
            else:
                freqs.append(1)
    x,y = zip(*pairs)
    area = np.pi * (dp * np.array(freqs))**2
    fig, ax = plt.subplots()
    xt = list(map(int,data["keys1"]))
    yt = list(map(int,data["keys2"]))
    ax.set_xticks(xt)
    ax.set_yticks(yt)
    ax.set_xbound(lower=xt[0]-0.5, upper=xt[-1]+0.5)
    ax.set_ybound(lower=yt[0]-0.5, upper=yt[-1]+0.5)
    if xtick_labels:
        names1 = xtick_labels
    else:
        names1 = data['names1']
    labels1_clipped = [u.label_clipper(text,xlabel_clip) for text in names1]
    labels1 = [textwrap.fill(text,xlabel_length) for text in labels1_clipped]
    if ytick_labels:
        names2 = ytick_labels
    else:
        names2 = data['names2']
    labels2_clipped = [u.label_clipper(text,ylabel_clip) for text in names2]
    labels2 = [textwrap.fill(text,ylabel_length) for text in labels2_clipped]
    ax.set_xticklabels(labels1)
    ax.set_yticklabels(labels2)
    ax.autoscale(enable=False, axis="both", tight=True)
    if xlabel:
        ax.set_xlabel(xlabel)
    if ylabel:
        ax.set_ylabel(ylabel)
        plt.subplots_adjust(left=0.15)
    if title:
        ax.set_title(title)
    xy = list(zip(x,y))
    for l in range(len(xy)):
        if freqs[l] > 0:
            plt.annotate(freqs[l], xy = xy[l], xytext = (-4, -4), textcoords = 'offset points')
    plt.scatter(x, y, c=c, s=area, alpha=0.5)
    plt.show(block=False)<|endoftext|>


<commit_before>from tablepy.table import Table

def main():
    table = Table(['ID','name'])
    table.append({'ID':2,'name':'a'})
    table.append({'ID':1,'name':'b'})
    print """ This is a simple Table:
    table = Table(['ID','name'])
    table.append({'ID':2,'name':'a'})
    table.append({'ID':1,'name':'b'})
    print table
    """
    print table,'\n'

    print """it supports len, iteration and indexing
    print len(table), not table
    print iter(table)
    print table[0]
    """
    print len(table), not table
    print iter(table)
    print table[0],'\n'

    print """ We can add a column with a constant value:
    table.addColumn('constant',3)
    print table
    """
    table.addColumn('constant',3)
    print table,'\n'

    print """ Also we can add a column with a interable arg (must be the same size as our table):
    table.addColumn('iterable arg',[4,5])
    print table
    """
    table.addColumn('iterable arg',[4,5])
    print table,'\n'

    print """ There are methods to get a row by a column-value pair, get the table schema, \nconvert a column to List, and rename columns!
    print table.getRowsByKey('name','a')
    print table.getSchema()
    print table.colToList('name')
    table.renameCol('iterable arg','new name')
    print table
    """
    print table.getRowsByKey('name','a')
    print table.getSchema()
    print table.colToList('name')
    table.renameCol('iterable arg','new name')
    print table,'\n'

    print """ it supports vertical concatenation (same schema!), \nand horizontal concatenation with SQL-like joins!
    table2 = Table(['ID','name','constant','new name'])
    table2.append({'new name': 6, 'constant': 3, 'ID': 3, 'name': 'c'})
    table.vConcat(table2)
    print table

    table3 = Table(['another column!'])
    table3.append({'another column!': 'x'})
    table3.append({'another column!': 'y'})
    table3.append({'another column!': 'z'})
    table.hConcat(table3)
    print table

    table3 = Table(['name','age'])
    table3.append({'name': 'a','age':3})
    table3.append({'name': 'a','age':2})
    table3.append({'name': 'c','age':1})
    table.hConcat(table3, join='name')
    print table
    """
    table2 = Table(['ID','name','constant','new name'])
    table2.append({'new name': 6, 'constant': 3, 'ID': 3, 'name': 'c'})
    table.vConcat(table2)
    print table

    table3 = Table(['another column!'])
    table3.append({'another column!': 'x'})
    table3.append({'another column!': 'y'})
    table3.append({'another column!': 'z'})
    table.hConcat(table3)
    print table

    table3 = Table(['name','age'])
    table3.append({'name': 'a','age':3})
    table3.append({'name': 'a','age':2})
    table3.append({'name': 'b','age':1})
    table.hConcat(table3, join='name')
    print table,'\n'

    print """ And of course, you can sort(cmp,key,reverse) it **in place**:
    table.sort(key=lambda x:x['age'],reverse =True)
    print table
    table.sort(key=lambda x:(x['constant'],x['ID']))
    print table
    """
    table.sort(key=lambda x:x['age'],reverse =True)
    print table
    table.sort(key=lambda x:(x['constant'],x['ID']))
    print table,'\n'

    print """ At last but no least, you can filter it like this (returns a new table):
    tableFiltered = table.filter(lambda age: age>=2,['age'])
    print tableFiltered
    tableFiltered2 = table.filter(lambda name,constant: name == 'a' and constant==3 ,['name','constant'])
    print tableFiltered2
    """
    tableFiltered = table.filter(lambda age: age>=2,['age'])
    print tableFiltered
    tableFiltered2 = table.filter(lambda name,constant: name == 'a' and constant==3 ,['name','constant'])
    print tableFiltered2,'\n'

if __name__ == '__main__':
    main()
<commit_msg>Update demo.py<commit_after>from tablepy.table import Table

def main():
    table = Table(['ID','name'])
    table.append({'ID':2,'name':'a'})
    table.append({'ID':1,'name':'b'})
    print """ This is a simple Table:
    table = Table(['ID','name'])
    table.append({'ID':2,'name':'a'})
    table.append({'ID':1,'name':'b'})
    print table
    """
    print table,'\n'

    print """it supports len, iteration and indexing
    print len(table), not table
    print iter(table)
    print table[0]
    """
    print len(table), not table
    print iter(table)
    print table[0],'\n'

    print """ We can add a column with a constant value:
    table.addColumn('constant',3)
    print table
    """
    table.addColumn('constant',3)
    print table,'\n'

    print """ Also we can add a column with a interable arg (must be the same size as our table):
    table.addColumn('iterable arg',[4,5])
    print table
    """
    table.addColumn('iterable arg',[4,5])
    print table,'\n'

    print """ There are methods to get a row by a column-value pair, get the table schema, \nconvert a column to List, and rename columns!
    print table.getRowsByKey('name','a')
    print table.getSchema()
    print table.colToList('name')
    table.renameCol('iterable arg','new name')
    print table
    """
    print table.getRowsByKey('name','a')
    print table.getSchema()
    print table.colToList('name')
    table.renameCol('iterable arg','new name')
    print table,'\n'

    print """ it supports vertical concatenation (same schema!), \nand horizontal concatenation with SQL-like joins!
    table2 = Table(['ID','name','constant','new name'])
    table2.append({'new name': 6, 'constant': 3, 'ID': 3, 'name': 'c'})
    table.vConcat(table2)
    print table

    table3 = Table(['another column!'])
    table3.append({'another column!': 'x'})
    table3.append({'another column!': 'y'})
    table3.append({'another column!': 'z'})
    table.hConcat(table3)
    print table

    table3 = Table(['name','age'])
    table3.append({'name': 'a','age':3})
    table3.append({'name': 'a','age':2})
    table3.append({'name': 'c','age':1})
    table.hConcat(table3, join='name')
    print table
    """
    table2 = Table(['ID','name','constant','new name'])
    table2.append({'new name': 6, 'constant': 3, 'ID': 3, 'name': 'c'})
    table.vConcat(table2)
    print table

    table3 = Table(['another column!'])
    table3.append({'another column!': 'x'})
    table3.append({'another column!': 'y'})
    table3.append({'another column!': 'z'})
    table.hConcat(table3)
    print table

    table3 = Table(['name','age'])
    table3.append({'name': 'a','age':3})
    table3.append({'name': 'c','age':2})
    table3.append({'name': 'b','age':1})
    table.hConcat(table3, join='name')
    print table,'\n'

    print """ And of course, you can sort(cmp,key,reverse) it **in place**:
    table.sort(key=lambda x:x['age'],reverse =True)
    print table
    table.sort(key=lambda x:(x['constant'],x['ID']))
    print table
    """
    table.sort(key=lambda x:x['age'],reverse =True)
    print table
    table.sort(key=lambda x:(x['constant'],x['ID']))
    print table,'\n'

    print """ You can filter it like this (returns a new table):
    tableFiltered = table.filter(lambda age: age>2,['age'])
    print tableFiltered
    tableFiltered2 = table.filter(lambda name,constant: name == 'a' and constant==3 ,['name','constant'])
    print tableFiltered2
    """
    tableFiltered = table.filter(lambda age: age>2,['age'])
    print tableFiltered
    tableFiltered2 = table.filter(lambda name,constant: name == 'a' and constant==3 ,['name','constant'])
    print tableFiltered2,'\n'

    print """ It also supports group by:
    table.append({'new name': 4, 'constant': 4, 'name': 'a', 'age': 5, 'ID': 2, 'another column!': 'x'})
    table.append({'new name': 5, 'constant': 4, 'name': 'a', 'age': 6, 'ID': 2, 'another column!': 'x'})
    table.append({'new name': 6, 'constant': 4, 'name': 'a', 'age': 8, 'ID': 2, 'another column!': 'x'})
    print table
    print table.groupBy(['new name'], lambda args: float(sum([i[0] for i in args]))/len(args),['age'])
    print table.groupBy(['new name', 'constant' ], lambda args: sum([i[0]+i[1] for i in args]),['age','constant'])
    """
    table.append({'new name': 4, 'constant': 4, 'name': 'a', 'age': 5, 'ID': 2, 'another column!': 'x'})
    table.append({'new name': 5, 'constant': 4, 'name': 'a', 'age': 6, 'ID': 2, 'another column!': 'x'})
    table.append({'new name': 6, 'constant': 4, 'name': 'a', 'age': 8, 'ID': 2, 'another column!': 'x'})
    print table
    print table.groupBy(['new name'], lambda args: float(sum([i[0] for i in args]))/len(args),['age'])
    print table.groupBy(['new name', 'constant' ], lambda args: sum([i[0]+i[1] for i in args]),['age','constant'])

if __name__ == '__main__':
    main()
<|endoftext|>


<commit_before># -*- coding: utf-8 -*-
"""

Realtime hpi monitor for Vectorview/TRIUX MEG systems.

@author: jussi (jnu@iki.fi)
"""

import sys
from PyQt4 import QtGui, QtCore, uic
from PyQt4.QtCore import pyqtSignal
from pkg_resources import resource_filename
import time
import struct
import FieldTrip
import numpy as np
import scipy.linalg
import os.path as op
import traceback
import socket
from config import cfg, cfg_user
import elekta
from rt_server import start_rt_server, stop_rt_server, rt_server_pid
from utils import running_var
import argparse
import logging

logger = logging.getLogger(__name__)

class HPImon(QtGui.QMainWindow):

    # new signals must be defined here
    new_data = pyqtSignal()

    def __init__(self):
        super(self.__class__, self).__init__()
        # hardcoded options
        self.VAR_WINDOW = 50  # window for variance computation (ms)
        self.apptitle = 'hpimon'
        # load user interface made with designer
        uifile = resource_filename(__name__, 'hpimon.ui')
        uic.loadUi(uifile, self)
        self.setWindowTitle(self.apptitle)
        self.timer = QtCore.QTimer()
        try:
            cfg._read_user()
        except (ValueError, SyntaxError, IOError):
            self.message_dialog('Cannot parse config file, creating %s '
                                'with default values. Please edit the file '
                                'according to your setup and restart.' %
                                cfg_user)
            cfg.__init__()  # reset to default values
            cfg._write_user()
            sys.exit()
        """ Parse some options """
        linefreq_, cfreqs_ = elekta.read_collector_config(cfg.hpi.
                                                          collector_config)
        self.linefreq = cfg.hpi.line_freq or linefreq_
        self.cfreqs = cfg.hpi.hpi_freqs or cfreqs_
        if not self.cfreqs:
            self.message_dialog('Cannot detect HPI frequencies and none are '
                                'specified in the config file. Aborting.')
            sys.exit()
        if not self.linefreq:
            self.message_dialog('Cannot detect line frequency and none was '
                                'specified in the config file. Aborting.')
            sys.exit()
        self.ncoils = len(self.cfreqs)
        self.serverp = None

        if cfg.server.server_autostart:
            if not op.isfile(cfg.server.server_path):
                self.message_dialog('Cannot find server binary at %s, please '
                                    'check config.' % cfg.server.server_path)
                sys.exit()
            server_bin = op.split(cfg.server.server_path)[1]
            if rt_server_pid(server_bin):
                self.message_dialog('Realtime server already running. '
                                    'Please kill it first.')
                sys.exit()
            if cfg.server.host == 'localhost':
                logger.debug('starting server')
                self.serverp = start_rt_server(cfg.server.server_path,
                                               cfg.server.server_opts.split())
                if not rt_server_pid(server_bin):
                    self.message_dialog('Could not start realtime server.')
                    sys.exit()
                time.sleep(1)  # give the server a second to get started

        self.init_widgets()
        self.ftclient = FieldTrip.Client()
        try:
            self.ftclient.connect(cfg.server.host, port=cfg.server.port)
        except socket.error:
            self.message_dialog('Cannot connect to the realtime server. '
                                'Possibly a networking problem or you have '
                                'specified a wrong TCP port.')
            stop_rt_server(self.serverp)
            sys.exit()

        """ Poll using timer until header info becomes available """
        self.statusbar.showMessage('Waiting for measurement to start...')
        self.timer.timeout.connect(self.start_if_header)
        self.timer.start(cfg.server.buffer_poll_interval)

    def start_if_header(self):
        """ Start if header info has become available. """
        logger.debug('polling for header info')
        if self.ftclient.getHeader():
            self.start()

    def start(self):
        """ We now have header info and can start running """
        self.timer.stop()
        self.pick_mag, self.pick_grad = self.get_ch_indices()
        self.pick_meg = np.sort(np.concatenate([self.pick_mag,
                                                self.pick_grad]))
        self.nchan = len(self.pick_meg)
        self.sfreq = self.get_header_info()['sfreq']

        self.var_window = int(self.sfreq * self.VAR_WINDOW/1000.)
        logger.debug('variance window %d samples' % self.var_window)
        logger.debug('sampling frequency %.2f Hz' % self.sfreq)
        self.init_glm()
        self.last_sample = self.buffer_last_sample()
        self.new_data.connect(self.update_display)
        # from now on, use the timer for data buffer polling
        self.timer.timeout.disconnect(self.start_if_header)
        self.timer.timeout.connect(self.poll_buffer)
        self.timer.start(cfg.server.buffer_poll_interval)
        self.statusbar.showMessage(self.msg_running())

    def init_widgets(self):
        # create SNR labels and add to grid
        for wnum in range(self.ncoils):
            label = QtGui.QLabel()
            label.setText(str(self.cfreqs[wnum]) + ' Hz')
            self.gridLayout_SNR.addWidget(label, wnum, 0)
        # create SNR progress bars dynamically and add to grid
        self.progbars_SNR = list()
        for wnum in range(self.ncoils):
            progbar = QtGui.QProgressBar()
            progbar.setMinimum(-100)
            progbar.setMaximum(100)
            progbar.setValue(0)
            progbar.setFormat(u'%v dB')
            progbar.setTextVisible(True)
            sty = '.QProgressBar {%s }' % cfg.display.bar_style
            progbar.setStyleSheet(sty)
            self.gridLayout_SNR.addWidget(progbar, wnum, 1)
            self.progbars_SNR.append(progbar)
        # create stylesheets for progress bars, according to goodness of value
        self.progbar_styles = dict()
        for val in ['good', 'ok', 'bad']:
            sty = ('QProgressBar {%s} QProgressBar::chunk { background-color: '
                   '%s; %s }' % (cfg.display.bar_style,
                                 cfg.display.bar_colors[val],
                                 cfg.display.bar_chunk_style))
            self.progbar_styles[val] = sty
        # buttons
        self.btnQuit.clicked.connect(self.close)
        self.btnStop.clicked.connect(self.toggle_timer)

    def toggle_timer(self):
        if self.timer.isActive():
            self.statusbar.showMessage(self.msg_stopped())
            self.btnStop.setText('Start monitoring')
            self.timer.stop()
        else:
            self.statusbar.showMessage(self.msg_running())
            self.btnStop.setText('Stop monitoring')
            self.timer.start()

    def msg_running(self):
        return ('Running [%s], poll every %d ms, window %d ms' %
                (cfg.server.host, cfg.server.buffer_poll_interval,
                 cfg.hpi.win_len))

    def msg_stopped(self):
        return 'Stopped'

    def get_ch_indices(self):
        """ Return indices of magnetometers and gradiometers in the
        FieldTrip data matrix """
        grads, mags = [], []
        for ind, ch in enumerate(self.ftclient.getHeader().labels):
            if ch[:3] == 'MEG':
                if ch[-1] == '1':
                    mags.append(ind)
                elif ch[-1] in ['2', '3']:
                    grads.append(ind)
                else:
                    raise ValueError('Unexpected channel name: ' + ch)
        logger.debug('got %d magnetometers and %d gradiometers' %
                     (len(mags), len(grads)))
        return np.array(mags), np.array(grads)

    def get_header_info(self):
        """ Get misc info from FieldTrip header """
        return {'sfreq': self.ftclient.getHeader().fSample}

    def buffer_last_sample(self):
        """ Return number of last sample available from server. """
        return self.ftclient.getHeader().nSamples

    def poll_buffer(self):
        """ Emit a signal if new data is available in the buffer. """
        buflast = self.buffer_last_sample()
        #logger.debug('polling, buffer last sample: %d, my last sample: %d' %
        #             (buflast, self.last_sample))
        # buffer last sample can also decrease (reset) if streaming from file
        if buflast != self.last_sample:
            self.new_data.emit()
            self.last_sample = buflast

    def fetch_buffer(self):
        start = self.last_sample - cfg.hpi.win_len + 1
        stop = self.last_sample
        logger.debug('fetching buffer %d - %d (start at t = %.2f s)'
                     % (start, stop, start / self.sfreq))
        try:
            data = self.ftclient.getData([start, stop])
        except struct.error:  # something wrong with the buffer
            logger.warning('errors with the buffer')
            return None
        if data is None:
            logger.warning('server returned no data')
            return None
        else:
            return data[:, self.pick_meg]

    def init_glm(self):
        """ Build general linear model for amplitude estimation """
        # get some info from fiff
        self.linefreqs = (np.arange(cfg.hpi.nharm+1)+1) * self.linefreq
        # time + dc and slope terms
        t = np.arange(cfg.hpi.win_len) / float(self.sfreq)
        self.model = np.empty((len(t),
                               2+2*(len(self.linefreqs)+len(self.cfreqs))))
        self.model[:, 0] = t
        self.model[:, 1] = np.ones(t.shape)
        # add sine and cosine term for each freq
        allfreqs = np.concatenate([self.linefreqs, self.cfreqs])
        self.model[:, 2::2] = np.cos(2 * np.pi * t[:, np.newaxis] * allfreqs)
        self.model[:, 3::2] = np.sin(2 * np.pi * t[:, np.newaxis] * allfreqs)
        self.inv_model = scipy.linalg.pinv(self.model)

    def compute_snr(self, data):
        coeffs = np.dot(self.inv_model, data)  # nterms * nchan
        coeffs_hpi = coeffs[2+2*len(self.linefreqs):]
        resid_vars = np.var(data - np.dot(self.model, coeffs), 0)
        # get total power by combining sine and cosine terms
        # sinusoidal of amplitude A has power of A**2/2
        hpi_pow = (coeffs_hpi[0::2, :]**2 + coeffs_hpi[1::2, :]**2)/2
        # average across channel types separately
        hpi_pow_grad_avg = hpi_pow[:, self.pick_grad].mean(1)
        # hpi_pow_mag_avg = hpi_pow[:, self.pick_mag].mean(1)
        # divide average HPI power by average variance
        snr_avg_grad = hpi_pow_grad_avg / \
            resid_vars[self.pick_grad].mean()
        # snr_avg_mag = hpi_pow_mag_avg / \
        #    resid_vars[self.pick_mag].mean()
        return 10 * np.log10(snr_avg_grad)

    def update_display(self):
        buf = self.fetch_buffer()
        if buf is not None:
            # update saturation widget
            # for each channel, find window with minimum std deviation
            va = running_var(buf, win=self.var_window, axis=0)
            va = np.clip(va, 0, np.inf)
            min_std = np.min(np.sqrt(va), axis=0)
            grad_sat = min_std[self.pick_grad] <= cfg.limits.grad_min_std
            mag_sat = min_std[self.pick_mag] <= cfg.limits.mag_min_std
            nsat = np.count_nonzero(grad_sat) + np.count_nonzero(mag_sat)
            logger.debug('%d saturated channels' % nsat)
            if nsat < cfg.limits.n_sat_ok:
                sty = self.progbar_styles['good']
            elif nsat < cfg.limits.n_sat_bad:
                sty = self.progbar_styles['ok']
            else:
                sty = self.progbar_styles['bad']
            self.progressBar_sat.setValue(nsat)
            self.progressBar_sat.setStyleSheet(sty)
            # update snr widgets
            snr = self.compute_snr(buf)
            for wnum in range(self.ncoils):
                this_snr = int(np.round(snr[wnum]))
                self.progbars_SNR[wnum].setValue(this_snr)
                if this_snr > cfg.limits.snr_ok:
                    sty = self.progbar_styles['good']
                elif this_snr > cfg.limits.snr_bad:
                    sty = self.progbar_styles['ok']
                else:
                    sty = self.progbar_styles['bad']
                self.progbars_SNR[wnum].setStyleSheet(sty)

    def message_dialog(self, msg):
        """ Show message with an 'OK' button. """
        dlg = QtGui.QMessageBox()
        dlg.setWindowTitle(self.apptitle)
        dlg.setText(msg)
        dlg.addButton(QtGui.QPushButton('Ok'), QtGui.QMessageBox.YesRole)
        dlg.exec_()

    def closeEvent(self, event):
        """ Confirm and close application. """
        self.timer.stop()
        # disconnect from server
        if self.ftclient:
            self.ftclient.disconnect()
        stop_rt_server(self.serverp)
        event.accept()

def main():

    app = QtGui.QApplication(sys.argv)

    # set up root logger
    logger = logging.getLogger()
    handler = logging.StreamHandler()
    filehandler = logging.FileHandler('hpimon.log')
    nullhandler = logging.NullHandler()
    logger.setLevel(logging.DEBUG)
    formatter = logging.Formatter('%(name)s: %(levelname)s: %(message)s')
    handler.setFormatter(formatter)
    filehandler.setFormatter(formatter)
    logger.addHandler(handler)  # change to 'handler' to get debug output

    hpimon = HPImon()

    def my_excepthook(type, value, tback):
        """ Custom exception handler for fatal (unhandled) exceptions:
        report to user via GUI and terminate. """
        tb_full = u''.join(traceback.format_exception(type, value, tback))
        hpimon.message_dialog('Terminating due to unhandled exception. %s'
                              % tb_full)
        stop_rt_server(hpimon.serverp)
        sys.__excepthook__(type, value, tback)
        app.quit()

    sys.excepthook = my_excepthook
    hpimon.show()
    app.exec_()

if __name__ == '__main__':
    main()
<commit_msg>std by stride tricks<commit_after># -*- coding: utf-8 -*-
"""

Realtime hpi monitor for Vectorview/TRIUX MEG systems.

@author: jussi (jnu@iki.fi)
"""

import sys
from PyQt4 import QtGui, QtCore, uic
from PyQt4.QtCore import pyqtSignal
from pkg_resources import resource_filename
import time
import struct
import FieldTrip
import numpy as np
import scipy.linalg
import os.path as op
import traceback
import socket
from config import cfg, cfg_user
import elekta
from rt_server import start_rt_server, stop_rt_server, rt_server_pid
from utils import rolling_var_strided
import argparse
import logging

logger = logging.getLogger(__name__)

class HPImon(QtGui.QMainWindow):

    # new signals must be defined here
    new_data = pyqtSignal()

    def __init__(self):
        super(self.__class__, self).__init__()
        # hardcoded options
        self.VAR_WINDOW = 50  # window for variance computation (ms)
        self.apptitle = 'hpimon'
        # load user interface made with designer
        uifile = resource_filename(__name__, 'hpimon.ui')
        uic.loadUi(uifile, self)
        self.setWindowTitle(self.apptitle)
        self.timer = QtCore.QTimer()
        try:
            cfg._read_user()
        except (ValueError, SyntaxError, IOError):
            self.message_dialog('Cannot parse config file, creating %s '
                                'with default values. Please edit the file '
                                'according to your setup and restart.' %
                                cfg_user)
            cfg.__init__()  # reset to default values
            cfg._write_user()
            sys.exit()
        """ Parse some options """
        linefreq_, cfreqs_ = elekta.read_collector_config(cfg.hpi.
                                                          collector_config)
        self.linefreq = cfg.hpi.line_freq or linefreq_
        self.cfreqs = cfg.hpi.hpi_freqs or cfreqs_
        if not self.cfreqs:
            self.message_dialog('Cannot detect HPI frequencies and none are '
                                'specified in the config file. Aborting.')
            sys.exit()
        if not self.linefreq:
            self.message_dialog('Cannot detect line frequency and none was '
                                'specified in the config file. Aborting.')
            sys.exit()
        self.ncoils = len(self.cfreqs)
        self.serverp = None

        if cfg.server.server_autostart:
            if not op.isfile(cfg.server.server_path):
                self.message_dialog('Cannot find server binary at %s, please '
                                    'check config.' % cfg.server.server_path)
                sys.exit()
            server_bin = op.split(cfg.server.server_path)[1]
            if rt_server_pid(server_bin):
                self.message_dialog('Realtime server already running. '
                                    'Please kill it first.')
                sys.exit()
            if cfg.server.host == 'localhost':
                logger.debug('starting server')
                self.serverp = start_rt_server(cfg.server.server_path,
                                               cfg.server.server_opts.split())
                if not rt_server_pid(server_bin):
                    self.message_dialog('Could not start realtime server.')
                    sys.exit()
                time.sleep(1)  # give the server a second to get started

        self.init_widgets()
        self.ftclient = FieldTrip.Client()
        try:
            self.ftclient.connect(cfg.server.host, port=cfg.server.port)
        except socket.error:
            self.message_dialog('Cannot connect to the realtime server. '
                                'Possibly a networking problem or you have '
                                'specified a wrong TCP port.')
            stop_rt_server(self.serverp)
            sys.exit()

        """ Poll using timer until header info becomes available """
        self.statusbar.showMessage('Waiting for measurement to start...')
        self.timer.timeout.connect(self.start_if_header)
        self.timer.start(cfg.server.buffer_poll_interval)

    def start_if_header(self):
        """ Start if header info has become available. """
        logger.debug('polling for header info')
        if self.ftclient.getHeader():
            self.start()

    def start(self):
        """ We now have header info and can start running """
        self.timer.stop()
        hdrinfo = self.get_header_info()
        self.sfreq = hdrinfo['sfreq']
        self.ch_labels = hdrinfo['labels']
        self.pick_mag, self.pick_grad = self.get_ch_indices(self.ch_labels)
        self.pick_meg = np.sort(np.concatenate([self.pick_mag,
                                                self.pick_grad]))
        self.nchan = len(self.pick_meg)
        self.grad_labels = np.array(self.ch_labels)[self.pick_grad]
        self.mag_labels = np.array(self.ch_labels)[self.pick_mag]
        self.var_window = int(self.sfreq * self.VAR_WINDOW/1000.)
        logger.debug('variance window %d samples' % self.var_window)
        logger.debug('sampling frequency %.2f Hz' % self.sfreq)
        self.init_glm()
        self.last_sample = self.buffer_last_sample()
        self.new_data.connect(self.update_display)
        # from now on, use the timer for data buffer polling
        self.timer.timeout.disconnect(self.start_if_header)
        self.timer.timeout.connect(self.poll_buffer)
        self.timer.start(cfg.server.buffer_poll_interval)
        self.statusbar.showMessage(self.msg_running())

    def init_widgets(self):
        # create SNR labels and add to grid
        for wnum in range(self.ncoils):
            label = QtGui.QLabel()
            label.setText(str(self.cfreqs[wnum]) + ' Hz')
            self.gridLayout_SNR.addWidget(label, wnum, 0)
        # create SNR progress bars dynamically and add to grid
        self.progbars_SNR = list()
        for wnum in range(self.ncoils):
            progbar = QtGui.QProgressBar()
            progbar.setMinimum(-100)
            progbar.setMaximum(100)
            progbar.setValue(0)
            progbar.setFormat(u'%v dB')
            progbar.setTextVisible(True)
            sty = '.QProgressBar {%s }' % cfg.display.bar_style
            progbar.setStyleSheet(sty)
            self.gridLayout_SNR.addWidget(progbar, wnum, 1)
            self.progbars_SNR.append(progbar)
        # create stylesheets for progress bars, according to goodness of value
        self.progbar_styles = dict()
        for val in ['good', 'ok', 'bad']:
            sty = ('QProgressBar {%s} QProgressBar::chunk { background-color: '
                   '%s; %s }' % (cfg.display.bar_style,
                                 cfg.display.bar_colors[val],
                                 cfg.display.bar_chunk_style))
            self.progbar_styles[val] = sty
        # buttons
        self.btnQuit.clicked.connect(self.close)
        self.btnStop.clicked.connect(self.toggle_timer)

    def toggle_timer(self):
        if self.timer.isActive():
            self.statusbar.showMessage(self.msg_stopped())
            self.btnStop.setText('Start monitoring')
            self.timer.stop()
        else:
            self.statusbar.showMessage(self.msg_running())
            self.btnStop.setText('Stop monitoring')
            self.timer.start()

    def msg_running(self):
        return ('Running [%s], poll every %d ms, window %d ms' %
                (cfg.server.host, cfg.server.buffer_poll_interval,
                 cfg.hpi.win_len))

    def msg_stopped(self):
        return 'Stopped'

    def get_ch_indices(self, labels):
        """ Return indices of magnetometers and gradiometers in the
        FieldTrip data matrix """
        grads, mags = [], []
        for ind, ch in enumerate(labels):
            if ch[:3] == 'MEG':
                if ch[-1] == '1':
                    mags.append(ind)
                elif ch[-1] in ['2', '3']:
                    grads.append(ind)
                else:
                    raise ValueError('Unexpected channel name: ' + ch)
        logger.debug('got %d magnetometers and %d gradiometers' %
                     (len(mags), len(grads)))
        return np.array(mags), np.array(grads)

    def get_header_info(self):
        """ Get misc info from FieldTrip header """
        hdr = self.ftclient.getHeader()
        return {'sfreq': hdr.fSample, 'labels': hdr.labels}

    def buffer_last_sample(self):
        """ Return number of last sample available from server. """
        return self.ftclient.getHeader().nSamples

    def poll_buffer(self):
        """ Emit a signal if new data is available in the buffer. """
        buflast = self.buffer_last_sample()
        #logger.debug('polling, buffer last sample: %d, my last sample: %d' %
        #             (buflast, self.last_sample))
        # buffer last sample can also decrease (reset) if streaming from file
        if buflast != self.last_sample:
            self.new_data.emit()
            self.last_sample = buflast

    def fetch_buffer(self):
        start = self.last_sample - cfg.hpi.win_len + 1
        stop = self.last_sample
        logger.debug('fetching buffer %d - %d (start at t = %.2f s)'
                     % (start, stop, start / self.sfreq))
        try:
            data = self.ftclient.getData([start, stop])
        except struct.error:  # something wrong with the buffer
            logger.warning('errors with the buffer')
            return None
        if data is None:
            logger.warning('server returned no data')
            return None
        else:
            return data[:, self.pick_meg]

    def init_glm(self):
        """ Build general linear model for amplitude estimation """
        # get some info from fiff
        self.linefreqs = (np.arange(cfg.hpi.nharm+1)+1) * self.linefreq
        # time + dc and slope terms
        t = np.arange(cfg.hpi.win_len) / float(self.sfreq)
        self.model = np.empty((len(t),
                               2+2*(len(self.linefreqs)+len(self.cfreqs))))
        self.model[:, 0] = t
        self.model[:, 1] = np.ones(t.shape)
        # add sine and cosine term for each freq
        allfreqs = np.concatenate([self.linefreqs, self.cfreqs])
        self.model[:, 2::2] = np.cos(2 * np.pi * t[:, np.newaxis] * allfreqs)
        self.model[:, 3::2] = np.sin(2 * np.pi * t[:, np.newaxis] * allfreqs)
        self.inv_model = scipy.linalg.pinv(self.model)

    def compute_snr(self, data):
        coeffs = np.dot(self.inv_model, data)  # nterms * nchan
        coeffs_hpi = coeffs[2+2*len(self.linefreqs):]
        resid_vars = np.var(data - np.dot(self.model, coeffs), 0)
        # get total power by combining sine and cosine terms
        # sinusoidal of amplitude A has power of A**2/2
        hpi_pow = (coeffs_hpi[0::2, :]**2 + coeffs_hpi[1::2, :]**2)/2
        # average across channel types separately
        hpi_pow_grad_avg = hpi_pow[:, self.pick_grad].mean(1)
        # hpi_pow_mag_avg = hpi_pow[:, self.pick_mag].mean(1)
        # divide average HPI power by average variance
        snr_avg_grad = hpi_pow_grad_avg / \
            resid_vars[self.pick_grad].mean()
        # snr_avg_mag = hpi_pow_mag_avg / \
        #    resid_vars[self.pick_mag].mean()
        return 10 * np.log10(snr_avg_grad)

    def update_display(self):
        buf = self.fetch_buffer()
        if buf is not None:
            # update saturation widget
            # for each channel, find window with minimum std deviation
            std = rolling_var_strided(buf, np.std, self.var_window, axis=0)
            min_std = np.min(std, axis=0)
            grad_sat = min_std[self.pick_grad] <= cfg.limits.grad_min_std
            mag_sat = min_std[self.pick_mag] <= cfg.limits.mag_min_std
            nsat = np.count_nonzero(grad_sat) + np.count_nonzero(mag_sat)
            logger.debug('%d saturated channels' % nsat)
            logger.debug('gradiometers: %s' % self.grad_labels[grad_sat])
            logger.debug('magnetometers: %s' % self.mag_labels[mag_sat])
            if nsat < cfg.limits.n_sat_ok:
                sty = self.progbar_styles['good']
            elif nsat < cfg.limits.n_sat_bad:
                sty = self.progbar_styles['ok']
            else:
                sty = self.progbar_styles['bad']
            self.progressBar_sat.setValue(nsat)
            self.progressBar_sat.setStyleSheet(sty)
            # update snr widgets
            snr = self.compute_snr(buf)
            for wnum in range(self.ncoils):
                this_snr = int(np.round(snr[wnum]))
                self.progbars_SNR[wnum].setValue(this_snr)
                if this_snr > cfg.limits.snr_ok:
                    sty = self.progbar_styles['good']
                elif this_snr > cfg.limits.snr_bad:
                    sty = self.progbar_styles['ok']
                else:
                    sty = self.progbar_styles['bad']
                self.progbars_SNR[wnum].setStyleSheet(sty)

    def message_dialog(self, msg):
        """ Show message with an 'OK' button. """
        dlg = QtGui.QMessageBox()
        dlg.setWindowTitle(self.apptitle)
        dlg.setText(msg)
        dlg.addButton(QtGui.QPushButton('Ok'), QtGui.QMessageBox.YesRole)
        dlg.exec_()

    def closeEvent(self, event):
        """ Confirm and close application. """
        self.timer.stop()
        # disconnect from server
        if self.ftclient:
            self.ftclient.disconnect()
        stop_rt_server(self.serverp)
        event.accept()

def main():

    app = QtGui.QApplication(sys.argv)

    # set up root logger
    logger = logging.getLogger()
    handler = logging.StreamHandler()
    filehandler = logging.FileHandler('hpimon.log')
    nullhandler = logging.NullHandler()
    logger.setLevel(logging.DEBUG)
    formatter = logging.Formatter('%(name)s: %(levelname)s: %(message)s')
    handler.setFormatter(formatter)
    filehandler.setFormatter(formatter)
    logger.addHandler(handler)  # change to 'handler' to get debug output

    hpimon = HPImon()

    def my_excepthook(type, value, tback):
        """ Custom exception handler for fatal (unhandled) exceptions:
        report to user via GUI and terminate. """
        tb_full = u''.join(traceback.format_exception(type, value, tback))
        hpimon.message_dialog('Terminating due to unhandled exception. %s'
                              % tb_full)
        stop_rt_server(hpimon.serverp)
        sys.__excepthook__(type, value, tback)
        app.quit()

    sys.excepthook = my_excepthook
    hpimon.show()
    app.exec_()

if __name__ == '__main__':
    main()
<|endoftext|>


<commit_before>import weakref
import functools

class ObserverFunction(object):
    """

    """
    def __init__(self, func, observed_obj, weakref_info):
        """
        func is the observer who will be called back.

        observed_obj is the object being observed.
        """
        self.observed_obj = observed_obj
        key, d = weakref_info
        self.func = weakref.ref(func, CleanupHandler(key, d))

    def __call__(self, *arg, **kw):
        if self.observed_obj:
            return self.func()(self.observed_obj, *arg, **kw)
        else:
            return self.func()(*arg, **kw)

class ObserverBoundMethod(object):
    def __init__(self, inst, method_name, observed_obj, weakref_info):
        self.observed_obj = observed_obj
        key, d = weakref_info
        self.inst = weakref.ref(inst, CleanupHandler(key, d))
        self.method_name = method_name

    def __call__(self, *arg, **kw):
        bound_method = getattr(self.inst(), self.method_name)
        if self.observed_obj:
            return bound_method(self.observed_obj, *arg, **kw)
        else:
            return bound_method(*arg, **kw)

class ObservableFunction(object):
    """
    A function which can be observed.

    I wrap a function and allow other callables to subscribe to be called
    whenever I am called.

    Observers (ie. callbacks) are added and removed from me through the
    following two methods:

    add_observer(observer)
        registers observer to be called whenever I am called

    discard_observer(observer)
        discards observer from the set of callbacks
    """

    def __init__(self, func):
        """Initialize an ObservableFunction"""
        functools.update_wrapper(self, func)
        self.func = func
        self.callbacks = {} #observing object ID -> weak ref, info

    def add_observer(self, observer, identify_observed=False):
        """
        Register an observer to observe me.

        The observing function or method will be called whenever I am called,
        and with the same arguments and keyword arguments.

        If identify_observed is True, then the observed function will be passed
        as an additional first argument to the observer.

        If a bound method or function has already been registered to as a
        callback, trying to add it again does nothing. In other words, there is
        no way to sign up an observer to be called back multiple times. This
        was a conscious design choice which users are invited to complain about
        if there are use cases which make this inconvenient.

        Returns True if the observer was odded, False otherwise.
        """
        if hasattr(observer, "__self__"):  # observer is a bound method
            result = self._add_bound_method(observer, identify_observed)
        else:  # observer is a normal function
            result = self._add_function(observer, identify_observed)
        return result

    def _add_function(self, func, identify_observed):
        key = self.make_key(func)
        if key not in self.callbacks:
            self.callbacks[key] = ObserverFunction(
                func, self if identify_observed else None,
                (key, self.callbacks))
            return True
        else:
            return False

    def _add_bound_method(self, bound_method, identify_observed):
        inst = bound_method.__self__
        method_name = bound_method.__name__
        key = self.make_key(bound_method)
        if key not in self.callbacks:
            self.callbacks[key] = ObserverBoundMethod(
                inst, method_name, self if identify_observed else None,
                (key, self.callbacks))
            return True
        else:
            return False

    def discard_observer(self, observer):
        """
        Un-register an observer.

        Returns true if an observer was removed, False otherwise.
        """
        discarded = False
        key = self.make_key(observer)
        if key in self.callbacks:
            del self.callbacks[key]
            discarded = True
        return discarded

    @staticmethod
    def make_key(observer):
        if hasattr(observer, "__self__"):
            inst = observer.__self__
            method_name = observer.__name__
            return (id(inst), method_name)
        else:
            return id(observer)

    def __call__(self, *arg, **kw):
        """
        Invoke the callable which I proxy, and all of my observers.

        The observers are called with the same *args and **kw as the main
        callable.

        Note:
        I think it is possible for observers to disappear while we execute
        callbacks. It might be better to make strong references to all
        observers before we start callback execution.
        """
        result = self.func(*arg, **kw)
        for key in self.callbacks:
            self.callbacks[key](*arg, **kw)
        return result

class ObservableBoundMethod(ObservableFunction):
    def __init__(self, inst, func, callbacks):
        self.func = func
        functools.update_wrapper(self, func)
        self.inst = inst #Should this be weak? No! Bound methods have strong references to their instances.
        self.callbacks = callbacks

    def __call__(self, *arg, **kw):
        result = self.func(self.inst, *arg, **kw)
        for key in self.callbacks:
            self.callbacks[key](*arg, **kw)
        return result

    @property
    def __self__(self):
        return self.inst

class ObservableBoundMethodManager(object):
    """
    I manage access to ObservableBoundMethods.

    I store no strong references to the instances I manage.
    """
    def __init__(self, func):
        self._func = func
        # instance id -> (inst weak ref, callbacks)
        self.instances = {}

    def __get__(self, inst, cls):
        """
        If accessed by instance I return an ObservableBoundMethod which handles
        that instance.

        If accessed by class I return myself; this is not yet compatible with
        how class access of a method is supposed to work.
        """
        if inst is None:
            return self
        # Only weak references to instances are stored. This garuntees that
        # the descriptor cannot prevent the instnaces it manages from being
        # garbage collected.
        # We can't use a WeakKeyDict because not all instances are hashable. We
        # handle this by using the instance's id as a key and setting up an
        # appropriate weakref callback which fires if the instance is
        # finalized.
        inst_id = id(inst)
        if inst_id in self.instances:
            wr, callbacks = self.instances[inst_id]
            if not wr():
                msg = "Unreachable: instance id=%d not cleaned up"%(inst_id,)
                raise RuntimeError(msg)
        else:
            wr = weakref.ref(inst, CleanupHandler(inst_id, self.instances))
            callbacks = {}
            self.instances[inst_id] = (wr, callbacks)
        return ObservableBoundMethod(inst, self._func, callbacks)

    def __set__(self, inst, val):
        """Disallow setting because we don't garuntee behavior."""
        raise RuntimeError("Assignment not supported")

def event(func):
    """
    I turn a callable into something that can be observed by other callables.

    Use me as a decorator on a function or method, like this:

    @event
    def my_func(x):
        print("my_func called with arg: %s"%(x,))

    Now other functions can sign up to get notified when my_func is called:

    def callback(x):
        print("callback called with arg: %s"%(x,))

    my_func.addObserver(callback)
    my_func('banana')
    >>> my_func called with arg: banana
    >>> callback called with arg: banana

    You can decorate methods in the same way, and you can sign up bound methods
    as callbacks.
    """
    return ObservableCallable(func)

class CleanupHandler(object):
    """
    I manage removal of weak references from dicts.

    Use me as a weakref.ref callback to remove an object's id from a dict
    when that object is garbage collected.
    """
    def __init__(self, key, d):
        self.key = key
        self.d = d

    def __call__(self, wr):
        if self.key in self.d:
            del self.d[self.key]
<commit_msg>Docstrings<commit_after>import weakref
import functools

class ObserverFunction(object):
    """

    """
    def __init__(self, func, observed_obj, weakref_info):
        """
        func is the observer who will be called back.

        observed_obj is the object being observed.
        """
        self.observed_obj = observed_obj
        key, d = weakref_info
        self.func = weakref.ref(func, CleanupHandler(key, d))

    def __call__(self, *arg, **kw):
        if self.observed_obj:
            return self.func()(self.observed_obj, *arg, **kw)
        else:
            return self.func()(*arg, **kw)

class ObserverBoundMethod(object):
    def __init__(self, inst, method_name, observed_obj, weakref_info):
        self.observed_obj = observed_obj
        key, d = weakref_info
        self.inst = weakref.ref(inst, CleanupHandler(key, d))
        self.method_name = method_name

    def __call__(self, *arg, **kw):
        bound_method = getattr(self.inst(), self.method_name)
        if self.observed_obj:
            return bound_method(self.observed_obj, *arg, **kw)
        else:
            return bound_method(*arg, **kw)

class ObservableFunction(object):
    """
    A function which can be observed.

    I wrap a function and allow other callables to subscribe to be called
    whenever I am called.

    Observers (ie. callbacks) are added and removed from me through the
    following two methods:

    add_observer(observer)
        registers observer to be called whenever I am called

    discard_observer(observer)
        discards observer from the set of callbacks
    """

    def __init__(self, func):
        """Initialize an ObservableFunction"""
        functools.update_wrapper(self, func)
        self.func = func
        self.callbacks = {} #observing object ID -> weak ref, info

    def add_observer(self, observer, identify_observed=False):
        """
        Register an observer to observe me.

        The observing function or method will be called whenever I am called,
        and with the same arguments and keyword arguments.

        If identify_observed is True, then the observed function will be passed
        as an additional first argument to the observer.

        If a bound method or function has already been registered to as a
        callback, trying to add it again does nothing. In other words, there is
        no way to sign up an observer to be called back multiple times. This
        was a conscious design choice which users are invited to complain about
        if there are use cases which make this inconvenient.

        Returns True if the observer was odded, False otherwise.
        """
        if hasattr(observer, "__self__"):  # observer is a bound method
            result = self._add_bound_method(observer, identify_observed)
        else:  # observer is a normal function
            result = self._add_function(observer, identify_observed)
        return result

    def _add_function(self, func, identify_observed):
        key = self.make_key(func)
        if key not in self.callbacks:
            self.callbacks[key] = ObserverFunction(
                func, self if identify_observed else None,
                (key, self.callbacks))
            return True
        else:
            return False

    def _add_bound_method(self, bound_method, identify_observed):
        inst = bound_method.__self__
        method_name = bound_method.__name__
        key = self.make_key(bound_method)
        if key not in self.callbacks:
            self.callbacks[key] = ObserverBoundMethod(
                inst, method_name, self if identify_observed else None,
                (key, self.callbacks))
            return True
        else:
            return False

    def discard_observer(self, observer):
        """
        Un-register an observer.

        Returns true if an observer was removed, False otherwise.
        """
        discarded = False
        key = self.make_key(observer)
        if key in self.callbacks:
            del self.callbacks[key]
            discarded = True
        return discarded

    @staticmethod
    def make_key(observer):
        if hasattr(observer, "__self__"):
            inst = observer.__self__
            method_name = observer.__name__
            return (id(inst), method_name)
        else:
            return id(observer)

    def __call__(self, *arg, **kw):
        """
        Invoke the callable which I proxy, and all of my observers.

        The observers are called with the same *args and **kw as the main
        callable.

        Note:
        I think it is possible for observers to disappear while we execute
        callbacks. It might be better to make strong references to all
        observers before we start callback execution.
        """
        result = self.func(*arg, **kw)
        for key in self.callbacks:
            self.callbacks[key](*arg, **kw)
        return result

class ObservableBoundMethod(ObservableFunction):
    """
    I am a bound method which fires callbacks when I am called.
    """
    def __init__(self, inst, func, callbacks):
        """
        inst is the object instance to which I'm bound.

        func is the function I wrap.

        callbacks is a dict shared by the ObservableBoundMethodManager which
        created me. See the documentation for ObservableBoundMethodManager.
        """
        self.func = func
        functools.update_wrapper(self, func)
        self.inst = inst #Should this be weak? No! Bound methods have strong references to their instances.
        self.callbacks = callbacks

    def __call__(self, *arg, **kw):
        """
        Call the function I wrap and all of my callbacks.
        """
        result = self.func(self.inst, *arg, **kw)
        for key in self.callbacks:
            self.callbacks[key](*arg, **kw)
        return result

    @property
    def __self__(self):
        """
        Return the instance to which I'm bound.
        """
        return self.inst

class ObservableBoundMethodManager(object):
    """
    I manage access to ObservableBoundMethods.

    I store no strong references to the instances I manage.
    """
    def __init__(self, func):
        self._func = func
        # instance id -> (inst weak ref, callbacks)
        self.instances = {}

    def __get__(self, inst, cls):
        """
        If accessed by instance I return an ObservableBoundMethod which handles
        that instance.

        If accessed by class I return myself; this is not yet compatible with
        how class access of a method is supposed to work.
        """
        if inst is None:
            return self
        # Only weak references to instances are stored. This guarantees that
        # the descriptor cannot prevent the instances it manages from being
        # garbage collected.
        # We can't use a WeakKeyDict because not all instances are hashable.
        # Instead we use the instance's id as a key which maps to a tuple of a
        # weak ref to the instance, and the callbacks for that instance. The
        # weak ref has a callback set up to clear the dict entry when the
        # instance is finalized.
        inst_id = id(inst)
        if inst_id in self.instances:
            wr, callbacks = self.instances[inst_id]
            if not wr():
                msg = "Unreachable: instance id=%d not cleaned up"%(inst_id,)
                raise RuntimeError(msg)
        else:
            wr = weakref.ref(inst, CleanupHandler(inst_id, self.instances))
            callbacks = {}
            self.instances[inst_id] = (wr, callbacks)
        return ObservableBoundMethod(inst, self._func, callbacks)

    def __set__(self, inst, val):
        """Disallow setting because we don't garuntee behavior."""
        raise RuntimeError("Assignment not supported")

def event(func):
    """
    I turn a callable into something that can be observed by other callables.

    Use me as a decorator on a function or method, like this:

    @event
    def my_func(x):
        print("my_func called with arg: %s"%(x,))

    Now other functions can sign up to get notified when my_func is called:

    def callback(x):
        print("callback called with arg: %s"%(x,))

    my_func.addObserver(callback)
    my_func('banana')
    >>> my_func called with arg: banana
    >>> callback called with arg: banana

    You can decorate methods in the same way, and you can sign up bound methods
    as callbacks.
    """
    return ObservableCallable(func)

class CleanupHandler(object):
    """
    I manage removal of weak references from dicts.

    Use me as a weakref.ref callback to remove an object's id from a dict
    when that object is garbage collected.
    """
    def __init__(self, key, d):
        self.key = key
        self.d = d

    def __call__(self, wr):
        if self.key in self.d:
            del self.d[self.key]
<|endoftext|>