# -*- coding: utf-8 -*-
"""This is a support module, used by many types of classifiers."""

import re
import math
import zlib

import numpy

from gmisclib import chunkio
import data_splitter as DS
from gmisclib import die
from gmisclib import mcmc_big as mcmc
from gmisclib import mcmc_helper
mcmc_helper.Debug = 0
mcmc.Debug = 0
from gmisclib import g_implements
from gmisclib import fiatio
from gmisclib import dictops
from gmisclib import gpkmisc
from gmisclib import dict_vector as DV
import gpkavg

# from g_classifiers import logP

ERGCOVER = 4.0
D = True
CONSTRAIN = 1e-6

def Hash(dl):
	"""@return: a hash of the UID's of data items.
	@rtype: str
	@param dl: A list of data
	@type dl: list(L{datum_c})
	"""
	Mask1 = 0xffffffff
	Mask2 = 0xffffffffffff
	rv = 0
	for x in sorted(dl):
		tmp = zlib.crc32(x.uid) & Mask1
		rv = ((rv+tmp) * 13) & Mask2
	return '%d' % rv


def Hash1(l):
	"""@return: a hash of a vector.
	@rtype: str
	@param l: A list of data
	"""
	Mask1 = 0xffffffff
	Mask2 = 0xffffffff
	rv = 0
	for t in l:
		tmp = zlib.crc32(str(t)) & Mask1
		rv = ((rv+tmp) * 11) & Mask2
	return '%d' % rv

assert Hash1([3.712, 3.71]) == Hash1([3.712, 3.71]) 


class datum_c(object):
	"""This is an unclassified datum, either in the
	test or training set.
	@ivar value: a feature vector
	@type value: L{numpy.ndarray}
	@ivar uid: a string that identfies a particular datum.
		This is not used for classification, but is passed
		through to the output.  (It can also used to define groups
		of data.)
	@type uid: str
	"""

	__slots__ = ['value', 'uid']

	def __init__(self, vector, uid=None):
		"""@param vector: a vector of numbers
		@param uid: an arbitrary identifier for a datum
		@type uid: str
		"""
		self.value = numpy.asarray(vector, numpy.float)
		if uid is None:
			uid = Hash1(vector)
		self.uid = uid

	def __repr__(self):
		return "<datum_c fv=%s uid=%s>" % (str(self.value), self.uid)


	def __cmp__(self, other):
		return cmp(self.uid, other.uid)


class datum_tr(datum_c):
	"""This is a datum where we know the true class, presumably in
	the training set.
	@ivar classid: the name of the class to which the datum belongs.
	@type classid: str
	"""

	__slots__ = ['classid']

	def __init__(self, vector, classid, uid=None):
		"""@param vector: a vector of numbers
		@param uid: an arbitrary identifier for a datum
		@type uid: str
		@param classid: is the true class,
		@type classid: str
		"""
		datum_c.__init__(self, vector, uid)
		self.classid = classid
	
	def __str__(self):
		return "<datum_tr class=%s fv=%s uid=%s>" % (self.classid,
								str(self.value),
								self.uid)
	__repr__ = __str__





def prior(training):
	"""This computes the probability of correct classification,
	assuming you can't see the feature vector.
	It is used to compute P(chance).
	It assumes that you choose class C
	with probability 1 if P(C) is the
	biggest among all the classes.
	"""
	dtr = dictops.dict_of_accums()
	for datum in training:
		dtr.add(datum.classid, 1)
	kbest, chance = DV.max_within(DV.dict_vector(dtr))
	return chance/float(len(training))



def max_correct(training, testing):
	"""This is a hard, conservative upper limit
	for the probability of correct classification.
	"""
	# One cannot hope to classify an item correctly,
	# if the item's class is not represented
	# in the training set.
	# If the class is present in the training
	# set but not in the test set, then what happens?
	# It could be buried among items from other classes,
	# in which case it wouldn't lead to any false
	# predictions.  Consequently, absences from the
	# test set doesn't hurt the maximum possible
	# classification probability.
	dtr = dictops.dict_of_accums()
	for datum in training:
		dtr.add(datum.classid, 1)

	impossible = 0
	for datum in testing:
		if not datum.classid in dtr:
			impossible += 1

	return float(len(testing)-impossible)/float(len(testing))






def _addwrongD(wrong, failures):
	for (k, v) in failures.items():
		wrong.add(k, v)


def _addwrong(wrong, failures):
	for t in failures:
		wrong.add(t, 1)


def _doclass(qc, testing, verbose=1):
	"""This classifies the data
	@return: yields a sequence of dictionaries,
		each of which contains a bunch of results and information.
	@param testing: a list of data.
	@param qc: a classifier.
	@type qc: subclass of L{classifier}.
	"""
	info = qc.info.copy()
	for datum in testing:
		P = qc.P(datum)
		tmp = info.copy()

		# It is tempting to use qc.bestc() here, but
		# not a good idea, as it would involve calculating
		# log(P) twice, and that can be expensive.
		bestcl = None
		bestp = 0.0
		for (cl, p) in P:
			if p > bestp:
				bestp = p
				bestcl = cl
		tmp['compclass'] = bestcl

		if datum.uid is not None:
			tmp['Duid'] = datum.uid

		if verbose:
			tmp['V'] = chunkio.chunkstring_w().write_NumArray(datum.value, converter=lambda x:"%.6g"%x).close()
			tmp['P'] = chunkio.chunkstring_w().write_dict(dict(P), converter=lambda x:"%.3e"%x).close()
		try:
			tmp['trueclass'] = datum.classid
		except AttributeError:
			pass
		yield tmp




def read_data(fd, commentarray=None):
	"""Reads in feature vectors where the first element is the true class.
	This is the main data input for l_classifier, qdg_classifier and qd_classifier.
	@type fd: L{file}
	@type commentarray: a L{list} or L{None}
	@rtype: L{list}(L{datum_tr})
	"""
	d = []
	len_a = None
	# die.info('Reading')
	print '# Reading'
	ln = 0
	for l in fd:
		ln += 1
		if l.startswith('#'):
			if commentarray is not None:
				commentarray.append(l[1:])
			continue
		aa = l.split('#', 1)
		if len(aa) > 1:
			uid = aa[1].strip()
		else:
			uid = 'Line:%d' % ln
		a = aa[0].split()
		if len(a) != len_a:
			if len_a is None:
				len_a = len(a)
			else:
				die.die('Not all vectors have length=%d.  Problem on line %d'
						% (len_a-1, ln)
					)
		d.append( datum_tr(numpy.array([float(x) for x in a[1:]]),
				a[0], uid)
				)
	return d


def get_dim(fd):
	"""This function takes a list of data (type datum_tr)
	and makes sure that they all have the same length feature
	vector.  If so, it reports the length (dimension) of the
	feature vector.
	@type fd: list(L{datum_tr})
	@param fd: input data
	@return: length of vectors
	@rtype: int
	"""
	dim = None
	for (i, x) in enumerate(fd):
		assert isinstance(x, datum_tr), "Whoops: needs a datum_tr, got %s" % str(x)
		if len(x.value) != dim:
			if dim is None:
				dim = len(x.value)
			if len(x.value) != dim:
				raise ValueError, "Not all vectors are length=%d (len(vec[%d])=%d)" % (dim, i, len(x.value))
	return dim
		


def compute_cross_class(training, testing,
			modelchoice=None, n_per_dim=None,
			builder=None, classout=None,
			trainingset_name=None, modify_class=None,
			verbose=1):
	"""Build classifiers based on the training set,
	and test them on the testing set.
	Modelchoice here is the completed class object,
	not a closure."""

	# print 'ccc: trainingset=', trainingset_name
	if len(training) == 0:
		die.die('No training data.')
	if len(testing) == 0:
		die.die('No data to classify.')

	dim = get_dim(training)
	if dim <= 0:
		die.die('zero dimensional data')
	assert get_dim(testing) == dim

	nok = 0
	total = 0
	wrong = dictops.dict_of_accums()
	k = None

	priorchance = prior(training)
	maxcorrect = max_correct(training, testing)
	classifiers = builder(training, int(round(n_per_dim*dim)),
				modelchoice=modelchoice,
				trainingset_name=trainingset_name,
				verbose=verbose)
	for qc in classifiers:
		if modify_class is not None:
			modify_class(qc, count_classes(training), count_classes(testing))
		failures = qc.list_wrong_classifications(testing)
		qc.add_info('correct', len(testing)-len(failures))
		qc.add_info('Ntests', len(testing))
		qc.add_info('Chance_on_this_trainingset', priorchance)
		_addwrong(wrong, failures)
		if classout is not None:
			classout.extend( _doclass(qc, testing, verbose=verbose) )
		nok += len(failures)
		total += len(testing)
	pcorrect = float(total-nok)/float(total)

	if priorchance < maxcorrect:
		k = (pcorrect-priorchance)/(maxcorrect-priorchance)
	print '#NOK=', nok, total, float(nok)/total

	summary = {'nok': nok, 'total': total,
			'Pcorrect': float(total-nok)/float(total),
			'Chance': priorchance,
			'Pperfect': maxcorrect,
			'N_per_dim': n_per_dim
			}
	if k is not None:
		summary['K'] = k

	return (summary, classifiers, wrong)



def compute_self_class(d, coverage=None, ftest=None,
		modelchoice=None, n_per_dim=None, modify_class=None,
		builder=None, classout=None, verbose=1):

	"""modelchoice here is expected to take one argument-- the data."""

	if len(d) == 0:
		die.die('No data to classify.')
	dim = get_dim(d)
	if dim <= 0:
		die.die('zero dimensional data')
	Ntry = int(round(coverage/ftest))
	bdata = DS.bluedata(d)
	nok = 0; total = 0;
	wrong = dictops.dict_of_accums()
	out = []; k = []; pch = []; pmx = []; pcrct = []
	for tr in range(Ntry):
		print '# Building'
		testing, training = bdata.split(ftest*len(bdata), seed=tr)

		# Modelchoice is a closure, and evaluation happens here:
		if modelchoice is not None:
			c_modelchoice = modelchoice(training)
		else:
			c_modelchoice = None
		tsum, classifiers, twr = compute_cross_class(training, testing, c_modelchoice,
							n_per_dim, builder, classout,
							trainingset_name=Hash(training),
							modify_class=modify_class,
							verbose=verbose
							)

		pmx.append( tsum['Pperfect'] )
		pch.append( tsum['Chance'] )
		total += tsum['total']
		nok += tsum['nok']
		pcrct.append( 1.0-float(tsum['nok'])/tsum['total'] )
		_addwrongD(wrong, twr)
		out.extend(classifiers)
		if 'K' in tsum:
			k.append( tsum['K'] )

	summary = {'nok': nok, 'total': total, 'Ftest': ftest,
			'N_per_dim': n_per_dim, 'Coverage': coverage}

	try:
		summary['K'], ksigma = gpkavg.avg(k, None, 0.0001)
		summary['KSigma'] = ksigma
	except ValueError, x:
		die.info('%s: K' % str(x))

	try:
		summary['Chance'], chsigma = gpkavg.avg(pch, None, 0.0001)
		summary['ChSigma'] = chsigma
	except ValueError, x:
		die.info('%s: PChance' % str(x))
	try:
		summary['Perfection'], prfsigma = gpkavg.avg(pmx, None, 0.0001)
		summary['PerfectionSigma'] = prfsigma
	except ValueError, x:
		die.info('%s: Pperfect' % str(x))

	try:
		summary['Pcorrect'], psigma = gpkavg.avg(pcrct, None, 0.0001)
		summary['PSigma'] = psigma
	except ValueError, x:
		die.info('%s: Pcorrect' % str(x))


	return (summary, out, wrong)





class grouper_c(object):
	"""A 'grouper' function takes a DUID (a unique i.d. string
	for a datum) and returns the name of the data group to which
	it belongs.    This group name is used in constructing the training
	and test sets.
	"""
	def __init__(self, pattern='^([^/]*)/', which=1):
		self.pattern = re.compile(pattern)
		self.which = which

	def __call__(self, x):
		"""
		@param x: a datum
		@type x: L{datum_c}
		@return: group name
		@rtype: str
		"""
		m = self.pattern.search(x.uid)
		if m:
			return m.group(self.which)
		return None



def list_groups(d, gr):
	groups = set()
	for t in d:
		groups.add(gr(t))
	return groups






def compute_group_class(dg, modelchoice=None, n_per_dim=None,
		builder=None, classout=None, ftest=None,
		grouper=None, coverage=None, modify_class=None,
		verbose=1):
	"""This function makes sure that the training set and testing set
	come from different groups.   The 'grouper' returns a group
	name, when given a datum.   Modelchoice is expected to take
	one argument, the training set.
	@param grouper: function returning a group name for each datum
	@type grouper: function from L{datum_tr} to C{str}
	"""

	if len(dg) == 0:
		die.die('No data to classify.')
	dim = get_dim(dg)
	if dim <= 0:
		die.die('zero dimensional data')

	Ntry = int(round(coverage/ftest))
	nok = 0; total = 0;
	wrong = dictops.dict_of_accums()
	out = []; k = []; pch = []; pcrct = []; pmx = []

	bdata = DS.bluedata_groups(dg, grouper)
	for i in range(Ntry):
		print '# Building'
		testing, training, trn = bdata.split(ftest*len(bdata), seed=i)
		print 'testing:', ftest*len(bdata), len(testing), "training:", len(training), "from:", len(bdata), len(dg)

		# Modelchoice is a closure, and evaluation happens here:
		if modelchoice is not None:
			c_modelchoice = modelchoice(training)
		else:
			c_modelchoice = None
		tsum, classifiers, twr = compute_cross_class(training, testing,
							c_modelchoice, n_per_dim,
							builder, classout, modify_class=modify_class,
							trainingset_name=Hash(training),
							verbose=verbose
							)

		pmx.append( tsum['Pperfect'] )
		pch.append( tsum['Chance'] )
		total += tsum['total']
		nok += tsum['nok']
		pcrct.append( 1.0-float(tsum['nok'])/tsum['total'] )
		_addwrongD(wrong, twr)
		out.extend(classifiers)
		if 'K' in tsum:
			k.append( tsum['K'] )

	summary = {'nok': nok, 'total': total, 'Ftest': ftest,
			'N_per_dim': n_per_dim, 'Coverage': coverage}

	try:
		summary['K'], ksigma = gpkavg.avg(k, None, 0.0001)
		summary['KSigma'] = ksigma
	except ValueError, x:
		die.info('%s: K' % str(x))

	try:
		summary['Chance'], chsigma = gpkavg.avg(pch, None, 0.0001)
		summary['ChSigma'] = chsigma
	except ValueError, x:
		die.info('%s: PChance' % str(x))
	try:
		summary['Perfection'], prfsigma = gpkavg.avg(pmx, None, 0.0001)
		summary['PerfectionSigma'] = prfsigma
	except ValueError, x:
		die.info('%s: Pperfect' % str(x))

	try:
		summary['Pcorrect'], psigma = gpkavg.avg(pcrct, None, 0.0001)
		summary['PSigma'] = psigma
	except ValueError, x:
		die.info('%s: Pcorrect' % str(x))

	return (summary, out, wrong)





class model_template(object):
	"""This class describes how to compute the relative probability that
	a datum is a member of a particular class.   It also knows how to
	package up all necessary information for storage in a file.
	"""
	# def __init__(self):
		# pass
	# __init__.g_implements = g_implements.varargs

	def logp(self, datum):
		raise RuntimeError, "Virtual Function"

	def tochunk(self, chunkwriter):
		raise RuntimeError, "Virtual Function"

	def __str__(self):
		raise RuntimeError, "Virtual Function"

	@staticmethod
	def fromchunk(chunk):
		raise RuntimeError, "Virtual Function"



class qmodel(model_template):
	def __init__(self, mu, invsigma, offset):
			# Copy the data, so there is no risk of
			# it changing underneath you...
		self.mu = numpy.array(mu, numpy.float, copy=True)
		self.invsigma = numpy.array(invsigma, numpy.float, copy=True)
		assert len(self.mu.shape) == 1
		assert self.invsigma.shape == (self.mu.shape * 2), "Shapes must match: %s vs %s" % (str(self.invsigma.shape), str(self.mu.shape))
		# The above is a duplication of a tuple, not multiplication.
		self.bias = offset

	def logp(self, datum):
		delta = datum - self.mu
		parab = gpkmisc.qform(delta, self.invsigma)
		return -parab/2.0 + self.bias


	def tochunk(self, chunkwriter):
		chunkwriter.groupstart('quadratic_class_model', b=1 )
		chunkwriter.comment('Offset:')
		chunkwriter.write_float(self.bias)
		chunkwriter.comment('Mu:')
		chunkwriter.write_NumArray(self.mu, b=1)
		chunkwriter.comment('Inverse(covariance):')
		chunkwriter.write_NumArray(self.invsigma, b=1)
		chunkwriter.groupend()


	def __str__(self):
		return '<qmodel: mu=%s invsigma=%s bias=%g>' % (
				str(self.mu), str(self.invsigma),
				self.bias)


	@staticmethod
	def fromchunk(chunk):
		"""This the inverse of qmodel.tochunk(),
		except the group start is already read.
		"""
		offset = chunk.read_float()
		mu = chunk.read_NumArray()
		if len(mu.shape) != 1:
			raise chunkio.BadFileFormat, 'mu must be 1-d array'
		invsigma = chunk.read_NumArray()
		if len(invsigma.shape) != 2:
			raise chunkio.BadFileFormat, 'invsigma must be 2-d array'
		if invsigma.shape != mu.shape*2:
			raise chunkio.BadFileFormat, 'sizes do not match'
		return qmodel(mu, invsigma, offset)




_qzmodel_cache = {}
def qzmodel(ndim):
	global _qzmodel_cache
	if ndim in _qzmodel_cache:
		return _qzmodel_cache[ndim]
	zmu = numpy.zeros((ndim,), numpy.float)
	zis = numpy.zeros((ndim, ndim), numpy.float)
	zm = qmodel(zmu, zis, 0.0)
	if len(_qzmodel_cache) > 20:
		_qzmodel_cache.popitem()
	_qzmodel_cache[ndim] = zm
	return zm




class lmodel(model_template):
	def __init__(self, direction, offset, reference_pt):
			# Copy direction, so no chance of it changing
			# due to external causes...
		self.direction = numpy.array(direction, numpy.float, copy=True)
		self.bias = offset
		self.reference = reference_pt

	def logp(self, datum):
		# print "\t bias=", self.bias
		# print "\tdir=", self.direction
		# print "\tdatum=", datum
		# print "\treference=", self.reference
		# print '\to=', self.bias+numpy.dot(datum-self.reference, self.direction)
		return self.bias + numpy.dot(datum-self.reference, self.direction)

	def __str__(self):
		return '<lmodel: %s ref=%s bias=%g>' % (str(self.direction), str(self.reference), self.bias)

	def tochunk(self, chunkwriter):
		chunkwriter.groupstart('linear_class_description', b=1 )
		chunkwriter.comment('Offset:')
		chunkwriter.write_float(self.bias)
		chunkwriter.comment('dir:')
		chunkwriter.write_NumArray(self.direction, b=1)
		chunkwriter.comment('reference_pt:')
		chunkwriter.write_NumArray(self.reference, b=1)
		chunkwriter.groupend()


	@staticmethod
	def fromchunk(chunk):
		"""This the inverse of lmodel.tochunk(),
		except the group start is already read.
		"""
		offset = chunk.read_float()
		direction = chunk.read_NumArray()
		if len(direction.shape) != 1:
			raise chunkio.BadFileFormat, 'direction must be 1-d array'
		ref = chunk.read_NumArray()
		if len(direction.shape) != 1:
			raise chunkio.BadFileFormat, 'reference_point must be 1-d array'
		if direction.shape != ref.shape:
			raise chunkio.BadFileFormat, 'sizes do not match'
		return lmodel(direction, offset, ref)


_lzmodel_cache = {}
def lzmodel(ndim):
	if ndim in _lzmodel_cache:
		return _lzmodel_cache[ndim]
	z = numpy.zeros((ndim,), numpy.float)
	zm = lmodel(z, 0.0, z)
	if len(_lzmodel_cache) > 50:
		_lzmodel_cache.popitem()
	_lzmodel_cache[ndim] = zm
	return zm




class classifier_desc(object):
	"""This is a thing that describes and generates L{classifier}s.
	"""

	def __init__(self, list_of_classes, fvdim, evaluator=None, ftrim=None):
		assert isinstance(fvdim, int)
		assert fvdim > 0
		self.ndim = fvdim

		# Make sure that list_of_classes is iterable
		self.c = list(list_of_classes)

		self.nc = len(self.c)
		assert self.nc > 0, "No classes!"
		self.evaluator = evaluator
		self.ftrim = ftrim


	def np(self):
		"""@return: The number of parameters required to define one class.
		@rtype: int
		"""
		raise RuntimeError, "Virtual Function."

	def npt(self):
		"""@return:The number of parameters required to define the classifier.
		@rtype: int
		"""
		raise RuntimeError, "Virtual Function."

	def unpack(self, prmvec, trainingset_name=None, uid=None):
		"""Produce a classifier from a parameter vector.
		@param prmvec: a vector of parameters that describe a classifier model.
		@type prmvec: numpy.ndarray
		@return: the classifier.
		@rtype: the corresponding subclass of L{classifier}.
		"""
		raise RuntimeError, "Virtual Function."

	def start(self, data):
		"""Starting position for Markov Chain Monte Carlo."""
		raise RuntimeError, "Virtual Function."

	def __str__(self):
		# return "<classifier_desc: ndim=%d nc=%d cmap=%s>" % (self.ndim, self.nc, self.cmap)
		return "<classifier_desc: ndim=%d nc=%d>" % (self.ndim, self.nc)
	__repr__ = __str__


	def typename(self):
		"""@return: a string that names the subclass - what kind of L{classifier_desc} is it?
		@rtype str
		"""
		raise RuntimeError, "Virtual function"


def _logp(x, (data, cdesc)):
	assert isinstance(cdesc, classifier_desc)
	cl = cdesc.unpack(x)
	assert isinstance(cl, classifier)
	o1 = evaluate_Bayes(cl, data, constrain=CONSTRAIN)
	return -o1



def forest_build(data, N, modelchoice=None, trainingset_name=None, verbose=0):
	"""Build a forest of classifiers.
	@param data: data to train the classifiers on.
	@type data: L{datum_c}
	@param N: How many to build.
	@type N: int,
	@param modelchoice: what kind of classifier to build
	@type modelchoice: subclass of L{model_template}
	@param trainingset_name: (stored for later use).
	@type trainingset_name: str
	"""
	# print 'forest: trainingset=', trainingset_name
	assert len(data) > 1, "Not enough data"
	assert modelchoice is not None
	start, V = modelchoice.start(data)
	x = mcmc.bootstepper(_logp, start, V, c=(data, modelchoice))
	mcmch = mcmc_helper.stepper(x)
	nsteps = mcmch.run_to_bottom()
	die.info("Converged.")
	mcmch.run_to_ergodic(1.0)
	die.info("Warmed up: ready to sample. nsteps=%d" % nsteps)
	o = []
	for i in range(N):
		mcmch.run_to_ergodic(ERGCOVER/float(N))
		tmp = modelchoice.unpack( x.current().prms(),
					trainingset_name=trainingset_name,
					uid='%s:%d' % (trainingset_name,i)
					)
		if verbose < 0:
			die.info('Forest evaluate=%s for %s' % (tmp.evaluate(data), tmp))
		o.append(tmp)
	return o



class classifier(object):
	"""This is the base class for all kinds of classifers.
	"""

	def __init__(self, typename, models, info=None,
			trainingset_name=None, uid=None, cdesc=None):
		"""
		@param models: a dictionary containg a probabilistic model for each class
		@type models: dict(str: subclass of L{model_template})
		@param info: not used in the internal operation of the classifier,
			but it is stuff that is important to write out.
		@type info: dict(str: whatever)
		"""
		self.typename = typename
		assert isinstance(models, dict)
		self.class_models = models
		if info is not None:
			self.info = info.copy()
		else:
			self.info = {}
		self.info['trainingset'] = trainingset_name
		self.info['Cuid'] = uid
		self.cdesc = cdesc


	def add_model(self, classname, model):
		"""Add a class to an existing classifier."""
		g_implements.check(model, model_template)
		assert isinstance(classname, str)
		self.class_models[classname] = model


	def list_classes(self):
		return self.class_models.keys()


	def add_info(self, k, v):
		self.info[k] = v

	def P(self, datum, whichclass=None):
		"""Determine the probability of being in each class.
		If whichclass=None, then it returns a list of
		tuples [(classname,P), ...] for all classes.
		Otherwise, it returns the probability of the
		specified class.
		"""
		if whichclass is None:
			return [ (cn, math.exp(lp)) for (cn, lp) in self.logPv(datum) ]
		return math.exp(self.logPw(datum, whichclass, 0.0))


	def logP(self, datum, whichclass=None):
		if whichclass is None:
			return self.logPv(datum)
		return self.logPw(datum, whichclass, 0.0)


	def logPw(self, datum, whichclass, constrain):
		"""Determine the probability of C{datum} being in C{whichclass}.
		"""

		Psum = 0.0
		bgst = -self.HUGE
		smlst = self.HUGE
		the_lP = None
		for (cn, cm) in self.class_models.items():
			lp = cm.logp(datum.value)
			if cn == whichclass:
				the_lP = lp
			if smlst > lp:
				smlst = lp
			if bgst < lp:
				Psum *= math.exp(bgst-lp)
				bgst = lp
			Psum += math.exp(lp-bgst)
		return the_lP - bgst - math.log(Psum) - constrain*(bgst-smlst)**2


	def logPv(self, datum):
		"""Determine the probability of being in each class.
		If whichclass=None, then it returns a list of
		tuples [(classname,P), ...] for all classes.
		"""

		Psum = 0.0
		lcnP = []
		bgst = None
		for (cn, cm) in self.class_models.items():
			lp = cm.logp(datum.value)
			lcnP.append( (cn, lp) )
			if bgst is None:
				bgst = lp
			elif bgst<lp:
				Psum *= math.exp(bgst-lp)
				bgst = lp
			Psum += math.exp(lp-bgst)
		q = bgst + math.log(Psum)
		return [ (cn, lp-q) for (cn,lp) in lcnP ]

	HUGE = 1e30

	def bestc(self, datum):
		"""Determine the best class for a datum."""
		# print "Datum=", datum
		bgst = -self.HUGE
		bgc = None
		for (c, qp) in self.class_models.items():
			tmp = qp.logp(datum.value)
			if tmp > bgst:
				bgst = tmp
				bgc = c
		return bgc


	def __str__(self):
		o = [ '<classifier: %s' % self.typename ]
		for (c, q) in self.class_models.items():
			o.append('%s: %s' % (c, str(q)))
		o.append( '>' )
		return '\n'.join(o)

	__repr__ = __str__


	def list_wrong_classifications(self, classdata):
		nok = []
		for datum in classdata:
			bestc = self.bestc(datum)
			if bestc != datum.classid:
				nok.append(datum.uid)
		return nok


	def writer(self, dcw):
		"""Write this classifier out (usually to a data file).
		@type dcw: L{chunkio.chunk_w}
		"""
		dcw.groupstart(self.typename)
		dcw.comment('Classifier:')
		dcw.write_dict_of(self.class_models,
				lambda dcw, ac: ac.tochunk(dcw), b=1)
		dcw.write_dict(self.info)
		dcw.groupend()




# class forest_mixin(object):
# 	def __init__(self, cdesc):
# 		self.cdesc = cdesc
# 
# 	def evaluate(self, data):
# 		if self.cdesc.evaluator is None:
# 			return evaluate_Bayes(self, data)
# 		return self.cdesc.evaluator(self, data)


def evaluate_match(cl, data):
	"""This can be passed into a classifier descriptor as
	the evaluate argument.
	It returns the number of exact matches between the classified
	data and the input, true classification.
	@param cl: some classifier that has a bestc() method.
	@type cl: typically a subclass of L{classifier}.
	@param data: list of classes that describe data points.
	@type data: Typically a subclass of L{datum_c}.
	@rtype: int
	@return: the number of classification errors made
	"""
	# print 'classifier=', cl
	o = 0
	for datum in data:
		sbc = cl.bestc(datum)
		# print 'sbc(', datum, ')=', sbc, 'dc=', datum.classid, 'P=', cl.P(datum)
		if sbc != datum.classid:
			o += 1
	return o



class evaluate_match_w_rare(object):
	"""This is called in the same way as L{evaluate_match}
		or L{evaluate_Bayes}.  It pretends to be a function,
		except that you can weight the values of different
		classes when you construct the class.
		"""
	__name__ = 'evaluate_match_w_rare'

	def __init__(self):
		self.weights = None
	
	def _compute_weights(self, data):
		classcounts = dictops.dict_of_accums()
		for datum in data:
			classcounts.add(datum.classid, 1)

		ctotal = 0
		for counts in classcounts.values():
			ctotal += counts
		ctotal = float(ctotal)
		weights = {}
		for (classname, counts) in classcounts.items():
			weights[classname] = -math.log(float(counts)/ctotal)
		return weights
	

	def __call__(self, clssfr, data):
		if self.weights is None:
			self.weights = self._compute_weights(data)

		o = 0
		for datum in data:
			if clssfr.bestc(datum) != datum.classid:
				o += self.weights[datum.classid]
		return o




def evaluate_Bayes(cl, data, constrain=0.0):
	"""Evaluates the negative log of the probability that the classifier would assign
	to the datum being in the observed class (i.e. whatever class
	is specified in the L{datum_tr}).   Obviously, you
	want this to be a relatively small number.
	@param cl: a classifier
	@type cl: L{classifier}
	@param data: data
	@type data: list(L{datum_c})
	@rtype: float
	@return: the log of the probability of being in the observed class.

	If C{cdesc.ftrim} is not None, we assume that some of the
	data in each class are dubious, and should
	be ignored if they are sufficiently improbable.
	We modify the probability scores of data that is
	among the worst (C{cl.cdesc.ftrim[0]} fraction),
	and limit those scores to be no larger than
	C{cl.cdesc.ftrim[1]} larger than the best score.
	This lets you limit by score or limit by fraction
	or any mixture in between.   If C{cl.cdesc.ftrim} is C{None},
	then no limiting or trimming is done.
	"""
	if cl.cdesc.ftrim is None:
		o = 0.0
		for datum in data:
			o += cl.logPw(datum, datum.classid, constrain)
		return -o
	# Below, we assume a fraction trimfrac of the
	# data in each class are dubious, and should
	# be ignored if they are sufficiently improbable.
	trimfrac, trimlevel = cl.cdesc.ftrim
	assert trimfrac>=0 and trimfrac<=1.0
	o = dictops.dict_of_lists()
	for datum in data:
		sb = cl.logPw(datum, datum.classid, constrain)
		assert isinstance(datum, datum_tr), "Use ftrim only with training set!"
		o.add(datum.classid, sb)
	oo = 0.0
	for classid, scorelist in o.items():
		scorelist.sort()
		n = int(round(trimfrac*(len(scorelist)-1)))
		tcut = scorelist[-1] - trimlevel
		assert n > 0
		for t1 in scorelist[n:]:
			if D and t1 < tcut:
				print 'Trimming %g %g in %s' % (t1, tcut, classid)
			oo += max(tcut, t1)
		if D:
			for t1 in scorelist[:n]:
				print 'Trimming %g (bot) in %s' % (t1, classid)
	return -oo





def default_writer(summary, out, classout, wrong, fname="classes.chunk"):
	"""This writes out classifiers to a data file.
	@attention: out needs to be a list, not an iterator, because we use it twice.
	"""

	def classifier_writer(dcw, a_classifier):
		"""Helper function to work with L{chunkio}."""
		a_classifier.writer(dcw)

	dc = chunkio.datachunk_w( open(fname, "w") )
	dc.comment('Header:')
	header = summary.copy()
	ctype = None
	for cl in out:
		if ctype is None:
			ctype = cl.typename
		else:
			assert cl.typename == ctype, 'Cannot handle mixtures of different classifiers.'
	header['classifier_type'] = ctype
	dc.write_dict( header )
	dc.comment('classifiers:')
	dc.write_array_of(out, classifier_writer, b=1)
	out = None
	dc = None
	for (uid, nfailures) in wrong.items():
		print 'WRONG', nfailures, summary['total'], uid


def count_classes(data):
	"""Count how many instances there are of each class.
	@type data: L{datum_c}
	@rtype map from str to int
	"""
	cids = dictops.dict_of_accums()
	for datum in data:
		assert isinstance(datum.classid, str)
		cids.add(datum.classid, 1)
	return cids


def list_classes(data):
	"""List the names of the classes in a dataset,
	with the most populus classes first.
	@type data: L{datum_c}
	@rtype list(str)
	"""
	cn = [(-n, cid) for (cid, n) in count_classes(data).items() ]
	cn.sort()
	return [ cid for (n, cid) in cn ]





def name_of_evaluator(e):
	"""Used to get the name of an evaluator, to write it
	to a file header.
	@rtype: str
	@param e:
	@type e: function, preferable with __name__ attribute.
	"""
	if e is None:
		return evaluate_Bayes.__name__
	elif hasattr(e, '__name__'):
		return e.__name__
	else:
		return str(e)



def evaluator_from_name(nm):
	"""Maps a name to a function that will evaluate how well a classifier performs.
	@param nm: a printable name
	@type nm: str
	@return: a function
	"""
	if nm is None or nm == 'Bayes' or nm == 'evaluate_Bayes':
		return evaluate_Bayes
	elif nm == 'match' or nm == 'evaluate_match':
		return evaluate_match
	elif nm == 'match_w_rare' or nm == 'evaluate_match_w_rare':
		return evaluate_match_w_rare()
	else:
		die.die('Bad name for evaluator: %s' % nm)






def default_modify_class(qc, training_counts, testing_counts):
	"""Modifies a classifier so it isn't so dominated by the most frequent classes.
	@type qc: L{classifier}
	@param training_counts: how many data are there in each class in the training set
	@type training_counts: map str to int
	@param testing_counts: how many data are there in each class in the testing set
	@type testing_counts: map str to int
	"""
	assert isinstance(qc, classifier)
	F = 0.5
	ntr = 0
	for n in training_counts.values():
		ntr += n
	nts = 0
	for n in testing_counts.values():
		nts += n
	ntyp = 0
	neff = {}
	for nm in qc.class_models.keys():
		tmp = (training_counts[nm]*nts+testing_counts[nm]*ntr)/(ntr+nts)
		ntyp += tmp
		neff[nm] = tmp
	ntyp = float(ntyp)/float(len(qc.class_models))
	for (nm, mod) in qc.class_models.items():
		mod.bias -= F*math.log(float(neff[nm])/float(ntyp))

