<commit_before>from __future__ import division, print_function

from contextlib import contextmanager

import numpy as np
from .windowed_sum import windowed_sum, windowed_sum_slow

@contextmanager
def setattr_context(obj, **kwargs):
    """
    Context manager to temporarily change the values of object attributes
    while executing a function.

    Example
    -------
    >>> class Foo: pass
    >>> f = Foo(); f.attr = 'hello'
    >>> with setattr_context(f, attr='goodbye'):
    ...     print(f.attr)
    goodbye
    >>> print(f.attr)
    hello
    """
    old_kwargs = dict([(key, getattr(obj, key)) for key in kwargs])
    [setattr(obj, key, val) for key, val in kwargs.items()]
    try:
        yield
    finally:
        [setattr(obj, key, val) for key, val in old_kwargs.items()]

def iterable(obj):
    """Utility to check if object is iterable"""
    try:
        iter(obj)
    except:
        return False
    else:
        return True

def validate_inputs(*arrays, **kwargs):
    """Validate input arrays

    This checks that
    - Arrays are mutually broadcastable
    - Broadcasted arrays are one-dimensional

    Optionally, arrays are sorted according to the ``sort_by`` argument.

    Parameters
    ----------
    *args : ndarrays
        All non-keyword arguments are arrays which will be validated
    sort_by : array
        If specified, sort all inputs by the order given in this array.
    """
    arrays = np.broadcast_arrays(*arrays)
    sort_by = kwargs.pop('sort_by', None)

    if kwargs:
        raise ValueError("unrecognized arguments: {0}".format(kwargs.keys()))

    if arrays[0].ndim != 1:
        raise ValueError("Input arrays should be one-dimensional.")

    if sort_by is not None:
        isort = np.argsort(sort_by)
        if isort.shape != arrays[0].shape:
            raise ValueError("sort shape must equal array shape.")
        arrays = tuple([a[isort] for a in arrays])
    return arrays

def _prep_smooth(t, y, dy, span, t_out, span_out, period):
    """Private function to prepare & check variables for smooth utilities"""

    # If period is provided, sort by phases. Otherwise sort by t
    if period:
        t = t % period
        if t_out is not None:
            t_out = t_out % period

    t, y, dy = validate_inputs(t, y, dy, sort_by=t)

    if span_out is not None:
        if t_out is None:
            raise ValueError("Must specify t_out when span_out is given")
        if span is not None:
            raise ValueError("Must specify only one of span, span_out")
        span, t_out = np.broadcast_arrays(span_out, t_out)
        indices = np.searchsorted(t, t_out)
    elif span is None:
        raise ValueError("Must specify either span_out or span")
    else:
        indices = None

    return t, y, dy, span, t_out, span_out, indices

def moving_average_smooth(t, y, dy, span=None, cv=True,
                          t_out=None, span_out=None, period=None):
    """Perform a moving-average smooth of the data

    Parameters
    ----------
    t, y, dy : array_like
        time, value, and error in value of the input data
    span : array_like
        the integer spans of the data
    cv : boolean (default=True)
        if True, treat the problem as a cross-validation, i.e. don't use
        each point in the evaluation of its own smoothing.
    t_out : array_like (optional)
        the output times for the moving averages
    span_out : array_like (optional)
        the spans associated with the output times t_out
    period : float
        if provided, then consider the inputs periodic with the given period

    Returns
    -------
    y_smooth : array_like
        smoothed y values at each time t (or t_out)
    """
    prep = _prep_smooth(t, y, dy, span, t_out, span_out, period)
    t, y, dy, span, t_out, span_out, indices = prep

    w = dy ** -2
    w, yw = windowed_sum([w, y * w], t=t, span=span, subtract_mid=cv,
                         indices=indices, period=period)

    if t_out is None or span_out is not None:
        return yw / w
    else:
        i = np.minimum(len(t) - 1, np.searchsorted(t, t_out))
        return yw[i] / w[i]

def linear_smooth(t, y, dy, span=None, cv=True,
                  t_out=None, span_out=None, period=None):
    """Perform a linear smooth of the data

    Parameters
    ----------
    t, y, dy : array_like
        time, value, and error in value of the input data
    span : array_like
        the integer spans of the data
    cv : boolean (default=True)
        if True, treat the problem as a cross-validation, i.e. don't use
        each point in the evaluation of its own smoothing.
    t_out : array_like (optional)
        the output times for the moving averages
    span_out : array_like (optional)
        the spans associated with the output times t_out
    period : float
        if provided, then consider the inputs periodic with the given period

    Returns
    -------
    y_smooth : array_like
        smoothed y values at each time t or t_out
    """
    t_input = t
    prep = _prep_smooth(t, y, dy, span, t_out, span_out, period)
    t, y, dy, span, t_out, span_out, indices = prep
    if period:
        t_input = np.asarray(t_input) % period

    w = dy ** -2
    w, yw, tw, tyw, ttw = windowed_sum([w, y * w, w, y * w, w], t=t,
                                       tpowers=[0, 0, 1, 1, 2],
                                       span=span, indices=indices,
                                       subtract_mid=cv, period=period)

    denominator = (w * ttw - tw * tw)
    slope = (tyw * w - tw * yw)
    intercept = (ttw * yw - tyw * tw)

    #if np.any(denominator == 0):
    #    raise ValueError("Zero denominator in linear smooth. This usually "
    #                     "indicates that the input contains duplicate points.")

    if t_out is None:
        return (slope * t_input + intercept) / denominator
    elif span_out is not None:
        return (slope * t_out + intercept) / denominator
    else:
        i = np.minimum(len(t) - 1, np.searchsorted(t, t_out))
        return (slope[i] * t_out + intercept[i]) / denominator[i]

def multinterp(x, y, xquery, slow=False):
    """Multiple linear interpolations

    Parameters
    ----------
    x : array_like, shape=(N,)
        sorted array of x values
    y : array_like, shape=(N, M)
        array of y values corresponding to each x value
    xquery : array_like, shape=(M,)
        array of query values
    slow : boolean, default=False
        if True, use slow method (used mainly for unit testing)

    Returns
    -------
    yquery : ndarray, shape=(M,)
        The interpolated values corresponding to each x query.
    """
    x, y, xquery = map(np.asarray, (x, y, xquery))
    assert x.ndim == 1
    assert xquery.ndim == 1
    assert y.shape == x.shape + xquery.shape

    # make sure xmin < xquery < xmax in all cases
    xquery = np.clip(xquery, x.min(), x.max())

    if slow:
        from scipy.interpolate import interp1d
        return np.array([interp1d(x, y)(xq) for xq, y in zip(xquery, y.T)])
    elif len(x) == 3:
        # Most common case: use a faster approach
        yq_lower = y[0] + (xquery - x[0]) * (y[1] - y[0]) / (x[1] - x[0])
        yq_upper = y[1] + (xquery - x[1]) * (y[2] - y[1]) / (x[2] - x[1])
        return np.where(xquery < x[1], yq_lower, yq_upper)
    else:
        i = np.clip(np.searchsorted(x, xquery, side='right') - 1,
                    0, len(x) - 2)
        j = np.arange(len(xquery))
        return y[i, j] + ((xquery - x[i]) *
                          (y[i + 1, j] - y[i, j]) / (x[i + 1] - x[i]))

<commit_msg>BUG: uncomment denominator check<commit_after>from __future__ import division, print_function

from contextlib import contextmanager

import numpy as np
from .windowed_sum import windowed_sum, windowed_sum_slow

@contextmanager
def setattr_context(obj, **kwargs):
    """
    Context manager to temporarily change the values of object attributes
    while executing a function.

    Example
    -------
    >>> class Foo: pass
    >>> f = Foo(); f.attr = 'hello'
    >>> with setattr_context(f, attr='goodbye'):
    ...     print(f.attr)
    goodbye
    >>> print(f.attr)
    hello
    """
    old_kwargs = dict([(key, getattr(obj, key)) for key in kwargs])
    [setattr(obj, key, val) for key, val in kwargs.items()]
    try:
        yield
    finally:
        [setattr(obj, key, val) for key, val in old_kwargs.items()]

def iterable(obj):
    """Utility to check if object is iterable"""
    try:
        iter(obj)
    except:
        return False
    else:
        return True

def validate_inputs(*arrays, **kwargs):
    """Validate input arrays

    This checks that
    - Arrays are mutually broadcastable
    - Broadcasted arrays are one-dimensional

    Optionally, arrays are sorted according to the ``sort_by`` argument.

    Parameters
    ----------
    *args : ndarrays
        All non-keyword arguments are arrays which will be validated
    sort_by : array
        If specified, sort all inputs by the order given in this array.
    """
    arrays = np.broadcast_arrays(*arrays)
    sort_by = kwargs.pop('sort_by', None)

    if kwargs:
        raise ValueError("unrecognized arguments: {0}".format(kwargs.keys()))

    if arrays[0].ndim != 1:
        raise ValueError("Input arrays should be one-dimensional.")

    if sort_by is not None:
        isort = np.argsort(sort_by)
        if isort.shape != arrays[0].shape:
            raise ValueError("sort shape must equal array shape.")
        arrays = tuple([a[isort] for a in arrays])
    return arrays

def _prep_smooth(t, y, dy, span, t_out, span_out, period):
    """Private function to prepare & check variables for smooth utilities"""

    # If period is provided, sort by phases. Otherwise sort by t
    if period:
        t = t % period
        if t_out is not None:
            t_out = t_out % period

    t, y, dy = validate_inputs(t, y, dy, sort_by=t)

    if span_out is not None:
        if t_out is None:
            raise ValueError("Must specify t_out when span_out is given")
        if span is not None:
            raise ValueError("Must specify only one of span, span_out")
        span, t_out = np.broadcast_arrays(span_out, t_out)
        indices = np.searchsorted(t, t_out)
    elif span is None:
        raise ValueError("Must specify either span_out or span")
    else:
        indices = None

    return t, y, dy, span, t_out, span_out, indices

def moving_average_smooth(t, y, dy, span=None, cv=True,
                          t_out=None, span_out=None, period=None):
    """Perform a moving-average smooth of the data

    Parameters
    ----------
    t, y, dy : array_like
        time, value, and error in value of the input data
    span : array_like
        the integer spans of the data
    cv : boolean (default=True)
        if True, treat the problem as a cross-validation, i.e. don't use
        each point in the evaluation of its own smoothing.
    t_out : array_like (optional)
        the output times for the moving averages
    span_out : array_like (optional)
        the spans associated with the output times t_out
    period : float
        if provided, then consider the inputs periodic with the given period

    Returns
    -------
    y_smooth : array_like
        smoothed y values at each time t (or t_out)
    """
    prep = _prep_smooth(t, y, dy, span, t_out, span_out, period)
    t, y, dy, span, t_out, span_out, indices = prep

    w = dy ** -2
    w, yw = windowed_sum([w, y * w], t=t, span=span, subtract_mid=cv,
                         indices=indices, period=period)

    if t_out is None or span_out is not None:
        return yw / w
    else:
        i = np.minimum(len(t) - 1, np.searchsorted(t, t_out))
        return yw[i] / w[i]

def linear_smooth(t, y, dy, span=None, cv=True,
                  t_out=None, span_out=None, period=None):
    """Perform a linear smooth of the data

    Parameters
    ----------
    t, y, dy : array_like
        time, value, and error in value of the input data
    span : array_like
        the integer spans of the data
    cv : boolean (default=True)
        if True, treat the problem as a cross-validation, i.e. don't use
        each point in the evaluation of its own smoothing.
    t_out : array_like (optional)
        the output times for the moving averages
    span_out : array_like (optional)
        the spans associated with the output times t_out
    period : float
        if provided, then consider the inputs periodic with the given period

    Returns
    -------
    y_smooth : array_like
        smoothed y values at each time t or t_out
    """
    t_input = t
    prep = _prep_smooth(t, y, dy, span, t_out, span_out, period)
    t, y, dy, span, t_out, span_out, indices = prep
    if period:
        t_input = np.asarray(t_input) % period

    w = dy ** -2
    w, yw, tw, tyw, ttw = windowed_sum([w, y * w, w, y * w, w], t=t,
                                       tpowers=[0, 0, 1, 1, 2],
                                       span=span, indices=indices,
                                       subtract_mid=cv, period=period)

    denominator = (w * ttw - tw * tw)
    slope = (tyw * w - tw * yw)
    intercept = (ttw * yw - tyw * tw)

    if np.any(denominator == 0):
        raise ValueError("Zero denominator in linear smooth. This usually "
                         "indicates that the input contains duplicate points.")

    if t_out is None:
        return (slope * t_input + intercept) / denominator
    elif span_out is not None:
        return (slope * t_out + intercept) / denominator
    else:
        i = np.minimum(len(t) - 1, np.searchsorted(t, t_out))
        return (slope[i] * t_out + intercept[i]) / denominator[i]

def multinterp(x, y, xquery, slow=False):
    """Multiple linear interpolations

    Parameters
    ----------
    x : array_like, shape=(N,)
        sorted array of x values
    y : array_like, shape=(N, M)
        array of y values corresponding to each x value
    xquery : array_like, shape=(M,)
        array of query values
    slow : boolean, default=False
        if True, use slow method (used mainly for unit testing)

    Returns
    -------
    yquery : ndarray, shape=(M,)
        The interpolated values corresponding to each x query.
    """
    x, y, xquery = map(np.asarray, (x, y, xquery))
    assert x.ndim == 1
    assert xquery.ndim == 1
    assert y.shape == x.shape + xquery.shape

    # make sure xmin < xquery < xmax in all cases
    xquery = np.clip(xquery, x.min(), x.max())

    if slow:
        from scipy.interpolate import interp1d
        return np.array([interp1d(x, y)(xq) for xq, y in zip(xquery, y.T)])
    elif len(x) == 3:
        # Most common case: use a faster approach
        yq_lower = y[0] + (xquery - x[0]) * (y[1] - y[0]) / (x[1] - x[0])
        yq_upper = y[1] + (xquery - x[1]) * (y[2] - y[1]) / (x[2] - x[1])
        return np.where(xquery < x[1], yq_lower, yq_upper)
    else:
        i = np.clip(np.searchsorted(x, xquery, side='right') - 1,
                    0, len(x) - 2)
        j = np.arange(len(xquery))
        return y[i, j] + ((xquery - x[i]) *
                          (y[i + 1, j] - y[i, j]) / (x[i + 1] - x[i]))

<|endoftext|>


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

import functools
from collections import OrderedDict
import numpy as np
from pgmpy import Exceptions
from pgmpy.Factor._factor_product import _factor_product

class Factor:
    """
    Base class for *Factor*.

    Public Methods
    --------------
    assignment(index)
    get_cardinality(variable)
    marginalize([variable_list])
    normalise()
    product(*factors)
    reduce([variable_values_list])
    """

    def __init__(self, variables, cardinality, value):
        """
        Initialize a Factor class.

        Defined above, we have the following mapping from variable
        assignments to the index of the row vector in the value field:

        +-----+-----+-----+-------------------+
        |  x1 |  x2 |  x3 |    phi(x1, x2, x2)|
        +-----+-----+-----+-------------------+
        | x1_0| x2_0| x3_0|     phi.value(0)  |
        +-----+-----+-----+-------------------+
        | x1_0| x2_0| x3_1|     phi.value(1)  |
        +-----+-----+-----+-------------------+
        | x1_0| x2_1| x3_0|     phi.value(2)  |
        +-----+-----+-----+-------------------+
        | x1_0| x2_1| x3_1|     phi.value(3)  |
        +-----+-----+-----+-------------------+
        | x1_1| x2_0| x3_0|     phi.value(4)  |
        +-----+-----+-----+-------------------+
        | x1_1| x2_0| x3_1|     phi.value(5)  |
        +-----+-----+-----+-------------------+
        | x1_1| x2_1| x3_0|     phi.value(6)  |
        +-----+-----+-----+-------------------+
        | x1_1| x2_1| x3_1|     phi.value(7)  |
        +-----+-----+-----+-------------------+

        Parameters
        ----------
        variables: list
            List of scope of factor
        cardinality: list, array_like
            List of cardinality of each variable
        value: list, array_like
            List or array of values of factor.
            A Factor's values are stored in a row vector in the value
            using an ordering such that the left-most variables as defined in
            the variable field cycle through their values the fastest. More
            concretely, for factor

        Examples
        --------
        >>> phi = Factor(['x1', 'x2', 'x3'], [2, 2, 2], np.ones(8))
        """
        self.variables = OrderedDict()
        if len(variables) != len(cardinality):
            raise ValueError("The size of variables and cardinality should be same")
        for variable, card in zip(variables, cardinality):
            self.variables[variable] = [variable + '_' + str(index)
                                        for index in range(card)]
        self.cardinality = np.array(cardinality)
        self.values = np.array(value, dtype=np.double)
        if not self.values.shape[0] == np.prod(self.cardinality):
            raise Exceptions.SizeError("Incompetant value array")

    def scope(self):
        """
        Returns the scope of the factor.

        Examples
        --------
        >>> phi = Factor(['x1', 'x2', 'x3'], [2, 3, 2], np.ones(8))
        >>> phi.scope()
        ['x1', 'x2', 'x3']
        """
        return list(self.variables)

    def assignment(self, index):
        """
        Returns a list of assignments for the corresponding index.

        Parameters
        ----------
        index: integer, list-type, ndarray
            index or indices whose assignment is to be computed

        Examples
        --------
        >>> from pgmpy.Factor import Factor
        >>> import numpy as np
        >>> phi = Factor(['diff', 'intel'], [2, 2], np.ones(4))
        >>> phi.assignment([1, 2])
        [['diff_1', 'intel_0'], ['diff_0', 'intel_1']]
        """
        if not isinstance(index, np.ndarray):
            index = np.atleast_1d(index)
        max_index = np.prod(self.cardinality) - 1
        if not all(i <= max_index for i in index):
            raise IndexError("Index greater than max possible index")
        mat = np.floor(np.tile(np.atleast_2d(index).T,
                               (1, self.cardinality.shape[0])) /
                       np.tile(np.cumprod(
                           np.concatenate(([1], self.cardinality[:-1]))),
                               (index.shape[0], 1)))\
              % np.tile(self.cardinality, (index.shape[0], 1))
        mat = mat.astype('int')
        return [[self.variables[key][val] for key, val in
                 zip(self.variables.keys(), values)] for values in mat]

    def get_cardinality(self, variable):
        """
        Returns cardinality of a given variable

        Parameters
        ----------
        variable: string

        Examples
        --------
        >>> from pgmpy.Factor import Factor
        >>> phi = Factor(['x1', 'x2', 'x3'], [2, 3, 2], range(12))
        >>> phi.get_cardinality('x1')
        2
        """
        if variable not in self.variables:
            raise Exceptions.ScopeError("%s not in scope" % variable)
        return self.cardinality[list(self.variables.keys()).index(variable)]

    def marginalize(self, variables):
        """
        Modifies the factor with marginalized values.

        Paramters
        ---------
        variables: string, list-type
            name of variable to be marginalized

        Examples
        --------
        >>> from pgmpy.Factor import Factor
        >>> phi = Factor(['x1', 'x2', 'x3'], [2, 3, 2], range(12))
        >>> phi.marginalize(['x1', 'x3'])
        >>> phi.values
        array([ 14.,  22.,  30.])
        >>> phi.variables
        OrderedDict([('x2', ['x2_0', 'x2_1', 'x2_2'])])
        """
        if not isinstance(variables, list):
            variables = [variables]
        for variable in variables:
            if variable not in self.variables:
                raise Exceptions.ScopeError("%s not in scope" % variable)
        for variable in variables:
            self.values = self._marginalize_single_variable(variable)
            index = list(self.variables.keys()).index(variable)
            del(self.variables[variable])
            self.cardinality = np.delete(self.cardinality, index)

    def _marginalize_single_variable(self, variable):
        """
        Returns marginalised factor for a single variable

        Parameters
        ---------
        variable_name: string
            name of variable to be marginalized

        """
        index = list(self.variables.keys()).index(variable)
        cum_cardinality = (np.product(self.cardinality) / np.concatenate(([1], np.cumprod(self.cardinality)))).astype(np.int64, copy=False)
        num_elements = cum_cardinality[0]
        sum_index = [j for i in range(0, num_elements,
                                      cum_cardinality[index])
                     for j in range(i, i+cum_cardinality[index+1])]
        marg_factor = np.zeros(num_elements/self.cardinality[index])
        for i in range(self.cardinality[index]):
            marg_factor += self.values[np.array(sum_index) +
                                       i*cum_cardinality[index+1]]
        return marg_factor

    def normalize(self):
        """
        Normalizes the values of factor so that they sum to 1.
        """
        self.values = self.values/np.sum(self.values)

    def reduce(self, values):
        """
        Reduces the factor to the context of given variable values.

        Parameters
        ----------
        values: string, list-type
            name of the variable values

        Examples
        --------
        >>> from pgmpy.Factor import Factor
        >>> phi = Factor(['x1', 'x2', 'x3'], [2, 3, 2], range(12))
        >>> phi.reduce(['x1_0', 'x2_0'])
        >>> phi.values
        array([0., 6.])
        """
        if not isinstance(values, list):
            values = [values]
        for value in values:
            if not '_' in value:
                raise TypeError("Values should be in the form of "
                                "variablename_index")
            var, value_index = value.split('_')
            if not var in self.variables:
                raise Exceptions.ScopeError("%s not in scope" % var)
            index = list(self.variables.keys()).index(var)
            if not (int(value_index) < self.cardinality[index]):
                raise Exceptions.SizeError("Value is "
                                           "greater than max possible value")
            cum_cardinality = np.concatenate(([1],
                                              np.cumprod(self.cardinality)))
            num_elements = cum_cardinality[-1]
            index_arr = [j for i in range(0, num_elements,
                                          cum_cardinality[index+1])
                         for j in range(i, i+cum_cardinality[index])]
            self.values = self.values[np.array(index_arr)]
            del(self.variables[var])
            self.cardinality = np.delete(self.cardinality, index)

    def product(self, *factors):
        """
        Returns the factor product with factors.

        Parameters
        ----------
        *factors: Factor1, Factor2, ...
            Factors to be multiplied

        Example
        -------
        >>> from pgmpy.Factor import Factor
        >>> phi1 = Factor(['x1', 'x2', 'x3'], [2, 3, 2], range(12))
        >>> phi2 = Factor(['x3', 'x4', 'x1'], [2, 2, 2], range(8))
        >>> phi = phi1.product(phi2)
        >>> phi.variables
        OrderedDict([('x1', ['x1_0', 'x1_1']), ('x2', ['x2_0', 'x2_1', 'x2_2']),
                ('x3', ['x3_0', 'x3_1']), ('x4', ['x4_0', 'x4_1'])])
        """
        return factor_product(self, *factors)

    def __str__(self):
        return self._str('phi')

    __repr__ = __str__

    def _str(self, phi_or_p):
        string = ""
        for var in self.variables:
            string += str(var) + "\t"
        string += phi_or_p + '(' + ', '.join(self.variables) + ')'
        string += "\n"

        #fun and gen are functions to generate the different values of variables in the table.
        #gen starts with giving fun initial value of b=[0, 0, 0] then fun tries to increment it
        #by 1.
        def fun(b, index=len(self.cardinality)-1):
            b[index] += 1
            if b[index] == self.cardinality[index]:
                b[index] = 0
                fun(b, index-1)
            return b

        def gen():
            b = [0] * len(self.variables)
            yield b
            for i in range(np.prod(self.cardinality)-1):
                yield fun(b)

        value_index = 0
        for prob in gen():
            prob_list = [list(self.variables)[i] + '_' + str(prob[i]) for i in range(len(self.variables))]
            string += '\t'.join(prob_list) + '\t' + str(self.values[value_index]) + '\n'
            value_index += 1

        return string

    def __mul__(self, other):
        return self.product(other)

    def __eq__(self, other):
        if type(self) != type(other):
            return False

        if self.variables == other.variables and all(self.cardinality == other.cardinality) \
                and all(self.values == other.values):
            return True
        else:
            return False

    def __hash__(self):
        """
        Returns the hash of the factor object based on the scope of the factor.
        """
        return hash(' '.join(self.variables) + ' '.join(map(str, self.cardinality)) +
                    ' '.join(list(map(str, self.values))))

def _bivar_factor_product(phi1, phi2):
    """
    Returns product of two factors.

    Parameters
    ----------
    phi1: Factor

    phi2: Factor

    See Also
    --------
    factor_product
    """
    vars1 = list(phi1.variables.keys())
    vars2 = list(phi2.variables.keys())
    common_var_list = [var1 for var1 in vars1 for var2 in vars2
                       if var1 == var2]
    if common_var_list:
        common_var_index_list = np.array([[vars1.index(var), vars2.index(var)]
                                          for var in common_var_list])
        common_card_product = np.prod([phi1.cardinality[index[0]] for index
                                       in common_var_index_list])
        size = np.prod(phi1.cardinality) * np.prod(
            phi2.cardinality) / common_card_product
        product = _factor_product(phi1.values,
                                  phi2.values,
                                  size,
                                  common_var_index_list,
                                  phi1.cardinality,
                                  phi2.cardinality)
        variables = vars1
        variables.extend(var for var in phi2.variables
                         if var not in common_var_list)
        cardinality = list(phi1.cardinality)
        cardinality.extend(phi2.get_cardinality(var) for var in phi2.variables
                           if var not in common_var_list)
        phi = Factor(variables, cardinality, product)
        return phi
    else:
        size = np.prod(phi1.cardinality) * np.prod(phi2.cardinality)
        product = _factor_product(phi1.values,
                                  phi2.values,
                                  size)
        variables = vars1 + vars2
        cardinality = list(phi1.cardinality) + list(phi2.cardinality)
        phi = Factor(variables, cardinality, product)
        return phi

def factor_product(*args):
    """
    Returns factor product of multiple factors.

    Parameters
    ----------
    factor1, factor2, .....: Factor
        factors to be multiplied

    Examples
    --------
    >>> from pgmpy.Factor import Factor, factor_product
    >>> phi1 = Factor(['x1', 'x2', 'x3'], [2, 3, 2], range(12))
    >>> phi2 = Factor(['x3', 'x4', 'x1'], [2, 2, 2], range(8))
    >>> phi = factor_product(phi1, phi2)
    >>> phi.variables
    OrderedDict([('x1', ['x1_0', 'x1_1']), ('x2', ['x2_0', 'x2_1', 'x2_2']),
                ('x3', ['x3_0', 'x3_1']), ('x4', ['x4_0', 'x4_1'])])
    """
    if not all(isinstance(phi, Factor) for phi in args):
        raise TypeError("Input parameters must be factors")
    return functools.reduce(_bivar_factor_product, args)<commit_msg>completely changed assignment method according to new representation and also reducing the execution time by half [refs #137]<commit_after>#!/usr/bin/env python3

import functools
from collections import OrderedDict
import numpy as np
from pgmpy import Exceptions
from pgmpy.Factor._factor_product import _factor_product

class Factor:
    """
    Base class for *Factor*.

    Public Methods
    --------------
    assignment(index)
    get_cardinality(variable)
    marginalize([variable_list])
    normalise()
    product(*factors)
    reduce([variable_values_list])
    """

    def __init__(self, variables, cardinality, value):
        """
        Initialize a Factor class.

        Defined above, we have the following mapping from variable
        assignments to the index of the row vector in the value field:

        +-----+-----+-----+-------------------+
        |  x1 |  x2 |  x3 |    phi(x1, x2, x2)|
        +-----+-----+-----+-------------------+
        | x1_0| x2_0| x3_0|     phi.value(0)  |
        +-----+-----+-----+-------------------+
        | x1_0| x2_0| x3_1|     phi.value(1)  |
        +-----+-----+-----+-------------------+
        | x1_0| x2_1| x3_0|     phi.value(2)  |
        +-----+-----+-----+-------------------+
        | x1_0| x2_1| x3_1|     phi.value(3)  |
        +-----+-----+-----+-------------------+
        | x1_1| x2_0| x3_0|     phi.value(4)  |
        +-----+-----+-----+-------------------+
        | x1_1| x2_0| x3_1|     phi.value(5)  |
        +-----+-----+-----+-------------------+
        | x1_1| x2_1| x3_0|     phi.value(6)  |
        +-----+-----+-----+-------------------+
        | x1_1| x2_1| x3_1|     phi.value(7)  |
        +-----+-----+-----+-------------------+

        Parameters
        ----------
        variables: list
            List of scope of factor
        cardinality: list, array_like
            List of cardinality of each variable
        value: list, array_like
            List or array of values of factor.
            A Factor's values are stored in a row vector in the value
            using an ordering such that the left-most variables as defined in
            the variable field cycle through their values the fastest. More
            concretely, for factor

        Examples
        --------
        >>> phi = Factor(['x1', 'x2', 'x3'], [2, 2, 2], np.ones(8))
        """
        self.variables = OrderedDict()
        if len(variables) != len(cardinality):
            raise ValueError("The size of variables and cardinality should be same")
        for variable, card in zip(variables, cardinality):
            self.variables[variable] = [variable + '_' + str(index)
                                        for index in range(card)]
        self.cardinality = np.array(cardinality)
        self.values = np.array(value, dtype=np.double)
        if not self.values.shape[0] == np.prod(self.cardinality):
            raise Exceptions.SizeError("Incompetant value array")

    def scope(self):
        """
        Returns the scope of the factor.

        Examples
        --------
        >>> phi = Factor(['x1', 'x2', 'x3'], [2, 3, 2], np.ones(8))
        >>> phi.scope()
        ['x1', 'x2', 'x3']
        """
        return list(self.variables)

    def assignment(self, index):
        """
        Returns a list of assignments for the corresponding index.

        Parameters
        ----------
        index: integer, list-type, ndarray
            index or indices whose assignment is to be computed

        Examples
        --------
        >>> from pgmpy.Factor import Factor
        >>> import numpy as np
        >>> phi = Factor(['diff', 'intel'], [2, 2], np.ones(4))
        >>> phi.assignment([1, 2])
        [['diff_0', 'intel_1'], ['diff_1', 'intel_0']]
        """
        if not isinstance(index, np.ndarray):
            index = np.atleast_1d(index)
        max_index = np.prod(self.cardinality) - 1
        if not all(i <= max_index for i in index):
            raise IndexError("Index greater than max possible index")
        assignments = []
        for ind in index:
            assign = []
            for card in self.cardinality[::-1]:
                assign.insert(0, ind % card)
                ind = ind/card
            assignments.append(map(int, assign))
        return [[self.variables[key][val] for key, val in
                 zip(self.variables.keys(), values)] for values in assignments]

    def get_cardinality(self, variable):
        """
        Returns cardinality of a given variable

        Parameters
        ----------
        variable: string

        Examples
        --------
        >>> from pgmpy.Factor import Factor
        >>> phi = Factor(['x1', 'x2', 'x3'], [2, 3, 2], range(12))
        >>> phi.get_cardinality('x1')
        2
        """
        if variable not in self.variables:
            raise Exceptions.ScopeError("%s not in scope" % variable)
        return self.cardinality[list(self.variables.keys()).index(variable)]

    def marginalize(self, variables):
        """
        Modifies the factor with marginalized values.

        Paramters
        ---------
        variables: string, list-type
            name of variable to be marginalized

        Examples
        --------
        >>> from pgmpy.Factor import Factor
        >>> phi = Factor(['x1', 'x2', 'x3'], [2, 3, 2], range(12))
        >>> phi.marginalize(['x1', 'x3'])
        >>> phi.values
        array([ 14.,  22.,  30.])
        >>> phi.variables
        OrderedDict([('x2', ['x2_0', 'x2_1', 'x2_2'])])
        """
        if not isinstance(variables, list):
            variables = [variables]
        for variable in variables:
            if variable not in self.variables:
                raise Exceptions.ScopeError("%s not in scope" % variable)
        for variable in variables:
            self.values = self._marginalize_single_variable(variable)
            index = list(self.variables.keys()).index(variable)
            del(self.variables[variable])
            self.cardinality = np.delete(self.cardinality, index)

    def _marginalize_single_variable(self, variable):
        """
        Returns marginalised factor for a single variable

        Parameters
        ---------
        variable_name: string
            name of variable to be marginalized

        """
        index = list(self.variables.keys()).index(variable)
        cum_cardinality = (np.product(self.cardinality) / np.concatenate(([1], np.cumprod(self.cardinality)))).astype(np.int64, copy=False)
        num_elements = cum_cardinality[0]
        sum_index = [j for i in range(0, num_elements,
                                      cum_cardinality[index])
                     for j in range(i, i+cum_cardinality[index+1])]
        marg_factor = np.zeros(num_elements/self.cardinality[index])
        for i in range(self.cardinality[index]):
            marg_factor += self.values[np.array(sum_index) +
                                       i*cum_cardinality[index+1]]
        return marg_factor

    def normalize(self):
        """
        Normalizes the values of factor so that they sum to 1.
        """
        self.values = self.values/np.sum(self.values)

    def reduce(self, values):
        """
        Reduces the factor to the context of given variable values.

        Parameters
        ----------
        values: string, list-type
            name of the variable values

        Examples
        --------
        >>> from pgmpy.Factor import Factor
        >>> phi = Factor(['x1', 'x2', 'x3'], [2, 3, 2], range(12))
        >>> phi.reduce(['x1_0', 'x2_0'])
        >>> phi.values
        array([0., 6.])
        """
        if not isinstance(values, list):
            values = [values]
        for value in values:
            if not '_' in value:
                raise TypeError("Values should be in the form of "
                                "variablename_index")
            var, value_index = value.split('_')
            if not var in self.variables:
                raise Exceptions.ScopeError("%s not in scope" % var)
            index = list(self.variables.keys()).index(var)
            if not (int(value_index) < self.cardinality[index]):
                raise Exceptions.SizeError("Value is "
                                           "greater than max possible value")
            cum_cardinality = np.concatenate(([1],
                                              np.cumprod(self.cardinality)))
            num_elements = cum_cardinality[-1]
            index_arr = [j for i in range(0, num_elements,
                                          cum_cardinality[index+1])
                         for j in range(i, i+cum_cardinality[index])]
            self.values = self.values[np.array(index_arr)]
            del(self.variables[var])
            self.cardinality = np.delete(self.cardinality, index)

    def product(self, *factors):
        """
        Returns the factor product with factors.

        Parameters
        ----------
        *factors: Factor1, Factor2, ...
            Factors to be multiplied

        Example
        -------
        >>> from pgmpy.Factor import Factor
        >>> phi1 = Factor(['x1', 'x2', 'x3'], [2, 3, 2], range(12))
        >>> phi2 = Factor(['x3', 'x4', 'x1'], [2, 2, 2], range(8))
        >>> phi = phi1.product(phi2)
        >>> phi.variables
        OrderedDict([('x1', ['x1_0', 'x1_1']), ('x2', ['x2_0', 'x2_1', 'x2_2']),
                ('x3', ['x3_0', 'x3_1']), ('x4', ['x4_0', 'x4_1'])])
        """
        return factor_product(self, *factors)

    def __str__(self):
        return self._str('phi')

    __repr__ = __str__

    def _str(self, phi_or_p):
        string = ""
        for var in self.variables:
            string += str(var) + "\t"
        string += phi_or_p + '(' + ', '.join(self.variables) + ')'
        string += "\n"

        #fun and gen are functions to generate the different values of variables in the table.
        #gen starts with giving fun initial value of b=[0, 0, 0] then fun tries to increment it
        #by 1.
        def fun(b, index=len(self.cardinality)-1):
            b[index] += 1
            if b[index] == self.cardinality[index]:
                b[index] = 0
                fun(b, index-1)
            return b

        def gen():
            b = [0] * len(self.variables)
            yield b
            for i in range(np.prod(self.cardinality)-1):
                yield fun(b)

        value_index = 0
        for prob in gen():
            prob_list = [list(self.variables)[i] + '_' + str(prob[i]) for i in range(len(self.variables))]
            string += '\t'.join(prob_list) + '\t' + str(self.values[value_index]) + '\n'
            value_index += 1

        return string

    def __mul__(self, other):
        return self.product(other)

    def __eq__(self, other):
        if type(self) != type(other):
            return False

        if self.variables == other.variables and all(self.cardinality == other.cardinality) \
                and all(self.values == other.values):
            return True
        else:
            return False

    def __hash__(self):
        """
        Returns the hash of the factor object based on the scope of the factor.
        """
        return hash(' '.join(self.variables) + ' '.join(map(str, self.cardinality)) +
                    ' '.join(list(map(str, self.values))))

def _bivar_factor_product(phi1, phi2):
    """
    Returns product of two factors.

    Parameters
    ----------
    phi1: Factor

    phi2: Factor

    See Also
    --------
    factor_product
    """
    vars1 = list(phi1.variables.keys())
    vars2 = list(phi2.variables.keys())
    common_var_list = [var1 for var1 in vars1 for var2 in vars2
                       if var1 == var2]
    if common_var_list:
        common_var_index_list = np.array([[vars1.index(var), vars2.index(var)]
                                          for var in common_var_list])
        common_card_product = np.prod([phi1.cardinality[index[0]] for index
                                       in common_var_index_list])
        size = np.prod(phi1.cardinality) * np.prod(
            phi2.cardinality) / common_card_product
        product = _factor_product(phi1.values,
                                  phi2.values,
                                  size,
                                  common_var_index_list,
                                  phi1.cardinality,
                                  phi2.cardinality)
        variables = vars1
        variables.extend(var for var in phi2.variables
                         if var not in common_var_list)
        cardinality = list(phi1.cardinality)
        cardinality.extend(phi2.get_cardinality(var) for var in phi2.variables
                           if var not in common_var_list)
        phi = Factor(variables, cardinality, product)
        return phi
    else:
        size = np.prod(phi1.cardinality) * np.prod(phi2.cardinality)
        product = _factor_product(phi1.values,
                                  phi2.values,
                                  size)
        variables = vars1 + vars2
        cardinality = list(phi1.cardinality) + list(phi2.cardinality)
        phi = Factor(variables, cardinality, product)
        return phi

def factor_product(*args):
    """
    Returns factor product of multiple factors.

    Parameters
    ----------
    factor1, factor2, .....: Factor
        factors to be multiplied

    Examples
    --------
    >>> from pgmpy.Factor import Factor, factor_product
    >>> phi1 = Factor(['x1', 'x2', 'x3'], [2, 3, 2], range(12))
    >>> phi2 = Factor(['x3', 'x4', 'x1'], [2, 2, 2], range(8))
    >>> phi = factor_product(phi1, phi2)
    >>> phi.variables
    OrderedDict([('x1', ['x1_0', 'x1_1']), ('x2', ['x2_0', 'x2_1', 'x2_2']),
                ('x3', ['x3_0', 'x3_1']), ('x4', ['x4_0', 'x4_1'])])
    """
    if not all(isinstance(phi, Factor) for phi in args):
        raise TypeError("Input parameters must be factors")
    return functools.reduce(_bivar_factor_product, args)<|endoftext|>


<commit_before>from pgmpy.models import BayesianModel

from pgmpy.factors import TabularCPD

from pgmpy.extern.six.moves import map, range

import numpy

from pyparsing import Word, alphanums, Suppress, Optional, CharsNotIn, Group, nums, ZeroOrMore, OneOrMore,\
        cppStyleComment, Literal, printables

import re

class BIFReader(object):

    """
    Base class for reading network file in bif format
    """

    def __init__(self, path=None, string=None):

        """
        Initialisation of BifReader object

        Parameters
        ----------------
        path : file or str
                File of bif data
        string : str
                String of bif data
        Examples
        -----------------
        # dog-problem.bif file is present at
        # http://www.cs.cmu.edu/~javabayes/Examples/DogProblem/dog-problem.bif
        >>> from pgmpy.readwrite import BIFReader
        >>> reader = BIFReader("bif_test.bif")
        >>> reader = BIFReader("bif_test.bif")
        <pgmpy.readwrite.BIF.BIFReader object at 0x7f2375621cf8>
        """
        # An pyparsing expression for checking mulitple spaces
        multi_space = OneOrMore(Literal(' ') | '\t' | '\r' | '\f')

        if path:
            with open(path, 'r') as network:
                self.network = network.read()

        elif string:
            self.network = string

        else:
            raise ValueError("Must specify either path or string")

        if '"' in self.network:
            # Replacing quotes by spaces to remove case sensitivity like:
            # "Dog-Problem" and Dog-problem
            # or "true""false" and "true" "false" and true false
            self.network = self.network.replace('"', ' ')

        # replacing mulitple spaces or tabs by one space
        self.network = multi_space.setParseAction(lambda t: ' ').transformString(self.network)
        if '/*' in self.network or '//' in self.network:
            self.network = cppStyleComment.suppress().transformString(self.network)  # removing comments from the file

        self.variable_block, self.name_expr, self.state_expr, self.property_expr = self.get_variable_grammar()
        self.probability_block, self.probability_expr, self.cpd_expr = self.get_probability_grammar()
        self.network_name = self.get_network_name()
        self.variable_names = self.get_variables()
        self.variable_states = self.get_states()
        self.variable_properties = self.get_property()
        self.variable_parents = self.get_parents()
        self.variable_cpds = self.get_cpd()
        self.variable_edges = self.get_edges()

    def get_variable_grammar(self):

        """
         A method that returns variable grammar
        """
        variable_block_starts = [x.start() for x in re.finditer('variable', self.network)]
        variable_block = []

        for i in variable_block_starts:
            variable_block_end = self.network.find('}\n', i)
            variable_block.append(self.network[i:variable_block_end])

        # Defining a expression for valid word
        word_expr = Word(alphanums + '_' + '-')
        word_expr2 = Word(initChars=printables, excludeChars=['{', '}', ',', ' '])
        name_expr = Suppress('variable') + word_expr + Suppress('{')
        state_expr = ZeroOrMore(word_expr2 + Optional(Suppress(",")))
        # Defining a variable state expression
        variable_state_expr = Suppress('type') + Suppress(word_expr) + Suppress('[') + Suppress(Word(nums)) + \
            Suppress(']') + Suppress('{') + Group(state_expr) + Suppress('}') + Suppress(';')
        # variable states is of the form type description [args] { val1, val2 }; (comma may or may not be present)

        property_expr = Suppress('property') + CharsNotIn(';') + Suppress(';')  # Creating a expr to find property

        return variable_block, name_expr, variable_state_expr, property_expr

    def get_probability_grammar(self):

        """
        A method that returns probability grammar
        """
        probability_block_starts = [x.start() for x in re.finditer('probability', self.network)]
        probability_block = []
        for i in probability_block_starts:
            probability_block_end = self.network.find('}\n', i)
            probability_block.append(self.network[i:probability_block_end])

        # Creating valid word expression for probability, it is of the format
        # wor1 | var2 , var3 or var1 var2 var3 or simply var
        word_expr = Word(alphanums + '-' + '_') + Suppress(Optional("|")) + Suppress(Optional(","))
        word_expr2 = Word(initChars=printables, excludeChars=[',', ')', ' ', '(']) + Suppress(Optional(","))
        # creating an expression for valid numbers, of the format
        # 1.00 or 1 or 1.00. 0.00 or 9.8e-5 etc
        num_expr = Word(nums + '-' + '+' + 'e' + 'E' + '.') + Suppress(Optional(","))
        probability_expr = Suppress('probability') + Suppress('(') + OneOrMore(word_expr) + Suppress(')')
        optional_expr = Suppress('(') + Suppress(OneOrMore(word_expr2)) + Suppress(')')
        probab_attributes = optional_expr | Suppress('table')
        cpd_expr = probab_attributes + OneOrMore(num_expr)

        return probability_block, probability_expr, cpd_expr

    def get_network_name(self):

        """
        Retruns the name of the network

        Example
        ---------------
        >>> from pgmpy.readwrite import BIFReader
        >>> reader = BIF.BifReader("bif_test.bif")
        >>> reader.network_name()
        'Dog-Problem'
        """
        # Creating a network attribute
        network_attribute = Suppress('network') + Word(alphanums + '_' + '-') + '{'
        network_name = network_attribute.searchString(self.network)[0][0]

        return network_name

    def get_variables(self):

        """
        Returns list of variables of the network

        Example
        -------------
        >>> from pgmpy.readwrite import BIFReader
        >>> reader = BIFReader("bif_test.bif")
        >>> reader.get_variables()
        ['light-on','bowel_problem','dog-out','hear-bark','family-out']
        """
        variable_names = []
        for block in self.variable_block:
            name = self.name_expr.searchString(block)[0][0]
            variable_names.append(name)

        return variable_names

    def get_states(self):

        """
        Returns the states of variables present in the network

        Example
        -----------
        >>> from pgmpy.readwrite import BIFReader
        >>> reader = BIFReader("bif_test.bif")
        >>> reader.get_states()
        {'bowel-problem': ['true','false'],
        'dog-out': ['true','false'],
        'family-out': ['true','false'],
        'hear-bark': ['true','false'],
        'light-on': ['true','false']}
        """
        variable_states = {}
        for i, name in enumerate(self.variable_names):
            block = self.variable_block[i]
            variable_states[name] = list(self.state_expr.searchString(block)[0][0])

        return variable_states

    def get_property(self):

        """
        Returns the property of the variable

        Example
        -------------
        >>> from pgmpy.readwrite import BIFReader
        >>> reader = BIFReader("bif_test.bif")
        >>> reader.get_property()
        {'bowel-problem': ['position = (335, 99)'],
        'dog-out': ['position = (300, 195)'],
        'family-out': ['position = (257, 99)'],
        'hear-bark': ['position = (296, 268)'],
        'light-on': ['position = (218, 195)']}
        """
        variable_properties = {}
        for i, name in enumerate(self.variable_names):
            block = self.variable_block[i]
            properties = self.property_expr.searchString(block)
            variable_properties[name] = [x[0].strip() for x in properties]
        return variable_properties

    def get_parents(self):

        """
        Returns the parents of the variables present in the network

        Example
        --------
        >>> from pgmpy.readwrite import BIFReader
        >>> reader = BIFReader("bif_test.bif")
        >>> reader.get_parents()
        {'bowel-problem': [],
        'dog-out': ['family-out', 'bowel-problem'],
        'family-out': [],
        'hear-bark': ['dog-out'],
        'light-on': ['family-out']}
        """
        variable_parents = {}
        for block in self.probability_block:
            names = self.probability_expr.searchString(block)[0]
            variable_parents[names[0]] = names[1:]
        return variable_parents

    def get_cpd(self):

        """
        Returns the CPD of the variables present in the network

        Example
        --------
        >>> from pgmpy.readwrite import BIFReader
        >>> reader = BIFReader("bif_test.bif")
        >>> reader.get_cpd()
        {'bowel-problem': np.array([[0.01],
                                    [0.99]]),
        'dog-out': np.array([[0.99, 0.97, 0.9, 0.3],
                            [0.01, 0.03, 0.1, 0.7]]),
        'family-out': np.array([[0.15],
                                [0.85]]),
        'hear-bark': np.array([[0.7, 0.01],
                                [0.3, 0.99]]),
        'light-on': np.array([[0.6, 0.05],
                            [0.4, 0.95]])}
         """
        variable_cpds = {}
        for block in self.probability_block:
            name = self.probability_expr.searchString(block)[0][0]
            cpds = self.cpd_expr.searchString(block)
            arr = [float(j) for i in cpds for j in i]
            arr = numpy.array(arr)
            arr = arr.reshape((len(self.variable_states[name]),
                              arr.size//len(self.variable_states[name])))
            variable_cpds[name] = arr

        return variable_cpds

    def get_edges(self):

        """
        Returns the edges of the network

        Example
        --------
        >>> from pgmpy.readwrite import BIFReader
        >>> reader = BIFReader("bif_test.bif")
        >>> reader.get_edges()
        [['family-out', 'light-on'],
         ['family-out', 'dog-out'],
         ['bowel-problem', 'dog-out'],
         ['dog-out', 'hear-bark']]
        """
        edges = [[value, key] for key in self.variable_parents.keys()
                 for value in self.variable_parents[key]]
        return edges

    def get_model(self):

        """
        Returns the fitted bayesian model

        Example
        ----------
        >>> from pgmpy.readwrite import BIFReader
        >>> reader = BIFReader("bif_test.bif")
        >>> reader.get_model()
        <pgmpy.models.BayesianModel.BayesianModel object at 0x7f20af154320>
        """
        try:
            model = BayesianModel(self.variable_edges)
            model.name = self.network_name
            model.add_nodes_from(self.variable_names)

            tabular_cpds = []
            for var, values in self.variable_cpds.items():
                cpd = TabularCPD(var, len(self.variable_states[var]), values,
                                 evidence=self.variable_parents[var],
                                 evidence_card=[len(self.variable_states[evidence_var])
                                                for evidence_var in self.variable_parents[var]])
            tabular_cpds.append(cpd)

            model.add_cpds(*tabular_cpds)
            for node, properties in self.variable_properties.items():
                for prop in properties:
                    prop_name, prop_value = map(lambda t: t.strip(), prop.split('='))
                    model.node[node][prop_name] = prop_value

            return model

        except AttributeError:
            raise AttributeError('First get states of variables, edges, parents and network name')
<commit_msg>Implemented BIFWriter class<commit_after>from pgmpy.models import BayesianModel

from pgmpy.factors import TabularCPD

from pgmpy.extern.six.moves import map, range

import numpy

from pyparsing import Word, alphanums, Suppress, Optional, CharsNotIn, Group, nums, ZeroOrMore, OneOrMore,\
        cppStyleComment, Literal, printables

import re

from string import Template

class BIFReader(object):

    """
    Base class for reading network file in bif format
    """

    def __init__(self, path=None, string=None):

        """
        Initialisation of BifReader object

        Parameters
        ----------------
        path : file or str
                File of bif data
        string : str
                String of bif data
        Examples
        -----------------
        # dog-problem.bif file is present at
        # http://www.cs.cmu.edu/~javabayes/Examples/DogProblem/dog-problem.bif
        >>> from pgmpy.readwrite import BIFReader
        >>> reader = BIFReader("bif_test.bif")
        >>> reader = BIFReader("bif_test.bif")
        <pgmpy.readwrite.BIF.BIFReader object at 0x7f2375621cf8>
        """
        # An pyparsing expression for checking mulitple spaces
        multi_space = OneOrMore(Literal(' ') | '\t' | '\r' | '\f')

        if path:
            with open(path, 'r') as network:
                self.network = network.read()

        elif string:
            self.network = string

        else:
            raise ValueError("Must specify either path or string")

        if '"' in self.network:
            # Replacing quotes by spaces to remove case sensitivity like:
            # "Dog-Problem" and Dog-problem
            # or "true""false" and "true" "false" and true false
            self.network = self.network.replace('"', ' ')

        # replacing mulitple spaces or tabs by one space
        self.network = multi_space.setParseAction(lambda t: ' ').transformString(self.network)
        if '/*' in self.network or '//' in self.network:
            self.network = cppStyleComment.suppress().transformString(self.network)  # removing comments from the file

        self.variable_block, self.name_expr, self.state_expr, self.property_expr = self.get_variable_grammar()
        self.probability_block, self.probability_expr, self.cpd_expr = self.get_probability_grammar()
        self.network_name = self.get_network_name()
        self.variable_names = self.get_variables()
        self.variable_states = self.get_states()
        self.variable_properties = self.get_property()
        self.variable_parents = self.get_parents()
        self.variable_cpds = self.get_cpds()
        self.variable_edges = self.get_edges()

    def get_variable_grammar(self):

        """
         A method that returns variable grammar
        """
        variable_block_starts = [x.start() for x in re.finditer('variable', self.network)]
        variable_block = []

        for i in variable_block_starts:
            variable_block_end = self.network.find('}\n', i)
            variable_block.append(self.network[i:variable_block_end])

        # Defining a expression for valid word
        word_expr = Word(alphanums + '_' + '-')
        word_expr2 = Word(initChars=printables, excludeChars=['{', '}', ',', ' '])
        name_expr = Suppress('variable') + word_expr + Suppress('{')
        state_expr = ZeroOrMore(word_expr2 + Optional(Suppress(",")))
        # Defining a variable state expression
        variable_state_expr = Suppress('type') + Suppress(word_expr) + Suppress('[') + Suppress(Word(nums)) + \
            Suppress(']') + Suppress('{') + Group(state_expr) + Suppress('}') + Suppress(';')
        # variable states is of the form type description [args] { val1, val2 }; (comma may or may not be present)

        property_expr = Suppress('property') + CharsNotIn(';') + Suppress(';')  # Creating a expr to find property

        return variable_block, name_expr, variable_state_expr, property_expr

    def get_probability_grammar(self):

        """
        A method that returns probability grammar
        """
        probability_block_starts = [x.start() for x in re.finditer('probability', self.network)]
        probability_block = []
        for i in probability_block_starts:
            probability_block_end = self.network.find('}\n', i)
            probability_block.append(self.network[i:probability_block_end])

        # Creating valid word expression for probability, it is of the format
        # wor1 | var2 , var3 or var1 var2 var3 or simply var
        word_expr = Word(alphanums + '-' + '_') + Suppress(Optional("|")) + Suppress(Optional(","))
        word_expr2 = Word(initChars=printables, excludeChars=[',', ')', ' ', '(']) + Suppress(Optional(","))
        # creating an expression for valid numbers, of the format
        # 1.00 or 1 or 1.00. 0.00 or 9.8e-5 etc
        num_expr = Word(nums + '-' + '+' + 'e' + 'E' + '.') + Suppress(Optional(","))
        probability_expr = Suppress('probability') + Suppress('(') + OneOrMore(word_expr) + Suppress(')')
        optional_expr = Suppress('(') + Suppress(OneOrMore(word_expr2)) + Suppress(')')
        probab_attributes = optional_expr | Suppress('table')
        cpd_expr = probab_attributes + OneOrMore(num_expr)

        return probability_block, probability_expr, cpd_expr

    def get_network_name(self):

        """
        Retruns the name of the network

        Example
        ---------------
        >>> from pgmpy.readwrite import BIFReader
        >>> reader = BIF.BifReader("bif_test.bif")
        >>> reader.network_name()
        'Dog-Problem'
        """
        # Creating a network attribute
        network_attribute = Suppress('network') + Word(alphanums + '_' + '-') + '{'
        network_name = network_attribute.searchString(self.network)[0][0]

        return network_name

    def get_variables(self):

        """
        Returns list of variables of the network

        Example
        -------------
        >>> from pgmpy.readwrite import BIFReader
        >>> reader = BIFReader("bif_test.bif")
        >>> reader.get_variables()
        ['light-on','bowel_problem','dog-out','hear-bark','family-out']
        """
        variable_names = []
        for block in self.variable_block:
            name = self.name_expr.searchString(block)[0][0]
            variable_names.append(name)

        return variable_names

    def get_states(self):

        """
        Returns the states of variables present in the network

        Example
        -----------
        >>> from pgmpy.readwrite import BIFReader
        >>> reader = BIFReader("bif_test.bif")
        >>> reader.get_states()
        {'bowel-problem': ['true','false'],
        'dog-out': ['true','false'],
        'family-out': ['true','false'],
        'hear-bark': ['true','false'],
        'light-on': ['true','false']}
        """
        variable_states = {}
        for i, name in enumerate(self.variable_names):
            block = self.variable_block[i]
            variable_states[name] = list(self.state_expr.searchString(block)[0][0])

        return variable_states

    def get_property(self):

        """
        Returns the property of the variable

        Example
        -------------
        >>> from pgmpy.readwrite import BIFReader
        >>> reader = BIFReader("bif_test.bif")
        >>> reader.get_property()
        {'bowel-problem': ['position = (335, 99)'],
        'dog-out': ['position = (300, 195)'],
        'family-out': ['position = (257, 99)'],
        'hear-bark': ['position = (296, 268)'],
        'light-on': ['position = (218, 195)']}
        """
        variable_properties = {}
        for i, name in enumerate(self.variable_names):
            block = self.variable_block[i]
            properties = self.property_expr.searchString(block)
            variable_properties[name] = [y.strip() for x in properties for y in x]
        return variable_properties

    def get_parents(self):

        """
        Returns the parents of the variables present in the network

        Example
        --------
        >>> from pgmpy.readwrite import BIFReader
        >>> reader = BIFReader("bif_test.bif")
        >>> reader.get_parents()
        {'bowel-problem': [],
        'dog-out': ['family-out', 'bowel-problem'],
        'family-out': [],
        'hear-bark': ['dog-out'],
        'light-on': ['family-out']}
        """
        variable_parents = {}
        for block in self.probability_block:
            names = self.probability_expr.searchString(block)[0]
            variable_parents[names[0]] = names[1:]
        return variable_parents

    def get_cpds(self):

        """
        Returns the CPD of the variables present in the network

        Example
        --------
        >>> from pgmpy.readwrite import BIFReader
        >>> reader = BIFReader("bif_test.bif")
        >>> reader.get_cpds()
        {'bowel-problem': np.array([[0.01],
                                    [0.99]]),
        'dog-out': np.array([[0.99, 0.97, 0.9, 0.3],
                            [0.01, 0.03, 0.1, 0.7]]),
        'family-out': np.array([[0.15],
                                [0.85]]),
        'hear-bark': np.array([[0.7, 0.01],
                                [0.3, 0.99]]),
        'light-on': np.array([[0.6, 0.05],
                            [0.4, 0.95]])}
         """
        variable_cpds = {}
        for block in self.probability_block:
            name = self.probability_expr.searchString(block)[0][0]
            cpds = self.cpd_expr.searchString(block)
            arr = [float(j) for i in cpds for j in i]
            arr = numpy.array(arr)
            arr = arr.reshape((len(self.variable_states[name]),
                              arr.size//len(self.variable_states[name])))
            variable_cpds[name] = arr

        return variable_cpds

    def get_edges(self):

        """
        Returns the edges of the network

        Example
        --------
        >>> from pgmpy.readwrite import BIFReader
        >>> reader = BIFReader("bif_test.bif")
        >>> reader.get_edges()
        [['family-out', 'light-on'],
         ['family-out', 'dog-out'],
         ['bowel-problem', 'dog-out'],
         ['dog-out', 'hear-bark']]
        """
        edges = [[value, key] for key in self.variable_parents.keys()
                 for value in self.variable_parents[key]]
        return edges

    def get_model(self):

        """
        Returns the fitted bayesian model

        Example
        ----------
        >>> from pgmpy.readwrite import BIFReader
        >>> reader = BIFReader("bif_test.bif")
        >>> reader.get_model()
        <pgmpy.models.BayesianModel.BayesianModel object at 0x7f20af154320>
        """
        try:
            model = BayesianModel(self.variable_edges)
            model.name = self.network_name
            model.add_nodes_from(self.variable_names)

            tabular_cpds = []
            for var in sorted(self.variable_cpds.keys()):
                values = self.variable_cpds[var]
                cpd = TabularCPD(var, len(self.variable_states[var]), values,
                                 evidence=self.variable_parents[var],
                                 evidence_card=[len(self.variable_states[evidence_var])
                                                for evidence_var in self.variable_parents[var]])
                tabular_cpds.append(cpd)

            model.add_cpds(*tabular_cpds)
            for node, properties in self.variable_properties.items():
                for prop in properties:
                    prop_name, prop_value = map(lambda t: t.strip(), prop.split('='))
                    model.node[node][prop_name] = prop_value

            return model

        except AttributeError:
            raise AttributeError('First get states of variables, edges, parents and network name')

class BIFWriter(object):

    """
    Base class for writing BIF network file format
    """

    def __init__(self, model):

        """
        Initialise a BIFWriter Object

        Parameters
        ----------
        model: BayesianModel Instance

        Examples
        ---------
        >>> from pgmpy.readwrite import BIFWriter
        >>> writer = BIFWriter(model)
        >>> writer
        <writer_BIF.BIFWriter at 0x7f05e5ea27b8>
        """
        if not isinstance(model, BayesianModel):
            raise TypeError("model must be an instance of BayesianModel")
        self.model = model
        if not self.model.name:
            self.network_name = 'unknown'
        else:
            self.network_name = self.model.name
        self.variable_states = self.get_states()
        self.property_tag = self.get_properties()
        self.variable_parents = self.get_parents()
        self.tables = self.get_cpds()

    def BIF_templates(self):

        """
        Create template for writing in BIF format
        """
        network_template = Template('network $name {\n}\n')
        # property tag may or may not be present in model,and since no of properties
        # can be more than one , will replace them accoriding to format otherwise null
        variable_template = Template("""variable $name {
    type discrete [ $no_of_states ] { $states };
$properties}\n""")
        property_template = Template('    property $prop ;\n')
        # $variable_ here is name of variable, used underscore for clarity
        probability_template = Template("""probability ( $variable_$seprator_$parents ) {
    table $values ;
}\n""")
        return network_template, variable_template, property_template, probability_template

    def __str__(self):
        """
        Returns the BIF format as string
        """
        network_template, variable_template, property_template, probability_template = self.BIF_templates()
        network = ''
        network += network_template.substitute(name=self.network_name)
        variables = self.model.nodes()

        for var in sorted(variables):
            no_of_states = str(len(self.variable_states[var]))
            states = ', '.join(self.variable_states[var])
            if not self.property_tag[var]:
                properties = ''
            else:
                properties = ''
                for prop_val in self.property_tag[var]:
                    properties += property_template.substitute(prop=prop_val)
            network += variable_template.substitute(name=var, no_of_states=no_of_states,
                                                    states=states, properties=properties)

        for var in sorted(variables):
            if not self.variable_parents[var]:
                parents = ''
                seprator = ''
            else:
                parents = ', '.join(self.variable_parents[var])
                seprator = ' | '
            cpd = ', '.join(map(str, self.tables[var]))
            network += probability_template.substitute(variable_=var, seprator_=seprator,
                                                       parents=parents, values=cpd)

        return network

    def get_variables(self):

        """
        Add variables to BIF

        Returns
        -------
        list: a list containing names of variable

        Example
        -------
        >>> from pgmpy.readwrite import BIFReader, BIFWriter
        >>> model = BIFReader('dog-problem.bif').get_model()
        >>> writer = BIFWriter(model)
        >>> writer.get_variables()
        ['bowel-problem', 'family-out', 'hear-bark', 'light-on', 'dog-out']
        """
        variables = self.model.nodes()
        return variables

    def get_states(self):

        """
        Add states to variable of BIF

        Returns
        -------
        dict: dict of type {variable: a list of states}

        Example
        -------
        >>> from pgmpy.readwrite import BIFReader, BIFWriter
        >>> model = BIFReader('dog-problem.bif').get_model()
        >>> writer = BIFWriter(model)
        >>> writer.get_states()
        {'bowel-problem': ['bowel-problem_0', 'bowel-problem_1'],
         'dog-out': ['dog-out_0', 'dog-out_1'],
         'family-out': ['family-out_0', 'family-out_1'],
         'hear-bark': ['hear-bark_0', 'hear-bark_1'],
         'light-on': ['light-on_0', 'light-on_1']}
        """
        variable_states = {}
        cpds = self.model.get_cpds()
        for cpd in cpds:
            variable = cpd.variable
            variable_states[variable] = []
            for state in range(cpd.get_cardinality([variable])[variable]):
                variable_states[variable].append(str(variable)+'_'+str(state))
        return variable_states

    def get_properties(self):

        """
        Add property to variables in BIF

        Returns
        -------
        dict: dict of type {variable: list of properties }

        Example
        -------
        >>> from pgmpy.readwrite import BIFReader, BIFWriter
        >>> model = BIFReader('dog-problem.bif').get_model()
        >>> writer = BIFWriter(model)
        >>> writer.get_properties()
        {'bowel-problem': ['position = (335, 99)'],
         'dog-out': ['position = (300, 195)'],
         'family-out': ['position = (257, 99)'],
         'hear-bark': ['position = (296, 268)'],
         'light-on': ['position = (218, 195)']}
        """
        variables = self.model.nodes()
        property_tag = {}
        for variable in sorted(variables):
            properties = self.model.node[variable]
            property_tag[variable] = []
            for prop, val in properties.items():
                property_tag[variable].append(str(prop) + " = " + str(val))
        return property_tag

    def get_parents(self):

        """
        Add the parents to BIF

        Returns
        -------
        dict: dict of type {variable: a list of parents}

        Example
        -------
        >>> from pgmpy.readwrite import BIFReader, BIFWriter
        >>> model = BIFReader('dog-problem.bif').get_model()
        >>> writer = BIFWriter(model)
        >>> writer.get_parents()
        {'bowel-problem': [],
         'dog-out': ['bowel-problem', 'family-out'],
         'family-out': [],
         'hear-bark': ['dog-out'],
         'light-on': ['family-out']}
        """
        cpds = self.model.get_cpds()
        cpds.sort(key=lambda x: x.variable)
        variable_parents = {}
        for cpd in cpds:
            variable_parents[cpd.variable] = []
            for parent in sorted([] if cpd.evidence is None else cpd.evidence):
                variable_parents[cpd.variable].append(parent)
        return variable_parents

    def get_cpds(self):

        """
        Adds tables to BIF

        Returns
        -------
        dict: dict of type {variable: array}

        Example
        -------
        >>> from pgmpy.readwrite import BIFReader, BIFWriter
        >>> model = BIFReader('dog-problem.bif').get_model()
        >>> writer = BIFWriter(model)
        >>> writer.get_cpds()
        {'bowel-problem': array([ 0.01,  0.99]),
         'dog-out': array([ 0.99,  0.97,  0.9 ,  0.3 ,  0.01,  0.03,  0.1 ,  0.7 ]),
         'family-out': array([ 0.15,  0.85]),
         'hear-bark': array([ 0.7 ,  0.01,  0.3 ,  0.99]),
         'light-on': array([ 0.6 ,  0.05,  0.4 ,  0.95])}
        """
        cpds = self.model.get_cpds()
        tables = {}
        for cpd in cpds:
            tables[cpd.variable] = cpd.values.ravel()
        return tables

    def write_bif(self, filename):

        """
        Writes the BIF data into a file

        Parameters
        ----------
        filename : Name of the file

        Example
        -------
        >>> from pgmpy.readwrite import BIFReader, BIFWriter
        >>> model = BIFReader('dog-problem.bif').get_model()
        >>> writer = BIFWriter(model)
        >>> writer.write_bif(filname='test_file.bif')
        """
        writer = self.__str__()
        with open(filename, 'w') as fout:
            fout.write(writer)
<|endoftext|>


<commit_before># coding=utf-8
from __future__ import absolute_import, division, print_function, \
  unicode_literals

from unittest import TestCase

import filters as f
from filters.test import BaseFilterTestCase
from iota import Address, ProposedTransaction
from iota.adapter import MockAdapter
from iota.crypto.types import Digest
from iota.multisig import MultisigIota
from iota.multisig.commands import PrepareMultisigTransferCommand
from iota.multisig.types import MultisigAddress

class PrepareMultisigTransferRequestFilterTestCase(BaseFilterTestCase):
  filter_type = PrepareMultisigTransferCommand(MockAdapter()).get_request_filter
  maxDiff = None
  skip_value_check = True

  # noinspection SpellCheckingInspection
  def setUp(self):
    super(PrepareMultisigTransferRequestFilterTestCase, self).setUp()

    # Define some tryte sequences that we can reuse between tests.
    self.digest_1 =\
      Digest(
        trytes =
          b'FWNEPVJNGUKTSHSBDO9AORBCVWWLVXC9KAMKYYNKPYNJDKSAUURI9ELKOEEYPKVTYP'
          b'CKOCJQESYFEMINIFKX9PDDGRBEEHYYXCJW9LHGWFZGHKCPVDBGMGQKIPCNKNITGMZT'
          b'DIWVUB9PCHCOPHMIWKSUKRHZOJPMAY',

        key_index = 0,
      )

    self.digest_2 =\
      Digest(
        trytes =
          b'PAIRLDJQY9XAUSKIGCTHRJHZVARBEY9NNHYJ9UI9HWWZXFSDWEZEGDCWNVVYSYDV9O'
          b'HTR9NGGZURISWTNECFTCMEWQQFJ9VKLFPDTYJYXC99OLGRH9OSFJLMEOGHFDHZYEAF'
          b'IMIZTJRBQUVCR9U9ZWTMUXTUEOUBLC',

        key_index = 0,
      )

    self.trytes_1 = (
      b'TESTVALUE9DONTUSEINPRODUCTION99999IIPEM9'
      b'LA9FLHEGHDACSA9DOBQHQCX9BBHCFDIIMACARHA9B'
    )

    self.trytes_2 = (
      b'TESTVALUE9DONTUSEINPRODUCTION99999BGUDVE'
      b'DGH9WFQDEDVETCOGEGCDI9RFHGFGXBI99EJICHNEM'
    )

    self.trytes_3 = (
      b'TESTVALUE9DONTUSEINPRODUCTION99999XBGEUC'
      b'LF9EIFXHM9KHQANBLBHFVGTEGBWHNAKFDGZHYGCHI'
    )

  def test_pass_happy_path(self):
    """
    Request is valid.
    """
    request = {
      'changeAddress':
        Address(self.trytes_1),

      'multisigInput':
        MultisigAddress(
          digests = [self.digest_1, self.digest_2],
          trytes  = self.trytes_2,
        ),

      'transfers':
        [
          ProposedTransaction(
            address = Address(self.trytes_3),
            value   = 42,
          ),
        ],
    }

    filter_ = self._filter(request)

    self.assertFilterPasses(filter_)
    self.assertDictEqual(filter_.cleaned_data, request)

  def test_pass_compatible_types(self):
    """
    Request contains values that can be converted to the expected
    types.
    """
    txn =\
      ProposedTransaction(
        address = Address(self.trytes_3),
        value   = 42,
      ),

    filter_ =\
      self._filter({
        # ``changeAddress`` can be any value that resolves to an
        # :py:class:`Address`.
        'changeAddress': self.trytes_1,

        # It is recommended that you use a MultisigAddress for
        # ``multisigInput``, but it is not required.
        'multisigInput': self.trytes_2,

        # ``transfers`` must contain an array of
        # :py:class:`ProposedTransaction` objects, however.
        'transfers': [txn],
      })

    self.assertFilterPasses(filter_)

    self.assertDictEqual(
      filter_.cleaned_data,

      {
        'changeAddress': Address(self.trytes_1),

        # We can't reconstruct the digests for the multisig address, so
        # we'll just have to trust that the user knows what they are
        # doing.
        'multisigInput': Address(self.trytes_2),

        'transfers': [txn],
      },
    )

  def test_pass_optional_parameters_excluded(self):
    """
    Request omits optional parameters.
    """
    txn =\
      ProposedTransaction(
        address = Address(self.trytes_3),
        value   = 42,
      ),

    filter_ =\
      self._filter({
        # ``changeAddress`` is optional.
        # Technically, it's required if there are unspent inputs, but
        # the filter has no way to know whether this is the case.
        # 'changeAddress': self.trytes_1,

        # These parameters are required.
        'multisigInput':
          MultisigAddress(
            digests = [self.digest_1, self.digest_2],
            trytes  = self.trytes_2,
          ),

        'transfers': [txn],
      })

    self.assertFilterPasses(filter_)

    self.assertDictEqual(
      filter_.cleaned_data,

      {
        'changeAddress': None,

        'multisigInput':
          MultisigAddress(
            digests = [self.digest_1, self.digest_2],
            trytes  = self.trytes_2,
          ),

        'transfers': [txn],
      },
    )

  def test_fail_unexpected_parameters(self):
    """
    Request contains unexpected parameters.
    """
    self.assertFilterErrors(
      {
        'changeAddress':
          Address(self.trytes_1),

        'multisigInput':
          MultisigAddress(
            digests = [self.digest_1, self.digest_2],
            trytes  = self.trytes_2,
          ),

        'transfers':
          [
            ProposedTransaction(
              address = Address(self.trytes_3),
              value   = 42,
            ),
          ],

        # Oh come on!
        'foo': 'bar',
      },

      {
        'foo': [f.FilterMapper.CODE_EXTRA_KEY],
      },
    )

  def test_fail_transfers_null(self):
    """
    ``transfers`` is null.
    """
    self.assertFilterErrors(
      {
        'changeAddress':
          Address(self.trytes_1),

        'multisigInput':
          MultisigAddress(
            digests = [self.digest_1, self.digest_2],
            trytes  = self.trytes_2,
          ),

        # On second thought, I changed my mind.
        'transfers': None,
      },

      {
        'transfers': [f.Required.CODE_EMPTY],
      },
    )

  def test_fail_transfers_wrong_type(self):
    """
    ``transfers`` is not an array.
    """
    self.assertFilterErrors(
      {
        'changeAddress':
          Address(self.trytes_1),

        'multisigInput':
          MultisigAddress(
            digests = [self.digest_1, self.digest_2],
            trytes  = self.trytes_2,
          ),

        # ``transfers`` must be an array, even if there's only one
        # transaction.
        'transfers':
          ProposedTransaction(
            address = Address(self.trytes_3),
            value   = 42,
          ),
      },

      {
        'transfers': [f.Array.CODE_WRONG_TYPE],
      },
    )

  def test_fail_transfers_empty(self):
    """
    ``transfers`` is an array, but it's empty.
    """
    # :todo: Implement test.
    self.skipTest('Not implemented yet.')

  def test_fail_transfers_contents_invalid(self):
    """
    ``transfers`` is an array, but it contains invalid values.
    """
    # :todo: Implement test.
    self.skipTest('Not implemented yet.')

  def test_fail_multisigInput_null(self):
    """
    ``multisigInput`` is null.
    """
    # :todo: Implement test.
    self.skipTest('Not implemented yet.')

  def test_fail_multisigInput_multiple(self):
    """
    ``multisigInput`` is an array.

    This is not valid; a bundle may only contain a single multisig
    input.
    """
    # :todo: Implement test.
    self.skipTest('Not implemented yet.')

  def test_fail_multisigInput_wrong_type(self):
    """
    ``multisigInput`` is not a MultisigAddress.
    """
    # :todo: Implement test.
    self.skipTest('Not implemented yet.')

  def test_pass_changeAddress_multisig_address(self):
    """
    ``changeAddress`` is allowed to be a MultisigAddress.
    """
    # :todo: Implement test.
    self.skipTest('Not implemented yet.')

  def test_fail_changeAddress_wrong_type(self):
    """
    ``changeAddress`` is not an Address.
    """
    # :todo: Implement test.
    self.skipTest('Not implemented yet.')

class PrepareMultisigTransferCommandTestCase(TestCase):
  def setUp(self):
    super(PrepareMultisigTransferCommandTestCase, self).setUp()

    self.adapter = MockAdapter()
    self.command = PrepareMultisigTransferCommand(self.adapter)

  def test_wireup(self):
    """
    Verifies the command is wired up correctly.
    """
    self.assertIsInstance(
      MultisigIota(self.adapter).prepareMultisigTransfer,
      PrepareMultisigTransferCommand,
    )

  def test_happy_path(self):
    """
    Preparing a bundle with a multisig input.
    """
    # :todo: Implement test.
    self.skipTest('Not implemented yet.')

  def test_unspent_inputs_with_change_address(self):
    """
    The bundle has unspent inputs, so it uses the provided change
    address.
    """
    # :todo: Implement test.
    self.skipTest('Not implemented yet.')

  def test_error_zero_iotas_transferred(self):
    """
    The bundle doesn't spend any IOTAs.

    This is considered an error case because
    :py:meth:`MultisigIota.prepare_multisig_transfer` is specialized
    for generating bundles that require multisig inputs.  Any bundle
    that doesn't require multisig functionality should be generated
    using :py:meth:`iota.api.Iota.prepare_transfer` instead.
    """
    # :todo: Implement test.
    self.skipTest('Not implemented yet.')

  def test_error_insufficient_inputs(self):
    """
    The multisig input does not contain sufficient IOTAs to cover the
    spends.
    """
    # :todo: Implement test.
    self.skipTest('Not implemented yet.')

  def test_error_unspent_inputs_no_change_address(self):
    """
    The bundle has unspent inputs, but no change address was specified.

    Unlike :py:meth:`iota.api.Iota.prepare_transfer` where all of the
    inputs are owned by the same seed, creating a multisig transfer
    usually involves multiple people.

    It would be unfair to the participants of the transaction if we
    were to automatically generate a change address using the seed of
    whoever happened to invoke the
    :py:meth:`MultisigIota.prepare_multisig_transfer` method!
    """
    # :todo: Implement test.
    self.skipTest('Not implemented yet.')
<commit_msg>[#10] `transfers` must not be empty.<commit_after># coding=utf-8
from __future__ import absolute_import, division, print_function, \
  unicode_literals

from unittest import TestCase

import filters as f
from filters.test import BaseFilterTestCase
from iota import Address, ProposedTransaction
from iota.adapter import MockAdapter
from iota.crypto.types import Digest
from iota.multisig import MultisigIota
from iota.multisig.commands import PrepareMultisigTransferCommand
from iota.multisig.types import MultisigAddress

class PrepareMultisigTransferRequestFilterTestCase(BaseFilterTestCase):
  filter_type = PrepareMultisigTransferCommand(MockAdapter()).get_request_filter
  maxDiff = None
  skip_value_check = True

  # noinspection SpellCheckingInspection
  def setUp(self):
    super(PrepareMultisigTransferRequestFilterTestCase, self).setUp()

    # Define some tryte sequences that we can reuse between tests.
    self.digest_1 =\
      Digest(
        trytes =
          b'FWNEPVJNGUKTSHSBDO9AORBCVWWLVXC9KAMKYYNKPYNJDKSAUURI9ELKOEEYPKVTYP'
          b'CKOCJQESYFEMINIFKX9PDDGRBEEHYYXCJW9LHGWFZGHKCPVDBGMGQKIPCNKNITGMZT'
          b'DIWVUB9PCHCOPHMIWKSUKRHZOJPMAY',

        key_index = 0,
      )

    self.digest_2 =\
      Digest(
        trytes =
          b'PAIRLDJQY9XAUSKIGCTHRJHZVARBEY9NNHYJ9UI9HWWZXFSDWEZEGDCWNVVYSYDV9O'
          b'HTR9NGGZURISWTNECFTCMEWQQFJ9VKLFPDTYJYXC99OLGRH9OSFJLMEOGHFDHZYEAF'
          b'IMIZTJRBQUVCR9U9ZWTMUXTUEOUBLC',

        key_index = 0,
      )

    self.trytes_1 = (
      b'TESTVALUE9DONTUSEINPRODUCTION99999IIPEM9'
      b'LA9FLHEGHDACSA9DOBQHQCX9BBHCFDIIMACARHA9B'
    )

    self.trytes_2 = (
      b'TESTVALUE9DONTUSEINPRODUCTION99999BGUDVE'
      b'DGH9WFQDEDVETCOGEGCDI9RFHGFGXBI99EJICHNEM'
    )

    self.trytes_3 = (
      b'TESTVALUE9DONTUSEINPRODUCTION99999XBGEUC'
      b'LF9EIFXHM9KHQANBLBHFVGTEGBWHNAKFDGZHYGCHI'
    )

  def test_pass_happy_path(self):
    """
    Request is valid.
    """
    request = {
      'changeAddress':
        Address(self.trytes_1),

      'multisigInput':
        MultisigAddress(
          digests = [self.digest_1, self.digest_2],
          trytes  = self.trytes_2,
        ),

      'transfers':
        [
          ProposedTransaction(
            address = Address(self.trytes_3),
            value   = 42,
          ),
        ],
    }

    filter_ = self._filter(request)

    self.assertFilterPasses(filter_)
    self.assertDictEqual(filter_.cleaned_data, request)

  def test_pass_compatible_types(self):
    """
    Request contains values that can be converted to the expected
    types.
    """
    txn =\
      ProposedTransaction(
        address = Address(self.trytes_3),
        value   = 42,
      ),

    filter_ =\
      self._filter({
        # ``changeAddress`` can be any value that resolves to an
        # :py:class:`Address`.
        'changeAddress': self.trytes_1,

        # It is recommended that you use a MultisigAddress for
        # ``multisigInput``, but it is not required.
        'multisigInput': self.trytes_2,

        # ``transfers`` must contain an array of
        # :py:class:`ProposedTransaction` objects, however.
        'transfers': [txn],
      })

    self.assertFilterPasses(filter_)

    self.assertDictEqual(
      filter_.cleaned_data,

      {
        'changeAddress': Address(self.trytes_1),

        # We can't reconstruct the digests for the multisig address, so
        # we'll just have to trust that the user knows what they are
        # doing.
        'multisigInput': Address(self.trytes_2),

        'transfers': [txn],
      },
    )

  def test_pass_optional_parameters_excluded(self):
    """
    Request omits optional parameters.
    """
    txn =\
      ProposedTransaction(
        address = Address(self.trytes_3),
        value   = 42,
      ),

    filter_ =\
      self._filter({
        # ``changeAddress`` is optional.
        # Technically, it's required if there are unspent inputs, but
        # the filter has no way to know whether this is the case.
        # 'changeAddress': self.trytes_1,

        # These parameters are required.
        'multisigInput':
          MultisigAddress(
            digests = [self.digest_1, self.digest_2],
            trytes  = self.trytes_2,
          ),

        'transfers': [txn],
      })

    self.assertFilterPasses(filter_)

    self.assertDictEqual(
      filter_.cleaned_data,

      {
        'changeAddress': None,

        'multisigInput':
          MultisigAddress(
            digests = [self.digest_1, self.digest_2],
            trytes  = self.trytes_2,
          ),

        'transfers': [txn],
      },
    )

  def test_fail_unexpected_parameters(self):
    """
    Request contains unexpected parameters.
    """
    self.assertFilterErrors(
      {
        'changeAddress':
          Address(self.trytes_1),

        'multisigInput':
          MultisigAddress(
            digests = [self.digest_1, self.digest_2],
            trytes  = self.trytes_2,
          ),

        'transfers':
          [
            ProposedTransaction(
              address = Address(self.trytes_3),
              value   = 42,
            ),
          ],

        # Oh come on!
        'foo': 'bar',
      },

      {
        'foo': [f.FilterMapper.CODE_EXTRA_KEY],
      },
    )

  def test_fail_transfers_null(self):
    """
    ``transfers`` is null.
    """
    self.assertFilterErrors(
      {
        'changeAddress':
          Address(self.trytes_1),

        'multisigInput':
          MultisigAddress(
            digests = [self.digest_1, self.digest_2],
            trytes  = self.trytes_2,
          ),

        # On second thought, I changed my mind.
        'transfers': None,
      },

      {
        'transfers': [f.Required.CODE_EMPTY],
      },
    )

  def test_fail_transfers_wrong_type(self):
    """
    ``transfers`` is not an array.
    """
    self.assertFilterErrors(
      {
        'changeAddress':
          Address(self.trytes_1),

        'multisigInput':
          MultisigAddress(
            digests = [self.digest_1, self.digest_2],
            trytes  = self.trytes_2,
          ),

        # ``transfers`` must be an array, even if there's only one
        # transaction.
        'transfers':
          ProposedTransaction(
            address = Address(self.trytes_3),
            value   = 42,
          ),
      },

      {
        'transfers': [f.Array.CODE_WRONG_TYPE],
      },
    )

  def test_fail_transfers_empty(self):
    """
    ``transfers`` is an array, but it's empty.
    """
    self.assertFilterErrors(
      {
        'changeAddress':
          Address(self.trytes_1),

        'multisigInput':
          MultisigAddress(
            digests = [self.digest_1, self.digest_2],
            trytes  = self.trytes_2,
          ),

        # This is a variation on the ``null`` test.
        'transfers': [],
      },

      {
        'transfers': [f.Required.CODE_EMPTY],
      },
    )

  def test_fail_transfers_contents_invalid(self):
    """
    ``transfers`` is an array, but it contains invalid values.
    """
    # :todo: Implement test.
    self.skipTest('Not implemented yet.')

  def test_fail_multisigInput_null(self):
    """
    ``multisigInput`` is null.
    """
    # :todo: Implement test.
    self.skipTest('Not implemented yet.')

  def test_fail_multisigInput_multiple(self):
    """
    ``multisigInput`` is an array.

    This is not valid; a bundle may only contain a single multisig
    input.
    """
    # :todo: Implement test.
    self.skipTest('Not implemented yet.')

  def test_fail_multisigInput_wrong_type(self):
    """
    ``multisigInput`` is not a MultisigAddress.
    """
    # :todo: Implement test.
    self.skipTest('Not implemented yet.')

  def test_pass_changeAddress_multisig_address(self):
    """
    ``changeAddress`` is allowed to be a MultisigAddress.
    """
    # :todo: Implement test.
    self.skipTest('Not implemented yet.')

  def test_fail_changeAddress_wrong_type(self):
    """
    ``changeAddress`` is not an Address.
    """
    # :todo: Implement test.
    self.skipTest('Not implemented yet.')

class PrepareMultisigTransferCommandTestCase(TestCase):
  def setUp(self):
    super(PrepareMultisigTransferCommandTestCase, self).setUp()

    self.adapter = MockAdapter()
    self.command = PrepareMultisigTransferCommand(self.adapter)

  def test_wireup(self):
    """
    Verifies the command is wired up correctly.
    """
    self.assertIsInstance(
      MultisigIota(self.adapter).prepareMultisigTransfer,
      PrepareMultisigTransferCommand,
    )

  def test_happy_path(self):
    """
    Preparing a bundle with a multisig input.
    """
    # :todo: Implement test.
    self.skipTest('Not implemented yet.')

  def test_unspent_inputs_with_change_address(self):
    """
    The bundle has unspent inputs, so it uses the provided change
    address.
    """
    # :todo: Implement test.
    self.skipTest('Not implemented yet.')

  def test_error_zero_iotas_transferred(self):
    """
    The bundle doesn't spend any IOTAs.

    This is considered an error case because
    :py:meth:`MultisigIota.prepare_multisig_transfer` is specialized
    for generating bundles that require multisig inputs.  Any bundle
    that doesn't require multisig functionality should be generated
    using :py:meth:`iota.api.Iota.prepare_transfer` instead.
    """
    # :todo: Implement test.
    self.skipTest('Not implemented yet.')

  def test_error_insufficient_inputs(self):
    """
    The multisig input does not contain sufficient IOTAs to cover the
    spends.
    """
    # :todo: Implement test.
    self.skipTest('Not implemented yet.')

  def test_error_unspent_inputs_no_change_address(self):
    """
    The bundle has unspent inputs, but no change address was specified.

    Unlike :py:meth:`iota.api.Iota.prepare_transfer` where all of the
    inputs are owned by the same seed, creating a multisig transfer
    usually involves multiple people.

    It would be unfair to the participants of the transaction if we
    were to automatically generate a change address using the seed of
    whoever happened to invoke the
    :py:meth:`MultisigIota.prepare_multisig_transfer` method!
    """
    # :todo: Implement test.
    self.skipTest('Not implemented yet.')
<|endoftext|>