// * * * * * * * * * * * * * * * Member Functions  * * * * * * * * * * * * * //
void Foam::extendedMomentInversion::invert(const univariateMomentSet& moments)
{   
    univariateMomentSet m(moments);
    reset();

    // Terminate execution if negative number density is encountered
    if (m[0] < 0.0)
    {
        FatalErrorIn
        (
            "Foam::extendedMomentInversion::invert\n"
            "(\n"
            "   const univariateMomentSet& moments\n"
            ")"
        )   << "The zero-order moment is negative."
            << abort(FatalError);
    }

    // Exclude cases where the zero-order moment is very small to avoid
    // problems in the inversion due to round-off error  
    if (m[0] < SMALL)
    {
        sigma_ = 0.0;
        nullSigma_ = true;

        return;
    }
    
    label nRealizableMoments = m.nRealizableMoments();
   
    if (nRealizableMoments % 2 == 0)
    {
        // If the number of realizable moments is even, we apply the standard
        // QMOM directly to maximize the number of preserved moments.

        // Info << "Even number of realizable moments: using QMOM" << endl;
        // Info << "Moments: " << m << endl;
        // Info << "Invertible: " << m.nInvertibleMoments() << endl;
        // Info << "Realizable: " << m.nRealizableMoments() << endl;
        m.invert();
        sigma_ = 0.0;
        nullSigma_ = true;
        secondaryQuadrature(m);
    }
    else
    {
        // Resizing the moment set to avoid copying again
        m.resize(nRealizableMoments);

        // Local set of starred moments
        univariateMomentSet mStar(nRealizableMoments, 0);

        // Compute target function for sigma = 0
        scalar sigmaLow = 0.0;
        scalar fLow = targetFunction(sigmaLow, m, mStar);
        sigma_ = sigmaLow;
        
        // Check if sigma = 0 is root
        if (mag(fLow) <= targetFunctionTol_)
        {
            m.invert();
            sigma_ = 0.0;
            nullSigma_ = true;
            secondaryQuadrature(m);
            
            return;
        }

        // Compute target function for sigma = sigmaMax 
        scalar sigMax = sigmaMax(m);    
        scalar sigmaHigh = sigMax;
        scalar fHigh = targetFunction(sigmaHigh, m, mStar);

        if (fLow*fHigh > 0)
        {
            // Root not found. Minimize target function in [0, sigma_]
            sigma_ = minimizeTargetFunction(0, sigmaHigh, m, mStar);
            
            // Check if sigma is small and use QMOM
            if (mag(sigma_) < sigmaTol_)
            {
                m.invert();
                sigma_ = 0.0;
                nullSigma_ = true;
                secondaryQuadrature(m);

                return;
            }
            
            targetFunction(sigma_, m, mStar);
            secondaryQuadrature(mStar);

            return;
        }

        // Apply Ridder's algorithm to find sigma
        for (label iter = 0; iter < maxSigmaIter_; iter++)
        {           
            scalar fMid, sigmaMid;

            sigmaMid = (sigmaLow + sigmaHigh)/2.0;
            fMid = targetFunction(sigmaMid, m, mStar);

            scalar s = sqrt(sqr(fMid) - fLow*fHigh);

            if (s == 0.0)
            {
                FatalErrorIn
                (
                    "Foam::extendedMomentInversion::invert\n"
                    "(\n"
                    "   const univariateMomentSet& moments\n"
                    ")"
                )<< "Singular value encountered while attempting to find root."
                 << "Moment set = " << m << endl
                 << "sigma = " << sigma_ << endl
                 << "fLow = " << fLow << endl
                 << "fMid = " << fMid << endl
                 << "fHigh = " << fHigh
                 << abort(FatalError);
            }

            sigma_ = sigmaMid + (sigmaMid - sigmaLow)*sign(fLow - fHigh)*fMid/s;
            
            momentsToMomentsStar(sigma_, m, mStar);

            scalar fNew = targetFunction(sigma_, m, mStar);
            scalar dSigma = (sigmaHigh - sigmaLow)/2.0;

            // Check for convergence
            if (mag(fNew) <= targetFunctionTol_ || mag(dSigma) <= sigmaTol_)
            {             
                if (mag(sigma_) < sigmaTol_)
                {
                    m.invert();
                    sigma_ = 0.0;
                    nullSigma_ = true;
                    secondaryQuadrature(m);

                    return;
                }
                
                scalar momentError = normalizedMomentError(sigma_, m, mStar);

                if 
                (
                    momentError < momentsTol_
                )
                {
                    // Found a value of sigma that preserves all the moments
                    secondaryQuadrature(mStar);

                    return;
                }
                else
                {
                    // Root not found. Minimize target function in [0, sigma_]
                    sigma_ = minimizeTargetFunction(0, sigma_, m, mStar);
                    
                    if (mag(sigma_) < sigmaTol_)
                    {
                        m.invert();
                        sigma_ = 0.0;
                        nullSigma_ = true;
                        secondaryQuadrature(m);
                        
                        return;
                    }

                    targetFunction(sigma_, m, mStar);
                    secondaryQuadrature(mStar);

                    return;
                }
            }
            else
            {
                if (fNew*fMid < 0 && sigma_ < sigmaMid)
                {
                    sigmaLow = sigma_;
                    fLow = fNew;
                    sigmaHigh = sigmaMid;
                    fHigh = fMid;
                }
                else if (fNew*fMid < 0 && sigma_ > sigmaMid)
                {
                    sigmaLow = sigmaMid;
                    fLow = fMid;
                    sigmaHigh = sigma_;
                    fHigh = fNew;
                }
                else if (fNew*fLow < 0)
                {
                    sigmaHigh = sigma_;
                    fHigh = fNew;
                }
                else if (fNew*fHigh < 0)
                {
                    sigmaLow = sigma_;
                    fLow = fNew;
                }
            }
        }

        FatalErrorIn
        (
            "Foam::extendedMomentInversion::invert\n"
            "(\n"
            "   const univariateMomentSet& moments\n"
            ")"
        )   << "Number of iterations exceeded."
            << abort(FatalError);
    }
}
// * * * * * * * * * * * * * * * Member Functions  * * * * * * * * * * * * * //
void Foam::extendedMomentInversion::invert(const univariateMomentSet& moments)
{
    univariateMomentSet m(moments);

    reset();

    // Terminate execution if negative number density is encountered
    if (m[0] < 0.0)
    {
        FatalErrorInFunction
            << "The zero-order moment is negative."
            << abort(FatalError);
    }

    // Exclude cases where the zero-order moment is very small to avoid
    // problems in the inversion due to round-off error
    if (m[0] < SMALL)
    {
        sigma_ = 0.0;
        nullSigma_ = true;

        return;
    }

    label nRealizableMoments = m.nRealizableMoments();

    // If the moment set is on the boundary of the moment space, the
    // distribution will be reconstructed by a summation of Dirac delta,
    // and no attempt to use the extended quadrature method of moments is made.
    if (m.isOnMomentSpaceBoundary())
    {

        sigma_ = 0.0;
        nullSigma_ = true;
        m.invert();
        secondaryQuadrature(m);

        return;
    }

    if (nRealizableMoments % 2 == 0)
    {
        // If the number of realizable moments is even, we apply the standard
        // QMOM directly to maximize the number of preserved moments.

        m.invert();
        sigma_ = 0.0;
        nullSigma_ = true;
        secondaryQuadrature(m);
    }
    else
    {
        // Do not attempt the EQMOM reconstruction if mean or variance of the
        //  moment set are small to avoid numerical problems. These problems are
        // particularly acute in the calculation of the recurrence relationship
        // of the Jacobi orthogonal polynomials used for the beta kernel density
        // function.
        if (m[1]/m[0] < minMean_ || (m[2]/m[0] - sqr(m[1]/m[0])) < minVariance_)
        {
            sigma_ = 0.0;
            nullSigma_ = true;

            m.invert();
            secondaryQuadrature(m);

            return;
        }

        // Resizing the moment set to avoid copying again
        m.resize(nRealizableMoments);

        // Local set of starred moments
        univariateMomentSet mStar(nRealizableMoments, 0, m.support());

        // Compute target function for sigma = 0
        scalar sigmaLow = 0.0;
        scalar fLow = targetFunction(sigmaLow, m, mStar);
        sigma_ = sigmaLow;

        // Check if sigma = 0 is root
        if (mag(fLow) <= targetFunctionTol_)
        {
            m.invert();
            sigma_ = 0.0;
            nullSigma_ = true;
            secondaryQuadrature(m);

            return;
        }

        // Compute target function for sigma = sigmaMax
        scalar sigMax = sigmaMax(m);
        scalar sigmaHigh = sigMax;
        scalar fHigh = targetFunction(sigmaHigh, m, mStar);

        // This should not happen with the new algorithm
        if (fLow*fHigh > 0)
        {
            // Root not found. Minimize target function in [0, sigma_]
            sigma_ = minimizeTargetFunction(0, sigmaHigh, m, mStar);

            // If sigma_ is small, use QMOM
            if (mag(sigma_) < sigmaMin_)
            {
                m.invert();
                sigma_ = 0.0;
                nullSigma_ = true;
                secondaryQuadrature(m);

                return;
            }

            targetFunction(sigma_, m, mStar);
            secondaryQuadrature(mStar);

            return;
        }

        // Apply Ridder's algorithm to find sigma
        for (label iter = 0; iter < maxSigmaIter_; iter++)
        {
            scalar fMid, sigmaMid;

            sigmaMid = (sigmaLow + sigmaHigh)/2.0;
            fMid = targetFunction(sigmaMid, m, mStar);

            scalar s = sqrt(sqr(fMid) - fLow*fHigh);

            if (s == 0.0)
            {
                FatalErrorInFunction
                    << "Singular value encountered searching for root.\n"
                    << "Moment set = " << m << endl
                    << "sigma = " << sigma_ << endl
                    << "fLow = " << fLow << endl
                    << "fMid = " << fMid << endl
                    << "fHigh = " << fHigh
                    << abort(FatalError);
            }

            sigma_ = sigmaMid + (sigmaMid - sigmaLow)*sign(fLow - fHigh)*fMid/s;

            momentsToMomentsStar(sigma_, m, mStar);

            scalar fNew = targetFunction(sigma_, m, mStar);
            scalar dSigma = (sigmaHigh - sigmaLow)/2.0;

            // Check for convergence
            if (mag(fNew) <= targetFunctionTol_ || mag(dSigma) <= sigmaTol_)
            {
                // Root finding converged

                // If sigma_ is small, use QMOM
                if (mag(sigma_) < sigmaMin_)
                {
                    m.invert();
                    sigma_ = 0.0;
                    nullSigma_ = true;
                    secondaryQuadrature(m);

                    return;
                }

                scalar momentError = normalizedMomentError(sigma_, m, mStar);

                if
                (
                    momentError < momentsTol_
                )
                {
                    // Found a value of sigma that preserves all the moments
                    secondaryQuadrature(mStar);

                    return;
                }
                else
                {
                    // Root not found. Minimize target function in [0, sigma_]
                    sigma_ = minimizeTargetFunction(0, sigma_, m, mStar);

                    // If sigma_ is small, use QMOM
                    if (mag(sigma_) < sigmaMin_)
                    {
                        m.invert();
                        sigma_ = 0.0;
                        nullSigma_ = true;
                        secondaryQuadrature(m);

                        return;
                    }

                    targetFunction(sigma_, m, mStar);
                    secondaryQuadrature(mStar);

                    return;
                }
            }
            else
            {
                // Root finding did not converge. Refine search.

                if (fNew*fMid < 0 && sigma_ < sigmaMid)
                {
                    sigmaLow = sigma_;
                    fLow = fNew;
                    sigmaHigh = sigmaMid;
                    fHigh = fMid;
                }
                else if (fNew*fMid < 0 && sigma_ > sigmaMid)
                {
                    sigmaLow = sigmaMid;
                    fLow = fMid;
                    sigmaHigh = sigma_;
                    fHigh = fNew;
                }
                else if (fNew*fLow < 0)
                {
                    sigmaHigh = sigma_;
                    fHigh = fNew;
                }
                else if (fNew*fHigh < 0)
                {
                    sigmaLow = sigma_;
                    fLow = fNew;
                }
            }
        }

        FatalErrorInFunction
            << "Number of iterations exceeded.\n"
            << "Max allowed iterations = " << maxSigmaIter_
            << abort(FatalError);
    }
}