/// <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>(η,ρ).</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()); } } }
/// <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>(η,ρ).</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)); } } }
/// <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); }
// 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)); } }
/// <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); }
/// <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>(η,ρ).</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 η > 0, an attractive potential by η < 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>(η,ρ) 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>ν</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>ν</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>ν</sub>(x), which is finite at the origin, and the irregular Bessel function Y<sub>ν</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 η > 0, an attractive potential by η < 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>(η,ρ) 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>(η,ρ).</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); }
/// <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>(η,ρ).</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); } } }