From 0c253baec9eef7e2bccec2de8c3c129c78bac0a4 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Mon, 12 Oct 2026 02:39:45 +0000 Subject: [PATCH 1/4] A secant or a cosecant beside a root is read as the cosine or the sine the other way up Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura --- .../Integration/IndefiniteIntegralSolver.cs | 21 +++++++- ...ecantOrACosecantBesideARootIntegralTest.cs | 48 +++++++++++++++++++ 2 files changed, 67 insertions(+), 2 deletions(-) create mode 100644 Sources/Tests/UnitTests/Calculus/ASecantOrACosecantBesideARootIntegralTest.cs diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs index 5f5414950..2f13f7a0e 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs @@ -11134,6 +11134,11 @@ static bool IsTwice(Entity argument, Entity.Variable x) Secantf(var a) when a == x => sign * rootOfSecantSquared, Powf(Secantf(var a), Number.Integer k) when a == x && k.EInteger.CanFitInInt32() && !k.EInteger.IsEven => MathS.Pow(sign * rootOfSecantSquared, k), + // And the cosecant the sine: `sqrt(1 + tan(x)) csc(x) sec(x)` is + // `sqrt(1 + u) (1 + u^2)/u`, the two signs one. + Cosecantf(var a) when a == x => sign * rootOfSecantSquared / tangent, + Powf(Cosecantf(var a), Number.Integer k) when a == x && k.EInteger.CanFitInInt32() && !k.EInteger.IsEven + => MathS.Pow(sign * rootOfSecantSquared / tangent, k), _ => node, }).Substitute(tangent, uSub); if (odd.ContainsNode(x) || !IsAlgebraicIn(odd, uSub)) @@ -14090,8 +14095,20 @@ static bool More(Entity difference) var raisable = new List<(Entity Base, Entity Exponent)>(); var logarithmicDerivativesKept = new List(); Entity kept = Number.Integer.One; - foreach (var (factor, underneath) in FactorsOfTheIntegrand(expr)) - { + foreach (var (written, writtenUnderneath) in FactorsOfTheIntegrand(expr)) + { + // A secant is a power of the cosine, and a cosecant of the sine, below the bar, + // where raising it by one takes it out of the candidate: `sec(x) sqrt(sec(x) + tan(x))` + // is `(2 sqrt(sec(x) + tan(x)))'`, and read as a power of the secant it could + // only be raised to a square. + var (factor, underneath) = written switch + { + Secantf(var a) => (MathS.Cos(a), !writtenUnderneath), + Cosecantf(var a) => (MathS.Sin(a), !writtenUnderneath), + Powf(Secantf(var a), var p) when !p.ContainsNode(x) => (MathS.Pow(MathS.Cos(a), p), !writtenUnderneath), + Powf(Cosecantf(var a), var p) when !p.ContainsNode(x) => (MathS.Pow(MathS.Sin(a), p), !writtenUnderneath), + _ => (written, writtenUnderneath) + }; if (!factor.ContainsNode(x)) { constant = underneath ? constant / factor : constant * factor; diff --git a/Sources/Tests/UnitTests/Calculus/ASecantOrACosecantBesideARootIntegralTest.cs b/Sources/Tests/UnitTests/Calculus/ASecantOrACosecantBesideARootIntegralTest.cs new file mode 100644 index 000000000..22ee4d85b --- /dev/null +++ b/Sources/Tests/UnitTests/Calculus/ASecantOrACosecantBesideARootIntegralTest.cs @@ -0,0 +1,48 @@ +// +// 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 +{ + /// + /// A secant or a cosecant beside a root, read as the cosine or the sine the other way up: + /// sec(x) sqrt(sec(x) + tan(x)) is (2 sqrt(sec(x) + tan(x)))', and + /// sqrt(1 + tan(x)) csc(x) sec(x) is a function of the tangent. Rubi's 4.7.7. + /// #718 + /// + [Trait("Area", "Calculus")] + public sealed class ASecantOrACosecantBesideARootIntegralTest + { + [Theory] + [InlineData("sec(x)*sqrt(sec(x) + tan(x))", 100)] + [InlineData("sec(x)*sqrt(sec(x) - tan(x))", 100)] + [InlineData("csc(x)*sqrt(csc(x) + cot(x))", 100)] + [InlineData("sec(a + b*x)*(sec(a + b*x) + tan(a + b*x))^c", 200)] + [InlineData("sqrt(1 + tan(x))*csc(x)*sec(x)", 400)] + 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 fe1db03467e405cf3bf364783f6d7b40e321a9e1 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Mon, 12 Oct 2026 06:36:48 +0000 Subject: [PATCH 2/4] Record the secant and cosecant read as powers of the cosine and the sine Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura --- BREAKING-CHANGES.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 32cb2980c..e392225da 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -519,6 +519,22 @@ left ([#718](https://github.com/asc-community/AngouriMath/issues/718)). | `"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 secant or a cosecant beside a power of a sum of them is read as a power of the cosine or the sine + +**Answers where there were none.** `sec(x) sqrt(sec(x) + tan(x))` was declined, with the rest of Rubi's 4.7.7 +that puts a secant beside a power of `sec + tan`: the rule that recognises a product of powers as a derivative +read `sec(x)` as a function of its own, not as `cos(x)^(-1)`, and the tangent substitution wrote an odd power of +the secant through the root of `1 + t^2` but not one of the cosecant. Both are read so now, and the integrand is +the derivative of a power of the sum +([#718](https://github.com/asc-community/AngouriMath/issues/718)). + +| Input | Was (2.5.0) | Now | +|---|---|---| +| `"sec(x)*sqrt(sec(x) + tan(x))".ToEntity().Integrate("x")` | `integral(...)` | `2 * sqrt(sec(x) + tan(x))` | +| `"csc(x)*sqrt(csc(x) + cot(x))".ToEntity().Integrate("x")` | `integral(...)` | `(-2) * sqrt(csc(x) + cotan(x))` | +| `"sec(a + b*x)*(sec(a + b*x) + tan(a + b*x))^c".ToEntity().Integrate("x")` | `integral(...)` | `1 / (b * c) * (sec(a + b * x) + tan(a + b * x)) ^ c` | +| `"sqrt(1 + tan(x))*csc(x)*sec(x)".ToEntity().Integrate("x")` | `integral(...)` | 94 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, From bc4337c51cdeaa37b875c79e051035c5335a2d07 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Mon, 12 Oct 2026 06:42:51 +0000 Subject: [PATCH 3/4] A root of a cos + b sin beside a half-odd power of the cosine is a root of a + b tan Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura --- .../Integration/IndefiniteIntegralSolver.cs | 100 ++++++++++++++++++ ...BesideAHalfPowerOfTheCosineIntegralTest.cs | 48 +++++++++ 2 files changed, 148 insertions(+) create mode 100644 Sources/Tests/UnitTests/Calculus/ARootBesideAHalfPowerOfTheCosineIntegralTest.cs diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs index 352cc3792..19600caad 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs @@ -10997,6 +10997,101 @@ bool Read(Entity e, int times) return Integration.ComputeAsTheSameQuestion(rewritten, x, integrateByParts)?.Pipe(answer => answer / constant); } + /// + /// (a cos(x) + b sin(x))^(m/2) cos(x)^(n/2), m and n odd, written as + /// (a + b tan(x))^(m/2) cos(x)^((m + n)/2), and the sine's the same way with + /// a cot(x) + b; where the integrand is not that shape. + /// + /// + /// + /// A^(m/2) c^(n/2) is (sqrt(A)/sqrt(c))^m c^((m + n)/2) for principal roots, + /// and sqrt(A)/sqrt(c) is sqrt(A/c) where A and c have one + /// sign: both roots real, or both imaginary. Where the signs differ the product is + /// imaginary, so with no other root of x in the integrand to make it real again, + /// the two agree wherever the integrand is real. Rubi's 4.7.7 + /// csc(x) sqrt(cos(x) + sin(x))/cos(x)^(3/2) is + /// sqrt(1 + tan(x)) csc(x) sec(x) so, and that is the tangent substitution's. + /// + /// + /// Only a linear A: sqrt(c^3) and c^(3/2) differ in sign where + /// c is negative, so a cubic over the cube of the cosine is not the same quotient. + /// https://github.com/asc-community/AngouriMath/issues/718 + /// + /// + private static Entity? WithARootOverTheCosineOrTheSine(Entity expr, Entity.Variable x) + { + var factors = FactorsOfTheIntegrand(expr); + int trigonometric = -1, root = -1; + for (var i = 0; i < factors.Count; i++) + { + var factor = factors[i].Factor; + if (factor is Powf(var @base, Number.Rational fraction) && fraction is not Number.Integer && @base.ContainsNode(x)) + { + if (!fraction.ERational.Denominator.Equals(EInteger.FromInt32(2))) + return null; + if (@base is Cosf or Sinf && @base.DirectChildren[0] == x) + { + if (trigonometric >= 0) + return null; + trigonometric = i; + } + else + { + if (root >= 0) + return null; + root = i; + } + } + else if (factor.Nodes.Any(node => node is Powf(var b, var p) && b.ContainsNode(x) && p is not Number.Integer)) + return null; + } + if (trigonometric < 0 || root < 0) + return null; + var (trigonometricPower, trigonometricUnderneath) = factors[trigonometric]; + var (rootPower, rootUnderneath) = factors[root]; + if (trigonometricPower is not Powf(var function, Number.Rational trigonometricExponent) + || rootPower is not Powf(var sum, Number.Rational rootExponent)) + return null; + + var cosine = MathS.Cos(x); + var sine = MathS.Sin(x); + var c = Variable.CreateUnique(expr, "c_cos"); + var s = Variable.CreateUnique(expr * c, "s_sin"); + var inCs = sum.Substitute(cosine, c).Substitute(sine, s); + if (inCs.ContainsNode(x) + || !TreeAnalyzer.TryGetPolyLinear(inCs, c, out var a, out var rest) + || !TreeAnalyzer.TryGetPolyLinear(rest, s, out var b, out var free) + || a.ContainsNode(c) || a.ContainsNode(s) || b.ContainsNode(c) || b.ContainsNode(s) + || free.Evaled != Number.Integer.Zero || a.Evaled == Number.Integer.Zero || b.Evaled == Number.Integer.Zero) + return null; + + var m = rootUnderneath ? rootExponent.ERational.Numerator.Negate() : rootExponent.ERational.Numerator; + var n = trigonometricUnderneath ? trigonometricExponent.ERational.Numerator.Negate() : trigonometricExponent.ERational.Numerator; + var tangent = MathS.Tan(x); + var quotient = function is Cosf ? a + b * tangent : a / tangent + b; + var whole = m.Add(n).Divide(EInteger.FromInt32(2)); + + Entity above = Number.Integer.One, below = Number.Integer.One; + for (var i = 0; i < factors.Count; i++) + if (i != trigonometric && i != root) + { + if (factors[i].Underneath) + below *= factors[i].Factor; + else + above *= factors[i].Factor; + } + var halfOfM = Number.Rational.Create(m.Abs(), EInteger.FromInt32(2)); + if (m.Sign > 0) + above *= MathS.Pow(quotient, halfOfM); + else + below *= MathS.Pow(quotient, halfOfM); + if (whole.Sign > 0) + above *= MathS.Pow(function, Number.Integer.Create(whole)); + else if (whole.Sign < 0) + below *= MathS.Pow(function, Number.Integer.Create(whole.Abs())); + return above / below; + } + /// /// An integrand that is a function of tan(x) and of nothing else, integrated by /// the substitution u = tan(x), under which dx is du/(1 + u^2). @@ -11061,6 +11156,11 @@ node is Powf(var product, Number.Rational fraction) && fraction is not Number.In ? apart : node); + // A half-odd power of `a cos(x) + b sin(x)` beside a half-odd power of the cosine is + // a root of their quotient, a function of the tangent; the sine the same way. + if (WithARootOverTheCosineOrTheSine(expr, x) is { } combined) + expr = combined; + var tangent = MathS.Tan(x); // Or a sine or a cosine under a root, for the writing by the sign below, or an even // power of the secant or the cosecant under one, which the next step writes in the diff --git a/Sources/Tests/UnitTests/Calculus/ARootBesideAHalfPowerOfTheCosineIntegralTest.cs b/Sources/Tests/UnitTests/Calculus/ARootBesideAHalfPowerOfTheCosineIntegralTest.cs new file mode 100644 index 000000000..bd55edcab --- /dev/null +++ b/Sources/Tests/UnitTests/Calculus/ARootBesideAHalfPowerOfTheCosineIntegralTest.cs @@ -0,0 +1,48 @@ +// +// 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 +{ + /// + /// A root of a cos(x) + b sin(x) beside a half-odd power of the cosine, read as a + /// root of a + b tan(x): csc(x) sqrt(cos(x) + sin(x))/cos(x)^(3/2) is + /// sqrt(1 + tan(x)) csc(x) sec(x), and the sine's the same way. Checked where both + /// roots are imaginary too, since the integrand is real there. Rubi's 4.7.7. + /// #718 + /// + [Trait("Area", "Calculus")] + public sealed class ARootBesideAHalfPowerOfTheCosineIntegralTest + { + [Theory] + [InlineData("csc(x)*sqrt(cos(x) + sin(x))/cos(x)^(3/2)", 200)] + [InlineData("sqrt(cos(x) + sin(x))/cos(x)^(5/2)", 100)] + [InlineData("sqrt(2*cos(x) + 3*sin(x))/(sin(x)*cos(x)^(3/2))", 300)] + [InlineData("sec(x)*sqrt(cos(x) - sin(x))/sin(x)^(3/2)", 200)] + 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}"); + var derivative = integral.Substitute("C", 0).Differentiate("x"); + var original = integrand.ToEntity(); + foreach (var at in new[] { "0.35", "0.6", "3.8", "3.9" }) + { + var want = original.Substitute("x", at.ToEntity()).EvalNumerical(); + var got = derivative.Substitute("x", at.ToEntity()).EvalNumerical(); + Assert.True(Math.Abs((double)want.ImaginaryPart) < 1e-9, $"{integrand} is not real at x = {at}"); + 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 9a1fadb22d5c8862ebeb75864bcda7057693bfe1 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Mon, 12 Oct 2026 11:14:47 +0000 Subject: [PATCH 4/4] Record the root of a sum of the cosine and the sine in the breaking changes Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura --- BREAKING-CHANGES.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index e392225da..453ba8b9c 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -554,6 +554,24 @@ and `sec^2` over its cube, the answers are a quarter and three-fifths the size | `"sec(c + d*x)^4/(a*cos(c + d*x) + i*a*sin(c + d*x))".ToEntity().Integrate("x")` | `integral(...)` | 209 characters, where the unreleased master gave 780 | | `"sec(c + d*x)^2/(a*cos(c + d*x) + i*a*sin(c + d*x))^3".ToEntity().Integrate("x")` | `integral(...)` | 488 characters, where the unreleased master gave 810 | +### A root of a sum of the cosine and the sine beside a half power of one of them is read in the tangent + +**Answers where there were none.** `csc(x) sqrt(cos(x) + sin(x))/cos(x)^(3/2)` was declined, Rubi's 4.7.7, and so +were its respellings below. `sqrt(a cos + b sin)` beside `cos^(n/2)` is `sqrt(a + b tan) cos^((n + 1)/2)` wherever +both sides are real, since the root of a product of two non-negative numbers is the product of their roots, and +when the sum and the cosine are both negative the two signs cancel the same way; the sine reads as `a/tan + b`. What +is left is a function of the tangent times a whole power of the cosine. Where the integrand is not real the answer +takes the branches of that respelling +([#718](https://github.com/asc-community/AngouriMath/issues/718)). + +| Input | Was (2.5.0) | Now | +|---|---|---| +| `"csc(x)*sqrt(cos(x) + sin(x))/cos(x)^(3/2)".ToEntity().Integrate("x")` | `integral(...)` | 94 characters | +| `"sqrt(cos(x) + sin(x))/cos(x)^(5/2)".ToEntity().Integrate("x")` | `integral(...)` | `(1 + tan(x)) ^ (3/2) / (3/2)` | +| `"sqrt(2*cos(x) + 3*sin(x))/(sin(x)*cos(x)^(3/2))".ToEntity().Integrate("x")` | `integral(...)` | 138 characters | +| `"sec(x)*sqrt(cos(x) - sin(x))/sin(x)^(3/2)".ToEntity().Integrate("x")` | `integral(...)` | 74 characters | +| `"1/(cos(x)^(3/2)*sqrt(3*cos(x) + sin(x)))".ToEntity().Integrate("x")` | `integral(...)` | `2 * sqrt(tan(x) + 3)` | + ### An even rational function with symbols in it is not searched for a substitution **Answers where there were none.** `(d + e x^2 + f x^4)/(x^4 (a + b x^2 + c x^4)^2)` ran past a minute: the