package infodynamics.measures.continuous.gaussian;

import infodynamics.measures.continuous.ConditionalMutualInfoCalculatorMultiVariateWithDiscreteSourceCommon;
import infodynamics.measures.continuous.ConditionalMutualInfoMultiVariateCommon;
import infodynamics.utils.MatrixUtils;

/**
 * <p>Computes the differential conditional mutual information of a given multivariate set of
 *  observations with a discrete variable, conditioned on another multivariate set
 *  of observations,
 *  assuming that the probability distribution function for these continuous observations is
 *  a multivariate Gaussian distribution.</p>
 *  
 * <p>This is done by examining the conditional probability distribution for
 *  the multivariate continuous variable C (given the discrete
 *  variable D) against the probability distribution for C, all conditioned
 *  on another continuous variable Z:
 *  MI(C;D|Z) := H(C|Z) - H(C|D,Z).</p>
 *  
 * <p><b>CAVEAT EMPTOR</b>: The real question this type of calculation asks
 * is to what extent does knowing the value of the discrete variable reduce
 * variance in the continuous variable(s), given the other known continuous variable.
 *  Indeed, it may not to behave (I should test this--)
 * as we would normally expect a mutual information calculation: if we add more
 * continuous variables in, it may increase in spite of redundancy between these
 * variables. TODO Further exploration should take place here ...</p>
 *  
 * <p>
 * Usage:
 * 	<ol>
 * 		<li>Construct {@link #ConditionalMutualnfoCalculatorMultiVariateWithDiscreteSourceGaussian()}</li>
 *		<li>{@link #initialise(int, int, int)}</li>
 * 		<li>Set properties using {@link #setProperty(String, String)}</li>
 * 		<li>Provide the observations to the calculator using:
 * 			{@link #setObservations(double[][], int[], double[][])}, or
 * 			a sequence of:
 * 			{@link #startAddObservations()},
 *          multiple calls to {@link #addObservations(double[][], int[], double[][])}
 *          and then
 *          {@link #finaliseAddObservations()}.</li>
 * 		<li>Compute the required information-theoretic results, primarily:
 * 			{@link #computeAverageLocalOfObservations()} to return the average differential
 *          entropy based on the variance of
 *          the supplied observations; or other calls to compute
 *          local values or statistical significance.</li>
 * 	</ol>
 * </p>
 * 
 * <p>
 * Alters behaviour slightly from parent class {@link ConditionalMutualInfoMultiVariateCommon}
 * in that property {@link ConditionalMutualInfoMultiVariateCommon#PROP_NORMALISE}
 * is set to false by default here (since this makes more sense for
 * linear-Gaussian analysis). 
 * </p>
 * 
 * @see <a href="http://mathworld.wolfram.com/DifferentialEntropy.html">Differential entropy for Gaussian random variables at Mathworld</a>
 * @see <a href="http://en.wikipedia.org/wiki/Differential_entropy">Differential entropy for Gaussian random variables at Wikipedia</a>
 * @see <a href="http://en.wikipedia.org/wiki/Multivariate_normal_distribution">Multivariate normal distribution on Wikipedia</a>
 * @author Joseph Lizier joseph.lizier_at_gmail.com
 * 
 */
public class ConditionalMutualInfoCalculatorMultiVariateWithDiscreteSourceGaussian
		extends	ConditionalMutualInfoCalculatorMultiVariateWithDiscreteSourceCommon 
		implements Cloneable {

	/**
	 * Entropy calculator applied to the whole set of conditional data Z
	 */
	protected EntropyCalculatorMultiVariateGaussian entCalcZ;
	/**
	 * Entropy calculator applied to the whole set of continuous data C
	 * and conditional data Z
	 * 
	 */
	protected EntropyCalculatorMultiVariateGaussian entCalcCZ;
	/**
	 * Entropy calculators applied to the set of conditional data Z
	 * associated with each discrete value
	 */
	protected EntropyCalculatorMultiVariateGaussian[] entCalcZForEachDiscrete;
	/**
	 * Entropy calculators applied to the set of continuous data C
	 * and conditional data Z
	 * associated with each discrete value
	 */
	protected EntropyCalculatorMultiVariateGaussian[] entCalcCZForEachDiscrete;

	public ConditionalMutualInfoCalculatorMultiVariateWithDiscreteSourceGaussian() {
		super();
		// Normalising data makes less sense for linear-Gaussian estimation,
		//  so we turn this off by default.
		normalise = false;
		entCalcZ = new EntropyCalculatorMultiVariateGaussian();
		entCalcZForEachDiscrete = null;
		entCalcCZ = new EntropyCalculatorMultiVariateGaussian();
		entCalcCZForEachDiscrete = null;
	}

	/* (non-Javadoc)
	 * @see infodynamics.measures.continuous.ConditionalMutualInfoCalculatorMultiVariateWithDiscreteSourceCommon#initialise(int, int, int)
	 */
	@Override
	public void initialise(int dimensions, int base, int dimensionsCond) {
		super.initialise(dimensions, base, dimensionsCond);
		entCalcZ.initialise(dimensionsCond);
		entCalcCZ.initialise(dimensions + dimensionsCond);
		entCalcZForEachDiscrete = new EntropyCalculatorMultiVariateGaussian[base];
		entCalcCZForEachDiscrete = new EntropyCalculatorMultiVariateGaussian[base];
		for (int b = 0; b < base; b++) {
			entCalcZForEachDiscrete[b] = new EntropyCalculatorMultiVariateGaussian();
			entCalcCZForEachDiscrete[b] = new EntropyCalculatorMultiVariateGaussian();
			// If any properties relevant for these calculators were set in
			//  setProperty then we should set them here
			entCalcZForEachDiscrete[b].initialise(dimensionsCond);
			entCalcCZForEachDiscrete[b].initialise(dimensions + dimensionsCond);
		}
	}

	/* (non-Javadoc)
	 * @see infodynamics.measures.continuous.ConditionalMutualInfoCalculatorMultiVariateWithDiscreteSourceCommon#finaliseAddObservations()
	 */
	@Override
	public void finaliseAddObservations() throws Exception {
		super.finaliseAddObservations();
		
		// Set the complete set of observations:
		//  (this will pick up any errors in the dimensions of the continuous
		//   observations)
		entCalcZ.setObservations(conditionedDataZ);
		double[][] joinedCZ = MatrixUtils.appendColumns(continuousDataX, conditionedDataZ);
		entCalcCZ.setObservations(joinedCZ);
		
		// Set the observations corresponding to each discrete value:
		int totalNumberOfSuppliedObservations = 0;
		for (int b = 0; b < base; b++) {
			// Extract the observations for when this base value occurs:
			double[][] obsZForThisDiscValue = MatrixUtils.extractSelectedPointsMatchingCondition(
					conditionedDataZ, discreteData, b);
			double[][] obsCZForThisDiscValue = MatrixUtils.extractSelectedPointsMatchingCondition(
					joinedCZ, discreteData, b);
			// Set the observations for each discrete value:
			entCalcZForEachDiscrete[b].setObservations(obsZForThisDiscValue);
			entCalcCZForEachDiscrete[b].setObservations(obsCZForThisDiscValue);
			totalNumberOfSuppliedObservations += obsZForThisDiscValue.length;
		}
		// Check that all of the supplied observations were extracted corresponding
		//  to one of the allowed discrete values
		if (totalNumberOfSuppliedObservations != discreteData.length) {
			throw new Exception("Some values in discreteObservations were not in the range 0..base-1");
		}
	}

	public double computeAverageLocalOfObservations() throws Exception {
		// The average mutual information can be expressed
		//  as a difference between the conditional entropy of the continuous observations
		//  and the conditional entropy of the continuous given the
		//  discrete observations, both given the conditional continuous observations:
		//		I(C;D|Z) = H(C|Z) - H(C|D,Z)
		//               = H(C,Z) - H(Z) - H(C,Z|D) + H(C|D)
		double meanConditionalEntropyCZ = 0;
		double meanConditionalEntropyZ = 0;
		for (int b = 0; b < base; b++) {
			double pOfB = (double) counts[b] / (double) totalObservations;
			meanConditionalEntropyZ += pOfB *
					entCalcZForEachDiscrete[b].computeAverageLocalOfObservations();
			meanConditionalEntropyCZ += pOfB *
					entCalcCZForEachDiscrete[b].computeAverageLocalOfObservations();
		}
		double entCZ = entCalcCZ.computeAverageLocalOfObservations();
		double entZ = entCalcZ.computeAverageLocalOfObservations();
		condMi = entCZ - entZ
				- meanConditionalEntropyCZ
				+ meanConditionalEntropyZ;
		if (debug) {
			System.out.printf("H(C,Z)=%.4f - H(Z)=%.4f - H(C,Z|D)=%.4f + H(Z|D)=%.4f = %.4f\n",
					entCZ, entZ, meanConditionalEntropyCZ, meanConditionalEntropyZ, condMi);
		}
		return condMi;
	}

	public double[] computeLocalOfPreviousObservations() throws Exception {
		throw new RuntimeException("Not implemented yet");
	}

	public double[] computeLocalUsingPreviousObservations(
			double[][] contStates, int[] discreteStates,
			double[][] conditionedStates) throws Exception {
		throw new RuntimeException("Not implemented yet");
	}

	/**
	 * Clone the object - note: while it does create new cloned instances of
	 *  the {@link EntropyCalculatorMultiVariateGaussian} objects,
	 *  I think these only
	 *  have shallow copies to the data.
	 * This is enough though to maintain the structure across 
	 *  various {@link #computeSignificance(boolean, int)} calls.
	 * 
	 * @see java.lang.Object#clone()
	 */
	@Override
	protected Object clone() throws CloneNotSupportedException {
		ConditionalMutualInfoCalculatorMultiVariateWithDiscreteSourceGaussian theClone = 
			(ConditionalMutualInfoCalculatorMultiVariateWithDiscreteSourceGaussian) super.clone();
		// Now assign clones of the EntropyCalculatorMultiVariateGaussian objects:
		// First clone those for the conditional variable:
		theClone.entCalcZ =
				(EntropyCalculatorMultiVariateGaussian) entCalcZ.clone();
		if (entCalcZForEachDiscrete != null) {
			theClone.entCalcZForEachDiscrete = new EntropyCalculatorMultiVariateGaussian[base];
			for (int b = 0; b < base; b++) {
				theClone.entCalcZForEachDiscrete[b] =
						(EntropyCalculatorMultiVariateGaussian)
							entCalcZForEachDiscrete[b].clone();
			}
		}
		// Next clone those for the conditional variable and the continuous source variable
		theClone.entCalcCZ =
				(EntropyCalculatorMultiVariateGaussian) entCalcCZ.clone();
		if (entCalcCZForEachDiscrete != null) {
			theClone.entCalcCZForEachDiscrete = new EntropyCalculatorMultiVariateGaussian[base];
			for (int b = 0; b < base; b++) {
				theClone.entCalcCZForEachDiscrete[b] =
						(EntropyCalculatorMultiVariateGaussian)
							entCalcCZForEachDiscrete[b].clone();
			}
		}
		return theClone;
	}
}
