From eaf9d911e06f263200b68765ead14070409debc3 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Sat, 3 Oct 2026 14:26:30 +0000 Subject: [PATCH 1/3] A half-angle integrand with symbols in it is not simplified 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 43 s in it and the fourth power more than two minutes, where integrating what it returned takes 0.14 s. With a symbol in it the integrand is put over one bar and not simplified; one without a symbol is simplified as before. Two things the simplifier did are done instead, since the steps after it read them: whole powers of products are written as products of powers, so that the 1 + t^2 the division takes off as a factor is not hidden inside ((1 + t^2)(1 - t^2))^6, 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) as 2 a, where read as a quadratic its leading coefficient is zero as a value only. Part of #718. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura --- BREAKING-CHANGES.md | 16 ++++ .../Integration/IndefiniteIntegralSolver.cs | 75 ++++++++++++++++++- .../HalfAngleSubstitutionIntegralTest.cs | 23 +++++- 3 files changed, 110 insertions(+), 4 deletions(-) diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 09c9c3b8a..ce3545e2b 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -630,6 +630,22 @@ 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 43 s 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. With a symbol in it the integrand 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 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 01e0779d5..96039e943 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs @@ -22842,7 +22842,25 @@ 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. + var combined = Functions.SingleQuotient.Combine(inT * 2 / (rate * (1 + tSquared))); + Entity integrand; + if (combined.Vars.Any(symbol => symbol != t)) + { + 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 @@ -22900,6 +22918,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) { From 489eb68b72deb7af2804f55c39421f3afee9123e Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Sat, 3 Oct 2026 22:00:16 +0000 Subject: [PATCH 2/3] The half-angle integrand is left unsimplified only where it is rational Under a root the simplifier's writing of the radicand is what the radical rules read: 1/(b cos(x) + c sin(x) - sqrt(b^2 + c^2))^(3/2) was declined in 3 s with it and ran past two minutes without it. Part of #718. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura --- BREAKING-CHANGES.md | 7 ++++--- .../Continuous/Integration/IndefiniteIntegralSolver.cs | 8 ++++++-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 4fc132020..c5a7130b3 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -664,9 +664,10 @@ poles of `tan(x/2)`, as every half-angle answer. **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 43 s 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. With a symbol in it the integrand 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 is simplified as before +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 | diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs index 5663ce2cb..f5845642c 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs @@ -23006,10 +23006,14 @@ static bool IsOneOfTheFour(Entity node, Entity argument) // `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. + // 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)) + 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) From e4048ed7994fdea6a8ef59a857e3fe345f88449c Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Sun, 4 Oct 2026 04:12:29 +0000 Subject: [PATCH 3/3] The half-angle entry gives its timing as measured more than once Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura --- BREAKING-CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index cd5ad142b..1b2b09dbe 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -794,7 +794,7 @@ poles of `tan(x/2)`, as every half-angle answer. **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 43 s in it and `sec(x)^2/(a + b cos(x))^4` more than two minutes, +`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