#!/usr/bin/env python
from numpy import reshape, transpose, exp
from numpy.linalg import solve as solve_linear_equations
from random import shuffle
from sys import exit, stdout

def normalize(p):
	sum = 0.0 
	for pi in p:
		sum += p[pi]
	for pi in p:
		p[pi]/=sum

def	ht(prob_bin, cls_cnt, max_iter=10000):
		all_cls = cls_cnt.keys()
		p = {}
		#start with some guess to p_i and corresponding u_ij
		for i in all_cls:
			p[i] = 1.0/len(all_cls)
		for iter in range(max_iter):
			stoperror = 0.0
			for i in all_cls:
				num, den = 0.0, 0.0
				for j in all_cls:
					if j!=i:
						num+=prob_bin[i,j]
						den+=p[i]/(p[i]+p[j])
				alpha=num/den
				diff = p[i]*(alpha-1)
				p[i] += diff
				stoperror+=abs(alpha-1.0) #stopping criteria
				for j in all_cls:
					p[j] /= (1+diff)
			if stoperror < 1e-3:
				break

		if iter == max_iter-1:
			print "****Too many Iterations:abort"
			print prob_bin
			print iter, stoperror, p
		return p;

def	minpair(prob_bin, cls_cnt):
		all_cls = cls_cnt.keys()
		#build diagonal summations
		sum = {}
		for i in all_cls:
			partsum = 0
			for j in all_cls:
				if i!=j:
					partsum += prob_bin[j,i]*prob_bin[j,i]
			sum[i] = partsum 
		#construct matrix Q
		matrix=[]
		labels=[]
		for i in all_cls:
			labels.append(i)
			for j in all_cls:
				if i==j:
					matrix.append(sum[i])
				else:
					matrix.append(-prob_bin[i,j]*prob_bin[j,i])
			matrix.append(1)
		#lastrow
		matrix.extend([1]*len(all_cls))
		matrix.append(0)

		Q = reshape(matrix, (len(all_cls)+1, len(all_cls)+1) )
		#create rhs column vector
		rhs = []
		rhs.extend([0]*len(all_cls))
		rhs.append(1)
		rhs=reshape(rhs,[len(all_cls)+1,1])
		#solve linear equations
		ans = transpose(solve_linear_equations(Q, rhs))[0][0:len(all_cls)]
		#reconstruct hash
		p = {}
		for i in range(len(all_cls)):
			p[labels[i]] = ans[i]
		return p

def markov(prob_bin, cls_cnt):
		all_cls = cls_cnt.keys()
		#build diagonal summations
		sum = {}
		for i in all_cls:
			partsum = 0
			for j in all_cls:
				if i!=j:
					partsum = partsum + prob_bin[i,j]
			sum[i] = partsum - len(all_cls) + 1
		#construct matrix
		matrix=[]
		labels=[]
		for i in all_cls:
			labels.append(i)
			for j in all_cls:
				if i==j:
					matrix.append(sum[i])
				else:
					matrix.append(prob_bin[i,j])
		#replace lastline with 1 1 1 1
		for i in range(len(all_cls)):
			matrix.pop()
		for i in range(len(all_cls)):
			matrix.append(1)
		matrix = reshape(matrix, [len(all_cls), len(all_cls)])
		#create rhs column vector
		rhs = []
		for i in range(len(all_cls)-1):
			rhs.append(0)
		rhs.append(1)
		rhs=reshape(rhs,[len(all_cls),1])
		#solve linear equations
		ans = transpose(solve_linear_equations(matrix, rhs))[0]
		#reconstruct hash
		p = {}
		for i in range(len(all_cls)):
			p[labels[i]] = ans[i]
		return p


def	pkpd(prob_bin, cls_cnt):
	all_cls = cls_cnt.keys()
	k = len(all_cls)
	R = prob_bin.copy()
	p = {}
	for i in all_cls:
		p[i] = 0.0
		for j in all_cls:
			if i!=j:
				if prob_bin[i,j] == 0.0:
					prob_bin[i,j] = 1e-300
				R[i,j] = 1.0/prob_bin[i,j]
				p[i] += R[i,j]
		p[i] = 1.0/(p[i]-(k-2))
	normalize(p)
	return p

def ass(prob_bin, cls_cnt):
	all_cls = cls_cnt.keys()
	nr_cls = len(cls_cnt)
	nr_pair = (nr_cls*(nr_cls-1)/2)
	pairM = [0] * nr_pair

	#create a nr_cls*nr_pair zero matrix
	codeM = []
	for i in range(nr_cls):
		codeM.append( ([0]*nr_pair)[:] )

	#init coding matrix and pair class at each column
	col = 0
	for i in range(nr_cls):
		for j in range(i+1, nr_cls):
			codeM[i][col] = +1
			codeM[j][col] = -1
			pairM[col] = (all_cls[i], all_cls[j])
			col += 1

	loss = dict.fromkeys(all_cls, 0)

	floss = lambda y:exp(-y)
	for i in range(nr_cls):
		for j in range(nr_pair):
			loss[all_cls[i]] += floss( codeM[i][j] * (prob_bin[pairM[j]]-0.5))
    		loss[all_cls[i]] = 1.0/loss[all_cls[i]] #reverse order
	normalize(loss)
	return loss

	
def	vote(prob_bin, cls_cnt):
	all_cls = cls_cnt.keys()
	count = {}
	for i in all_cls:
		count[i] = 0
	for i in all_cls:
		for j in all_cls:
			if i != j:
				if prob_bin[i,j] >= prob_bin[j,i]:
					count[i] += 1
				elif prob_bin[i,j] == prob_bin[j,i]:
					choice = [i, j]
					shuffle(choice)
					print "tie on class" ,choice 
					count[choice[0]]+=1
	normalize(count)
	return count

global coupler_list 
coupler_list = [ht,pkpd,markov,minpair,vote,ass]

def main():
	#Unit Test using values from
	#Classification by Pairwise Coupling(Hastie & Tibshrani) Page 7
	answer = { \
		ht:{0: 0.28601714240255655, 1: 0.34111909441522903, 2: 0.16237191035527967, 3: 0.21049185282693506},\
		pkpd:{0: 0.35694252966113982, 1: 0.33963275066334814, 2: 0.045568375062231194, 3: 0.25785634461328089},\
		markov:{0: 0.29693955304880887, 1: 0.32519092572946245, 2: 0.15443361762911531, 3: 0.22343590359261339},\
		minpair:{0: 0.33246596146685409, 1: 0.28749577312399272, 2: 0.13823129182562749, 3: 0.24180697358352565},\
		vote:{0: 0.5, 1: 0.16666666666666666, 2: 0.16666666666666666, 3: 0.16666666666666666},\
		ass:{0: 0.25896983667656831, 1: 0.26264606812030206, 2: 0.23225321255651887, 3: 0.24613088264661084},\
	}
	cls_cnt = {0:1, 1:1, 2:1, 3:1}
	prob_bin = {\
		(0,0) : None, (0,1) : 0.56, (0,2) : 0.51, (0,3) : 0.60,\
		(1,0) : 0.44, (1,1) : None, (1,2) : 0.96, (1,3) : 0.44,\
		(2,0) : 0.49, (2,1) : 0.04, (2,3) : None, (2,3) : 0.59,\
		(3,0) : 0.40, (3,1) : 0.56, (3,2) : 0.41, (3,3) : None \
	}
	for m in coupler_list:
		ret = m(prob_bin, cls_cnt)
		#print ret, answer[m]
		if(ret == answer[m]):
			print  m.__name__,"OK" 
		else:
			print  m.__name__,"Fail, dist=", sum(map(lambda c:abs(ret[c]-answer[m][c]), cls_cnt) ), ret
			#exit(1)

if __name__ == "__main__":
	main()

