diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 9efef8955..0e5303cfa 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -665,6 +665,25 @@ written in `u = c + d x` first. Rubi's 1.1.3.2, 1.2.3.2 and 1.3.1 write whole se | `"(c*m + d*m*x)^3/(a + b*(c + d*x)^3)^2".ToEntity().Integrate("x")` | `integral(...)` | `m^3` times a rational function of `c + d x`, the logarithms and the arctangent | | `"(c + d*x)^4/(a + b*(c + d*x)^2 + k*(c + d*x)^4)^2".ToEntity().Integrate("x")` | `integral(...)` | a rational function of `c + d x` and arctangents | +### A root of a square inside a sum is integrated on each side of the square's zero + +**Answers where there were none.** `1/(1 + (x^2)^(3/2))` is `1/(1 + x^3)` for a positive `x` and +`1/(1 - x^3)` for a negative one; 2.5.0 declined it. The unreleased master wrote the root as +`sgn(x) x^3` and took the sign out in front of the integral, which holds for a factor of the +integrand and not inside a sum, and its answer was the first one's on both sides of zero. A sign +goes in front only where every occurrence of the root is a factor now; otherwise the integrand is +integrated with the sign 1 and with it -1, and the answer is the piecewise of the two on the sign of +the root's linear. The same where a square factor is taken out of a root: `x/(x + sqrt(x^6))` is +`1/(1 + x^2)` for a positive `x` and `1/(1 - x^2)` for a negative one, and was `sgn(x) arctan(x)` +([#718](https://github.com/asc-community/AngouriMath/issues/718)). + +| Input | Was (2.5.0) | Now | +|---|---|---| +| `"1/(1+(x^2)^(3/2))".ToEntity().Integrate("x")` | `integral(...)` | the integral of `1/(1 + x^3)` where `x > 0`, of `1/(1 - x^3)` otherwise | +| `"1/(1+sqrt(x^2))".ToEntity().Integrate("x")` | `integral(...)` | `ln(x + 1)` where `x > 0`, `-ln(1 - x)` otherwise | +| `"1/(2+sqrt(x^2+2*x+1))".ToEntity().Integrate("x")` | `integral(...)` | `ln(x + 3)` where `x + 1 > 0`, `-ln(1 - x)` otherwise | +| `"x/(x+sqrt(x^6))".ToEntity().Integrate("x")` | `integral(...)` | `arctan(x)` where `x > 0`, a logarithm of `(1 + x)/(1 - x)` otherwise | + ### A root of an even power is the modulus, and is no longer read as the power **Wrong answers, silent.** `(u^2)^(3/2)` is `|u|^3`, and two readers took it for `u^3`: the diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs index 858c776e3..0f7d33b19 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs @@ -16189,6 +16189,16 @@ bool MayBeARootOfASquare(Entity node, Entity.Variable x) var changed = false; Entity assumed = Entity.Boolean.True; Entity signs = Number.Integer.One; + // A sign goes in front of the integral only from a factor of the integrand. Inside a + // sum it is a factor of nothing: `1/(1 + (x^2)^(3/2))` is `1/(1 + x^3)` for a positive + // x and `1/(1 - x^3)` for a negative one, and with the sign taken out in front the + // answer was the first's for both. There the root is written with a sign of its own, + // and the integrand is integrated with it 1 and with it -1, each on its side of the + // linear's zero. One such linear only. + var factors = FactorsOfTheIntegrand(expr).Select(pair => pair.Factor).ToList(); + var sign = Variable.CreateUnique(expr, "s_sgn"); + Entity? signedLinear = null; + var signsOfMoreThanOneLinear = false; var written = expr.Replace(node => { if (!MayBeARootOfASquare(node, x)) @@ -16260,14 +16270,45 @@ bool MayBeARootOfASquare(Entity node, Entity.Variable x) // `sqrt(a^2 + 2abx + b^2x^2) sqrt(c + ex + dx^2)` threw with it left in place. // None where it is plainly one: an even power of x plus a positive number, or a // root of x of an even order plus one, `sqrt(x) + 1`. + var modulus = leading == Number.Integer.One ? power : MathS.Pow(leading, r) * power; if (!whole && !((half.IsEven || wBase is Powf(_, Number.Rational { ERational.Denominator.IsEven: true })) && h.Evaled is Number.Real { IsPositive: true })) + { + // Each occurrence a factor, or the sign of none of them goes in front: the + // two cases are exact for a factor as well, and `(x^2)^(3/2)/(1 + (x^2)^(3/2))` + // holds the one root as a factor and inside the sum both. + if (factors.Count(factor => factor == node) < expr.Nodes.Count(inner => inner == node)) + { + if (signedLinear is not null && signedLinear != linear) + { + signsOfMoreThanOneLinear = true; + return node; + } + signedLinear = linear; + changed = true; + return sign * modulus; + } signs = signs * MathS.Signum(linear); + } changed = true; - return leading == Number.Integer.One ? power : MathS.Pow(leading, r) * power; + return modulus; }); - if (!changed || Integration.ComputeAsTheSameQuestion(written, x, integrateByParts) is not { } answer) + if (!changed || signsOfMoreThanOneLinear) return null; + Entity answer; + if (signedLinear is null) + { + if (Integration.ComputeAsTheSameQuestion(written, x, integrateByParts) is not { } unsigned) + return null; + answer = unsigned; + } + else + { + if (Integration.ComputeAsTheSameQuestion(written.Substitute(sign, Number.Integer.One).InnerSimplified, x, integrateByParts) is not { } above + || Integration.ComputeAsTheSameQuestion(written.Substitute(sign, Number.Integer.MinusOne).InnerSimplified, x, integrateByParts) is not { } below) + return null; + answer = MathS.Piecewise(new[] { new Providedf(above, signedLinear > Number.Integer.Zero) }, below); + } if (signs != Number.Integer.One) answer = signs * answer; return assumed == Entity.Boolean.True ? answer : answer.Provided(assumed); @@ -18894,13 +18935,24 @@ static bool IsASumOfPowersOfX(Entity sum, Entity.Variable x) var atTheTop = Integration.AnsweringTheQuestionAsked; // The signs are constants, one on each side of a factor's root, so they come out // in front of the integral: the integrand is asked without them, and the answer - // is the product of the signs to their powers, an even power being one. + // is the product of the signs to their powers, an even power being one. From a root + // that is a factor of the integrand only: inside a sum the sign is a factor of + // nothing, `x/(x + sqrt(x^6))` being `1/(1 + x^2)` for a positive x and `1/(1 - x^2)` + // for a negative one. There the root is written with a sign of its own, and the + // integrand is integrated with it 1 and with it -1, each on its side of the + // factor's root; one such factor only. Entity signs = Number.Integer.One; + var factorsOfTheIntegrand = FactorsOfTheIntegrand(expr).Select(pair => pair.Factor).ToList(); + var sign = Variable.CreateUnique(expr, "s_sgn"); + Entity? signedFactor = null; + var signsOfMoreThanOneFactor = false; var rewritten = expr.Replace(node => { if (node is not Powf(var radicand, Number.Rational exponent) || exponent is Number.Integer || !exponent.ERational.Denominator.Equals(EInteger.FromInt32(2)) || !radicand.ContainsNode(x)) return node; + var asAFactor = factorsOfTheIntegrand.Count(factor => factor == node) == expr.Nodes.Count(inner => inner == node); + var signsHere = new List(); if (Functions.PolynomialFactorization.FactorComplete(radicand, x) is not { } factorization || factorization.Parts.All(part => part.Multiplicity < 2)) return node; @@ -18928,19 +18980,46 @@ static bool IsASumOfPowersOfX(Entity sum, Entity.Variable x) } outside = outside * (half == 1 ? factor : MathS.Pow(factor, half)); if (withASign) - signs = signs * MathS.Signum(factor); + signsHere.Add(factor); } if (part.Multiplicity % 2 == 1) inside = inside * factor; } if (!outside.ContainsNode(x)) return node; - return MathS.Pow(outside, Number.Integer.Create(numerator)) * MathS.Pow(inside, exponent); + var taken = MathS.Pow(outside, Number.Integer.Create(numerator)) * MathS.Pow(inside, exponent); + if (signsHere.Count == 0) + return taken; + if (asAFactor) + { + foreach (var signed in signsHere) + signs = signs * MathS.Signum(signed); + return taken; + } + if (signsHere.Count > 1 || signedFactor is not null && signedFactor != signsHere[0]) + { + signsOfMoreThanOneFactor = true; + return node; + } + signedFactor = signsHere[0]; + return sign * taken; }); - if (rewritten == expr) - return null; - if (Integration.ComputeAsAQuestionOfItsOwn(rewritten, x, integrateByParts) is not { } result) + if (rewritten == expr || signsOfMoreThanOneFactor) return null; + Entity result; + if (signedFactor is null) + { + if (Integration.ComputeAsAQuestionOfItsOwn(rewritten, x, integrateByParts) is not { } unsigned) + return null; + result = unsigned; + } + else + { + if (Integration.ComputeAsAQuestionOfItsOwn(rewritten.Substitute(sign, Number.Integer.One).InnerSimplified, x, integrateByParts) is not { } above + || Integration.ComputeAsAQuestionOfItsOwn(rewritten.Substitute(sign, Number.Integer.MinusOne).InnerSimplified, x, integrateByParts) is not { } below) + return null; + result = MathS.Piecewise(new[] { new Providedf(above, signedFactor > Number.Integer.Zero) }, below); + } var answer = signs == Number.Integer.One ? result : signs * result; return answer.Nodes.Any(node => node == MathS.NaN) ? null : answer; } diff --git a/Sources/Tests/UnitTests/Calculus/RootOfAPerfectSquareIntegralTest.cs b/Sources/Tests/UnitTests/Calculus/RootOfAPerfectSquareIntegralTest.cs index 6494308f7..4bbceba3e 100644 --- a/Sources/Tests/UnitTests/Calculus/RootOfAPerfectSquareIntegralTest.cs +++ b/Sources/Tests/UnitTests/Calculus/RootOfAPerfectSquareIntegralTest.cs @@ -54,6 +54,25 @@ private static void DifferentiatesBack(string integrand) [InlineData("sqrt(sinh(x)^2)")] public void ARootOfAPerfectSquareIsTheModulus(string integrand) => DifferentiatesBack(integrand); + /// + /// A root of a square inside a sum, where its sign is a factor of nothing: + /// 1/(1 + (x^2)^(3/2)) is 1/(1 + x^3) for a positive x and + /// 1/(1 - x^3) for a negative one, each integrated on its own side of zero. With the + /// sign taken out in front, the answer was the first's on both sides. The same with a + /// square factor taken out of a root: x/(x + sqrt(x^6)) is 1/(1 + x^2) and + /// 1/(1 - x^2). Rubi's 1.1.3.2 and 1.3.2. + /// #718 + /// + [Theory] + [InlineData("1/(1+(x^2)^(3/2))")] + [InlineData("1/(1+sqrt(x^2))")] + [InlineData("x/(1+(x^2)^(3/2))")] + [InlineData("(x^2)^(3/2)/(1+(x^2)^(3/2))")] + [InlineData("1/(2+sqrt(x^2+2*x+1))")] + [InlineData("x/(x+sqrt(x^6))")] + [InlineData("(x-sqrt(x^6))/(x*(1-x^4))")] + public void ARootOfASquareInsideASumIsIntegratedOnEachSide(string integrand) => DifferentiatesBack(integrand); + /// /// A square written with symbols: a^2 + 2 a b x + b^2 x^2 is (a + b x)^2, /// whose discriminant 4a^2b^2 - 4a^2b^2 does