/// <summary>
        /// Computes the regular 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 F<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="CoulombG"/>
        /// <seealso href="http://en.wikipedia.org/wiki/Coulomb_wave_function" />
        /// <seealso href="http://mathworld.wolfram.com/CoulombWaveFunction.html" />
        public static double CoulombF(int L, double eta, double rho)
        {
            if (L < 0)
            {
                throw new ArgumentOutOfRangeException(nameof(L));
            }
            if (rho < 0)
            {
                throw new ArgumentOutOfRangeException(nameof(rho));
            }

            double rho0 = Coulomb_Series_Limit(L, eta);

            if (rho <= rho0)
            {
                //if ((rho < 4.0 + 2.0 * Math.Sqrt(L)) && (Math.Abs(rho * eta) < 8.0  + 4.0 * L)) {
                // If rho and rho * eta are small enough, use the series expansion at the origin.
                CoulombF_Series(L, eta, rho, out double F, out double FP);
                double C = CoulombFactor(L, eta);
                return(C * F);
            }
            else if (rho >= Coulomb_Asymptotic_Limit(L, eta))
            {
                // If rho is large enough, use the asymptotic expansion.
                SolutionPair s = Coulomb_Asymptotic(L, eta, rho);
                return(s.FirstSolutionValue);
            }
            else if (rho >= CoulombTurningPoint(L, eta))
            {
                // Beyond the turning point, use the Barnett/Steed method.
                SolutionPair result = Coulomb_Steed(L, eta, rho);
                return(result.FirstSolutionValue);
            }
            else
            {
                // We are below the transition point but above the series limit.
                // Choose the approach with the least cost.
                ISolutionStrategy <double> above = new CoulombFFromSeriesAbove(L, eta, rho);
                ISolutionStrategy <double> left  = new CoulombFFromOutwardIntegration(L, eta, rho);

                if (left.Cost < above.Cost)
                {
                    return(left.Evaluate());
                }
                else
                {
                    return(above.Evaluate());
                }
            }
        }
Exemple #2
0
        /// <summary>
        /// Computes both Airy functions and their derivatives.
        /// </summary>
        /// <param name="x">The argument.</param>
        /// <returns>The values of Ai(x), Ai'(x), Bi(x), and Bi'(x).</returns>
        public static SolutionPair Airy(double x)
        {
            if (x < -2.0)
            {
                // Map to Bessel functions for negative values
                double       z  = 2.0 / 3.0 * Math.Pow(-x, 3.0 / 2.0);
                SolutionPair p  = Bessel(1.0 / 3.0, z);
                double       a  = (p.FirstSolutionValue - p.SecondSolutionValue / Global.SqrtThree) / 2.0;
                double       b  = (p.FirstSolutionValue / Global.SqrtThree + p.SecondSolutionValue) / 2.0;
                double       sx = Math.Sqrt(-x);
                return(new SolutionPair(
                           sx * a, (x * (p.FirstSolutionDerivative - p.SecondSolutionDerivative / Global.SqrtThree) - a / sx) / 2.0,
                           -sx * b, (b / sx - x * (p.FirstSolutionDerivative / Global.SqrtThree + p.SecondSolutionDerivative)) / 2.0
                           ));
            }
            else if (x < 2.0)
            {
                // Use series near origin
                return(Airy_Series(x));
            }
            else
            {
                // Map to modified Bessel functions for positive values
                double       z  = 2.0 / 3.0 * Math.Pow(x, 3.0 / 2.0);
                SolutionPair p  = ModifiedBessel(1.0 / 3.0, z);
                double       a  = 1.0 / (Global.SqrtThree * Math.PI);
                double       b  = 2.0 / Global.SqrtThree * p.FirstSolutionValue + p.SecondSolutionValue / Math.PI;
                double       sx = Math.Sqrt(x);
                return(new SolutionPair(
                           a * sx * p.SecondSolutionValue, a * (x * p.SecondSolutionDerivative + p.SecondSolutionValue / sx / 2.0),
                           sx * b, x * (2.0 / Global.SqrtThree * p.FirstSolutionDerivative + p.SecondSolutionDerivative / Math.PI) + b / sx / 2.0
                           ));
            }

            // NR recommends against using the Ai' and Bi' expressions obtained from simply differentiating the Ai and Bi expressions
            // They give instead expressions involving Bessel functions of different orders. But their reason is to avoid the cancellations
            // among terms ~1/x that get large near x ~ 0, and their method would require two Bessel evaluations.
            // Since we use the power series near x ~ 0, we avoid their cancelation problem and can get away with a single Bessel evaluation.

            // It might appear that we can optimize by not computing both \sqrt{x} and x^{3/2} explicitly, but instead computing \sqrt{x} and
            // then (\sqrt{x})^3. But the latter method looses accuracy, presumably because \sqrt{x} compresses range and cubing then expands
            // it, skipping over the double that is actuall closest to x^{3/2}. So we compute both explicitly.
        }
        /// <summary>
        /// Computes the regular 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 F<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="CoulombG"/>
        /// <seealso href="http://en.wikipedia.org/wiki/Coulomb_wave_function" />
        /// <seealso href="http://mathworld.wolfram.com/CoulombWaveFunction.html" />
        public static double CoulombF(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 + 2.0 * Math.Sqrt(L)) && (Math.Abs(rho * eta) < 8.0 + 4.0 * L))
            {
                // if rho and rho * eta are small enough, use the series expansion at the origin
                double F, FP;
                CoulombF_Series(L, eta, rho, out F, out FP);
                return(F);
            }
            else if (rho > 32.0 + (L * L + eta * eta) / 2.0)
            {
                // if rho is large enrough, use the asymptotic expansion
                SolutionPair s = Coulomb_Asymptotic(L, eta, rho);
                return(s.FirstSolutionValue);
                //double F, G;
                //Coulomb_Asymptotic(L, eta, rho, out F, out G);
                //return (F);
            }
            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.FirstSolutionValue);
                }
                else
                {
                    // inside the turning point, integrate out from the series limit
                    return(CoulombF_Integrate(L, eta, rho));
                }
            }
        }
Exemple #4
0
 /// <summary>
 /// Computes the Airy function of the second kind.
 /// </summary>
 /// <param name="x">The argument.</param>
 /// <returns>The value Bi(x).</returns>
 /// <remarks>
 /// <para>For information on the Airy functions, see <see cref="AdvancedMath.Airy"/>.</para>
 /// <para>While the notation Bi(x) was chosen simply as a natural complement to Ai(x), it has influenced the common
 /// nomenclature for this function, which is now often called the "Bairy function".</para>
 /// </remarks>
 /// <seealso cref="AiryAi"/>
 /// <seealso href="http://en.wikipedia.org/wiki/Airy_functions" />
 public static double AiryBi(double x)
 {
     if (x <= -14.0)
     {
         return(Airy_Asymptotic_Negative(-x).SecondSolutionValue);
     }
     else if (x < -3.0)
     {
         // Get from Bessel functions.
         double       y = 2.0 / 3.0 * Math.Pow(-x, 3.0 / 2.0);
         SolutionPair s = Bessel(1.0 / 3.0, y);
         return(-Math.Sqrt(-x) / 2.0 * (s.FirstSolutionValue / Global.SqrtThree + s.SecondSolutionValue));
     }
     else if (x < 5.0)
     {
         // The Bi series is better than the Ai series for positive values, because it blows up rather than cancelling down.
         // It's also slightly better for negative values, because a given number of oscillations occur further out.
         return(AiryBi_Series(x));
     }
     else if (x < 14.0)
     {
         // Get from modified Bessel functions.
         double       y = 2.0 / 3.0 * Math.Pow(x, 3.0 / 2.0);
         SolutionPair s = ModifiedBessel(1.0 / 3.0, y);
         return(Math.Sqrt(x) * (2.0 / Global.SqrtThree * s.FirstSolutionValue + s.SecondSolutionValue / Math.PI));
     }
     else if (x < 108.0)
     {
         SolutionPair s = Airy_Asymptotic_Positive_Scaled(x, out double xi);
         return(Math.Exp(xi) * s.SecondSolutionValue);
     }
     else if (x <= Double.PositiveInfinity)
     {
         return(Double.PositiveInfinity);
     }
     else
     {
         Debug.Assert(Double.IsNaN(x));
         return(x);
     }
 }
        // use Steed's method to compute F and G for a given L
        // the method uses a real continued fraction (1 constraint), an imaginary continued fraction (2 constraints)
        // and the Wronskian (4 constraints) to compute the 4 quantities F, F', G, G'
        // it is reliable past the truning point, but becomes slow if used far past the turning point

        // Solution (a la Barnett)
        //   F = s \left[ (f - p)^2 / q + q \right]^{-1/2}
        //   F' = f F
        //   G = g F \qquad g = (f - p) / q
        //   G' = (p g - q) F

        private static SolutionPair Coulomb_Steed(double L, double eta, double rho)
        {
            // compute CF1 (F'/F)
            double f = Coulomb_CF1(L, eta, rho, out int sign);

            // compute CF2 ((G' + iF')/(G + i F))
            Complex z = Coulomb_CF2(L, eta, rho);
            double  p = z.Re;
            double  q = z.Im;

            // use CF1, CF2, and Wronskian (FG' - GF' = 1) to solve for F, F', G, G'
            double g = (f - p) / q;

            double       F      = sign / Math.Sqrt(g * g * q + q);
            double       FP     = f * F;
            double       G      = g * F;
            double       GP     = (p * g - q) * F;
            SolutionPair result = new SolutionPair(F, FP, G, GP);

            return(result);
        }
Exemple #6
0
        // Neuman series no good for computing K0; it relies on a near-perfect cancelation between the I0 term
        // and the higher terms to achieve an exponentially supressed small value
#endif


        /// <summary>
        /// Computes the Airy function of the first kind.
        /// </summary>
        /// <param name="x">The argument.</param>
        /// <returns>The value Ai(x).</returns>
        /// <remarks>
        /// <para>Airy functions are solutions to the Airy differential equation:</para>
        /// <img src="../images/AiryODE.png" />
        /// <para>The Airy functions appear in quantum mechanics in the semiclassical WKB solution to the wave functions in a potential.</para>
        /// <para>For negative arguments, Ai(x) is oscilatory. For positive arguments, it decreases exponentially with increasing x.</para>
        /// </remarks>
        /// <seealso cref="AiryBi"/>
        /// <seealso href="http://en.wikipedia.org/wiki/Airy_functions" />
        public static double AiryAi(double x)
        {
            if (x < -3.0)
            {
                double       y = 2.0 / 3.0 * Math.Pow(-x, 3.0 / 2.0);
                SolutionPair s = Bessel(1.0 / 3.0, y);
                return(Math.Sqrt(-x) / 2.0 * (s.FirstSolutionValue - s.SecondSolutionValue / Global.SqrtThree));
            }
            else if (x < 2.0)
            {
                // Close to the origin, use a power series.
                // Convergence is slightly better for negative x than for positive, so we use it further out to the left of the origin.
                return(AiryAi_Series(x));
                // The definitions in terms of bessel functions becomes 0 X infinity at x=0, so we can't use them directly here anyway.
            }
            else
            {
                double y = 2.0 / 3.0 * Math.Pow(x, 3.0 / 2.0);
                return(Math.Sqrt(x / 3.0) / Math.PI * ModifiedBesselK(1.0 / 3.0, y));
            }
        }
Exemple #7
0
 /// <summary>
 /// Computes the Airy function of the first kind.
 /// </summary>
 /// <param name="x">The argument.</param>
 /// <returns>The value Ai(x).</returns>
 /// <remarks>
 /// <para>For information on the Airy functions, see <see cref="AdvancedMath.Airy"/>.</para>
 /// </remarks>
 /// <seealso cref="AiryBi"/>
 /// <seealso href="http://en.wikipedia.org/wiki/Airy_functions" />
 public static double AiryAi(double x)
 {
     if (x <= -14.0)
     {
         return(Airy_Asymptotic_Negative(-x).FirstSolutionValue);
     }
     else if (x < -3.0)
     {
         double       y = 2.0 / 3.0 * Math.Pow(-x, 3.0 / 2.0);
         SolutionPair s = Bessel(1.0 / 3.0, y);
         return(Math.Sqrt(-x) / 2.0 * (s.FirstSolutionValue - s.SecondSolutionValue / Global.SqrtThree));
     }
     else if (x < 2.0)
     {
         // Close to the origin, use a power series.
         // Convergence is slightly better for negative x than for positive, so we use it further out to the left of the origin.
         return(AiryAi_Series(x));
         // The definitions in terms of bessel functions become 0 X infinity at x=0, so we can't use them directly here anyway.
     }
     else if (x < 14.0)
     {
         double y = 2.0 / 3.0 * Math.Pow(x, 3.0 / 2.0);
         return(Math.Sqrt(x / 3.0) / Math.PI * ModifiedBesselK(1.0 / 3.0, y));
     }
     else if (x < 108.0)
     {
         SolutionPair s = Airy_Asymptotic_Positive_Scaled(x, out double xi);
         return(Math.Exp(-xi) * s.FirstSolutionValue);
     }
     else if (x <= Double.PositiveInfinity)
     {
         return(0.0);
     }
     else
     {
         Debug.Assert(Double.IsNaN(x));
         return(x);
     }
 }
        // use Steed's method to compute F and G for a given L
        // the method uses a real continued fraction (1 constraint), an imaginary continued fraction (2 constraints)
        // and the Wronskian (4 constraints) to compute the 4 quantities F, F', G, G'
        // it is reliable past the truning point, but becomes slow if used far past the turning point

        private static SolutionPair Coulomb_Steed(double L, double eta, double rho)
        {
            // compute CF1 (F'/F)
            int    sign;
            double f = Coulomb_CF1(L, eta, rho, out sign);

            // compute CF2 ((G' + iF')/(G + i F))
            Complex z = Coulomb_CF2(L, eta, rho);
            double  p = z.Re;
            double  q = z.Im;

            // use CF1, CF2, and Wronskian (FG' - GF' = 1) to solve for F, F', G, G'
            double g = (f - p) / q;

            SolutionPair result = new SolutionPair();

            result.FirstSolutionValue       = sign / Math.Sqrt(g * g * q + q);
            result.FirstSolutionDerivative  = f * result.FirstSolutionValue;
            result.SecondSolutionValue      = g * result.FirstSolutionValue;
            result.SecondSolutionDerivative = (p * g - q) * result.FirstSolutionValue;
            return(result);
        }
Exemple #9
0
 /// <summary>
 /// Computes the Airy function of the second kind.
 /// </summary>
 /// <param name="x">The argument.</param>
 /// <returns>The value Bi(x).</returns>
 /// <remarks>
 /// <para>For information on the Airy functions, see <see cref="AdvancedMath.Airy"/>.</para>
 /// <para>While the notation Bi(x) was chosen simply as a natural complement to Ai(x), it has influenced the common
 /// nomenclature for this function, which is now often called the "Bairy function".</para>
 /// </remarks>
 /// <seealso cref="AiryAi"/>
 /// <seealso href="http://en.wikipedia.org/wiki/Airy_functions" />
 public static double AiryBi(double x)
 {
     if (x < -3.0)
     {
         // Get from Bessel functions.
         double       y = 2.0 / 3.0 * Math.Pow(-x, 3.0 / 2.0);
         SolutionPair s = Bessel(1.0 / 3.0, y);
         return(-Math.Sqrt(-x) / 2.0 * (s.FirstSolutionValue / Global.SqrtThree + s.SecondSolutionValue));
     }
     else if (x < 5.0)
     {
         // The Bi series is better than the Ai series for positive values, because it blows up rather than cancelling down.
         // It's also slightly better for negative values, because a given number of oscillations occur further out.
         return(AiryBi_Series(x));
     }
     else
     {
         // Get from modified Bessel functions.
         double       y = 2.0 / 3.0 * Math.Pow(x, 3.0 / 2.0);
         SolutionPair s = ModifiedBessel(1.0 / 3.0, y);
         return(Math.Sqrt(x) * (2.0 / Global.SqrtThree * s.FirstSolutionValue + s.SecondSolutionValue / Math.PI));
     }
 }
 /// <summary>
 /// Computes the Airy function of the second kind.
 /// </summary>
 /// <param name="x">The argument.</param>
 /// <returns>The value Bi(x).</returns>
 /// <remarks>
 /// <para>For information on the Airy functions, see <see cref="AdvancedMath.Airy"/>.</para>
 /// <para>While the notation Bi(x) was chosen simply as a natural complement to Ai(x), it has influenced the common
 /// nomenclature for this function, which is now often called the "Bairy function".</para>
 /// </remarks>
 /// <seealso cref="AiryAi"/>
 /// <seealso href="http://en.wikipedia.org/wiki/Airy_functions" />
 public static double AiryBi(double x)
 {
     if (x < -3.0)
     {
         // change to use a function that returns J and Y together
         double y = 2.0 / 3.0 * Math.Pow(-x, 3.0 / 2.0);
         double J = BesselJ(1.0 / 3.0, y);
         double Y = BesselY(1.0 / 3.0, y);
         return(-Math.Sqrt(-x) / 2.0 * (J / Global.SqrtThree + Y));
     }
     else if (x < 5.0)
     {
         // The Bi series is better than the Ai series for positive values, because it blows up rather than down.
         // It's also slightly better for negative values, because a given number of oscilations occur further out.
         return(AiryBi_Series(x));
     }
     else
     {
         double       y = 2.0 / 3.0 * Math.Pow(x, 3.0 / 2.0);
         SolutionPair s = ModifiedBessel(1.0 / 3.0, y);
         return(Math.Sqrt(x) * (2.0 / Global.SqrtThree * s.FirstSolutionValue + s.SecondSolutionValue / Math.PI));
     }
 }
        // use Steed's method to compute F and G for a given L
        // the method uses a real continued fraction (1 constraint), an imaginary continued fraction (2 constraints)
        // and the Wronskian (4 constraints) to compute the 4 quantities F, F', G, G'
        // it is reliable past the truning point, but becomes slow if used far past the turning point
        private static SolutionPair Coulomb_Steed(double L, double eta, double rho)
        {
            // compute CF1 (F'/F)
            int sign;
            double f = Coulomb_CF1(L, eta, rho, out sign);

            // compute CF2 ((G' + iF')/(G + i F))
            Complex z = Coulomb_CF2(L, eta, rho);
            double p = z.Re;
            double q = z.Im;

            // use CF1, CF2, and Wronskian (FG' - GF' = 1) to solve for F, F', G, G'
            double g = (f - p) / q;

            SolutionPair result = new SolutionPair();
            result.FirstSolutionValue = sign / Math.Sqrt(g * g * q + q);
            result.FirstSolutionDerivative = f * result.FirstSolutionValue;
            result.SecondSolutionValue = g * result.FirstSolutionValue;
            result.SecondSolutionDerivative = (p * g - q) * result.FirstSolutionValue;
            return (result);
        }
        /// <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);
                }
            }
        }
        // **** Integer order Bessel functions ****

        /// <summary>
        /// Computes the regular Bessel function for integer orders.
        /// </summary>
        /// <param name="n">The order parameter.</param>
        /// <param name="x">The argument.</param>
        /// <returns>The value of J<sub>n</sub>(x).</returns>
        /// <remarks>
        /// <para>For information on the cylindrical Bessel functions, see <see cref="AdvancedMath.Bessel"/>.</para>
        /// </remarks>
        /// <seealso href="http://en.wikipedia.org/wiki/Bessel_function"/>
        /// <seealso href="https://mathworld.wolfram.com/BesselFunctionoftheFirstKind.html"/>
        public static double BesselJ(int n, double x)
        {
            // Relate negative n to positive n.
            if (n < 0)
            {
                if ((n % 2) == 0)
                {
                    return(BesselJ(-n, x));
                }
                else
                {
                    return(-BesselJ(-n, x));
                }
            }

            // Relate negative x to positive x.
            if (x < 0)
            {
                if ((n % 2) == 0)
                {
                    return(BesselJ(n, -x));
                }
                else
                {
                    return(-BesselJ(n, -x));
                }
            }

            if (x <= Math.Sqrt(2 * (n + 1)))
            {
                // If x is small enough, use the series.
                // The transition point is chosen so that 2nd term cannot overwhelm 1st.
                return(BesselJ_Series(n, x));
            }
            else if (x >= 32.0 + 0.5 * n * n)
            {
                // If x is large enough, use the asymptotic expansion.
                return(Bessel_Asymptotic(n, x).FirstSolutionValue);
            }
            else if ((x > n) && (n > 32))
            {
                // We are in the transition region \sqrt{n} < x < n^2. If x > n we are still allowed to recurr upward,
                // and as long as we can find an n below us for which x lies in the asymptotic region, we have a value
                // to recur upward from. In general we expect this to be better than Miller's algorithm, because
                // it will require fewer recurrance steps.

                int m = (int)Math.Floor(Math.Sqrt(2.0 * (x - 32.0)));
                Debug.Assert(m <= n);

                SolutionPair s  = Bessel_Asymptotic(m, x);
                double       J  = s.FirstSolutionValue;
                double       JP = s.FirstSolutionDerivative;
                Bessel_RecurrUpward(m, x, ref J, ref JP, n - m);
                return(J);
            }
            else
            {
                // We are in the transition region; x is too large for the series but still less than n. In this region
                // upward recurrance is unstable, since even a tiny mixture of the Y solution will come to dominate as n increases.

                // Instead we will use Miller's algorithm: recurr downward from far above and use a sum rule to normalize the result
                // The sum rule we will use is:
                //   \sum_{k=-\infty}^{\infty} J_{2k}(x) = J_0(x) + 2 J_2(x) + 2 J_4(x) + \cdots = 1
                return(Bessel_Miller(n, -1, x).FirstSolutionValue);
                //return (BesselJ_Miller(n, x));
            }
        }
        /// <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 regular Bessel function for real orders.
        /// </summary>
        /// <param name="nu">The order parameter.</param>
        /// <param name="x">The argument, which must be non-negative.</param>
        /// <returns>The value of J<sub>&#x3BD;</sub>(x).</returns>
        /// <remarks>
        /// <para>For information on the cylindrical Bessel functions, see <see cref="AdvancedMath.Bessel"/>.</para>
        /// </remarks>
        /// <exception cref="ArgumentOutOfRangeException"><paramref name="x"/> is negative.</exception>
        /// <seealso href="http://en.wikipedia.org/wiki/Bessel_function"/>
        /// <seealso href="https://mathworld.wolfram.com/BesselFunctionoftheFirstKind.html"/>
        public static double BesselJ(double nu, double x)
        {
            if (x < 0.0)
            {
                throw new ArgumentOutOfRangeException(nameof(x));
            }

            // Use reflection to turn negative orders into positive orders.
            if (nu < 0.0)
            {
                double mu = -nu;
                return(MoreMath.CosPi(mu) * BesselJ(mu, x) - MoreMath.SinPi(mu) * BesselY(mu, x));
            }

            if (x <= Math.Sqrt(2.0 * (nu + 1.0)))
            {
                // We are close enough to origin to use the series.
                return(BesselJ_Series(nu, x));
            }
            else if (x >= 32.0 + 0.5 * nu * nu)
            {
                // We are far enough from origin to use the asymptotic expansion.
                SolutionPair result = Bessel_Asymptotic(nu, x);
                return(result.FirstSolutionValue);
            }
            else if (x >= nu)
            {
                // We are far enough from origin to evaluate CF2, so use Steed's method.
                SolutionPair result = Bessel_Steed(nu, x);
                return(result.FirstSolutionValue);
            }
            else
            {
                // We have x < nu, but x is still not small enough to use the series; this only occurs for nu >~ 6.
                // To handle this case, compute J_{nu+1}/J_{nu}, recurse down to mu where mu ~ x < nu; use
                // Steed's method to evaluate J_{mu} and re-normalize J_{nu}.

                // for example, for mu = 16, x = 12, we can't evaluate CF2 because x < 16; so assume J_16=1, compute J_17 / J_16,
                // and recurse down to J_11; since 12 > 11, we can compute CF2 and get J_11 and Y_11; comparing this
                // with our value of J_11 gives the proper renormalization factor for J_16

                int    sign;
                double r = nu / x - Bessel_CF1(nu, x, out sign);

                double J = sign * Bessel_Tower_Start;
                //double J = 1.0 * sign;
                double JP = r * J;
                double mu = nu;
                while (mu > x)
                {
                    double t = J;
                    J   = mu / x * J + JP;
                    mu -= 1.0;
                    JP  = mu / x * J - t;
                    // Not sure if this is really okay in all cases.
                    if (Double.IsInfinity(J))
                    {
                        return(0.0);
                    }
                }

                Complex      z      = Bessel_CF2(mu, x);
                SolutionPair result = Bessel_Steed(JP / J, z, 2.0 / Math.PI / x, Math.Sign(J));
                //SolutionPair result = Bessel_Steed(mu / x - Jp1 / J, z, 2.0 / Math.PI / x, Math.Sign(J));

                return(result.FirstSolutionValue / J * Bessel_Tower_Start);
            }
        }
        /// <summary>
        /// Computes the irregual Bessel function for real orders.
        /// </summary>
        /// <param name="nu">The order parameter.</param>
        /// <param name="x">The argument, which must be non-negative.</param>
        /// <returns>The value of Y<sub>&#x3BD;</sub>(x).</returns>
        /// <remarks>
        /// <para>For information on the cylindrical Bessel functions, see <see cref="AdvancedMath.Bessel"/>.</para>
        /// </remarks>
        /// <exception cref="ArgumentOutOfRangeException"><paramref name="x"/> is negative.</exception>
        /// <seealso cref="Bessel"/>
        /// <seealso href="http://en.wikipedia.org/wiki/Bessel_function"/>
        /// <seealso href="https://mathworld.wolfram.com/BesselFunctionoftheSecondKind.html"/>
        public static double BesselY(double nu, double x)
        {
            if (x < 0.0)
            {
                throw new ArgumentOutOfRangeException(nameof(x));
            }

            if (nu < 0.0)
            {
                return(MoreMath.CosPi(nu) * BesselY(-nu, x) - MoreMath.SinPi(nu) * BesselJ(-nu, x));
            }

            if (x == 0.0)
            {
                return(Double.NegativeInfinity);
            }
            else if (x >= 32.0 + nu * nu / 2.0)
            {
                // far from the origin, use the asymptotic expansion

                SolutionPair result = Bessel_Asymptotic(nu, x);
                return(result.SecondSolutionValue);
            }
            else if (x < 4.0)
            {
                // close to the origin, use the Temme series

                double Y0, Y1;
                if (nu <= 0.5)
                {
                    BesselY_Series(nu, x, out Y0, out Y1);
                    return(Y0);
                }
                else if (nu <= 1.5)
                {
                    BesselY_Series(nu - 1.0, x, out Y0, out Y1);
                    return(Y1);
                }
                else
                {
                    // compute for mu -0.5 <= mu <= 0.5
                    double mu = nu - Math.Round(nu);
                    BesselY_Series(mu, x, out Y0, out Y1);

                    // recurr upward to nu
                    int n = ((int)Math.Round(nu - mu)) - 1;
                    for (int i = 0; i < n; i++)
                    {
                        // use recurrence on Y, Y':
                        //   Y_{mu+1} = (2 mu/x) Y_{mu} - Y_{mu}
                        mu += 1.0;
                        double t = Y0;
                        Y0 = Y1;
                        Y1 = (2.0 * mu / x) * Y1 - t;
                        if (Double.IsNegativeInfinity(Y1))
                        {
                            break;
                        }
                    }
                    return(Y1);
                }
            }
            else if (x > nu)
            {
                SolutionPair result = Bessel_Steed(nu, x);
                return(result.SecondSolutionValue);
            }
            else
            {
                // we have 4 < x < nu; evaluate at mu~x and recurr upward to nu

                // figure out mu
                int    n  = (int)Math.Ceiling(nu - x);
                double mu = nu - n;

                // evaluate at mu
                SolutionPair result = Bessel_Steed(mu, x);
                double       Y      = result.SecondSolutionValue;
                double       YP     = result.SecondSolutionDerivative;


                // recurr upward to nu
                Bessel_RecurrUpward(mu, x, ref Y, ref YP, n);

                return(Y);
            }
        }
        /// <summary>
        /// Computes both solutions of the Bessel differential equation.
        /// </summary>
        /// <param name="nu">The order, which must be non-negative.</param>
        /// <param name="x">The argument, which must be non-negative.</param>
        /// <returns>The values of J<sub>nu</sub>(x), J'<sub>nu</sub>(x), Y<sub>nu</sub>(x), and Y'<sub>nu</sub>(x).</returns>
        /// <remarks>
        /// <para>Bessel functions often occur in the analysis of physical phenomena with cylindrical symmetry.
        /// They satisfy the differential equation:</para>
        /// <img src="../images/BesselODE.png" />
        /// <para>Since this is a second order linear equation, it has two linearly independent solutions. The regular Bessel function
        /// J<sub>&#x3BD;</sub>(x), which is finite at the origin, and the irregular Bessel function Y<sub>&#x3BD;</sub>(x), which diverges
        /// at the origin. Far from the origin, both functions are oscilatory.</para>
        /// <para>This method simultaneously computes both Bessel functions and their derivatives. If you need both J and Y, it is faster to call this method once than to call
        /// <see cref="BesselJ(double,double)"/> and <see cref="BesselY(double,double)"/> seperately. If on, the other hand, you need only J or only Y, it is faster to
        /// call the appropriate method to compute the one you need.</para>
        /// </remarks>
        /// <exception cref="ArgumentOutOfRangeException"><paramref name="nu"/> or <paramref name="x"/> is negative.</exception>
        /// <seealso href="http://en.wikipedia.org/wiki/Bessel_function"/>
        public static SolutionPair Bessel(double nu, double x)
        {
            if (nu < 0.0)
            {
                throw new ArgumentOutOfRangeException(nameof(nu));
            }
            if (x < 0.0)
            {
                throw new ArgumentOutOfRangeException(nameof(x));
            }

            if (x == 0.0)
            {
                return(Bessel_Zero(nu, +1));
            }
            else if (x < 4.0)
            {
                // Close to the origin, use the series for Y and J.
                // The range does not grow with increasing order because our series for Y is only for small orders.

                // for J, we can use the series directly
                double J, JP;
                BesselJ_Series(nu, x, out J, out JP);

                // for Y, we only have a series for -0.5 < mu < 0.5, so pick an order in that range offset from our
                // desired order by an integer number of recurrence steps. Evaluate at the mu order and recurr upward to nu.
                double mu = nu - Math.Round(nu);
                int    n = (int)Math.Round(nu - mu);
                double Y, YP1;
                BesselY_Series(mu, x, out Y, out YP1);
                double YP = (mu / x) * Y - YP1;
                Bessel_RecurrUpward(mu, x, ref Y, ref YP, n);

                // return J, J', Y, Y' in a solution pair
                return(new SolutionPair(J, JP, Y, YP));
            }
            else if (x > 32.0 + nu * nu / 2.0)
            {
                // Far enough out, use the asymptotic series.
                return(Bessel_Asymptotic(nu, x));
            }
            else if (x > nu)
            {
                // In the intermediate region, use Steed's method directly if x > nu.
                return(Bessel_Steed(nu, x));
            }
            else
            {
                // If x < nu, we can't use Steed's method directly because CF2 does not converge.
                // So pick a mu ~ x < nu, offset from nu by an integer; use Steed's method there,
                // then recurr up to nu.

                // this only occurs for 4 < x < nu, e.g. nu, x = 16, 8

                int          n      = (int)Math.Ceiling(nu - x);
                double       mu     = nu - n;
                SolutionPair result = Bessel_Steed(mu, x);
                double       Y      = result.SecondSolutionValue;
                double       YP     = result.SecondSolutionDerivative;
                Bessel_RecurrUpward(mu, x, ref Y, ref YP, n);

                // being in a region with x < nu, we can evaluate CF1 to get J'/J
                // this ratio, together with Y and Y' and the Wronskian, gives us J' and J seperately
                int    sign;
                double r  = nu / x - Bessel_CF1(nu, x, out sign);
                double J  = (2.0 / Math.PI / x) / (YP - r * Y);
                double JP = r * J;

                // return the result
                return(new SolutionPair(J, JP, Y, YP));
            }
        }
        // Hankel's asymptotic expansions for Bessel function (A&S 9.2)
        //   J = \sqrt{\frac{2}{\pi x}} \left[ P \cos\omega - Q \sin\omega \right]
        //   Y = \sqrt{\frac{2}{\pi x}} \left[ P \sin\omega + Q \cos\omega \right]
        // where \omega = x - \left( \nu / 2 + 1 / 4 \right) \pi and \mu = 4 \nu^2 and
        //   P = 1 - \frac{(\mu-1)(\mu-9)}{2! (8x)^2} + \frac{(\mu-1)(\mu-9)(\mu-25)(\mu-49}{4! (8x)^4} + \cdots
        //   Q = \frac{(\mu-1)}{8x} - \frac{(\mu-1)(\mu-9)(\mu-25)}{3! (8x)^3} + \cdots
        // Derivatives have similiar expressions
        //   J' = - \sqrt{\frac{2}{\pi x}} \left[ R \sin\omega + S \cos\omega \right]
        //   Y' = \sqrt{\frac{2}{\pi x}} \left[ R \cos\omega - S \sin\omega \right]
        // where
        //   R = 1 - \frac{(\mu-1)(\mu+15)}{2! (8x)^2} + \cdots
        //   S = \frac{(\mu+3)}{8x} - \frac{(\mu-1)(\mu - 9)(\mu+35)}{3! (8x)^3} + \cdots

        // For nu=0, this series converges to full precision in about 10 terms at x~100, and in about 25 terms even as low as x~25.
        // It fails to converge to full precision for x <~ 25.
        // Since the first correction term is ~ (4 \nu^2)^2 / (8 x)^2 ~ (\nu^2 / 2 x)^2, the minimum x should grow like \nu^2 / 2

        private static SolutionPair Bessel_Asymptotic(double nu, double x)
        {
            Debug.Assert(nu >= 0.0);
            Debug.Assert(x > 0.0);

            // pre-compute factors of nu and x as they appear in the series
            double mu = 4.0 * nu * nu;
            double xx = 8.0 * x;

            // initialize P and Q
            double P = 1.0; double R = 1.0;
            double Q = 0.0; double S = 0.0;

            // k is the current term number, k2 is (2k - 1), and t is the value of the current term
            int    k  = 0;
            int    k2 = -1;
            double t  = 1.0;

            while (true)
            {
                double Q_old = Q; double P_old = P;
                double R_old = R; double S_old = S;

                k++; k2 += 2;
                t       /= k * xx;
                S       += (mu + k2 * (k2 + 2)) * t;
                t       *= (mu - k2 * k2);
                Q       += t;

                k++; k2 += 2;
                t       /= -k * xx;
                R       += (mu + k2 * (k2 + 2)) * t;
                t       *= (mu - k2 * k2);
                P       += t;

                if ((P == P_old) && (Q == Q_old) && (R == R_old) && (S == S_old))
                {
                    break;
                }

                if (k > Global.SeriesMax)
                {
                    throw new NonconvergenceException();
                }
            }

            // The computation of \sin\omega and \cos\omega is fairly delicate. Since x is large,
            // it's important to use MoreMath.Sin and MoreMath.Cos to ensure correctness for
            // large arguments. Furthermore, since the shift by 1/2 ( \nu + 1/2) \pi is a
            // multiple of \pi and could be dwarfed by or have significant cancellation with x,
            // it's better to use the trig addition formulas to handle it seperately using
            // SinPi and CosPi functions.

            // This works well, but it took a long time to get to this.

            double a  = 0.5 * (nu + 0.5);
            double sa = MoreMath.SinPi(a);
            double ca = MoreMath.CosPi(a);

            double sx = MoreMath.Sin(x);
            double cx = MoreMath.Cos(x);

            // It would be great to have a SinAndCos function so as not to do the range reduction twice.

            double s1 = sx * ca - cx * sa;
            double c1 = cx * ca + sx * sa;

            // Assemble the solution
            double       N      = Math.Sqrt(2.0 / Math.PI / x);
            SolutionPair result = new SolutionPair(
                N * (c1 * P - s1 * Q), -N * (R * s1 + S * c1),
                N * (s1 * P + c1 * Q), N * (R * c1 - S * s1)
                );

            return(result);
        }
        /// <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 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);
            }
        }
        // Hankel's asymptotic expansions for Bessel function (A&S 9.2)
        //   J = \sqrt{\frac{2}{\pi x}} \left[ P \cos\phi - Q \sin\phi \right]
        //   Y = \sqrt{\frac{2}{\pi x}} \left[ P \sin\phi + Q \cos\phi \right]
        // where \phi = x - \left( \nu / 2 + 1 / 4 \right) \pi and \mu = 4 \nu^2 and
        //   P = 1 - \frac{(\mu-1)(\mu-9)}{2! (8x)^2} + \frac{(\mu-1)(\mu-9)(\mu-25)(\mu-49}{4! (8x)^4} + \cdots
        //   Q = \frac{(\mu-1)}{8x} - \frac{(\mu-1)(\mu-9)(\mu-25)}{3! (8x)^3} + \cdots
        // Derivatives have similiar expressions
        //   J' = - \sqrt{\frac{2}{\pi x}} \left[ R \sin\phi + S \cos\phi \right]
        //   Y' = \sqrt{\frac{2}{\pi x}} \left[ R \cos\phi - S \sin\phi \right]
        // where
        //   R = 1 - \frac{(\mu-1)(\mu+15)}{2! (8x)^2} + \cdots
        //   S = \frac{(\mu+3)}{8x} - \frac{(\mu-1)(\mu - 9)(\mu+35)}{3! (8x)^3} + \cdots
        // For nu=0, this series converges to full precision in about 10 terms at x~100, and in about 25 terms even as low as x~25
        // It fails to converge at all for lower x <~ 25
        // Since the first correction term is ~ (4 \nu^2)^2 / (8 x)^2 ~ (\nu^2 / 2 x)^2, the minimum x should grow like \nu^2 / 2
        private static SolutionPair Bessel_Asymptotic(double nu, double x)
        {
            // pre-compute factors of nu and x as they appear in the series
            double mu = 4.0 * nu * nu;
            double xx = 8.0 * x;

            // initialize P and Q
            double P = 1.0; double R = 1.0;
            double Q = 0.0; double S = 0.0;

            // k is the current term number, k2 is (2k - 1), and t is the value of the current term
            int k = 0;
            int k2 = -1;
            double t = 1.0;

            while (true) {

                double Q_old = Q; double P_old = P;
                double R_old = R; double S_old = S;

                k++; k2 += 2;
                t /= k * xx;
                S += (mu + k2 * (k2 + 2)) * t;
                t *= (mu - k2 * k2);
                Q += t;

                k++; k2 += 2;
                t /= -k * xx;
                R += (mu + k2 * (k2 + 2)) * t;
                t *= (mu - k2 * k2);
                P += t;

                if ((P == P_old) && (Q == Q_old) && (R == R_old) && (S == S_old)) {
                    break;
                }

                if (k > Global.SeriesMax) throw new NonconvergenceException();

            }

            // We attempted to move to a single trig evaluation so as to avoid errors when the two terms nearly cancel,
            // but this seemed to cause problems, perhaps because the arctan angle cannot be determined with sufficient
            // resolution. Investigate further.
            /*
            double M = N * MoreMath.Hypot(Q, P);
            double phi = Math.Atan2(Q, P);
            SolutionPair result2 = new SolutionPair(
                M * Cos(x + phi, -(nu + 0.5) / 4.0),
                -N * (R * s + S * c),
                M * Sin(x + phi, -(nu + 0.5) / 4.0),
                N * (R * c - S * s)
            );
             */

            // Compute sin and cosine of x - (\nu + 1/2)(\pi / 2)
            // Then we compute sine and cosine of (x1 - u1) with shift appropriate to (x0 - u0).

            // For maximum accuracy, we first reduce x = (x_0 + x_1) (\pi / 2), where x_0 is an integer and -0.5 < x1 < 0.5
            long x0; double x1;
            RangeReduction.ReduceByPiHalves(x, out x0, out x1);

            // Then we reduce (\nu + 1/2) = u_0 + u_1 where u_0 is an integer and -0.5 < u1 < 0.5
            double u = nu + 0.5;
            double ur = Math.Round(u);
            long u0 = (long) ur;
            double u1 = u - ur;

            // FInally, we compute sine and cosine, having reduced the evaluation interval to -0.5 < \theta < 0.5
            double s1 = RangeReduction.Sin(x0 - u0, x1 - u1);
            double c1 = RangeReduction.Cos(x0 - u0, x1 - u1);

            // Assemble the solution
            double N = Math.Sqrt(2.0 / Math.PI / x);
            SolutionPair result = new SolutionPair(
                N * (c1 * P - s1 * Q), -N * (R * s1 + S * c1),
                N * (s1 * P + c1 * Q), N * (R * c1 - S * s1)
            );

            return (result);
        }
Exemple #22
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 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="CoulombF" />.</para>
        /// </remarks>
        /// <exception cref="ArgumentOutOfRangeException"><paramref name="L"/> or <paramref name="rho"/> is negative.</exception>
        /// <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("L");
            }
            if (rho < 0)
            {
                throw new ArgumentOutOfRangeException("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
                double F, G;
                Coulomb_Asymptotic(L, eta, rho, out F, out G);
                return(G);
            }
            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;

                    if (rho < 2.0 * eta)
                    {
                        // 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;
                        //if (eta > 12.0) {
                        //    result = Coulomb_Zero_Turning_Expansion(eta);
                        //} else {
                        result = Coulomb_Steed(0, eta, 2.0 * eta);
                        //}

                        G  = result.SecondSolutionValue;
                        GP = result.SecondSolutionDerivative;

                        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 desired L
                    Coulomb_Recurse_Upward(0, L, eta, rho, ref G, ref GP);

                    return(G);
                }
            }
        }