diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 33abc4553..014d8b820 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -335,6 +335,24 @@ and the complex arcsine's real part moved two units away. A caller comparing a h result to a pinned string will see the last digits differ; a caller comparing to a tolerance will not. [#1338](https://github.com/asc-community/AngouriMath/issues/1338). +### Whole powers of a root are written as powers of its radicand, and hold on both sides of 0 + +**Answers where there were none.** Rubi's 1.1.3.2 `1/(x^(7/2) sqrt(a + b x^5))` and three like it, and 1.3.2's +`sqrt(a x^23)/sqrt(1 + x^5)` and `sqrt(a x^13)/sqrt(1 + x^5)`, and 1.1.1.2's `1/(sqrt(x) sqrt(2 x - 3))`, were answered +`provided x >= 0`. The integrand is real +beyond `-(a/b)^(1/5)` as a product of two imaginary roots, and the answer holds there. But it spelled `x^5` as +`((x^(1/2))^5)^2`, whose value at a negative x has a zero imaginary part, and the measurement of where the answer holds +could not evaluate the outer root on its cut. Under `u = sqrt(x)` a whole power of `u` that is a multiple of the +root's order is now a power of the radicand, which it is on every branch, and the condition is gone +([#233](https://github.com/asc-community/AngouriMath/issues/233)). + +| Input | Was (2.5.0) | Now | +|---|---|---| +| `"1/(x^(7/2)*sqrt(a+b*x^5))".ToEntity().Integrate("x")` | `integral(...)` | 60 characters, where before this change master gave 92 for a positive x only | +| `"1/(x^(7/2)*sqrt(1+x^5))".ToEntity().Integrate("x")` | `integral(...)` | 50 characters | +| `"sqrt(a*x^23)/sqrt(1+x^5)".ToEntity().Integrate("x")` | `integral(...)` | 426 characters | +| `"1/(sqrt(x)*sqrt(-3+2*x))".ToEntity().Integrate("x")` | `integral(...)` | 141 characters, where before this change master gave an answer for a positive x only | + ### A function of an inverse sine of `sqrt(1 + b x^2)` beside its derivative keeps the sign of x **A wrong answer, and answers where there were none.** `1/(asin(sqrt(1 + b x^2)) sqrt(1 + b x^2))` was diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs index 743b3329e..a710984bb 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs @@ -15817,7 +15817,12 @@ node is Powf(var radical, Number.Rational e) && e is not Number.Integer if (Integration.ComputeIndefiniteIntegral(integrand, u, integrateByParts) is not { } result) return null; - var back = result.Substitute(u, MathS.Pow(radicalBase, Number.Rational.Create(1, q))); + // A whole power of u that is a multiple of q is a whole power of the radicand on every + // branch, `(B^(1/q))^(k q) = B^k`, and is written so: as `((x^(1/2))^5)^2`, the `x^5` in Rubi's + // `1/(x^(7/2) sqrt(a + b x^5))` was real at a negative x with a zero imaginary part, the outer + // root on its branch cut there, and the measurement below could not read the answer where + // the integrand is real, and kept `provided x >= 0` on an answer that holds on both sides. + var back = WithWholePowersOfARootAsPowersOfItsRadicand(result.Substitute(u, MathS.Pow(radicalBase, Number.Rational.Create(1, q))), radicalBase); // Beside a second root the integrand can be real where this radicand is negative, and // the condition was the whole of its domain there: the remainder parts leaves from // Timofeev's `arcsec(x) sqrt(x^2 - 1)/x^4`, in `w = x^2`, is @@ -48128,6 +48133,7 @@ private static bool HoldsAnEvenRootBesides(Entity expr, Entity radicand, Entity. /// private static Entity ConditionedOnTheRadicand(Entity answer, Entity expr, Entity radicand, Entity.Variable x) { + answer = WithWholePowersOfARootAsPowersOfItsRadicand(answer, radicand); var condition = radicand >= Number.Integer.Zero; if (!HoldsAnEvenRootBesides(expr, radicand, x)) return answer.Provided(condition); @@ -48143,6 +48149,29 @@ when TheOneOtherRadicandOfAHalfOddPower(expr, radicand, x) is { } other: } } + /// + /// with every whole power of a root of that + /// is a multiple of the root's order written as a power of the radicand, which it is on every + /// branch: (B^(1/q))^(k q) = B^k, nested whole powers flattened first. Spelled + /// ((x^(1/2))^5)^2, the x^5 in the answer to Rubi's 1.1.3.2 + /// 1/(x^(7/2) sqrt(a + b x^5)) was real at a negative x with a zero imaginary part, the + /// outer root on its cut there, which the interval and decimal evaluations rightly refuse, and + /// the measurement of where the answer holds kept provided x >= 0 on an answer that holds + /// on both sides; so did (x^(1/2))^2 in 1.1.1.2's 1/(sqrt(x) sqrt(2 x - 3)). + /// + private static Entity WithWholePowersOfARootAsPowersOfItsRadicand(Entity answer, Entity radicand) + => answer + .Replace(node => node is Powf(Powf(var inner, Number.Integer k), Number.Integer m) && IsARootOf(inner, radicand) + ? MathS.Pow(inner, Number.Integer.Create(k.EInteger * m.EInteger)) : node) + .Replace(node => node is Powf(var inner, Number.Integer { EInteger: var n }) && IsARootOf(inner, radicand) + && ((Powf)inner).Exponent is Number.Rational { ERational: var root } && n.Remainder(root.Denominator).IsZero + ? MathS.Pow(radicand, Number.Integer.Create(n.Divide(root.Denominator))) : node); + + /// Whether is radicand^(1/q) for a whole q past 1. + private static bool IsARootOf(Entity node, Entity radicand) + => node is Powf(var @base, Number.Rational { ERational: var root }) && @base == radicand + && root.Numerator.Equals(EInteger.One) && root.Denominator.CompareTo(EInteger.One) > 0; + /// /// The one base other than under a power of /// that is not whole, where every such power is half-odd; else null. diff --git a/Sources/Tests/UnitTests/Calculus/WholePowersOfTheRootAsPowersOfTheRadicandIntegralTest.cs b/Sources/Tests/UnitTests/Calculus/WholePowersOfTheRootAsPowersOfTheRadicandIntegralTest.cs new file mode 100644 index 000000000..48b98bc5f --- /dev/null +++ b/Sources/Tests/UnitTests/Calculus/WholePowersOfTheRootAsPowersOfTheRadicandIntegralTest.cs @@ -0,0 +1,51 @@ +// +// Copyright (c) 2019-2026 Angouri. +// AngouriMath is licensed under MIT. +// Details: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md. +// Website: https://am.angouri.org. +// + +using System; +using AngouriMath.Extensions; +using Xunit; + +namespace AngouriMath.Tests.Calculus +{ + /// + /// Under u = sqrt(x), a whole power of u that is a multiple of the root's order written as a + /// power of the radicand, (x^(1/2))^10 = x^5: spelled ((x^(1/2))^5)^2, the answer to Rubi's + /// 1.1.3.2 1/(x^(7/2) sqrt(a + b x^5)) could not be read at a negative x, its root on the cut, + /// and kept provided x >= 0, where the integrand is real beyond -(a/b)^(1/5) and the + /// answer holds there. + /// #233 + /// + [Trait("Area", "Calculus")] + public sealed class WholePowersOfTheRootAsPowersOfTheRadicandIntegralTest + { + [Theory] + [InlineData("1/(x^(7/2)*sqrt(a + b*x^5))")] + [InlineData("1/(x^(17/2)*sqrt(1 + x^5))")] + [InlineData("sqrt(a*x^13)/sqrt(1 + x^5)")] + [InlineData("1/(sqrt(x)*sqrt(-3 + 2*x))")] + public void HoldsOnBothSides(string integrand) + { + var integral = integrand.ToEntity().Integrate("x"); + var text = integral.Stringize(); + Assert.DoesNotContain("integral(", text); + Entity Pinned(Entity e) => e.Substitute("a", 1.3).Substitute("b", 0.7); + var derivative = Pinned(integral.Substitute("C", 0)).Differentiate("x"); + var original = Pinned(integrand.ToEntity()); + // Beyond -(a/b)^(1/5), about -1.13, the integrand is real as a product of two imaginary roots, + // and `1/(sqrt(x) sqrt(2 x - 3))` is real for every negative x the same way. + foreach (var at in new[] { -2.3, -1.4, 0.4, 1.3, 2.2 }) + { + var want = original.Substitute("x", at).EvalNumerical(); + var got = derivative.Substitute("x", at).EvalNumerical(); + Assert.False(got.IsNaN, $"d/dx of the antiderivative of {integrand} has no value at x = {at}, where the integrand is {want}"); + Assert.True(Math.Abs((double)(got - want).RealPart) + Math.Abs((double)(got - want).ImaginaryPart) + < 1e-9 * Math.Max(1, Math.Abs((double)want.RealPart) + Math.Abs((double)want.ImaginaryPart)), + $"d/dx of the antiderivative of {integrand} is {got} at x = {at}, where the integrand is {want}"); + } + } + } +}