/* Copyright 2009-2013 David Hadka
 *
 * This file is part of the MOEA Framework.
 *
 * The MOEA Framework is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or (at your
 * option) any later version.
 *
 * The MOEA Framework is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public
 * License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with the MOEA Framework.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.moeaframework.analysis.sensitivity;

import java.util.ArrayList;
import java.util.List;

/**
 * Computes the fractal dimension for a set of points.  
 */
class FractalDimension {

	/**
	 * The maximum radius.
	 */
	private static final double R_MAX = 0.25;
	
	/**
	 * The minimum radius.
	 */
	private static final double R_MIN = 0.0005;
	
	/**
	 * Private constructor to prevent instantiation.
	 */
	private FractalDimension() {
		super();
	}
	
	/**
	 * Returns the correlation sum for the specified points using the specified
	 * radius.
	 * 
	 * @param points the points
	 * @param r the radius
	 * @return the correlation sum for the specified points using the specified
	 *         radius
	 */
	private static double computeCorrelationSum(double[][] points, double r) {
		double sum = 0.0;
		int n = points.length;
		
		for (int i=0; i<=n-2; i++) {
			for (int j=i+1; j<=n-1; j++) {
				if ((r - distance(points[i], points[j])) >= 0) {
					sum += 1.0;
				}
			}
		}
		
		return sum / (n*(n-1)/2.0);
	}
	
	/**
	 * Returns the Euclidean distance between two points.
	 */
	private static double distance(double[] p1, double[] p2) {
		double sum = 0.0;
		
		for (int i=0; i<p1.length; i++) {
			sum += Math.pow(p2[i] - p1[i], 2.0);
		}
		
		return Math.sqrt(sum);
	}
	
	/**
	 * Identifies the "plateau region" --- the region where the slope is a
	 * constant.  For different values of i, this method removes the first and
	 * last i points from consideration.  Computes the correlation coefficient
	 * of the remaining points to determine how well those points approximate
	 * a line.
	 */
	private static int getCorrelationDimensionBounds(List<Double> X, 
			List<Double> Y) {
		int n = X.size();
		
		for (int i=0; i<(n - 4)/2; i++) {
			double sumX = 0.0;
			double sumXX = 0.0;
			double sumY = 0.0;
			double sumXY = 0.0;
			double sumYY = 0.0;
			
			for (int j=i; j<n-i; j++) {
				sumX += X.get(j);
				sumXX += X.get(j)*X.get(j);
				sumY += Y.get(j);
				sumXY += X.get(j)*Y.get(j);
				sumYY += Y.get(j)*Y.get(j);
			}
			
			int k = n - 2*i;
			double correlationCoefficient =  (k*sumXY - sumX*sumY) /
					(Math.sqrt(k*sumXX - sumX*sumX)*Math.sqrt(k*sumYY - 
							sumY*sumY));
			
			if (correlationCoefficient >= 0.99) {
				return i;
			}
		}
		
		return 0;
	}
	
	/**
	 * Returns the fractal dimension for the specified points.
	 * 
	 * @param parameters the points
	 * @return the fractal dimension for the specified points
	 */
	public static double computeDimension(double[][] parameters) {
		if (parameters.length == 0) {
			return 0.0;
		}
		
		List<Double> X = new ArrayList<Double>();
		List<Double> Y = new ArrayList<Double>();

		for (double r=R_MAX; r>= R_MIN; r-= R_MIN) {
			double lr = Math.log(r);
			double lc = Math.log(computeCorrelationSum(parameters, r));
			
			if (lc == Double.NEGATIVE_INFINITY) {
				break;
			}
				
			X.add(lr);
			Y.add(lc);
		}
		
		//perform linear least-squares regression to find slope
		int n = X.size();
		
		if (n < 5) {
			return 0.0;
		}
		
		double sumX = 0.0;
		double sumXX = 0.0;
		double sumY = 0.0;
		double sumXY = 0.0;
		int remove = getCorrelationDimensionBounds(X, Y);

		for (int i=remove; i<n-remove; i++) {
			sumX += X.get(i);
			sumXX += X.get(i)*X.get(i);
			sumY += Y.get(i);
			sumXY += X.get(i)*Y.get(i);
		}
		
		return (sumX*sumY - n*sumXY) / (sumX*sumX - n*sumXX);
	}

}

