From 18b368d309d9ab7990f831be3351b0dfc1d86942 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Sun, 11 Oct 2026 23:41:27 +0000 Subject: [PATCH 1/2] The half angle takes the factors a combined denominator's terms share out of the sum Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura --- .../Integration/IndefiniteIntegralSolver.cs | 92 ++++++++++++++++++- .../ASecantAndATangentInOneSumIntegralTest.cs | 46 ++++++++++ 2 files changed, 137 insertions(+), 1 deletion(-) create mode 100644 Sources/Tests/UnitTests/Calculus/ASecantAndATangentInOneSumIntegralTest.cs diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs index 5f5414950..a38895bcb 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs @@ -29308,7 +29308,7 @@ static bool IsOneOfTheFour(Entity node, Entity argument) { var (combinedAbove, combinedBelow) = Functions.SingleQuotient.Of(combined.InnerSimplified); integrand = (WithDegenerateBasesLowered(WithWholePowersDistributed(combinedAbove), t) - / WithDegenerateBasesLowered(WithWholePowersDistributed(combinedBelow), t)).InnerSimplified; + / WithDegenerateBasesLowered(WithTheSharedFactorsOutOfEachSum(WithWholePowersDistributed(combinedBelow), t), t)).InnerSimplified; } else integrand = combined.Simplify(); @@ -29369,6 +29369,96 @@ static bool NoRemainder(Entity rest) : null; } + /// + /// with each factor that is a sum of products written as the + /// factors every term shares, with in them, times the sum of what + /// is left: A (1 + t^2)(1 - t^2) + 2 b t (1 + t^2)(1 - t^2) is + /// (1 + t^2)(1 - t^2)(A + 2 b t). + /// + /// + /// 1/(a + c sec(x) + b tan(x)) combines over cos(x) into a denominator + /// with both of the half angle's factors in each term, and read as a quartic with a + /// symbol in every coefficient it was declined after five seconds. Its numerator is + /// 2 (1 - t^2)^2: with the factors out, the division below takes off the + /// 1 + t^2, the partial fractions cancel the 1 - t^2, and what is left + /// is a quadratic beside 1 + t^2. + /// + private static Entity WithTheSharedFactorsOutOfEachSum(Entity product, Entity.Variable t) + { + Entity written = Number.Integer.One; + foreach (var factor in Mulf.LinearChildren(product)) + written *= factor is Sumf ? WithTheSharedFactorsOut(factor, t) : factor; + return written; + } + + /// + /// as the factors with in them that every + /// term has, each to the least power any term has it in, times the sum of what is left; + /// as written where no term shares one. + /// + private static Entity WithTheSharedFactorsOut(Entity sum, Entity.Variable t) + { + static Dictionary Powers(Entity term, Entity.Variable t) + { + var powers = new Dictionary(); + foreach (var factor in Mulf.LinearChildren(term)) + { + var (@base, power) = factor is Powf(var raised, Number.Integer { EInteger.Sign: > 0 } exponent) + ? (raised, exponent.EInteger) : (factor, EInteger.One); + if (@base.ContainsNode(t)) + powers[@base] = powers.TryGetValue(@base, out var before) ? before.Add(power) : power; + } + return powers; + } + // Inner sums first: in `(a (1 + t^2) 2 t + b (1 - t^2)(1 + t^2)) 2 t + c (1 + t^2)^2 2 t`, + // which `1/(a + b cot(x) + c csc(x))` combines into, the first term has its + // `1 + t^2` inside a sum of its own. + var terms = Sumf.LinearChildren(sum).Select(term => WithTheSharedFactorsOutOfEachSum(term, t)).ToList(); + if (terms.Count < 2) + return sum; + var shared = Powers(terms[0], t); + foreach (var term in terms.Skip(1)) + { + var powers = Powers(term, t); + foreach (var @base in shared.Keys.ToList()) + if (powers.TryGetValue(@base, out var power)) + shared[@base] = EInteger.Min(shared[@base], power); + else + shared.Remove(@base); + } + // A bare `t` shared by every term is a root at zero, which the partial fractions + // find on their own; only a sum is worth taking out. + if (!shared.Keys.Any(@base => @base is Sumf or Minusf)) + return terms.Aggregate((left, right) => left + right); + Entity common = Number.Integer.One; + foreach (var (@base, power) in shared) + common *= power.Equals(EInteger.One) ? @base : MathS.Pow(@base, Number.Integer.Create(power)); + Entity rest = Number.Integer.Zero; + foreach (var term in terms) + { + var left = new Dictionary(shared); + Entity kept = Number.Integer.One; + foreach (var factor in Mulf.LinearChildren(term)) + { + var (@base, power) = factor is Powf(var raised, Number.Integer { EInteger.Sign: > 0 } exponent) + ? (raised, exponent.EInteger) : (factor, EInteger.One); + if (left.TryGetValue(@base, out var owed) && owed.Sign > 0) + { + var taken = EInteger.Min(owed, power); + left[@base] = owed.Subtract(taken); + power = power.Subtract(taken); + if (power.IsZero) + continue; + kept *= power.Equals(EInteger.One) ? @base : MathS.Pow(@base, Number.Integer.Create(power)); + } + else + kept *= factor; + } + rest += kept; + } + return common * rest; + } + /// /// 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 diff --git a/Sources/Tests/UnitTests/Calculus/ASecantAndATangentInOneSumIntegralTest.cs b/Sources/Tests/UnitTests/Calculus/ASecantAndATangentInOneSumIntegralTest.cs new file mode 100644 index 000000000..194e47629 --- /dev/null +++ b/Sources/Tests/UnitTests/Calculus/ASecantAndATangentInOneSumIntegralTest.cs @@ -0,0 +1,46 @@ +// +// 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 +{ + /// + /// 1/(a + c sec + b tan) and 1/(a + b cot + c csc) in the half angle's tangent, + /// with the factors every term of the combined denominator shares taken out. Rubi's 4.7.7. + /// #718 + /// + [Trait("Area", "Calculus")] + public sealed class ASecantAndATangentInOneSumIntegralTest + { + [Theory] + [InlineData("1/(a + c*sec(x) + b*tan(x))", 4000)] + [InlineData("1/(a + b*cot(x) + c*csc(x))", 4000)] + [InlineData("csc(x)/(a + b*cot(x) + c*csc(x))", 4000)] + [InlineData("1/(a + c*sec(2*x + 1) + b*tan(2*x + 1))", 4000)] + public void DifferentiatesBack(string integrand, int atMost) + { + var integral = integrand.ToEntity().Integrate("x"); + var text = integral.Stringize(); + Assert.DoesNotContain("integral(", text); + Assert.True(text.Length < atMost, $"{text.Length} characters of answer for {integrand}"); + Entity Pinned(Entity e) => e.Substitute("a", 2).Substitute("b", 3).Substitute("c", "5/4".ToEntity()); + var derivative = Pinned(integral.Substitute("C", 0)).Differentiate("x"); + var original = Pinned(integrand.ToEntity()); + foreach (var at in new[] { "0.35", "0.6", "1.1", "1.4" }) + { + var want = original.Substitute("x", at.ToEntity()).EvalNumerical(); + var got = derivative.Substitute("x", at.ToEntity()).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}"); + } + } + } +} From 6a28adc3879dcc7b9828682b24358e2f80bde12f Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Mon, 12 Oct 2026 02:04:15 +0000 Subject: [PATCH 2/2] Record the secant and tangent half-angle answers, and build on netstandard2.0 Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura --- BREAKING-CHANGES.md | 14 ++++++++++++++ .../Integration/IndefiniteIntegralSolver.cs | 4 ++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index c953e08fc..1bec563d1 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -505,6 +505,20 @@ two more of Rubi's 4.5.2.3. The two sums multiply to `-a c tan^2`, so in `u = a | `"sec(e + f*x)*(a + a*sec(e + f*x))^m*(c - c*sec(e + f*x))^(5/2)".ToEntity().Integrate("x")` | `integral(...)` | 469 characters | | `"sec(e + f*x)*(a + a*sec(e + f*x))^m*sqrt(c - c*sec(e + f*x))".ToEntity().Integrate("x")` | `integral(...)` | 268 characters | +### A secant and a tangent in one sum below the bar are integrated in the half angle + +**Answers where there were none.** `1/(a + c sec(x) + b tan(x))` was declined, and so was +`1/(a + b cot(x) + c csc(x))`, Rubi's 4.7.7. Combined over the cosine, the half-angle tangent `t` gives a denominator +with `1 + t^2` and `1 - t^2` in every term, which was read as a quartic with a symbol in every coefficient. Those +shared factors are now taken out of the sum first, so the numerator cancels them and a quadratic beside `1 + t^2` is +left ([#718](https://github.com/asc-community/AngouriMath/issues/718)). + +| Input | Was (2.5.0) | Now | +|---|---|---| +| `"1/(a + c*sec(x) + b*tan(x))".ToEntity().Integrate("x")` | `integral(...)` | 2,211 characters | +| `"1/(a + b*cot(x) + c*csc(x))".ToEntity().Integrate("x")` | `integral(...)` | 2,738 characters | +| `"1/(a + c*sec(2*x + 1) + b*tan(2*x + 1))".ToEntity().Integrate("x")` | `integral(...)`; past 290 s on the unreleased master | 2,890 characters | + ### `a cos + i a sin` beside powers of the secant is written over the cosine **Answers where there were none, and shorter ones.** `sec(c + d x)^5/(a cos(c + d x) + i a sin(c + d x))^2` was declined, diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs index a38895bcb..8a9d3ef73 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs @@ -29431,8 +29431,8 @@ static Dictionary Powers(Entity term, Entity.Variable t) if (!shared.Keys.Any(@base => @base is Sumf or Minusf)) return terms.Aggregate((left, right) => left + right); Entity common = Number.Integer.One; - foreach (var (@base, power) in shared) - common *= power.Equals(EInteger.One) ? @base : MathS.Pow(@base, Number.Integer.Create(power)); + foreach (var pair in shared) + common *= pair.Value.Equals(EInteger.One) ? pair.Key : MathS.Pow(pair.Key, Number.Integer.Create(pair.Value)); Entity rest = Number.Integer.Zero; foreach (var term in terms) {