diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index e8604ab5f..1b2b09dbe 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -790,6 +790,23 @@ poles of `tan(x/2)`, as every half-angle answer. | `"1/(a+b*cos(x)+c*cos(x)^2)".Integrate("x")` | unevaluated | the cosine form | | `"csc(x)^2/(a-a*sin(x)^2)".Integrate("x")` | unevaluated | `-cot(x)/a` and two terms rational in `tan(x/2)` | +### A rational function of the sine and cosine with symbols in it is no longer simplified under the half angle + +**Answers where there were none.** Under `t = tan(x/2)` the integrand was simplified before it was +integrated, and with symbols in it the simplifier's search grew past any budget: +`sec(x)^2/(a + b cos(x))^3` spent about forty seconds in it and `sec(x)^2/(a + b cos(x))^4` more than two minutes, +where integrating what it returned takes a tenth of a second. A rational function of `t` with a +symbol in it is now put over one bar with its whole powers of products written as products of +powers, and not simplified further; one without a symbol, or with a root of `t` in it, is +simplified as before +([#718](https://github.com/asc-community/AngouriMath/issues/718)). + +| Input | Was (2.5.0) | Now | +|---|---|---| +| `"sec(x)^2/(a + b*cos(x))^3".ToEntity().Integrate("x")` | `integral(...)` | a piecewise antiderivative in `tan(x/2)` | +| `"sec(x)^2/(a + b*cos(x))^4".ToEntity().Integrate("x")` | `integral(...)` | the same | +| `"sec(c + d*x)^2/(a + b*cos(c + d*x))^4".ToEntity().Integrate("x")` | `integral(...)` | the same in `tan((c + d x)/2)` | + ### A linear over a quadratic beside the root of another quadratic is integrated `(g + h x)/(A sqrt(B))`, `A` and `B` two different quadratics, was left unevaluated wherever a diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs index b382d6f0d..ab8942487 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs @@ -23348,7 +23348,29 @@ static bool IsOneOfTheFour(Entity node, Entity argument) // 1/(1 - sin(x)) rewrote to 2/((t^2 + 1)(1 + (-2)t/(t^2 + 1))) and stopped there, one // distribution short of 2/(t^2 - 2t + 1), which is integrated at once. // https://github.com/asc-community/AngouriMath/issues/1239 - var integrand = Functions.SingleQuotient.Combine(inT * 2 / (rate * (1 + tSquared))).Simplify(); + // Once combined, a quotient with a symbol in its coefficients is not simplified: the + // simplifier's search grows with the symbols, and `sec(x)^2/(a + b cos(x))^3` spent 43 s + // in it where integrating what it returned took a tenth of a second. Two things the + // simplifier did are done here, since what follows reads them. Whole powers of + // products are written as products of powers: `((1 + t^2)(1 - t^2))^6` keeps the + // `1 + t^2` the division below takes off as a factor out of its sight, and + // `tan(x)^6/(a + b sec(x))` was declined that way. And a base of a lower degree than it + // is written in is written as its polynomial: `a (1 + t^2) + a (1 - t^2)` is `2 a`, and + // read as a quadratic its leading coefficient is zero as a value only. Only for a + // rational function of t: under a root the simplifier's writing of the radicand is + // what the radical rules read, and `1/(b cos(x) + c sin(x) - sqrt(b^2 + c^2))^(3/2)` + // went from a decline in three seconds to a search past two minutes without it. + var combined = Functions.SingleQuotient.Combine(inT * 2 / (rate * (1 + tSquared))); + Entity integrand; + if (combined.Vars.Any(symbol => symbol != t) + && !combined.Nodes.Any(node => node is Powf(var radicand, var power) && radicand.ContainsNode(t) && power.Evaled is not Number.Integer)) + { + var (combinedAbove, combinedBelow) = Functions.SingleQuotient.Of(combined.InnerSimplified); + integrand = (WithDegenerateBasesLowered(WithWholePowersDistributed(combinedAbove), t) + / WithDegenerateBasesLowered(WithWholePowersDistributed(combinedBelow), t)).InnerSimplified; + } + else + integrand = combined.Simplify(); // Collapsing the nesting attaches a condition saying the denominator it cleared is // non-zero, and that denominator is 1 + t^2 -- so 1/(1 + cos(x)) comes out as @@ -23406,6 +23428,61 @@ static bool NoRemainder(Entity rest) : null; } + /// + /// with each whole power of a product written as the product + /// of the powers, the rest as written: ((1 + t^2)(1 - t^2))^6 is + /// (1 + t^2)^6 (1 - t^2)^6. + /// + private static Entity WithWholePowersDistributed(Entity product) + { + Entity written = Number.Integer.One; + foreach (var factor in Mulf.LinearChildren(product)) + { + if (factor is Powf(Mulf inner, Number.Integer { EInteger.Sign: > 0 } power)) + foreach (var innerFactor in Mulf.LinearChildren(WithWholePowersDistributed(inner))) + written *= innerFactor is Powf(var @base, Number.Integer exponent) + ? MathS.Pow(@base, Number.Integer.Create(exponent.EInteger.Multiply(power.EInteger))) + : MathS.Pow(innerFactor, power); + else + written *= factor; + } + return written; + } + + /// + /// with each base that is a sum written as its polynomial in + /// where that is of a lower degree than the base is written in, and + /// as written otherwise: a (1 + t^2) + a (1 - t^2) is 2 a. + /// + private static Entity WithDegenerateBasesLowered(Entity product, Entity.Variable t) + { + Entity written = Number.Integer.One; + foreach (var factor in Mulf.LinearChildren(product)) + { + var (@base, power) = factor is Powf(var raised, Number.Integer exponent) ? (raised, (Entity)exponent) : (factor, Number.Integer.One); + if (@base is Sumf or Minusf && @base.ContainsNode(t) && TreeAnalyzer.TryGetPolynomial(@base, t, out var terms) + && terms.Count > 0 && terms.All(term => term.Key.Sign >= 0 && !term.Value.ContainsNode(t))) + { + var writtenDegree = @base.Nodes.Select(node => node is Powf(var raised, Number.Integer exponent) && raised == t ? exponent.EInteger + : node == t ? EInteger.One : EInteger.Zero).Max()!; + var kept = terms.Where(term => !Functions.PartialFractions.IsZeroAsAValue(term.Value)).ToList(); + var degree = kept.Select(term => term.Key).DefaultIfEmpty(EInteger.Zero).Max()!; + if (degree.CompareTo(writtenDegree) < 0) + { + Entity polynomial = Number.Integer.Zero; + foreach (var term in kept.OrderByDescending(term => term.Key)) + { + var coefficient = term.Value.InnerSimplified; + polynomial += term.Key.IsZero ? coefficient : coefficient * MathS.Pow(t, Number.Integer.Create(term.Key)); + } + @base = polynomial.InnerSimplified; + } + } + written *= power == Number.Integer.One ? @base : MathS.Pow(@base, power); + } + return written; + } + /// /// with one power fewer of a factor that is /// 1 + t^2, the rest of the product as written; null where no factor is. diff --git a/Sources/Tests/UnitTests/Calculus/HalfAngleSubstitutionIntegralTest.cs b/Sources/Tests/UnitTests/Calculus/HalfAngleSubstitutionIntegralTest.cs index 25bdc66b7..e7d8a8188 100644 --- a/Sources/Tests/UnitTests/Calculus/HalfAngleSubstitutionIntegralTest.cs +++ b/Sources/Tests/UnitTests/Calculus/HalfAngleSubstitutionIntegralTest.cs @@ -235,12 +235,29 @@ public void ASumTheOrdinaryRulesAnswerBetter(string integrand) [InlineData("sec(x)^2/(a + b*cos(x))")] [InlineData("tan(x)^4/(a + a*cos(x))")] [InlineData("csc(x)^2/(a + a*cos(x))")] - public void ARationalFunctionWithSymbolsForCoefficients(string integrand) + public void ARationalFunctionWithSymbolsForCoefficients(string integrand) => DifferentiatesBackWithSymbols(integrand); + + /// + /// A power of a + b cos(x) past the square beside a power of the secant. The + /// substitution's integrand in t is not simplified where a symbol is in it: + /// simplified, it took 43 s at the cube and more than two minutes at the fourth power, + /// where integrating it takes a tenth of a second. + /// + [Theory] + [InlineData("sec(x)^2/(a + b*cos(x))^3")] + [InlineData("sec(x)^2/(a + b*cos(x))^4")] + [InlineData("sec(c + d*x)^2/(a + b*cos(c + d*x))^4")] + [InlineData("sec(x)/(a + b*cos(x))^3")] + [InlineData("1/(cos(x)*(a + b*cos(x))^2)")] + public void APowerOfALinearInTheCosineBesideASecant(string integrand) => DifferentiatesBackWithSymbols(integrand); + + private static void DifferentiatesBackWithSymbols(string integrand) { var integral = integrand.ToEntity().Integrate("x"); Assert.DoesNotContain("integral(", integral.Stringize()); - var derivative = integral.Substitute("C", 0).Differentiate("x").Substitute("a", 1.7).Substitute("b", 0.6); - var original = integrand.ToEntity().Substitute("a", 1.7).Substitute("b", 0.6); + Entity Pin(Entity e) => e.Substitute("a", 1.7).Substitute("b", 0.6).Substitute("c", 0.3).Substitute("d", 1.1); + var derivative = Pin(integral.Substitute("C", 0).Differentiate("x")); + var original = Pin(integrand.ToEntity()); var compared = 0; foreach (var at in Points) {