Пример #1
0
        public void OdeAiry()
        {
            // This is the airy differential equation
            Func <double, double, double> f = (double x, double y) => x * y;

            // Solutions should be of the form f(x) = a Ai(x) + b Bi(x).
            // Given initial value of f and f', this equation plus the Wronskian can be solved to give
            //   a = \pi ( f Bi' - f' Bi )    b = \pi ( f' Ai - f Ai' )

            // Start with some initial conditions
            double x0  = 0.0;
            double y0  = 0.0;
            double yp0 = 1.0;

            // Find the a and b coefficients consistent with those values
            SolutionPair s0 = AdvancedMath.Airy(x0);
            double       a  = Math.PI * (y0 * s0.SecondSolutionDerivative - yp0 * s0.SecondSolutionValue);
            double       b  = Math.PI * (yp0 * s0.FirstSolutionValue - y0 * s0.FirstSolutionDerivative);

            Assert.IsTrue(TestUtilities.IsNearlyEqual(y0, a * s0.FirstSolutionValue + b * s0.SecondSolutionValue));
            Assert.IsTrue(TestUtilities.IsNearlyEqual(yp0, a * s0.FirstSolutionDerivative + b * s0.SecondSolutionDerivative));

            // Integrate to a new point (pick a negative one so we test left integration)
            double    x1     = -5.0;
            OdeResult result = FunctionMath.IntegrateConservativeOde(f, x0, y0, yp0, x1);

            Assert.IsTrue(TestUtilities.IsNearlyEqual(result.X, x1));
            Console.WriteLine(result.EvaluationCount);

            // The solution should still hold
            SolutionPair s1 = AdvancedMath.Airy(x1);

            Assert.IsTrue(TestUtilities.IsNearlyEqual(result.Y, a * s1.FirstSolutionValue + b * s1.SecondSolutionValue, result.Settings));
            Assert.IsTrue(TestUtilities.IsNearlyEqual(result.YPrime, a * s1.FirstSolutionDerivative + b * s1.SecondSolutionDerivative, result.Settings));
        }
Пример #2
0
        public void OdeSine()
        {
            // The sine and cosine functions satisfy
            //   y'' = - y
            // This is perhaps the simplest conservative differential equation.
            // (i.e. right hand side depends only on y, not y')

            Func <double, double, double> f = (double x, double y) => - y;

            int         count    = 0;
            OdeSettings settings = new OdeSettings()
            {
                Listener = (OdeResult r) => {
                    Assert.IsTrue(TestUtilities.IsNearlyEqual(
                                      MoreMath.Sqr(r.Y) + MoreMath.Sqr(r.YPrime), 1.0, r.Settings
                                      ));
                    Assert.IsTrue(TestUtilities.IsNearlyEqual(
                                      r.Y, MoreMath.Sin(r.X), r.Settings
                                      ));
                    count++;
                }
            };
            OdeResult result = FunctionMath.IntegrateConservativeOde(f, 0.0, 0.0, 1.0, 5.0, settings);

            Assert.IsTrue(TestUtilities.IsNearlyEqual(result.Y, MoreMath.Sin(5.0)));
            Assert.IsTrue(count > 0);
        }
Пример #3
0
        public static void IntegrateOde()
        {
            Func <double, double, double> rhs = (x, y) => - x * y;
            OdeResult sln = FunctionMath.IntegrateOde(rhs, 0.0, 1.0, 2.0);

            Console.WriteLine($"Numeric solution y({sln.X}) = {sln.Y}.");
            Console.WriteLine($"Required {sln.EvaluationCount} evaluations.");
            Console.WriteLine($"Analytic solution y({sln.X}) = {Math.Exp(-MoreMath.Sqr(sln.X) / 2.0)}");

            // Lotka-Volterra equations
            double A = 0.1;
            double B = 0.02;
            double C = 0.4;
            double D = 0.02;
            Func <double, IReadOnlyList <double>, IReadOnlyList <double> > lkRhs = (t, y) => {
                return(new double[] {
                    A *y[0] - B * y[0] * y[1], D *y[0] * y[1] - C * y[1]
                });
            };
            MultiOdeSettings lkSettings = new MultiOdeSettings()
            {
                Listener = r => { Console.WriteLine($"t={r.X} rabbits={r.Y[0]}, foxes={r.Y[1]}"); }
            };

            MultiFunctionMath.IntegrateOde(lkRhs, 0.0, new double[] { 20.0, 10.0 }, 50.0, lkSettings);

            Func <double, IReadOnlyList <double>, IReadOnlyList <double> > rhs1 = (x, u) => {
                return(new double[] { u[1], -u[0] });
            };
            MultiOdeSettings settings1 = new MultiOdeSettings()
            {
                EvaluationBudget = 100000
            };
            MultiOdeResult result1 = MultiFunctionMath.IntegrateOde(
                rhs1, 0.0, new double[] { 0.0, 1.0 }, 500.0, settings1
                );
            double s1 = MoreMath.Sqr(result1.Y[0]) + MoreMath.Sqr(result1.Y[1]);

            Console.WriteLine($"y({result1.X}) = {result1.Y[0]}, (y)^2 + (y')^2 = {s1}");
            Console.WriteLine($"Required {result1.EvaluationCount} evaluations.");

            Func <double, double, double> rhs2 = (x, y) => - y;
            OdeSettings settings2 = new OdeSettings()
            {
                EvaluationBudget = 100000
            };
            OdeResult result2 = FunctionMath.IntegrateConservativeOde(
                rhs2, 0.0, 0.0, 1.0, 500.0, settings2
                );
            double s2 = MoreMath.Sqr(result2.Y) + MoreMath.Sqr(result2.YPrime);

            Console.WriteLine($"y({result2.X}) = {result2.Y}, (y)^2 + (y')^2 = {s2}");
            Console.WriteLine($"Required {result2.EvaluationCount} evaluations");

            Console.WriteLine(MoreMath.Sin(500.0));
        }
        private static double CoulombF_Integrate(int L, double eta, double rho)
        {
            // start at the series limit
            double rho1 = Math.Min(
                4.0 + 2.0 * Math.Sqrt(L),
                (8.0 + 4.0 * L) / Math.Abs(eta)
                );

            double F, FP;

            CoulombF_Series(L, eta, rho1, out F, out FP);

            // TODO: switch so we integrate w/o the C factor, then apply it afterward
            if ((F == 0.0) && (FP == 0.0))
            {
                return(0.0);
            }

            OdeResult r = FunctionMath.IntegrateConservativeOde(
                (double x, double y) => ((L * (L + 1) / x + 2.0 * eta) / x - 1.0) * y,
                rho1, F, FP, rho,
                new OdeEvaluationSettings()
            {
                RelativePrecision = 2.5E-13,
                AbsolutePrecision = 0.0,
                EvaluationBudget  = 8192 * 2
            }
                );

            /*
             * F = r.Y;
             * FP = r.YPrime;
             *
             * // integrate out to rho
             * BulrischStoerStoermerStepper s = new BulrischStoerStoermerStepper();
             * s.RightHandSide = delegate(double x, double U) {
             *  return ((L * (L + 1) / x / x + 2.0 * eta / x - 1.0) * U);
             * };
             * s.X = rho0;
             * s.Y = F;
             * s.YPrime = FP;
             * s.DeltaX = 0.25;
             * s.Accuracy = 2.5E-13;
             * s.Integrate(rho);
             */

            // return the result
            return(r.Y);
        }
Пример #5
0
        public void OdePendulum()
        {
            // Without the small-angle approximation, the period of a pendulum is not
            // independent of angle. Instead, it is given by
            //   P = 4 K(\sin(\phi_0) / 2)
            // where \phi_0 is the release angle and K is the complete elliptic function
            // of the first kind.
            // See https://en.wikipedia.org/wiki/Pendulum_(mathematics)
            // We compute for an initial angle of 45 degrees, far beyond a small angle.

            Func <double, double, double> rhs = (double t, double u) => - MoreMath.Sin(u);

            double u0 = Math.PI / 4.0;
            double p  = 4.0 * AdvancedMath.EllipticK(MoreMath.Sin(u0 / 2.0));

            OdeResult r = FunctionMath.IntegrateConservativeOde(rhs, 0.0, u0, 0.0, p);

            Assert.IsTrue(TestUtilities.IsNearlyEqual(r.Y, u0));

            Console.WriteLine(r.EvaluationCount);
        }
Пример #6
0
            public double Evaluate()
            {
                CoulombF_Series(L, eta, rho0, out double F, out double FP);

                if ((F == 0.0) && (FP == 0.0))
                {
                    return(0.0);
                }

                OdeResult r = FunctionMath.IntegrateConservativeOde(
                    (double x, double y) => ((L * (L + 1) / x + 2.0 * eta) / x - 1.0) * y,
                    rho0, F, FP, rho,
                    new OdeSettings()
                {
                    RelativePrecision = 2.5E-13,
                    AbsolutePrecision = 0.0,
                    EvaluationBudget  = 25000
                }
                    );

                double C = CoulombFactor(L, eta);

                return(C * r.Y);
            }
        /// <summary>
        /// Computes the irregular Coulomb wave function.
        /// </summary>
        /// <param name="L">The angular momentum number, which must be non-negative.</param>
        /// <param name="eta">The charge parameter, which can be postive or negative.</param>
        /// <param name="rho">The radial distance parameter, which must be non-negative.</param>
        /// <returns>The value of G<sub>L</sub>(&#x3B7;,&#x3C1;).</returns>
        /// <remarks>
        /// <para>For information on the Coulomb wave functions, see the remarks on <see cref="Coulomb" />.</para>
        /// </remarks>
        /// <exception cref="ArgumentOutOfRangeException"><paramref name="L"/> or <paramref name="rho"/> is negative.</exception>
        /// <seealso cref="Coulomb"/>
        /// <seealso cref="CoulombF"/>
        /// <seealso href="http://en.wikipedia.org/wiki/Coulomb_wave_function" />
        /// <seealso href="http://mathworld.wolfram.com/CoulombWaveFunction.html" />
        public static double CoulombG(int L, double eta, double rho)
        {
            if (L < 0)
            {
                throw new ArgumentOutOfRangeException(nameof(L));
            }
            if (rho < 0)
            {
                throw new ArgumentOutOfRangeException(nameof(rho));
            }

            if ((rho < 4.0) && Math.Abs(rho * eta) < 8.0)
            {
                // For small enough rho, use the power series for L=0, then recurse upward to desired L.
                double F, FP, G, GP;
                Coulomb_Zero_Series(eta, rho, out F, out FP, out G, out GP);
                Coulomb_Recurse_Upward(0, L, eta, rho, ref G, ref GP);
                return(G);
            }
            else if (rho > 32.0 + (L * L + eta * eta) / 2.0)
            {
                // For large enough rho, use the asymptotic series.
                SolutionPair s = Coulomb_Asymptotic(L, eta, rho);
                return(s.SecondSolutionValue);
            }
            else
            {
                // Transition region
                if (rho >= CoulombTurningPoint(L, eta))
                {
                    // Beyond the turning point, use Steed's method.
                    SolutionPair result = Coulomb_Steed(L, eta, rho);
                    return(result.SecondSolutionValue);
                }
                else
                {
                    // we will start at L=0 (which has a smaller turning point radius) and recurse up to the desired L
                    // this is okay because G increasees with increasing L

                    double G, GP;

                    double rho0 = 2.0 * eta;
                    if (rho < rho0)
                    {
                        // if inside the turning point even for L=0, start at the turning point and integrate in
                        // this is okay becaue G increases with decraseing rho

                        // use Steed's method at the turning point
                        // for large enough eta, we could use the turning point expansion at L=0, but it contributes
                        // a lot of code for little overall performance increase so we have chosen not to
                        SolutionPair result = Coulomb_Steed(0, eta, 2.0 * eta);
                        G  = result.SecondSolutionValue;
                        GP = result.SecondSolutionDerivative;

                        OdeResult r = FunctionMath.IntegrateConservativeOde(
                            (double x, double y) => (2.0 * eta / x - 1.0) * y,
                            rho0, G, GP, rho,
                            new OdeEvaluationSettings()
                        {
                            RelativePrecision = 2.5E-13,
                            AbsolutePrecision = 0.0,
                            EvaluationBudget  = 8192 * 2
                        }
                            );

                        G  = r.Y;
                        GP = r.YPrime;

                        /*
                         * BulrischStoerStoermerStepper s = new BulrischStoerStoermerStepper();
                         * s.RightHandSide = delegate(double x, double U) {
                         *  return ((2.0 * eta / x - 1.0) * U);
                         * };
                         * s.X = 2.0 * eta;
                         * s.Y = G;
                         * s.YPrime = GP;
                         * s.DeltaX = 0.25;
                         * s.Accuracy = 2.5E-13;
                         * s.Integrate(rho);
                         *
                         * G = s.Y;
                         * GP = s.YPrime;
                         */
                    }
                    else
                    {
                        // if beyond the turning point for L=0, just use Steeds method

                        SolutionPair result = Coulomb_Steed(0, eta, rho);
                        G  = result.SecondSolutionValue;
                        GP = result.SecondSolutionDerivative;
                    }


                    // Recurse up to the desired L.
                    Coulomb_Recurse_Upward(0, L, eta, rho, ref G, ref GP);

                    return(G);
                }
            }
        }
        /// <summary>
        /// Computes the regular and irregular Coulomb wave functions and their derivatives.
        /// </summary>
        /// <param name="L">The angular momentum number, which must be non-negative.</param>
        /// <param name="eta">The charge parameter, which can be postive or negative.</param>
        /// <param name="rho">The radial distance parameter, which must be non-negative.</param>
        /// <returns>The values of F, F', G, and G' for the given parameters.</returns>
        /// <remarks>
        /// <para>The Coulomb wave functions are the radial wave functions of a non-relativistic particle in a Coulomb
        /// potential.</para>
        /// <para>They satisfy the differential equation:</para>
        /// <img src="../images/CoulombODE.png" />
        /// <para>A repulsive potential is represented by &#x3B7; &gt; 0, an attractive potential by &#x3B7; &lt; 0.</para>
        /// <para>F is oscilatory in the region beyond the classical turning point. In the quantum tunneling region inside
        /// the classical turning point, F is exponentially supressed and vanishes at the origin, while G grows exponentially and
        /// diverges at the origin.</para>
        /// <para>Many numerical libraries compute Coulomb wave functions in the quantum tunneling region using a WKB approximation,
        /// which accurately determine only the first handfull of digits; our library computes Coulomb wave functions even in this
        /// computationaly difficult region to nearly full precision -- all but the last 3-4 digits can be trusted.</para>
        /// <para>The irregular Coulomb wave functions G<sub>L</sub>(&#x3B7;,&#x3C1;) are the complementary independent solutions
        /// of the same differential equation.</para>
        /// </remarks>
        /// <exception cref="ArgumentOutOfRangeException"><paramref name="L"/> or <paramref name="rho"/> is negative.</exception>
        /// <seealso cref="CoulombF"/>
        /// <seealso cref="CoulombG"/>
        /// <seealso href="http://en.wikipedia.org/wiki/Coulomb_wave_function" />
        /// <seealso href="http://mathworld.wolfram.com/CoulombWaveFunction.html" />
        public static SolutionPair Coulomb(int L, double eta, double rho)
        {
            if (L < 0)
            {
                throw new ArgumentOutOfRangeException(nameof(L));
            }
            if (rho < 0)
            {
                throw new ArgumentOutOfRangeException(nameof(rho));
            }

            if (rho == 0.0)
            {
                if (L == 0)
                {
                    double C = CoulombFactor(L, eta);
                    return(new SolutionPair(0.0, C, 1.0 / C, Double.NegativeInfinity));
                }
                else
                {
                    return(new SolutionPair(0.0, 0.0, Double.PositiveInfinity, Double.NegativeInfinity));
                }
            }
            else if ((rho < 4.0) && Math.Abs(rho * eta) < 8.0)
            {
                // Below the safe series radius for L=0, compute using the series
                double F, FP, G, GP;
                Coulomb_Zero_Series(eta, rho, out F, out FP, out G, out GP);

                // For higher L, recurse G upward, but compute F via the direct series.
                // G is safe to compute via recursion and F is not because G is increasing
                // rapidly and F is decreasing rapidly with increasing L.
                if (L > 0)
                {
                    CoulombF_Series(L, eta, rho, out F, out FP);
                    Coulomb_Recurse_Upward(0, L, eta, rho, ref G, ref GP);
                }
                return(new SolutionPair(F, FP, G, GP));
            }
            else if (rho > 32.0 + (L * L + eta * eta) / 2.0)
            {
                return(Coulomb_Asymptotic(L, eta, rho));
            }
            else
            {
                double rho0 = CoulombTurningPoint(L, eta);
                if (rho > rho0)
                {
                    return(Coulomb_Steed(L, eta, rho));
                }
                else
                {
                    // First F
                    double F, FP;

                    double rho1 = Math.Min(
                        4.0 + 2.0 * Math.Sqrt(L),
                        (8.0 + 4.0 * L) / Math.Abs(eta)
                        );

                    if (rho < rho1)
                    {
                        CoulombF_Series(L, eta, rho, out F, out FP);
                    }
                    else
                    {
                        CoulombF_Series(L, eta, rho1, out F, out FP);

                        OdeResult r = FunctionMath.IntegrateConservativeOde(
                            (double x, double y) => ((L * (L + 1) / x + 2.0 * eta) / x - 1.0) * y,
                            rho1, F, FP, rho,
                            new OdeEvaluationSettings()
                        {
                            RelativePrecision = 2.5E-13,
                            AbsolutePrecision = 0.0,
                            EvaluationBudget  = 8192 * 2
                        }
                            );

                        F  = r.Y;
                        FP = r.YPrime;
                    }

                    // Then G
                    double G, GP;

                    // For L = 0 the transition region is smaller, so we will determine G
                    // at L = 0, where non-integration methods apply over a larger region,
                    // and then recurse upward.

                    double rho2 = CoulombTurningPoint(0, eta);

                    if (rho > 32.0 + eta * eta / 2.0)
                    {
                        SolutionPair s = Coulomb_Asymptotic(0, eta, rho);
                        G  = s.SecondSolutionValue;
                        GP = s.SecondSolutionDerivative;
                    }
                    else if (rho > rho2)
                    {
                        SolutionPair s = Coulomb_Steed(0, eta, rho);
                        G  = s.SecondSolutionValue;
                        GP = s.SecondSolutionDerivative;
                    }
                    else
                    {
                        SolutionPair s = Coulomb_Steed(0, eta, rho2);
                        G  = s.SecondSolutionValue;
                        GP = s.SecondSolutionDerivative;

                        // Integrate inward from turning point.
                        // G increases and F decreases in this direction, so this is stable.
                        OdeResult r = FunctionMath.IntegrateConservativeOde(
                            (double x, double y) => (2.0 * eta / x - 1.0) * y,
                            rho2, G, GP, rho,
                            new OdeEvaluationSettings()
                        {
                            RelativePrecision = 2.5E-13,
                            AbsolutePrecision = 0.0,
                            EvaluationBudget  = 8192 * 2
                        }
                            );

                        G  = r.Y;
                        GP = r.YPrime;
                    }

                    Coulomb_Recurse_Upward(0, L, eta, rho, ref G, ref GP);

                    return(new SolutionPair(F, FP, G, GP));
                }
            }
        }
Пример #9
0
        /// <summary>
        /// Computes the irregular Coulomb wave function.
        /// </summary>
        /// <param name="L">The angular momentum number, which must be non-negative.</param>
        /// <param name="eta">The charge parameter, which can be positive or negative.</param>
        /// <param name="rho">The radial distance parameter, which must be non-negative.</param>
        /// <returns>The value of G<sub>L</sub>(&#x3B7;,&#x3C1;).</returns>
        /// <remarks>
        /// <para>For information on the Coulomb wave functions, see the remarks on <see cref="Coulomb" />.</para>
        /// </remarks>
        /// <exception cref="ArgumentOutOfRangeException"><paramref name="L"/> or <paramref name="rho"/> is negative.</exception>
        /// <seealso cref="Coulomb"/>
        /// <seealso cref="CoulombF"/>
        /// <seealso href="http://en.wikipedia.org/wiki/Coulomb_wave_function" />
        /// <seealso href="http://mathworld.wolfram.com/CoulombWaveFunction.html" />
        public static double CoulombG(int L, double eta, double rho)
        {
            if (L < 0)
            {
                throw new ArgumentOutOfRangeException(nameof(L));
            }
            if (rho < 0)
            {
                throw new ArgumentOutOfRangeException(nameof(rho));
            }

            if (rho <= Coulomb_Series_Limit(0, eta))
            {
                // For small enough rho, use the power series for L=0, then recurse upward to desired L.
                Coulomb_Zero_Series(eta, rho, out double F, out double FP, out double G, out double GP);
                Coulomb_Recurse_Upward(0, L, eta, rho, ref G, ref GP);
                return(G);
            }
            else if (rho >= Coulomb_Asymptotic_Limit(L, eta))
            {
                // For large enough rho, use the asymptotic series.
                SolutionPair s = Coulomb_Asymptotic(L, eta, rho);
                return(s.SecondSolutionValue);
            }
            else if (rho >= CoulombTurningPoint(L, eta))
            {
                // Beyond the turning point, use Steed's method.
                SolutionPair result = Coulomb_Steed(L, eta, rho);
                return(result.SecondSolutionValue);
            }
            else
            {
                // We will start at L=0 (which has a smaller turning point radius) and recurse up to the desired L.
                // This is okay because G increases with increasing L. We already know that we are beyond the L=0
                // series limit; otherwise we would have taken the branch for it above. We might still be
                // in the asymptotic region, in the Steed region, below the L=0 turning point (if \eta > 0).
                double G, GP;
                if (rho >= Coulomb_Asymptotic_Limit(0, eta))
                {
                    SolutionPair s = Coulomb_Asymptotic(0, eta, rho);
                    G  = s.SecondSolutionValue;
                    GP = s.SecondSolutionDerivative;
                }
                else
                {
                    double rho1 = CoulombTurningPoint(0, eta);
                    if (rho >= rho1)
                    {
                        SolutionPair s = Coulomb_Steed(0, eta, rho);
                        G  = s.SecondSolutionValue;
                        GP = s.SecondSolutionDerivative;
                    }
                    else
                    {
                        SolutionPair s = Coulomb_Steed(0, eta, rho1);
                        G  = s.SecondSolutionValue;
                        GP = s.SecondSolutionDerivative;

                        OdeResult r = FunctionMath.IntegrateConservativeOde(
                            (double x, double y) => (2.0 * eta / x - 1.0) * y,
                            rho1, G, GP, rho,
                            new OdeSettings()
                        {
                            RelativePrecision = 2.5E-13,
                            AbsolutePrecision = 0.0,
                            EvaluationBudget  = 25000
                        }
                            );

                        G  = r.Y;
                        GP = r.YPrime;
                    }
                }

                Coulomb_Recurse_Upward(0, L, eta, rho, ref G, ref GP);

                return(G);
            }
        }
Пример #10
0
        /// <summary>
        /// Computes the regular and irregular Coulomb wave functions and their derivatives.
        /// </summary>
        /// <param name="L">The angular momentum number, which must be non-negative.</param>
        /// <param name="eta">The charge parameter, which can be positive or negative.</param>
        /// <param name="rho">The radial distance parameter, which must be non-negative.</param>
        /// <returns>The values of F, F', G, and G' for the given parameters.</returns>
        /// <remarks>
        /// <para>The Coulomb wave functions are the radial wave functions of a non-relativistic particle in a Coulomb
        /// potential.</para>
        /// <para>They satisfy the differential equation:</para>
        /// <img src="../images/CoulombODE.png" />
        /// <para>A repulsive potential is represented by &#x3B7; &gt; 0, an attractive potential by &#x3B7; &lt; 0.</para>
        /// <para>F is oscillatory in the region beyond the classical turning point. In the quantum tunneling region inside
        /// the classical turning point, F is exponentially suppressed and vanishes at the origin, while G grows exponentially and
        /// diverges at the origin.</para>
        /// <para>Many numerical libraries compute Coulomb wave functions in the quantum tunneling region using a WKB approximation,
        /// which accurately determine only the first few decimal digits; our library computes Coulomb wave functions even in this
        /// computationally difficult region to nearly full precision -- all but the last 4-5 decimal digits can be trusted.</para>
        /// <para>The irregular Coulomb wave functions G<sub>L</sub>(&#x3B7;,&#x3C1;) are the complementary independent solutions
        /// of the same differential equation.</para>
        /// </remarks>
        /// <exception cref="ArgumentOutOfRangeException"><paramref name="L"/> or <paramref name="rho"/> is negative.</exception>
        /// <seealso cref="CoulombF"/>
        /// <seealso cref="CoulombG"/>
        /// <seealso href="http://en.wikipedia.org/wiki/Coulomb_wave_function" />
        /// <seealso href="http://mathworld.wolfram.com/CoulombWaveFunction.html" />
        public static SolutionPair Coulomb(int L, double eta, double rho)
        {
            if (L < 0)
            {
                throw new ArgumentOutOfRangeException(nameof(L));
            }
            if (rho < 0)
            {
                throw new ArgumentOutOfRangeException(nameof(rho));
            }

            if (rho == 0.0)
            {
                if (L == 0)
                {
                    double C = CoulombFactor(L, eta);
                    return(new SolutionPair(0.0, C, 1.0 / C, Double.NegativeInfinity));
                }
                else
                {
                    return(new SolutionPair(0.0, 0.0, Double.PositiveInfinity, Double.NegativeInfinity));
                }
            }
            else if (rho <= Coulomb_Series_Limit(0, eta))
            {
                // Below the safe series radius for L=0, compute using the series.
                Coulomb_Zero_Series(eta, rho, out double F, out double FP, out double G, out double GP);

                // For higher L, recurse G upward, but compute F via the direct series.
                // G is safe to compute via recursion and F is not because G is increasing
                // rapidly and F is decreasing rapidly with increasing L. Since the series
                // for F was good for L = 0, it's certainly good for L > 0.
                if (L > 0)
                {
                    CoulombF_Series(L, eta, rho, out F, out FP);
                    double C = CoulombFactor(L, eta);
                    F  *= C;
                    FP *= C;
                    Coulomb_Recurse_Upward(0, L, eta, rho, ref G, ref GP);
                }
                return(new SolutionPair(F, FP, G, GP));
            }
            else if (rho >= Coulomb_Asymptotic_Limit(L, eta))
            {
                return(Coulomb_Asymptotic(L, eta, rho));
            }
            else if (rho >= CoulombTurningPoint(L, eta))
            {
                return(Coulomb_Steed(L, eta, rho));
            }
            else
            {
                // This code is copied from CoulombG; factor it into a seperate method.
                double G, GP;
                if (rho >= Coulomb_Asymptotic_Limit(0, eta))
                {
                    SolutionPair s = Coulomb_Asymptotic(0, eta, rho);
                    G  = s.SecondSolutionValue;
                    GP = s.SecondSolutionDerivative;
                }
                else
                {
                    double rho1 = CoulombTurningPoint(0, eta);
                    if (rho >= rho1)
                    {
                        SolutionPair s = Coulomb_Steed(0, eta, rho);
                        G  = s.SecondSolutionValue;
                        GP = s.SecondSolutionDerivative;
                    }
                    else
                    {
                        SolutionPair s = Coulomb_Steed(0, eta, rho1);
                        G  = s.SecondSolutionValue;
                        GP = s.SecondSolutionDerivative;

                        OdeResult r = FunctionMath.IntegrateConservativeOde(
                            (double x, double y) => (2.0 * eta / x - 1.0) * y,
                            rho1, G, GP, rho,
                            new OdeSettings()
                        {
                            RelativePrecision = 2.5E-13,
                            AbsolutePrecision = 0.0,
                            EvaluationBudget  = 25000
                        }
                            );

                        G  = r.Y;
                        GP = r.YPrime;
                    }
                }

                Coulomb_Recurse_Upward(0, L, eta, rho, ref G, ref GP);

                // We can determine F and FP via CF1 and Wronskian, but if we are within
                // series limit, it's likely a little faster and more accurate.
                double F, FP;
                if (rho < Coulomb_Series_Limit(L, eta))
                {
                    CoulombF_Series(L, eta, rho, out F, out FP);
                    double C = CoulombFactor(L, eta);
                    F  *= C;
                    FP *= C;
                }
                else
                {
                    double f = Coulomb_CF1(L, eta, rho, out int _);
                    F  = 1.0 / (f * G - GP);
                    FP = f * F;
                }

                return(new SolutionPair(F, FP, G, GP));
            }
        }