// $Id: bayescurve.cpp,v 1.23 2018/01/03 21:32:54 mkkuhner Exp $

/*
Copyright 2002-2005 Mary K. Kuhner, Peter Beerli, and Joseph Felsenstein

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

*/

/* Authors: Lucian Smith, Mary Kuhner, Jon Yamato and Joseph Felsenstein */


/*
  A bayes curve is a probability density function, aka a curve whose area
  under its curve is 1.0

  It's stored as a vector of doubles, with an offset and a bin width, such
  that if vector[X]=y, the point in question is [(offset + width*X), y].

  Important functions include the creation of a new bayes curve from a vector
  of other bayes curves, and integration of the curve.
*/

#include <cassert>
#include <cmath>
#include <numeric>                      // for std::accumulate

#include "bayescurve.h"
#include "errhandling.h"
#include "runreport.h"
#include "registry.h"
#include "mathx.h"                      // for PI

using namespace std;

//------------------------------------------------------------------------------------

BayesCurve::BayesCurve()
    :m_offset(0), m_binwidth(0), m_curr_numpoints(0), m_target_numpoints(0),
     m_probabilities(), m_integration(), m_islog(false)
{
    // intentionally blank
}

//------------------------------------------------------------------------------------

BayesCurve::BayesCurve(const vector<BayesCurve> srcvec, combine_type ctype)
    : m_probabilities(), m_integration()
{
    if (srcvec.size() == 0)
    {
        throw implementation_error("Cannot create a new probability density curve, since there are no original curves to add together!");
    }

    m_offset = srcvec[0].m_offset;
    m_binwidth = srcvec[0].m_binwidth;
    m_curr_numpoints = srcvec[0].m_curr_numpoints;
    m_target_numpoints = srcvec[0].m_target_numpoints;
    m_islog = srcvec[0].m_islog;

    if (m_binwidth == 0 && m_offset == 0 && m_curr_numpoints == 0 && m_target_numpoints == 0)
    {
        //The curve is uninitialized.  This often happens since we need placeholder
        // objects for the invalid parameters--just return.
        return;
    }

    if (m_binwidth == 0)
    {
        throw implementation_error("Tried to create a BayesCurve object from a vector of BayesCurves, but one had a binwidth of zero.");
    }

    if (srcvec.size() == 1)
    {
        m_probabilities = srcvec[0].m_probabilities;
        m_integration = srcvec[0].m_integration;
        return;
    }

    double lastpoint = m_offset + (m_binwidth * srcvec[0].m_probabilities.size());
    double maxlike = 0;
    for (unsigned long int curve = 1; curve < srcvec.size(); curve++)
    {
        if (srcvec[curve].m_offset < m_offset)
        {
            m_offset = srcvec[curve].m_offset;
        }
        if (srcvec[curve].m_binwidth != m_binwidth)
        {
            throw implementation_error("Cannot add probability density curves with different spacings.");
        }
        m_target_numpoints += srcvec[curve].m_target_numpoints;
        m_curr_numpoints += srcvec[curve].m_curr_numpoints;
        double thislastpoint = (m_binwidth * srcvec[curve].m_probabilities.size())
            + srcvec[curve].m_offset;
        if (thislastpoint > lastpoint)
        {
            lastpoint = thislastpoint;
        }
        maxlike = max(maxlike, srcvec[curve].GetLikeAtMax());
    }

    assert (m_curr_numpoints == m_target_numpoints);
    // This will be true if all original curves were finalized.
    assert (m_binwidth != 0);

    double effectivezero = min(maxlike/100000, BiweightKernel(.99)/m_target_numpoints);

    unsigned long int veclength = static_cast<unsigned long int>((lastpoint - m_offset)/m_binwidth);
    switch(ctype)
    {
        case ADD:
            m_probabilities.assign(veclength+1,0.0);
            break;
        case MULTIPLY:
            m_probabilities.assign(veclength+1,1.0);
            break;
    }

    //Now, go through the various vectors and add or multiply, cf 'ctype'.
    for (unsigned long int curve = 0; curve<srcvec.size(); curve++)
    {
        double offsetdiff = srcvec[curve].m_offset - m_offset;
        unsigned long int thisindex = static_cast<unsigned long int>(offsetdiff/m_binwidth);

        if (ctype == MULTIPLY)
        {
            //We need to zero out the beginning and end of the vector.
            for (unsigned long int ind = 0; ind<thisindex; ind++)
            {
                m_probabilities[ind] *= effectivezero;
            }
            for (unsigned long int ind = thisindex + srcvec[curve].m_probabilities.size() - 1;
                 ind < m_probabilities.size(); ind++)
            {
                m_probabilities[ind] *= effectivezero;
            }
        }

        for (unsigned long int srcindex = 0;
             srcindex<srcvec[curve].m_probabilities.size();
             thisindex++, srcindex++)
        {
            switch (ctype)
            {
                case ADD:
                    m_probabilities[thisindex] +=
                        srcvec[curve].m_probabilities[srcindex] / srcvec.size();
                    //Note that this treats all curves as having equal weight.  If we
                    // wanted to change this, we'd divide by
                    // (m_target_numpoints / srcvec[curve].m_target_numpoints)
                    // instead of srcvec.size().
                    break;
                case MULTIPLY:
                    m_probabilities[thisindex] *=
                        max(srcvec[curve].m_probabilities[srcindex], effectivezero);
                    break;
            }
        }
    }

    if (ctype == MULTIPLY)
    {
        double scalingFactor = accumulate(m_probabilities.begin(), m_probabilities.end(), 0.0);
        scalingFactor *= m_binwidth;
        transform(m_probabilities.begin(),
                  m_probabilities.end(),
                  m_probabilities.begin(),
                  bind2nd(divides<double>(), scalingFactor));
    }

    Integrate();
}

//------------------------------------------------------------------------------------

BayesCurve::~BayesCurve()
{
    // intentionally blank
}

//------------------------------------------------------------------------------------

void BayesCurve::Initialize(unsigned long int numpoints, double width, bool islog,
                            double minval, double maxval, double maxkernwidth)
{
    m_binwidth = width;
    m_curr_numpoints = 0;
    m_target_numpoints = numpoints;
    m_islog = islog;

    //When using the biweight kernel, find (x-Xi)/h = 1, or:
    double truemin = minval - maxkernwidth;
    double low, high;
    ClosestWidthsTo(truemin, low, high);
    m_offset = low;

    //Now find highest value and initialize the vectors:
    double truehigh = maxval + maxkernwidth;
    ClosestWidthsTo(truehigh, low, high);
    unsigned long int veclength = static_cast<unsigned long int>(ceil((high - m_offset)/m_binwidth));
    m_probabilities.assign(veclength+1,0.0);
}

//------------------------------------------------------------------------------------
//AddKernel is the heart of the curve smoothing functionality.  It takes
// a point (center) and transforms it into a probability density function
//
//kernelwidth needs to be an argument instead of a member variable
// because a possible implementation of kernel width is to have it be
// adaptive, i.e. wider at the edges, and thinner in the middle.

void BayesCurve::AddKernel(double center, unsigned long int num, double kernelwidth)
{
    if (m_curr_numpoints + num > m_target_numpoints)
    {
        string error = "Can't add " + ToString(num)
            + " point(s) to this curve, since we've already added "
            + ToString(m_curr_numpoints) + " points to the curve, while expecting "
            + ToString(m_target_numpoints) + " points.";
        throw implementation_error(error);
    }
    m_curr_numpoints += num;

    //'start' and 'end' are highly dependent on the particular kernel being
    // used.  If a gaussian kernel is used, in fact, they'll need to be
    // arbitrarily wide, since that kernel is unbounded.  For a biweight
    // kernel, the bounds are +/- 1, so it's fairly simple:
    double high, low;
    ClosestWidthsTo(center-kernelwidth, high, low);
    unsigned long int start = static_cast<unsigned long int> ((high - m_offset) / m_binwidth);
    ClosestWidthsTo(center+kernelwidth, high, low);
    unsigned long int end   = static_cast<unsigned long int> ((low - m_offset)  / m_binwidth);
    for (unsigned long int i = start; i <= end; i++)
    {
        double kern_result = BiweightKernel((center - (m_offset + m_binwidth*i)) / kernelwidth);
        m_probabilities[i] += num * kern_result / (m_target_numpoints * kernelwidth);
    }
}

//------------------------------------------------------------------------------------

void BayesCurve::Integrate(bool warnuser)
{
    //To integrate, we'll simply add the heights of the curve throughout.
    // This means that the integration is accurate halfway between the stored
    // point and the next point.  Or, to put it another way:
    // m_probabilities[index] is for x = m_offset + (m_binwidth*index).
    // m_integration[index] is for x = m_offset + (m_binwidth*(index + 0.5)).
    m_integration.assign(m_probabilities.size(), 0.0);
    assert (m_curr_numpoints == m_target_numpoints);
    //We need to have added the right number of points, or else we're guaranteed
    // to not add up to 1.0

    m_integration[0] = m_probabilities[0]*m_binwidth;
    for (unsigned long int i = 1; i < m_integration.size(); ++i)
    {
        m_integration[i] = m_integration[i-1] + (m_probabilities[i]*m_binwidth);
    }

    double total=m_integration[m_integration.size()-1];
    if (fabs(total-1.0) > .0001)
    {
        RunReport& runreport = registry.GetRunReport();
        //Scale everything so that it actually adds up to 1.0
        transform(m_integration.begin(),m_integration.end(), m_integration.begin(), bind2nd(divides<double>(),total));
        if (warnuser)
        {
            string message = "Finished integrating a probability density function.  The total area under the curve should be 1.0; it's actually ";
            message += Pretty(total)
                + ".  The most likely cause of this is having too few data points; consider longer runtimes while collecting data.";
            runreport.ReportNormal(message);
        }
        runreport.ReportDebug("Scaling this curve so it actually integrates to 1.0; if you're bug-hunting, you might want to turn this feature off (see bayescurve.cpp).");
    }
}

//------------------------------------------------------------------------------------

double BayesCurve::GetValAtPercentile(double percentile) const
{
    assert(m_integration.size() > 1);
    unsigned long int i = 1;
    while ((i < m_integration.size()) && (m_integration[i] < percentile))
        i++;
    double fraction;
    if (i == m_integration.size())
        fraction = 1;
    else
    {
        double lowperc  = m_integration[i-1];
        double highperc = m_integration[i];
        fraction = (percentile - lowperc) / (highperc - lowperc);
    }
    double val = m_offset + (m_binwidth*(i + 0.5 + fraction));
    if (m_islog)
        val = exp(val);
    return val;
}

//------------------------------------------------------------------------------------

double BayesCurve::GetPercentileAtVal(double val) const
{
    assert(m_integration.size() != 0);
    if (m_islog)
        val = log(val);

    long int lowbin = static_cast<long int> (floor((val-m_offset-(m_binwidth*0.5))/m_binwidth));
    long int highbin = lowbin + 1;
    if (highbin < 0)
    {
        return 0.0;
    }
    if (lowbin >= static_cast<long int> (m_integration.size()))
    {
        return 1.0;
    }
    double lowperc, highperc;
    double distance;
    if (lowbin < 0)
        lowperc = 0;
    else
        lowperc = m_integration[lowbin];
    if (highbin >= static_cast<long int> (m_integration.size()))
        highperc = 1.0;
    else
        highperc = m_integration[highbin];

    distance = val - (m_offset + (m_binwidth*(lowbin + 0.5)));
    assert (distance <= m_binwidth);
    return ((highperc-lowperc)/m_binwidth)*distance + lowperc;
}

//------------------------------------------------------------------------------------

double BayesCurve::GetLikeAtVal(double val) const
{
    //LS NOTE:  These numbers will be slightly low for the bin that
    // contains the maximum:
    //                          _,
    //      *_,                * \              .
    //     /  \_              /   \             .
    //    /     *_           /     *_
    //   /        \_        /        \_
    //  *           *      *           *
    //
    // If the ' is the maximum, and at the same X coordinate in both, but at
    // different y coordinates.  The *'s represent the known values (the edges
    // of the bins).  The GetMax functions report it correctly, but this function
    // acts like the first curve is true (i.e. drawing straight lines between
    // each known point).
    assert(m_probabilities.size() != 0);
    if (m_islog)
        val = log(val);
    double high, low;
    ClosestWidthsTo(val, low, high);
    long int lowbin = static_cast<long int> ((low-m_offset)/m_binwidth);
    long int highbin = lowbin + 1;

    if (highbin < 0)
        return 0.0;
    if (lowbin >= static_cast<long int>(m_integration.size()))
        return 0.0;

    double lowlike, highlike;
    double distance = val-low;
    if (lowbin < 0)
        lowlike = 0;
    else
        lowlike = m_probabilities[lowbin];
    if (highbin >= static_cast<long int>(m_integration.size()))
        highlike = 0.0;
    else
        highlike = m_probabilities[highbin];

    // EWFIX -- get better epsilon from Lucian
    assert (distance <= m_binwidth * 1.00000001);
    return ((highlike-lowlike)/m_binwidth)*distance + lowlike;
}

//------------------------------------------------------------------------------------

double BayesCurve::GetMax() const
{
    if (m_curr_numpoints != m_target_numpoints)
    {
        RunReport& runreport = registry.GetRunReport();
        string error = "Warning:  attempted to get the maximum of a curve with";
        error = error + ToString(m_curr_numpoints) + " points when we wanted "
            + ToString(m_target_numpoints) + ".";
        runreport.ReportNormal(error);
    }
    double retval;
    if (m_probabilities.size() <= 1)
        retval = m_offset;
    else if (m_probabilities.size() == 2)
    {
        if (m_probabilities[0] > m_probabilities[1])
            retval = m_offset;
        else
            retval = m_offset + m_binwidth;
    }
    else
    {
        //There are at least three points, which we can use to extrapolate the
        // maximum.
        unsigned long int max = 1;
        for (unsigned long int i = 2; i < m_probabilities.size() - 1; i++)
        {
            if (m_probabilities[i] > m_probabilities[max] )
                max = i;
        }
        //If the following is too intensive just do:
        // return m_offset + (max * m_binwidth);

        //The following interpolates the most likely spot for the max between
        // the bins.
        double higher, lower;
        double maxval = m_probabilities[max];
        bool firsthigh;
        if (m_probabilities[max-1] > m_probabilities[max+1])
        {
            higher = m_probabilities[max-1];
            lower  = m_probabilities[max+1];
            firsthigh = true;
        }
        else
        {
            higher = m_probabilities[max+1];
            lower  = m_probabilities[max-1];
            firsthigh = false;
        }

        double distance = (m_binwidth/2) * (1-((maxval - higher)/(maxval - lower)));
        assert (distance < m_binwidth);
        if (firsthigh)
            distance *= -1;
        retval = (m_offset + (max * m_binwidth) + distance);
    }
    if (m_islog)
        retval = exp(retval);
    return retval;
}

//------------------------------------------------------------------------------------

double BayesCurve::GetLikeAtMax() const
{
    assert(m_probabilities.size());
    double bestlike = m_probabilities[0];
    for (unsigned long int i = 1; i < m_probabilities.size(); i++)
    {
        if (m_probabilities[i] > bestlike )
            bestlike = m_probabilities[i];
    }
    return bestlike;
    //Note:  This is a slight approximation, given that we extrapolate the best
    // value for this curve.
}

//------------------------------------------------------------------------------------

DoublePairVec BayesCurve::GetXYvec() const
{
    DoublePairVec results;
    for (unsigned long int i = 0; i < m_probabilities.size(); ++i)
    {
        double x = m_offset + (m_binwidth*i);
        double y = m_probabilities[i];
        results.push_back(pair<double, double>(x, y));
    }
    return results;
}

//------------------------------------------------------------------------------------

void BayesCurve::ClosestWidthsTo(const double val, double& low, double& high) const
{
    double highint = ceil(val/m_binwidth);
    high = highint*m_binwidth;
    double lowint = floor(val/m_binwidth);
    low = lowint*m_binwidth;
}

//------------------------------------------------------------------------------------
//Three possible kernel functions; we're most likely to use the biweight.

double BayesCurve::BiweightKernel(double t) const
{
    if (fabs(t) > 1.0)
        return 0.0;
    return (15.0/16.0) * pow((1.0-pow(t,2)),2);
}

//------------------------------------------------------------------------------------
//The problem with the Epanechnikov kernel is that it's a bit more
// expensive to compute, and that the bounds are weirder.

double BayesCurve::EpanechnikovKernel(double t) const
{
    if (pow(t,2) > 5.0)
        return 0.0;
    return (3.0/4.0) * (1 - (pow(t,2)/5.0)) / sqrt(5.0);
}

//------------------------------------------------------------------------------------
//The main problem with the gaussian kernel is that it's unbounded.  And
// probably more expensive to compute, too.

double BayesCurve::GaussianKernel(double t) const
{
    return (1.0/sqrt(2*PI)) * exp(-.5 * pow(t,2));
}

//____________________________________________________________________________________
