diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 9cd0928b3..26888986c 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -1006,6 +1006,22 @@ of `Q`, times `sqrt(Q)`, and the same two integrals that are not algebraic | `"(1 + 3*x + 4*x^2)/((1 + 2*x)^3*(2 + 3*x^2)^(5/2))".ToEntity().Integrate("x")` | `integral(...)` | linears over `(2 + 3 x^2)^2` and `2 + 3 x^2` and powers of `1/(1 + 2 x)`, times the root, and a logarithm | | `"(d + k*x + f*x^2)/((g + h*x)^2*(a + c*x^2)^(3/2))".ToEntity().Integrate("x")` | `integral(...)` | the same in the symbols, and a logarithm or an arctangent by the sign of `a h^2 + c g^2` | +### An even root of a quotient with an odd power below the bar keeps its sign below zero + +**Wrong answers on the unreleased master, right ones now.** `sqrt((1 + x)/x^3)` is real for `x < -1` as +well as for `x > 0`, and was integrated as `sqrt(1 + x) x^(-3/2)`, which it is for `x > 0` only: below +`-1` the two differ by a sign, and the answer was wrong at every point there. The check that decides +whether an even root of a quotient may be written apart counted a factor below the bar as though it +were above it; it counts against those above it now, and where the root does not come apart, a linear's +odd power below it comes out with its sign in front of the answer, `|1 + x|` being `sgn(1 + x) (1 + x)`. +2.5.0 declined these ([#718](https://github.com/asc-community/AngouriMath/issues/718)). + +| Input | Was (2.5.0) | Now | +|---|---|---| +| `"sqrt((1 + x)/x^3)".ToEntity().Integrate("x")` | `integral(...)` | `sgn(x)` times the antiderivative for `x > 0`, right on both sides | +| `"sqrt(x/(1 + x)^3)".ToEntity().Integrate("x")` | `integral(...)` | the same with `sgn(1 + x)` | +| `"((1 + x)/x^3)^(3/2)".ToEntity().Integrate("x")` | `integral(...)` | the same as the first | + ### A function comes out of a fractional power of its even power with its sign **Improvement, not silent.** The entry two above made `(sin(x)^2)^(3/2)` the modulus `|sin(x)|^3` diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs index 404083637..d130e8fbb 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs @@ -20779,16 +20779,23 @@ private static bool NotRealAt(Entity expr, Entity.Variable x, double at) continue; } // g^(2m r) is |g|^(2 m r): g itself where that power is even or g is - // positive, and for x the answer for x > 0 extended by parity; - // otherwise the factor stays where it is. + // positive, for x the answer for x > 0 extended by parity, and for a + // linear an odd whole power of it with its sign in front of the answer, + // `|g|` being `sgn(g) g` -- `sqrt(x/(1 + x)^3)` is + // `sgn(1 + x) sqrt(x/(1 + x))/(1 + x)`; only where the root is a factor + // of the integrand, as the sign goes in front of the answer. Otherwise + // the factor stays where it is. if (factor is Powf(var g, Number.Integer n) && n.EInteger.CompareTo(EInteger.FromInt32(2)) >= 0 && g.ContainsNode(x) && n.EInteger.Divide(EInteger.FromInt32(2)).Multiply(EInteger.FromInt32(2)) is var even && (Number.Integer.Create(underneath ? even.Negate() : even) * exponent).InnerSimplified is var power - && (g == x || power is Number.Integer { EInteger.IsEven: true } || IsPositiveForReal(g, x))) + && (g == x || power is Number.Integer { EInteger.IsEven: true } || IsPositiveForReal(g, x) + || power is Number.Integer && factors.Contains(node) && TreeAnalyzer.TryGetPolyLinear(g, x, out var slopeOfG, out _) && !TreeAnalyzer.IsZero(slopeOfG))) { var evenPower = MathS.Pow(g, Number.Integer.Create(even)); if (g == x && !factors.Contains(node)) everyPowerOfXFromAFactor = false; + if (g != x && power is Number.Integer { EInteger.IsEven: false } && !IsPositiveForReal(g, x)) + signs[g] = signs.TryGetValue(g, out var timesTaken) ? timesTaken + 1 : 1; taken = taken * (g == x ? PowerOfAPositive(evenPower, underneath ? -exponent : exponent) : power is Number.Integer ? MathS.Pow(g, power) : MathS.Pow(evenPower, underneath ? -exponent : exponent)); if (!n.EInteger.IsEven) @@ -20920,12 +20927,20 @@ Entity WithANegativeConstantInsideAFactor(Entity product) // out below zero -- which Timofeev's `sqrt(tan(x) tan(2x))` found. The constant must // be positive, since a negative one would take the phase the other way. // `((x - 1)^3 (x + 2)^5)^(1/4)` splits: above 1 both are positive, below -2 both - // negative with 3 + 5 a multiple of 8, and between the product is negative. + // negative with 3 + 5 a multiple of 8, and between the product is negative. A factor + // below the bar counts against those above it, its power being the negative of the + // exponent: `sqrt((1 + x)/x^3)` below -1 has the phases of `sqrt(1 + x)` and of + // `x^(-3/2)`, pi/2 and -3 pi/2, which add up to -pi, 1 - 3 = -2, and is + // `-sqrt(1 + x) x^(-3/2)` there; counted as one sum, 1 + 3 = 4 split it, right for + // x > 0 and wrong for every x < -1. The sum is still asked as well, so that only a + // split taken before is taken: the difference alone splits `sqrt((1 + x)/x)` below + // -1, rightly, and the rules after it answer `sqrt(1 + x)/sqrt(x)` for x > 0 only, + // where the root whole is answered on both sides. bool AnEvenRootOfAProductSplits(Entity above, Entity below, EInteger q, out bool onlyWhereTheRootIsReal) { onlyWhereTheRootIsReal = false; var factors = new List<(Entity Polynomial, EInteger Times)>(); - foreach (var side in new[] { above, below }) + foreach (var (side, underneath) in new[] { (above, false), (below, true) }) foreach (var factor in Mulf.LinearChildren(side)) { if (!factor.ContainsNode(x)) @@ -20937,7 +20952,7 @@ bool AnEvenRootOfAProductSplits(Entity above, Entity below, EInteger q, out bool var (polynomial, times) = factor is Powf(var g, Number.Integer n) && n.EInteger.Sign > 0 ? (g, n.EInteger) : (factor, EInteger.One); if (!TreeAnalyzer.TryGetPolynomial(polynomial, x, out _)) return false; - factors.Add((polynomial, times)); + factors.Add((polynomial, underneath ? times.Negate() : times)); } if (factors.Count < 2) return false; @@ -20945,11 +20960,15 @@ bool AnEvenRootOfAProductSplits(Entity above, Entity below, EInteger q, out bool var splits = OnEveryRealIntervalAt(factors.Select(f => f.Polynomial).ToList(), x, (at, signs) => { var negativeTimes = EInteger.Zero; + var negativeTimesUnsigned = EInteger.Zero; for (var i = 0; i < signs.Count; i++) if (signs[i] < 0) + { negativeTimes = negativeTimes.Add(factors[i].Times); + negativeTimesUnsigned = negativeTimesUnsigned.Add(factors[i].Times.Abs()); + } if (negativeTimes.IsEven) - return negativeTimes.Remainder(q.ShiftLeft(1)).IsZero; + return negativeTimes.Remainder(q.ShiftLeft(1)).IsZero && negativeTimesUnsigned.Remainder(q.ShiftLeft(1)).IsZero; if (!NotRealAt(expr, x, at)) realBeyondTheRoot = true; return true; diff --git a/Sources/Tests/UnitTests/Calculus/RootOfAQuotientOnBothSidesIntegralTest.cs b/Sources/Tests/UnitTests/Calculus/RootOfAQuotientOnBothSidesIntegralTest.cs new file mode 100644 index 000000000..206196702 --- /dev/null +++ b/Sources/Tests/UnitTests/Calculus/RootOfAQuotientOnBothSidesIntegralTest.cs @@ -0,0 +1,54 @@ +// +// 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 +{ + /// + /// An even root of a quotient with an odd power below the bar, written apart: below the bar + /// the power's phase counts against those above it, so sqrt((1 + x)/x^3), real for + /// x < -1 as well, is not sqrt(1 + x) x^(-3/2) there, and was integrated as + /// though it were -- right for x > 0 and wrong for every x < -1. Each row is + /// checked at the points where the integrand is real, on both sides. + /// #718 + /// + [Trait("Area", "Calculus")] + public sealed class RootOfAQuotientOnBothSidesIntegralTest + { + [Theory] + [InlineData("sqrt((1 + x)/x^3)")] + [InlineData("((1 + x)/x^3)^(3/2)")] + [InlineData("sqrt(x/(1 + x)^3)")] + [InlineData("sqrt((2 + x)/(1 + x)^3)")] + [InlineData("x^2*sqrt(x/(1 + x)^3)")] + public void RightOnBothSides(string integrand) + { + var integral = integrand.ToEntity().Integrate("x"); + Assert.DoesNotContain("integral(", integral.Stringize()); + Assert.DoesNotContain("NaN", integral.Stringize()); + var derivative = integral.Substitute("C", 0).Differentiate("x"); + var original = integrand.ToEntity(); + var below = 0; + var above = 0; + foreach (var at in new[] { -2.7, -2.3, -1.9, -1.3, 0.31, 0.83, 1.77 }) + { + var want = original.Substitute("x", at).EvalNumerical(); + if (want.IsNaN || Math.Abs((double)want.ImaginaryPart) > 1e-12) + continue; + if (at < 0) below++; else above++; + var got = derivative.Substitute("x", at).EvalNumerical(); + Assert.True(Math.Abs((double)(got - want).RealPart) + Math.Abs((double)(got - want).ImaginaryPart) + < 1e-9 * Math.Max(1, Math.Abs((double)want.RealPart)), + $"d/dx of the antiderivative of {integrand} is {got} at x = {at}, where the integrand is {want}"); + } + Assert.True(below >= 2 && above >= 3, $"only {below} points below zero and {above} above it could be compared for {integrand}"); + } + } +}