#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""A classifier that assumes that P is linear in position.
This is known as a (linear) logistic discriminant analysis::

@note: A useful general reference is:
	@inbook{webb:spr:logistic,
   	author    = {Andrew Webb},
   	title     = {Statistical Pattern Recognition},
   	pages     = {124--132},
   	year      = {1999},
   	publisher = {Arnold},
   	address   = {London, New York},
   	note = {ISBN 0 340 74164  3}
	}

@note: This code was described in an appendix to
	"Dimensions of durational variation in speech",
	by Anastassia Loukina, Greg Kochanski, Burton Rosner, Chilin Shih, and Elinor Keane,
	submitted 2010 to J. Acoustical Society of America.
"""


import sys
import math
import random
import numpy
from gmisclib import die
from gmisclib import fiatio
from gmisclib import g_closure
from gmisclib import gpkmisc
from gmisclib import Numeric_gpk as NG

import q_classifier_r as Q

COVERAGE = 3
FTEST = 0.25
N_PER_DIM = 10.0




def ref_from_data(data):
	tmp = [ datum.value for datum in data ]
	return NG.median_across(tmp)


def ref_from_models(models):
	tmp = [m.reference for m in models.values()]
	return NG.median_across(tmp)


class l_classifier_desc(Q.classifier_desc):
	"""This class is metadata and helper functions for a linear discriminant classifier.
	"""

	def __init__(self, data=None, evaluator=None, models=None, ftrim=None):
		if data is not None:
			classes = Q.list_classes(data)
			fvdim = Q.get_dim(data)	# length of the feature vector
			reference_pt = ref_from_data(data)
		elif models is not None:
			classes = models.keys()
			fvdim = models.values()[0].direction.shape[0]
			reference_pt = ref_from_models(models)
		else:
			raise ValueError, "Needs either a set of models or data to build a l_classifier_desc."
		Q.classifier_desc.__init__(self, classes, fvdim, evaluator=evaluator, ftrim=ftrim)
		self.reference = reference_pt


	def np(self):
		"""The number of parameters required to define each class."""
		assert self.ndim > 0
		return self.ndim + 1

	def npt(self):
		"""The total number of parameters required to define all classes."""
		assert self.nc > 1, "Only %d classes" % self.nc
		assert self.np() > 0
		return (self.nc-1) * self.np()

	def unpack(self, prmvec, trainingset_name=None, uid=None):
		"""Produce a classifier from a parameter vector."""
		m = self.np()
		assert len(prmvec) == self.npt()
		assert self.c	# Can't do much with zero classes.

		o = l_classifier(self, trainingset_name=trainingset_name, uid=uid)
		for (i, c) in enumerate(self.c):
			if i >= self.nc-1:
				break	# Last one is the zero model.
			# print 'ic', i, c, i*m, (i+1)*m, prmvec.shape
			pc = prmvec[i*m : (i+1)*m]
			bias = pc[0]
			prms = pc[1:]
			o.add_model( c, Q.lmodel( prms, bias, self.reference ) )
		o.add_model(c, Q.lzmodel( self.ndim ) )
		return o


	def start(self, data):
		"""Starting position for Markov Chain Monte Carlo."""
		assert self.npt() > 0
		p = numpy.zeros((self.npt(),))
		v = numpy.identity(self.npt(), numpy.float)
		var = NG.vec_variance( [datum.value for datum in data] )
		if not numpy.greater(var, 0.0).all():
			die.die("Variance of some data column is not positive: %s" % str(var))
		# print 'data[0].value=', data[0].value
		print 'START: var=', var
		assert var.shape == (self.ndim,)
		assert self.npt() % self.np() == 0
		for i in range(self.npt()):
			snp = self.np()
			isnp = i % snp
			if isnp == 0:
				sig = 1.0
			else:
				sig = math.sqrt(var[isnp-1])
			p[i] = random.normalvariate(0.0, 1.0/sig)
			v[i,i] = sig**-2
		# print 'p=', p
		# print 'v=', v
		return (p, v)

	def typename(self):
		return 'linear_discriminant_classifier'





# class l_classifier(Q.classifier, Q.forest_mixin):
class l_classifier(Q.classifier):
	"""This is a linear discriminant classifier.
	@type cdesc: L{l_classifier_desc} or None
	"""

	def __init__(self, cdesc=None, models=None, trainingset_name=None, uid=None):
		# print 'l_classifier class: trainingset=', trainingset_name
		if cdesc is None:
			cdesc = l_classifier_desc(models=models)
		if models is None:
			models = {}
		Q.classifier.__init__(self, cdesc.typename(),
					models, trainingset_name=trainingset_name,
					uid=uid, cdesc=cdesc)
#		Q.forest_mixin.__init__(self, cdesc)






def go_auto(fd, coverage=COVERAGE, ftest=FTEST, n_per_dim = N_PER_DIM,
			ftrim=None, verbose=True, modify_class=None,
#			evnm=None
		):
	d = Q.read_data(fd)
	print '# classes: %s' % (' '.join(Q.list_classes(d)))
	modelchoice = g_closure.Closure(l_classifier_desc, g_closure.NotYet,
#					evaluator=Q.evaluator_from_name(evnm),
					ftrim=ftrim)
	classout = fiatio.writer(open('classified.fiat', 'w'))
# 	classout.header('evaluator', evnm)
	classout.header('ftrim', ftrim)
	summary, out, wrong = Q.compute_self_class(d, coverage=coverage, ftest=ftest,
						n_per_dim = n_per_dim,
						modelchoice = modelchoice,
						builder=Q.forest_build,
						classout=classout, verbose=verbose,
						modify_class=modify_class)
	Q.default_writer(summary, out, classout, wrong)




#def classifier_reader(dc):
	#"""Read the classifier definitions produced by classifier_writer()."""
	#def model_reader(dc):
		#if dc.groupstart() != 'linear_class_description':
			#raise chunkio.BadFileFormat, "Wrong type of class description"
		#bias = dc.read_float()
		#direction = dc.read_NumArray()
		#if len(direction.shape) != 1:
			#raise chunkio.BadFileFormat, "Bad shape direction"
		#reference_pt = dc.read_NumArray()
		#dc.groupend()
		#q = Q.lmodel(direction, bias, reference_pt)
		#return q

	#if dc.groupstart() != 'linear_discriminant_classifier':
		#raise chunkio.BadFileFormat, "Missing outer group"

	#models = dc.read_dict_of(model_reader)
	#info = dc.read_dict(lambda x:x)
	#dc.groupend()
	#return l_classifier( cdesc=None, models=models,
				#trainingset_name=info['trainingset'],
				#uid=info['Cuid'])



#def read_classes(fd):
	#dc = chunkio.datachunk(fd)
	#hdr = Q.read_classes_header(dc)
	#classes = dc.read_array_of(classifier_reader)
	#return (hdr, classes)




def test_build1(classifier_choice):
	print 'TEST_BUILD1:'
	data = [ Q.datum_tr([1.0], 'A'), Q.datum_tr([0.0], 'B') ] * 2
	assert issubclass(classifier_choice, Q.classifier_desc)
	modelchoice = classifier_choice(data, evaluator=Q.evaluate_match)
	assert isinstance(modelchoice, Q.classifier_desc)
	print 'modelchoice=', modelchoice
	print 'data=', data
	ok = 0
	for cdef in Q.forest_build(data, 1, modelchoice=modelchoice):
		print 'cdef=', cdef
		eigenvalue = Q.evaluate_match(cdef, data)
		print 'eval=', eigenvalue
		if eigenvalue < 1:
			ok = 1
			break
	if not ok:
		raise RuntimeError, "can't find a perfect classifier in this trivial situation!"


def test_build1n(classifier_choice):
	print 'TEST_BUILD1:'
	NDAT = 300
	data = []
	for i in range(NDAT):
		data.append(Q.datum_tr([random.normalvariate(1.0, 1./2.828)], 'A') )
		data.append(Q.datum_tr([random.normalvariate(0.0, 1./2.828)], 'B') )
	assert issubclass(classifier_choice, Q.classifier_desc)
	modelchoice = classifier_choice(data, evaluator=Q.evaluate_match)
	assert isinstance(modelchoice, Q.classifier_desc)
	print 'modelchoice=', modelchoice
	print 'data=', data
	tmp = 0
	ntmp = 0
	for cdef in Q.forest_build(data, 10, modelchoice=modelchoice):
		sc = Q.evaluate_match(cdef, data)
		print 'cdef=', cdef, "score=", sc
		tmp += sc
		ntmp += 1
	print 'tmp/ntmp=', tmp/ntmp
	if tmp/float(ntmp) > 0.1 * 2*NDAT:
		raise RuntimeError, "can't find a perfect classifier in this trivial situation!"



def test_build1t(classifier_choice):
	print 'TEST_BUILD1t:'
	# Two bad points to be trimmed off:
	data = [ Q.datum_tr([100.0], 'A'), Q.datum_tr([-101.0], 'B') ]
	# Lots of good points:
	for i in range(30):
		data.append( Q.datum_tr([random.random()-1.5], 'A'))
		data.append( Q.datum_tr([random.random()+0.5], 'B'))
	print 'data=', data
	assert issubclass(classifier_choice, Q.classifier_desc)
	modelchoice = classifier_choice(data, evaluator=Q.evaluate_match, ftrim=(0.1,6))
	assert isinstance(modelchoice, Q.classifier_desc)
	ok = False
	for cdef in Q.forest_build(data, 20, modelchoice=modelchoice):
		e = Q.evaluate_match(cdef, data)
		if e < 3:
			ok = 1
			break
		else:
			print 'cdef=', cdef, 'evaluation=', e
	if not ok:
		raise RuntimeError, "can't find a perfect classifier in this trivial situation!"



def test_build32(classifier_choice):
	print 'TEST_BUILD32:'
	data = [
		Q.datum_tr([1.0, 0.0], 'A'),
		Q.datum_tr([-0.01, 0.0], 'B'),
		Q.datum_tr([-.01, 1.0], 'C'),
		Q.datum_tr([1.02, 0.01], 'A'),
		Q.datum_tr([-0.02, 0.01], 'B'),
		Q.datum_tr([-.02, 1.01], 'C'),
		Q.datum_tr([1.01, -0.01], 'A'),
		Q.datum_tr([0.0, -0.01], 'B'),
		Q.datum_tr([0, 0.99], 'C'),
		Q.datum_tr([0.98, 0.02], 'A'),
		Q.datum_tr([0.02, 0.0], 'B'),
		Q.datum_tr([-0.01, 0.98], 'C'),
		Q.datum_tr([0.99, 0.01], 'A'),
		Q.datum_tr([0.01, 0.03], 'B'),
		Q.datum_tr([-.02, 0.97], 'C'),
		]
	assert issubclass(classifier_choice, Q.classifier_desc)
	modelchoice = classifier_choice(data, evaluator=Q.evaluate_match)
	assert isinstance(modelchoice, Q.classifier_desc)
	ok = False
	for cdef in Q.forest_build(data, 20, modelchoice=modelchoice):
		e = Q.evaluate_match(cdef, data)
		print cdef
		if e < 1:
			ok = True
			break
	if not ok:
		raise RuntimeError, "can't find a perfect classifier in this trivial situation!"


def test_build2(classifier_choice):
	print 'TEST_BUILD2:'
	data = [
		Q.datum_tr([1.0, 2.0, 0.0], 'A'),
		Q.datum_tr([0.0, 1.0, 0.0], 'A'),
		Q.datum_tr([1.0, 1.0, 1.0], 'A'),
		Q.datum_tr([2.0, 0.0, 0.0], 'A'),
		Q.datum_tr([2.0, 2.0, 2.0], 'A'),
		Q.datum_tr([1.0, 0.0, 1.0], 'A'),
		Q.datum_tr([0.0, -1.0, -2.0], 'B'),
		Q.datum_tr([-1.0, -1.0, -2.0], 'B'),
		Q.datum_tr([0.0, -1.0, -1.0], 'B'),
		Q.datum_tr([-2.0, -1.0, -2.0], 'B'),
		Q.datum_tr([0.0, -1.0, 0.0], 'B'),
		Q.datum_tr([0.0, 0.0, -2.0], 'B'),
		Q.datum_tr([-2.0, -2.0, -2.0], 'B'),
		Q.datum_tr([-1.0, -1.0, 0.0], 'B')
		]
	assert issubclass(classifier_choice, Q.classifier_desc)
	modelchoice = classifier_choice(data, evaluator=Q.evaluate_match)
	assert isinstance(modelchoice, Q.classifier_desc)
	ok = False
	for cdef in Q.forest_build(data, 20, modelchoice=modelchoice):
		e = Q.evaluate_match(cdef, data)
		if e < 1:
			ok = True
			break
	if not ok:
		raise RuntimeError, "can't find a perfect classifier in this trivial situation!"



def test_4_2(classifier_choice):
	print 'TEST_4_2:'
	data = []
	for i in range(400):
		data.append( Q.datum_tr( [ random.random() ], 'a') )
		data.append( Q.datum_tr( [ random.random() ], 'b') )
		data.append( Q.datum_tr( [ 5+2*random.random() ], 'c') )
		data.append( Q.datum_tr( [ 5+2*random.random() ], 'd') )
	assert issubclass(classifier_choice, Q.classifier_desc)
	modelchoice = g_closure.Closure(classifier_choice, g_closure.NotYet,
					evaluator=Q.evaluate_match)
	summary, out, wrong = Q.compute_self_class(data, ftest=FTEST, coverage=COVERAGE,
						modelchoice = modelchoice,
						n_per_dim = N_PER_DIM,
						builder=Q.forest_build)
	assert abs(summary['Chance'] - 0.25) < 0.1
	assert abs(summary['Pcorrect'] - 0.5) < 0.1
	assert summary['Pcorrect'] > summary['Chance']
	print 'summary:', summary



def test_2_bias(classifier_choice):
	print 'TEST_2_bias:'
	data = []
	for i in range(400):
		data.append( Q.datum_tr( [ random.random() ], 'a') )
	for i in range(40):
		data.append( Q.datum_tr( [ random.random() ], 'b') )
	assert issubclass(classifier_choice, Q.classifier_desc)
	modelchoice = g_closure.Closure(classifier_choice, g_closure.NotYet,
					evaluator=Q.evaluate_match)
	classout = []
	summary, out, wrong = Q.compute_self_class(data, ftest=FTEST, coverage=COVERAGE,
						modelchoice = modelchoice,
						n_per_dim = N_PER_DIM,
						builder=Q.forest_build,
						classout=classout)
	print 'summary:', summary
	assert abs(summary['Chance'] - 0.9) < 0.05
	assert abs(summary['Pcorrect'] - 0.9) < 0.05
	nbc = 0
	nbt = 0
	ntot = 0
	for tmp in classout:
		if tmp['trueclass']=='b':
			nbt += 1
		if tmp['compclass']=='b':
			nbc += 1
		ntot += 1
	print 'true b=', nbt/float(ntot), 'computed b=', nbc/float(ntot)
	assert abs(nbt/float(ntot) - 0.1) < 0.03
	assert nbc < 1.3*nbt


def _t2sguts(off, f):
	o = []
	for i in range(1,8):
		o.append( random.gauss(off*i, f*i*i) )
	return o

def test_2_scale(classifier_choice):
	print 'TEST_2_scale:'
	data = []
	for i in range(300):
		data.append( Q.datum_tr( _t2sguts(0.0, 0.001), 'a') )
	for i in range(100):
		data.append( Q.datum_tr( _t2sguts(0.0, 0.001), 'b') )
	assert issubclass(classifier_choice, Q.classifier_desc)
	modelchoice = g_closure.Closure(classifier_choice, g_closure.NotYet,
					evaluator=Q.evaluate_match)
	classout = []
	summary, out, wrong = Q.compute_self_class(data, ftest=FTEST, coverage=COVERAGE,
						modelchoice = modelchoice,
						n_per_dim = N_PER_DIM,
						builder=Q.forest_build,
						classout=classout)
	print 'summary:', summary
	assert abs(summary['Chance'] - 0.75) < 0.05
	assert abs(summary['Pcorrect'] - 0.75) < 0.10
	nbc = 0
	nbt = 0
	ntot = 0
	for tmp in classout:
		if tmp['trueclass']=='b':
			nbt += 1
		if tmp['compclass']=='b':
			nbc += 1
		ntot += 1
	print 'true b=', nbt/float(ntot), 'computed b=', nbc/float(ntot)
	assert abs(nbt/float(ntot) - 0.25) < 0.10
	assert nbc < 1.3*nbt



def test_var():
	print 'TEST_var:'
	data = []
	for i in range(80):
		data.append( Q.datum_tr( [ random.gauss(0.0, 1.0), random.gauss(0.0, 1.0) ], 'a') )
	for i in range(160):
		data.append( Q.datum_tr( [ random.gauss(0.0, 1.0), random.gauss(0.0, 1.0)+0.01 ], 'b') )
	modelchoice = g_closure.Closure(l_classifier_desc, data,
					evaluator=Q.evaluate_match)
	summary, out, wrong = Q.compute_self_class(data,
					modelchoice = modelchoice,
					builder=Q.forest_build)
	print 'summary:', summary
	# assert abs(summary['Chance'] - 0.5) < 0.1
	# assert abs(summary['Pcorrect'] - 0.75) < 0.1


def calibrate_var():
	for i in range(15):
		test_var()
		sys.stdout.flush()

def test():
	cc = l_classifier_desc
	test_build1(cc)
	test_build1n(cc)
	test_build1t(cc)
	test_build32(cc)
	test_build2(cc)
	# test_var()
	test_2_scale(cc)
	test_2_bias(cc)
	test_4_2(cc)


PSYCO = False

if __name__ == '__main__':
	test()
