package math.statistics;

import stdlib.StdOut;

/**
 *  Function to compute the Gaussian pdf (probability density function)
 *  and the Gaussian cdf (cumulative density function)
 *	<p>
 *  The approximation is accurate to absolute error less than 8 * 10<sup>-16</sup>.<br />
 *  Reference: Evaluating the Normal Distribution by George Marsaglia.
 *  http://www.jstatsoft.org/v11/a04/paper
 */
public class Gaussian{

	/**
	 * &phi;(x) = 1/&radic;(2&pi;) &middot; e<sup>-x<sup>2</sup>/2</sup>
	 * @param x
	 * @return standard Gaussian pdf
	 */
    public static double phi(double x) {
        return Math.exp(-x*x / 2) / Math.sqrt(2 * Math.PI);
    }

    /**
     * &phi;(z,µ,&sigma;) = &phi;((x-µ)/&sigma;)/&sigma;
     * @param x
     * @param mu mean µ
     * @param sigma stddev &sigma;
     * @return Gaussian pdf
     */
    public static double phi(double x, double mu, double sigma) {
        return phi((x - mu) / sigma) / sigma;
    }

    /**
     * &Phi;(z) = &int;<sub>-&infin;</sub><sup>z</sup>&phi;(x)dx 
     * = 1/2 + &phi;(z) (z + z<sup>3</sup>/3 + z<sup>5</sup>/3&middot;5 + z<sup>7</sup>/3&middot;5&middot;7 + ...)
     * @param z
     * @return standard Gaussian cdf using Taylor approximation
     */
    public static double Phi(double z) {
        if (z < -8.0) return 0.0;
        if (z >  8.0) return 1.0;
        int i;
        double sum = 0.0, term = z;
        for(i=3; sum+term!=sum; i+=2){
            sum  = sum + term;
            term = term * z * z / i;
        }
        return 0.5 + sum * phi(z);
    }

    /**
     * &Phi;(z,µ,&sigma;) = &Phi;((z-µ)/&sigma;)
     * Gaussian cdf with mean mu and stddev sigma.
     * <p>
     * z = 820, mu = 1019, sigma = 209 => Phi = 0.17050966869132111<br />
     * z = 1500, mu = 1019, sigma = 209 => Phi = 0.17050966869132111<br />
     * z = 1500, mu = 1025, sigma = 231 => Phi = 0.17050966869132111<br />
     * @param z
     * @param mu mean
     * @param sigma stddev
     * @return Gaussian cdf
     */
    public static double Phi(double z, double mu, double sigma) {
        return Phi((z - mu) / sigma);
    } 

    /**
     * Compute z such that Phi(z) = y via bisection search
     * @param y
     * @return z such that Phi(z) = y
     */
    public static double PhiInverse(double y) {
        return PhiInverse(y, .00000001, -8, 8);
    } 

    /**
     * Compute z such that Phi(z) = y via bisection search
     * @param y
     * @param delta
     * @param lo
     * @param hi
     * @return z such that Phi(z) = y
     */
    private static double PhiInverse(double y, double delta, double lo, double hi) {
        double mid = lo + (hi - lo) / 2;
        if(hi - lo < delta) return mid;
        if(Phi(mid) > y) return PhiInverse(y, delta, lo, mid);
        else             return PhiInverse(y, delta, mid, hi);
    }

    /**
     * fractional error in math formula less than 1.2 &middot; 10<sup>-7</sup>.
     * although subject to catastrophic cancellation when z in very close to 0
     * from Chebyshev fitting formula for erf(z) from Numerical Recipes, 6.2
     * <p>
     * erf(1.0) = 0.8427007877600067, actual = 0.84270079294971486934<br />
     * erf(3.0) = 0.9999779095015785, actual = 0.99997790950300141456<br />
     * erf(1.0e-20) = -3.0000000483809686E-8, true answer 1.13e-20
     * @param z
     * @return erf(z) = 2 / &radic;&pi; &middot; &int;e<sup>-t<sup>2</sup></sup>, t = 0..z
     */
    public static double erf(double z) {
        double t = 1.0 / (1.0 + 0.5 * Math.abs(z));

        // use Horner's method
        double ans = 1 - t * Math.exp( -z*z   -   1.26551223 +
                                            t * ( 1.00002368 +
                                            t * ( 0.37409196 + 
                                            t * ( 0.09678418 + 
                                            t * (-0.18628806 + 
                                            t * ( 0.27886807 + 
                                            t * (-1.13520398 + 
                                            t * ( 1.48851587 + 
                                            t * (-0.82215223 + 
                                            t * ( 0.17087277))))))))));
        if(z >= 0) return  ans;
        else       return -ans;
    }

    // fractional error less than x.xx * 10 ^ -4.
    // Algorithm 26.2.17 in Abromowitz and Stegun, Handbook of Mathematical.
    public static double erf2(double z) {
        double t = 1.0 / (1.0 + 0.47047 * Math.abs(z));
        double poly = t * (0.3480242 + t * (-0.0958798 + t * (0.7478556)));
        double ans = 1.0 - poly * Math.exp(-z*z);
        if (z >= 0) return  ans;
        else        return -ans;
    }

    /**
     * test client
     * @param args
     */
    public static void main(String[] args) {
		double z     = 820;
		double mu    = 1019;
		double sigma = 209;
        StdOut.println("Phi("+z+", "+mu+", "+sigma+") = "+Phi(z, mu, sigma));
        double y = Phi(z);
        StdOut.println("PhiInverse(Phi("+z+")) = "+PhiInverse(y));
        
        double x = 1.0;
        System.out.println("MyMath library");
        System.out.println("erf(" + x + ")  = " + erf(x));
        System.out.println("erf2(" + x + ") = " + erf2(x));
        System.out.println("Phi(" + x + ")  = " + Phi(x));
        System.out.println();
    }

}