Source code for pyGM.factor

"""
factor.py

Defines variables, variable sets, and dense factors over discrete variables (tables) for graphical models

Version 0.0.1 (2015-09-28)
(c) 2015 Alexander Ihler under the FreeBSD license; see license.txt for details.
"""

import numpy as np
#import autograd.numpy as np
from sortedcontainers import SortedSet as sset

try:
  from pyGM.varset_c import Var,VarSet
except ImportError:
  #print "Compiled version not loaded; importing python version"
  from pyGM.varset_py import Var,VarSet    # sortedcontainers version
  #from .varset_py2 import Var,VarSet  # numpy array version



inf = float('inf')


orderMethod = 'F'   # TODO: currently stores in fortran order (as Matlab); should be trivially changable
#orderMethod = 'C'  #   Can we make this "seamless" to the user, and/or force them to do something consistent?

# Notes: column-major (order=F) puts last index sequentially ("big endian"): t[0 0 0], t[0 0 1], t[0 1 0] ...
#        row major (order=C) puts 1st index sequentially ("little endian"): t[0 0 0], t[1 0 0], t[0 1 0], ...

class Factor(object):
  """A basic factor<float> class 

  Factors are the basic building block of our graphical model representations.  In general, a factor
  consists of a set of variables (its "scope"), and a table of values indicating f(x) for each
  joint configuration x (a tuple of values) of its variables.

  Variables are stored in sorted order; most of the time, factors are constructed by reading from files,
  but if built by hand it is safest to use indexing to set the values, e.g.,

  >>> f = Factor( [X,Y,Z], 0.0 )   # builds a factor over X,Y,Z filled with zeros
  >>> f[0,1,0] = 1.5               # set f(X=0,Y=1,Z=0) to 1.5

  Useful attributes are f.vars (the scope) and f.table (the table, a numpy array).

  Factors are imbued with many basic operations for manipulation:
    Operators:  *, +, /, -, **, exp, log, abs, etc.
    In-place versions:  *=, +=, /=, -=, **=, expIP, logIP, etc.
    Elimination: max, min, sum, lse (log-sum-exp), etc.
    Conditioning: return a factor defined by a sub-table, assigning some variables to values
    Other: argmax, argmin, sample, etc., return configurations of X (tuples)

  """

  #v = VarSet([])       # internal storage for variable set (VarSet)
  #t = np.ndarray([])   # internal storage for table (numpy array)

[docs] def __init__(self,vars=VarSet(),vals=1.0): """Constructor for Factor class >>> f = Factor( [X,Y,Z],[vals] ) # creates factor over [X,Y,Z] with table [vals] [vals] should be a correctly sized numpy array, or something that can be cast to the same. """ # TODO: add user-specified order method for values (order=) # TODO: accept out-of-order vars list (=> permute vals as req'd) try: self.v = VarSet(vars) # try building varset with args except TypeError: # if not iterable (e.g. single variable) self.v = VarSet() # try just adding it self.v.add(vars) #assert( self.v.nrStates() > 0) #if self.v.nrStatesDouble() > 1e8: raise ValueError("Too big!"); try: self.t = np.empty(self.v.dims(), float, orderMethod); self.t[:] = vals # try filling factor with "vals" except ValueError: # if it's an incompatible shape, self.t = np.reshape(np.array(vals, float), self.v.dims(), orderMethod) # try again using reshape
def __build(self,vs,ndarray): """Internal build function from numpy ndarray""" self.v = vs self.t = ndarray return self #TODO: def assign(self, F) : set self equal to rhs F, e.g., *this = F
[docs] def copy(self): """Copy constructor; make a copy of a factor""" return Factor().__build(self.v.copy(),self.t.copy('K')) # order=orderMethod?
[docs] def changeVars(self, vars, copy=True): """Copy a factor but change its arguments (scope). >>> f = Factor([X0,X1], table) >>> g = changeVars( f, [X7,X5]) # now, g(X5=b,X7=a) = f(X0=a,X1=b) """ v = VarSet(vars) newOrder = map(lambda x:vars.index(x), v) if copy: ret = Factor(v, self.t.transpose(newOrder)) else: ret = Factor().__build(v, self.t.transpose(newOrder)) # try not to copy if possible return ret
[docs] def __repr__(self): """Detailed representation: scope (varset) + table memory location""" return 'Factor({:s},[0x{:x}])'.format(str(self.v),self.t.ctypes.data)
[docs] def __str__(self): """Basic string representation: scope (varset) only""" return 'Factor({:s})'.format(str(self.v))
@property def vars(self): """Variables (scope) of the factor; read-only""" return self.v @vars.setter def vars(self,value): raise AttributeError("Read-only attribute") @property def table(self): """Table (values, as numpy array) of the factor""" return self.t @table.setter def table(self,values): try: self.t[:] = values # try filling factor with "values" except ValueError: # if it's an incompatible shape, self.t = np.array(values,dtype=float).reshape(self.v.dims(),order=orderMethod) # try again using reshape @property def nvar(self): """Number of arguments (variables, scope size) for the factor""" return len(self.v) #@property
[docs] def dims(self): """Dimensions (table shape) of the tabular factor""" return self.t.shape
#@property # TODO: make property?
[docs] def numel(self): """Number of elements (size) of the tabular factor""" return self.t.size
################## METHODS ##########################################
[docs] def __getitem__(self,loc): """Accessor: F[x1,x2] = F[sub2ind(x1,x2)] = F(X1=x1,X2=x2)""" if self.t.ndim == 1 or isinstance(loc, (tuple, list)): return self.t[loc] else: try: return self.t[self.v.ind2sub(loc)] except ValueError: raise IndexError("Index {} invalid for table with size {}".format(loc,self.t.shape))
[docs] def __setitem__(self,loc,val): """Assign values of the factor: F[i,j,k] = F[idx] = val if idx=sub2ind(i,j,k)""" if self.t.ndim == 1 or isinstance(loc, (tuple, list)): self.t[loc] = val else: try: self.t[self.v.ind2sub(loc)] = val #self.t.flat[loc] = val # uses c-contiguous order... except ValueError: raise IndexError("Index {} invalid for table with size {}".format(loc,self.t.shape))
value = __getitem__ # def f.value(loc): Alternate name for __getitem__
[docs] def valueMap(self,x): """Accessor: F[x[i],x[j]] where i,j = F.vars, i.e, x is a map from variables to their state values""" if self.nvar == 0: return self.t[0] # if a scalar f'n, nothing to index return self.t[tuple(x[v] for v in self.v)] # otherwise, find entry of table
def setValueMap(self,x,val): """Set F[x[i],x[j]] = val, where i,j = F.vars, i.e, x is a map from variables to their state values""" self.t[tuple(x[v] for v in self.v) if len(self.v) else 0] = val # lookup location to set, or 0 if scalar f'n
[docs] def __float__(self): """Convert factor F to scalar float if possible; otherwise raises ValueError""" if (self.nvar == 0): return self.t[0] else: raise ValueError("Factor is not a scalar; scope {}".format(self.v))
# TODO missing comparator functions?
[docs] def isnan(self): """Check for NaN (not-a-number) entries in the factor's values; true if any NaN present""" return self.isAny( (lambda x: np.isnan(x)) )
[docs] def isfinite(self): """Check for infinite (-inf, inf) or NaN values in the factor; false if any present""" return not self.isAny( (lambda x: not np.isfinite(x)) )
[docs] def isAny(self,test): """Generic check for any entries satisfying lambda-expression "test" in the factor""" for x in np.nditer(self.t, op_flags=['readonly']): if test(x): return True return False
#### UNARY OPERATIONS ####
[docs] def __abs__(self): """Return the absolute value of F: G = F.abs() => G(x) = | F(x) | for all x""" return Factor().__build( self.v.copy() , np.fabs(self.t) )
abs = __abs__
[docs] def __neg__(self): """Return the negative of F: G = -F => G(x) = -F(x) for all x""" return Factor().__build( self.v.copy() , np.negative(self.t) )
[docs] def exp(self): """Return the exponential of F: G = F.exp() => G(x) = exp(F(x)) for all x""" return Factor().__build( self.v.copy() , np.exp(self.t) )
[docs] def __pow__(self,power): """Return F raised to a power: G = F.power(p) => G(x) = ( F(x) )^p for all x""" return Factor().__build( self.v.copy() , np.power(self.t,power) )
power = __pow__
[docs] def log(self): # just use base? """Return the natural log of F: G = F.log() => G(x) = log( F(x) ) for all x""" with np.errstate(divide='ignore'): return Factor().__build( self.v.copy() , np.log(self.t) )
[docs] def log2(self): """Return the log base 2 of F: G = F.log2() => G(x) = log2( F(x) ) for all x""" with np.errstate(divide='ignore'): return Factor().__build( self.v.copy() , np.log2(self.t) )
[docs] def log10(self): """Return the log base 10 of F: G = F.log10() => G(x) = log10( F(x) ) for all x""" with np.errstate(divide='ignore'): return Factor().__build( self.v.copy() , np.log10(self.t) )
#### IN-PLACE UNARY OPERATIONS #### # always return "self" for chaining: f.negIP().expIP() = exp(-f(x)) in-place
[docs] def absIP(self): """Take the absolute value of F: F.absIP() => F(x) <- |F(x)| (in-place)""" np.fabs(self.t, out=self.t) return self
[docs] def expIP(self): """Take the exponential of F: F.expIP() => F(x) <- exp(F(x)) (in-place)""" np.exp(self.t, out=self.t) return self
def powerIP(self,power): """Raise F to a power: F.powerIP(p) => F(x) <- ( F(x) )^p (in-place)""" np.power(self.t, power, out=self.t) return self __ipow__ = powerIP
[docs] def logIP(self): # just use base? """Take the natural log of F: F.logIP() => F(x) <- log( F(x) ) (in-place)""" with np.errstate(divide='ignore'): np.log(self.t, out=self.t) return self
[docs] def log2IP(self): """Take the log base 2 of F: F.log2IP() => F(x) <- log2( F(x) ) (in-place)""" with np.errstate(divide='ignore'): np.log2(self.t, out=self.t) return self
[docs] def log10IP(self): """Take the log base 10 of F: F.log10IP() => F(x) <- log10( F(x) ) (in-place)""" with np.errstate(divide='ignore'): np.log10(self.t, out=self.t) return self
def negIP(self): """Take the negation of F: F.negIP() => F(x) <- (-F(x)) (in-place)""" np.negative(self.t, out=self.t) return self #### BINARY OPERATIONS #### # TODO: add boundary cases: 0/0 = ? inf - inf = ?
[docs] def __add__(self,that): """Addition of factors, e.g., G(x_1,x_2) = F1(x_1) + F2(x_2)""" return self.__opExpand2(that,np.add)
def __radd__(self,that): """Right-addition, e.g. G(x) = 3.0 + F(x)""" return self.__opExpand2(that,np.add)
[docs] def __iadd__(self,that): """In-place addition, F1 += F2. Most efficient if F2.vars <= F1.vars""" return self.__opExpand2(that,np.add, out=self)
[docs] def __sub__(self,that): """Subtraction of factors, e.g., G(x_1,x_2) = F1(x_1) - F2(x_2)""" return self.__opExpand2(that,np.subtract)
def __rsub__(self,that): """Right-subtraction, e.g. G(x) = 3.0 - F(x)""" B = that if isinstance(that,Factor) else Factor([],that) return B.__opExpand2(self, np.subtract)
[docs] def __isub__(self,that): """In-place subtraction, F1 -= F2. Most efficient if F2.vars <= F1.vars""" return self.__opExpand2(that,np.subtract, out=self)
[docs] def __mul__(self,that): """Multiplication of factors, e.g., G(x_1,x_2) = F1(x_1) * F2(x_2)""" return self.__opExpand2(that, np.multiply)
def __rmul__(self,that): """Right-multiplication, e.g. G(x) = 3.0 * F(x)""" return self.__opExpand2(that, np.multiply)
[docs] def __imul__(self,that): """In-place multiplication, F1 *= F2. Most efficient if F2.vars <= F1.vars""" return self.__opExpand2(that,np.multiply, out=self)
[docs] def __div__(self,that): """Division of factors, e.g., G(x_1,x_2) = F1(x_1) / F2(x_2)""" with np.errstate(divide='ignore'): return self.__opExpand2(that, np.divide)
__truediv__ = __div__ def __rdiv__(self,that): """Right-divide, e.g. G(x) = 3.0 / F(x)""" B = that if isinstance(that,Factor) else Factor([],that) with np.errstate(divide='ignore'): return B.__opExpand2(self, np.divide) __rtruediv__ = __rdiv__
[docs] def __idiv__(self,that): """In-place divide, F1 /= F2. Most efficient if F2.vars <= F1.vars""" with np.errstate(divide='ignore'): return self.__opExpand2(that,np.divide, out=self)
__itruediv__ = __idiv__ #### ELIMINATION OPERATIONS ####
[docs] def sum(self, elim=None, out=None): """Eliminate via sum on F, e.g., f(X_2) = \sum_{x_1} F(x_1,X_2) = F.sum([X1])""" if (elim is None): elim = self.v return self.__opReduce2(self.v & elim,np.sum, out=out)
[docs] def marginal(self, target, out=None): """Compute the marginal of F, e.g., f(X_2) = \sum_{x_1} F(x_1,X_2) = F.marginal([X2])""" return self.__opReduce2(self.v - target,np.sum, out=out)
[docs] def sumPower(self, elim=None, power=1.0, out=None): """Eliminate via powered sum, e.g., f(X_2) = \\root^{1/p}{ sum_{x_1} F(x_1,X_2)^p } = F.sumPower([X1],p)""" if (elim is None): elim = self.v tmp = (self ** power).sum(elim) tmp **= (1.0/power) return tmp
[docs] def lse(self, elim=None, out=None): """Eliminate via log-sum-exp on F, e.g., f(X_2) = log \sum_{x_1} exp F(x_1,X_2) = F.lse([X1])""" if (elim is None): elim = self.v return self.__opReduce3(self.v & elim, np.logaddexp.reduce, out=out)
[docs] def lsePower(self, elim=None, power=1.0, out=None): """Eliminate via powered log-sum-exp, e.g., f(X_2) = 1/p log \sum_{x_1} exp F(x_1,X_2)*p = F.lsePower([X_1],p)""" if (elim is None): elim = self.v if power == inf: return self.max(elim) elif power == -inf: return self.min(elim) elif power == 1.0: return self.lse(elim) else: tmp = (self*power).lse(elim) tmp *= (1.0/power) return tmp
[docs] def max(self, elim=None, out=None): """Eliminate via max on F, e.g., f(X_2) = \max_{x_1} F(x_1,X_2) = F.max([X1])""" if (elim is None): elim = self.v return self.__opReduce2(self.v & elim,np.max, out=out)
[docs] def maxmarginal(self, target, out=None): """Compute the max-marginal of F, e.g., f(X_2) = \max_{x_1} F(x_1,X_2) = F.maxmarginal([X2])""" return self.__opReduce2(self.v - target,np.max, out=out)
[docs] def min(self, elim=None, out=None): """Eliminate via min on F, e.g., f(X_2) = \min_{x_1} F(x_1,X_2) = F.min([X1])""" if (elim is None): elim = self.v return self.__opReduce2(self.v & elim,np.min, out=out)
[docs] def minmarginal(self, target, out=None): """Compute the min-marginal of F, e.g., f(X_2) = \min_{x_1} F(x_1,X_2) = F.minmarginal([X2])""" return self.__opReduce2(self.v - target,np.min, out=out)
# use ufunc.reduceat? reduce etc seem not good? # frompyfunc to make ufunc from python function? # use "externalloop" flag? #return t.max(axis=None,out=None) # use axis to specific dimensions to eliminate; out for IP version #### TUPLE OPERATIONS ####
[docs] def argmax2(self, cvars=None, ctuple=None): """Find the argmax of the factor, with partial conditioning (as var list + value list) if desired. Returns a maximizing configuration of f(X|Xc=xc) as a tuple of states """ if (cvars is None): return self.v.ind2sub(self.t.argmax()) ax = tuple(ctuple[cvars.index(x)] if x in cvars else slice(None) for x in self.v) return self.v.ind2sub(self.t[ax].argmax())
[docs] def argmax(self, evidence={}): """Find the argmax of the factor, with partial conditioning (as dict evidence[v]) if desired Returns a maximizing configuration of f(X|Xc=xc) as a tuple of states """ if len(evidence)==0: return self.v.ind2sub(self.t.argmax()) ax = tuple(evidence[v] if v in evidence else slice(None) for v in self.v) return self.v.ind2sub( self.t[ax].argmax() )
[docs] def argmin2(self, cvars=None, ctuple=None): """Find the argmin of the factor, with partial conditioning if desired (list+list version)""" if (cvars is None): return self.v.ind2sub(self.t.argmin()) ax = tuple(ctuple[cvars.index(x)] if x in cvars else slice(None) for x in self.v) return self.v.ind2sub(self.t[ax].argmin())
[docs] def argmin(self, evidence={}): """Find the argmin of the factor, with partial conditioning if desired (dict version)""" if len(evidence)==0: return self.v.ind2sub(self.t.argmax()) ax = tuple(evidence[v] if v in evidence else slice(None) for v in self.v) return self.v.ind2sub( self.t[ax].argmax() )
[docs] def sample(self, Z=None): """Draw a random sample (as a tuple of states) from the factor; assumes positivity. If option Z=<float> set, the function will assume normalization factor Z """ Z = Z if Z is not None else self.sum() # normalize if desired / by default assert (Z > 0), 'Non-normalizable factor (perhaps log factor?)' # also check for positivity? pSoFar = 0.0 pDraw = Z * np.random.random_sample() it = np.nditer(self.t, op_flags=['readonly'], flags=['multi_index']) # for tuple return #it = np.nditer(self.t, op_flags=['readonly'], flags=[orderMethod+'_index']) # for index return while not it.finished: pSoFar += it[0] if ( pSoFar > pDraw ): return it.multi_index # multi_index for tuple return #return it.index # index for index return it.iternext() return self.v.ind2sub(self.numel()-1) # if numerical issue: return final state
[docs] def condition2(self, cvars=[],ctuple=[]): """Create a clamped (or "sliced") factor using partial conditioning (list+list version) >>> F.condition2([0,2],[a,b]) # returns f(X_1,X_3) = F(X_0=a, X_1, X_2=b, X_3) """ ax = tuple(ctuple[cvars.index(x)] if x in cvars else slice(None) for x in self.v) return Factor(self.v - cvars, self.t[ax]) # forces table copy in constructor
[docs] def condition(self, evidence): """Create a clamped (or "sliced") factor using partial conditioning (dict version) >>> F.condition({0:a,2:b}) # returns f(X_1,X_3) = F(X_0=a, X_1, X_2=b, X_3) """ ax = tuple( evidence[v] if v in evidence else slice(None) for v in self.v ) cvars = [ v for j,v in enumerate(self.v) if ax[j] != slice(None) ] return Factor(self.v - cvars, self.t[ax]) # forces table copy in constructor
slice2 = condition2 # alternate, libDAI-like names slice = condition # TODO: assign_slice( evidence, conditional_table ) # create ax = tuple( ... ); self.table(ax) = conditional_table # TODO: assign_slice2( vars, vals, conditional_table )
[docs] def entropy(self): """Compute the entropy of the factor (normalizes, assumes positive)""" Z = self.sum() if not (Z > 0): raise ValueError('Non-normalizable factor (perhaps log factor?)') tmp = np.ravel(self.t) H = -np.dot( tmp, np.log(tmp.clip(min=1e-300)) )/Z + np.log(Z) # entropy of tmp/Z return H
def norm(self, distance): """Compute any of several norm-like functions on F(x). 'distance' can be any of: 'L1' : L1 or manhattan distance, sum of absolute values 'L2' : L2 or Euclidean distance, sum of squares 'LInf' : L-Infinity distance, maximum value 'KL' : Shannon entropy (KL = Kullback Leibler) 'HPM' : Hilbert's projective metric """ distance = distance.lower() if distance == 'l1': return self.abs().sum() elif distance == 'l2': return (self*self).sum() elif distance == 'linf': return self.abs().max() elif distance == 'kl': return self.entropy() elif distance == 'hpm': F = self.log(); return F.max() - F.min(); else: raise ValueError("Unrecognized norm type {}; 'L1','L2','LInf','KL','HPM'".format(distance));
[docs] def distance(self, that, distance): """Compute any of several norm-like functions on F(x). 'distance' can be any of: 'L1' : L1 or manhattan distance, sum of absolute values 'L2' : L2 or Euclidean distance, sum of squares 'LInf' : L-Infinity distance, maximum value 'KL' : Shannon entropy (KL = Kullback Leibler) 'HPM' : Hilbert's projective metric """ distance = distance.lower() tmp = self.copy() if distance == 'l1': tmp -= that; tmp.absIP(); return tmp.sum() elif distance == 'l2': tmp -= that; tmp *= tmp; return tmp.sum() elif distance == 'linf': tmp -= that; tmp.absIP(); return tmp.max() elif distance == 'kl': Z=tmp.sum(); tmp/=that; tmp*=that.sum()/Z; tmp.logIP(); tmp*=self; return tmp.sum()/Z; elif distance == 'hpm': tmp /= that; tmp.logIP(); return tmp.max() - tmp.min(); else: raise ValueError("Unrecognized norm type {}; 'L1','L2','LInf','KL','HPM'".format(distance));
#useful things: # np.ndindex(shape) : iterate over tuples consistent with shape # for index, x in np.ndenumerate(a): iterate over tuples, values #def mean(factorList): # return #def geomean(factorList): # return ############################ INTERNAL ############################################## # slow version with arbitrary operator def __opUnaryIP(self,op): for x in np.nditer(self.t, op_flags=['readwrite']): x[...] = op(x) return self def __opUnary(self,op): return Factor( self.v.copy() , self.t.copy(order=orderMethod) ).__opUnaryIP(op) #def __opAccumulate(self,r,op): # for x in np.nditer(self.t, op_flags=['readonly']): # r = op(r,x) # return r # TODO: at least use numpy "broadcast" / "external_loop" etc ; maybe define ufuncs or compile them? # def __opExpand1(self,that,op, out=None): """Internal combination function; brute force application of arbitrary function "op"; slow """ A = self B = that if isinstance(that,Factor) else Factor([],that) vall = A.v | B.v axA = list(A.v.index(x) if x in A.v else -1 for x in vall) axB = list(B.v.index(x) if x in B.v else -1 for x in vall) if ( (not (out is None)) and (out.v == vall) ): f = out else: f = Factor(vall) # TODO: should also change "out" if specified! it = np.nditer([A.t, B.t, f.t], op_axes = [ axA, axB, None ], op_flags=[['readonly'], ['readonly'], ['writeonly']]) for (i,j,k) in it: op(i,j,out=k) return f def __opExpand2(self,that,op, out=None): """Internal combination function; assumes "op" is a numpy build-in (using a ufunc)""" if not isinstance(that,Factor): # if not a Factor, must be a scalar; use scalar version: if out is None: return Factor(self.v, op(self.t,that)) # with constructor else: op(self.t, that, out=out.t); return out # or direct write # Non-scalar 2nd argument version: A = self B = that if isinstance(that,Factor) else Factor([],that) vall = A.v | B.v dA,dB = vall.expand_dims(A.v, B.v); #dA = tuple(x.states if x in A.v else 1 for x in vall); #dB = tuple(x.states if x in B.v else 1 for x in vall); if ( (out is not None) and (out.v == vall) ): # if out can be written to directly, do so op( A.t.reshape(dA,order='A') , B.t.reshape(dB,order='A'), out=out.t ) # TODO: order=A necessary? else: t = op( A.t.reshape(dA,order='A') , B.t.reshape(dB,order='A') ) # TODO: order=A necessary? if (out is None): out = Factor() if (len(vall)==0): t = np.asarray([t],dtype=float) out.__build(vall,t) return out def __opReduce1(self,elim,op,init): # TODO: change to IP; caller initializes? """Internal reduce / eliminate function; brute force application of arbitrary f'n "op"; slow """ A = self.t f = Factor( self.v - elim , init) # TODO: fill with ??? (0.0 for sum, -inf for lse, etc) axA = list(range(len(self.v))) axC = list(map(lambda x:f.v.index(x) if x in f.v else -1 ,self.v)) C = f.t it = np.nditer([A, C], op_axes = [ axA, axC ], flags=['reduce_ok'], op_flags=[['readonly'], ['readwrite']]) for (i,j) in it: op(i,j,out=j) return f def __opReduce2(self, elim, op, out=None): # assumes elim <= self.v """Internal reduce / eliminate function; assumes "op" is a numpy build-in (using a ufunc)""" if ((elim is None) or (len(elim)==len(self.v))): return op(self.t) elif (out is None): # non-in-place version ax = tuple(self.v.index(x) for x in elim) out = Factor(self.v - elim, op(self.t, axis=ax)) else: # in-place version assert (out.v == (self.v-elim) ), "Cannot eliminate into an existing factor with incorrect scope" ax = tuple(self.v.index(x) for x in elim) op(self.t, axis=ax, out=out.t) return out def __opReduce3(self, elim, op, out=None): # assumes elim <= self.v """Internal reduce / eliminate function; assumes "op" is a numpy build-in (using a ufunc) works with numpy reduce ops that require single axes at a time """ if ((elim is None) or (len(elim)==len(self.v))): return op(np.ravel(self.t)) else: if (out is None): out = Factor(self.v - elim) else: assert (out.v == (self.v-elim) ), "Cannot eliminate into an existing factor with incorrect scope" ax = tuple(self.v.index(x) for x in elim) src = self.t while len(ax) > 1: src = op(src,axis=ax[-1]) ax = ax[:-1] op(src, axis=ax, out=out.t) return out """ NumPy reduce example: >>> a = np.arange(24).reshape(2,3,4) >>> b = np.array(0) >>> for x, y in np.nditer([a, b], flags=['reduce_ok', 'external_loop'], ... op_flags=[['readonly'], ['readwrite']]): ... y[...] += x ... Notes xhat[ [v.label for v in f.var] ] = list(f.argmax()) """