Example #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));
        }
Example #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);
        }
Example #3
0
        public void OdeErf()
        {
            Func <double, double, double> rhs = (double t, double u) =>
                                                2.0 / Math.Sqrt(Math.PI) * Math.Exp(-t * t);

            OdeResult r = FunctionMath.IntegrateOde(rhs, 0.0, 0.0, 5.0);

            Console.WriteLine(r.Y);
        }
Example #4
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));
        }
Example #5
0
        public void OdeOrbit()
        {
            // This is a simple Keplerian orbit.
            // Hull (1972) constructed initial conditions that guarantee a given orbital eccentricity
            // and a period of 2 \pi with unit masses and unit gravitational constant.

            Func <double, IList <double>, IList <double> > rhs = (double t, IList <double> r) => {
                double d  = MoreMath.Hypot(r[0], r[1]);
                double d3 = MoreMath.Pow(d, 3);
                return(new double[] { -r[0] / d3, -r[1] / d3 });
            };

            EvaluationSettings settings = new EvaluationSettings()
            {
                RelativePrecision = 1.0E-12, AbsolutePrecision = 1.0E-24, EvaluationBudget = 10000
            };

            settings.UpdateHandler = (EvaluationResult a) => {
                OdeResult <IList <double> > b = (OdeResult <IList <double> >)a;
                Console.WriteLine("{0} {1}", b.EvaluationCount, b.X);
            };

            //double e = 0.5;
            foreach (double e in TestUtilities.GenerateUniformRealValues(0.0, 1.0, 8))
            {
                Console.WriteLine("e = {0}", e);

                ColumnVector r0  = new ColumnVector(1.0 - e, 0.0);
                ColumnVector rp0 = new ColumnVector(0.0, Math.Sqrt((1.0 + e) / (1.0 - e)));

                settings.UpdateHandler = (EvaluationResult a) => {
                    OdeResult <IList <double> > b = (OdeResult <IList <double> >)a;
                    Console.WriteLine("  {0} {1}", b.EvaluationCount, b.X);
                    Console.WriteLine("  {0} ?= {1}", OrbitEnergy(b.Y, b.YPrime), OrbitEnergy(r0, rp0));
                    Assert.IsTrue(TestUtilities.IsNearlyEqual(OrbitAngularMomentum(b.Y, b.YPrime), OrbitAngularMomentum(r0, rp0), settings));
                    Assert.IsTrue(TestUtilities.IsNearlyEqual(OrbitEnergy(b.Y, b.YPrime), OrbitEnergy(r0, rp0), new EvaluationSettings()
                    {
                        RelativePrecision = 2.0E-12
                    }));
                };

                ColumnVector r1 = MultiFunctionMath.SolveConservativeOde(rhs, 0.0, r0, rp0, 2.0 * Math.PI, settings);

                // The

                Assert.IsTrue(TestUtilities.IsNearlyEqual(r0, r1, new EvaluationSettings()
                {
                    RelativePrecision = 1.0E-9
                }));
            }
        }
Example #6
0
        public void OdeExponential()
        {
            // The exponential function y = e^x satisfies
            //    y' = y
            // This is perhaps the simplest differential equation.

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

            OdeResult r = FunctionMath.IntegrateOde(f, 0.0, 1.0, 2.0);

            Assert.IsTrue(TestUtilities.IsNearlyEqual(r.Y, MoreMath.Sqr(Math.E)));

            Console.WriteLine(r.EvaluationCount);
        }
        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);
        }
Example #8
0
        public void OdeExample()
        {
            Func <double, double, double> rhs      = (double t, double u) => (1.0 - 2.0 * t) * u;
            Func <double, double, double> solution = (double u0, double t) => u0 *Math.Exp(t - t *t);

            int count = 0;

            foreach (double t in new double[] { 0.5, 0.75, 1.50, 2.25, 3.25 })
            {
                OdeResult r  = FunctionMath.IntegrateOde(rhs, 0.0, 1.0, t);
                double    y1 = r.Y;
                Assert.IsTrue(TestUtilities.IsNearlyEqual(y1, solution(1.0, t)));
                count += r.EvaluationCount;
            }
            Console.WriteLine(count);
        }
Example #9
0
        public void OdeNonlinear()
        {
            // y = \frac{y_0}{1 - y_0 (x - x_0)}
            Func <double, double, double> f = (double x, double y) => MoreMath.Sqr(y);

            int         count    = 0;
            OdeSettings settings = new OdeSettings()
            {
                RelativePrecision = 1.0E-8,
                EvaluationBudget  = 1024,
                Listener          = (OdeResult) => count++
            };
            OdeResult result = FunctionMath.IntegrateOde(f, 0.0, 1.0, 0.99, settings);

            Assert.IsTrue(TestUtilities.IsNearlyEqual(result.Y, 1.0 / (1.0 - 1.0 * (0.99 - 0.0)), result.Settings));

            Assert.IsTrue(count > 0);

            Console.WriteLine(result.EvaluationCount);
        }
Example #10
0
        public void OdeLogistic()
        {
            // y = \frac{y_0}{y_0 + (1 - y_0) e^{-(x - x_0)}
            Func <double, double, double> rhs      = (double x, double y) => y * (1.0 - y);
            Func <double, double, double> solution = (double y0, double x) => y0 / (y0 + (1.0 - y0) * Math.Exp(-x));

            int count = 0;

            foreach (double y0 in new double[] { -0.1, 0.0, 0.4, 1.0, 1.6 })
            {
                Console.WriteLine(y0);
                Interval  r  = Interval.FromEndpoints(0.0, 2.0);
                OdeResult s  = FunctionMath.IntegrateOde(rhs, 0.0, y0, 2.0);
                double    y1 = s.Y;
                Console.WriteLine(y1);
                Assert.IsTrue(TestUtilities.IsNearlyEqual(y1, solution(y0, 2.0)));
                count += s.EvaluationCount;
            }
            Console.WriteLine(count);
        }
Example #11
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);
        }
Example #12
0
        public void OdeDawson()
        {
            // The Dawson function fulfills a simple ODE.
            //   \frac{dF}{dx} + 2 x F = 1 \qquad F(0) = 0
            // See e.g. https://en.wikipedia.org/wiki/Dawson_function
            // Verify that we get correct values via ODE integration.

            Func <double, double, double> rhs = (double x, double F) => 1.0 - 2.0 * x * F;

            foreach (double x1 in TestUtilities.GenerateRealValues(0.1, 10.0, 8))
            {
                EvaluationSettings s = new EvaluationSettings()
                {
                    RelativePrecision = 1.0E-13,
                    AbsolutePrecision = 0.0
                };
                OdeResult r = FunctionMath.IntegrateOde(rhs, 0.0, 0.0, x1);
                Debug.WriteLine("{0}: {1} {2}: {3}", x1, r.Y, AdvancedMath.Dawson(x1), r.EvaluationCount);
                Assert.IsTrue(TestUtilities.IsNearlyEqual(r.Y, AdvancedMath.Dawson(x1), s));
            }
        }
Example #13
0
        public void OdeEuler()
        {
            // Euler's equations for rigid body motion (without external forces) are
            //   I_1 \dot{\omega}_1  = (I_2 - I_3) \omega_2 \omega_3
            //   I_2 \dot{\omega}_2  = (I_3 - I_1) \omega_3 \omega_1
            //   I_3 \dot{\omega}_3  = (I_1 - I_2) \omega_1 \omega_2
            // where \omega's are rotations about the principal axes and I's are the moments
            // of inertia about those axes. The rotational kinetic energy
            //   I_1 \omega_1^2 + I_2 \omega_2^2 + I_3 \omega_3^2 = 2E
            // and angular momentum
            //   I_1^2 \omega_1^2 + I_2^2 \omega_2^2 + I_3^2 \omega_3^2 = M^2
            // are conserved quantities.

            ColumnVector I = new ColumnVector(1.0, 2.0, 3.0);

            Func <double, IList <double>, IList <double> > rhs = (double t, IList <double> w) => {
                return(new ColumnVector((I[1] - I[2]) * w[1] * w[2] / I[0], (I[2] - I[0]) * w[2] * w[0] / I[1], (I[0] - I[1]) * w[0] * w[1] / I[2]));
            };

            ColumnVector w0 = new ColumnVector(1.0, 1.0, 1.0);

            double             eps      = 1.0E-12;
            EvaluationSettings settings = new EvaluationSettings()
            {
                RelativePrecision = eps,
                AbsolutePrecision = eps * eps,
                EvaluationBudget  = 10000
            };

            settings.UpdateHandler = (EvaluationResult r) => {
                OdeResult <IList <double> > q = (OdeResult <IList <double> >)r;
                Console.WriteLine("{0} {1}", q.EvaluationCount, q.X);
                Assert.IsTrue(TestUtilities.IsNearlyEqual(EulerKineticEnergy(I, q.Y), EulerKineticEnergy(I, w0), settings));
                Assert.IsTrue(TestUtilities.IsNearlyEqual(EulerAngularMomentum(I, q.Y), EulerAngularMomentum(I, w0), settings));
            };

            MultiFunctionMath.SolveOde(rhs, 0.0, w0, 10.0, settings);
        }
            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 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));
            }
        }
        /// <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));
                }
            }
        }
        /// <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);
            }
        }