picomath

Ada

gamma.a

generic
    type Float_Type is digits <>;
package Gamma is

    function Gamma(x: Float_Type'Base) return Float_Type'Base;
    function Log_Gamma(x: Float_Type'Base) return Float_Type'Base;

end Gamma;

with Ada.Numerics.Generic_Elementary_Functions;

package body Gamma is

    package Elementary_Functions is new Ada.Numerics.Generic_Elementary_Functions(Float_Type'Base);
    use Elementary_Functions;

    function Positive_Infinity return Float_Type'Base is

        result: Float_Type'Base := Float_Type'Base'Last;

    begin
        return Float_Type'Base'Succ(result);
    exception
        when Constraint_Error =>
            return result;
    end Positive_Infinity;

    function Gamma(x: Float_Type'Base) return Float_Type'Base is

        gamma: constant := 0.577215664901532860606512090; -- Euler's gamma constant

        -- numerator coefficients for approximation over the interval (1,2)
        p: constant array(1..8) of Float_Type'Base := (
            -1.71618513886549492533811E+0,
             2.47656508055759199108314E+1,
            -3.79804256470945635097577E+2,
             6.29331155312818442661052E+2,
             8.66966202790413211295064E+2,
            -3.14512729688483675254357E+4,
            -3.61444134186911729807069E+4,
             6.64561438202405440627855E+4
        );

        -- denominator coefficients for approximation over the interval (1,2)
        q: constant array(1..8) of Float_Type'Base := (
            -3.08402300119738975254353E+1,
             3.15350626979604161529144E+2,
            -1.01515636749021914166146E+3,
            -3.10777167157231109440444E+3,
             2.25381184209801510330112E+4,
             4.75584627752788110767815E+3,
            -1.34659959864969306392456E+5,
            -1.15132259675553483497211E+5
        );

        y, num, den, z, result: Float_Type'Base;
        n: Natural;
        arg_was_less_than_one: Boolean;

    begin
        if x <= 0.0 then
            raise Ada.Numerics.Argument_Error;
        end if;

        -- Split the function domain into three intervals:
        -- (0, 0.001), [0.001, 12), and (12, infinity)

        ---------------------------------------------------------------------------
        -- First interval: (0, 0.001)
        --
        -- For small x, 1/Gamma(x) has power series x + gamma x^2  - ...
        -- So in this range, 1/Gamma(x) = x + gamma x^2 with error on the order of x^3.
        -- The relative error over this interval is less than 6e-7.

        if x < 0.001 then
            return 1.0/(x*(1.0 + gamma*x));

        ---------------------------------------------------------------------------
        -- Second interval: [0.001, 12)

        elsif x < 12.0 then
            -- The algorithm directly approximates gamma over (1,2) and uses
            -- reduction identities to reduce other arguments to this interval.
            
            y := x;
            n := 0;
            arg_was_less_than_one := (y < 1.0);

            -- Add or subtract integers as necessary to bring y into (1,2)
            -- Will correct for this below
            if arg_was_less_than_one then
                y := y + 1.0;
            else
                n := Natural(Float_Type'Base'Floor(y)) - 1;  -- will use n later
                y := y - Float_Type'Base(n);
            end if;

            num := 0.0;
            den := 1.0;

            z := y - 1.0;
            for i in 1..8 loop
                num := (num + p(i))*z;
                den := den*z + q(i);
            end loop;
            result := num/den + 1.0;

            -- Apply correction if argument was not initially in (1,2)
            if arg_was_less_than_one then
                -- Use identity gamma(z) = gamma(z+1)/z
                -- The variable "result" now holds gamma of the original y + 1
                -- Thus we use y-1 to get back the orginal y.
                result := result / (y-1.0);
            else
                -- Use the identity gamma(z+n) = z*(z+1)* ... *(z+n-1)*gamma(z)
                for i in 1..n loop
                    result := result * y;
                    y := y + 1.0;
                end loop;
            end if;

            return result;

        ---------------------------------------------------------------------------
        -- Third interval: [12, infinity)

        elsif x <= 171.624 then
            return Exp(Log_Gamma(x));

        else
            -- Correct answer too large to display. 
            return Positive_Infinity; -- float infinity

        end if;
    end Gamma;

    function Log_Gamma(x: Float_Type'Base) return Float_Type'Base is

        c: constant array(1..8) of Float_Type'Base := (
             1.0/12.0,
            -1.0/360.0,
             1.0/1260.0,
            -1.0/1680.0,
             1.0/1188.0,
            -691.0/360360.0,
             1.0/156.0,
            -3617.0/122400.0
        );
        halfLogTwoPi: constant := 0.91893853320467274178032973640562;

        z, sum, series: Float_Type'Base;

    begin
        if x <= 0.0 then
            raise Ada.Numerics.Argument_Error;
        end if;

        if x < 12.0 then
            return Log(Abs(Gamma(x)));
        end if;

        -- Abramowitz and Stegun 6.1.41
        -- Asymptotic series should be good to at least 11 or 12 figures
        -- For error analysis, see Whittiker and Watson
        -- A Course in Modern Analysis (1927), page 252

        z := 1.0/(x*x);
        sum := c(8);
        for i in reverse 1..7 loop
            sum := sum * z;
            sum := sum + c(i);
        end loop;
        series := sum/x;

        return (x - 0.5)*Log(x) - x + halfLogTwoPi + series;
    end Log_Gamma;

end Gamma;