From 4517be5b7ad8907e9cf2487c1b05cadd959e5765 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Sun, 4 Oct 2026 17:41:29 +0000 Subject: [PATCH] A root of a square in a trigonometric function is its modulus b^2 + 2 a b f + a^2 f^2 is a^2 (f + b/a)^2, so a half-odd power of it is that power of sqrt(a^2) |f + b/a|, written with the sign of f + b/a in front of the integral, as the root of a perfect square in x already is. At the top, for one trigonometric function f and where each such root is a factor of the integrand. Rubi's (a + b sin(x)) sqrt(b^2 + 2 a b sin(x) + a^2 sin(x)^2) and the rest of its 4.7.7 with a tangent or a secant were declined or past the budget. Part of #718. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura --- BREAKING-CHANGES.md | 13 +++ .../Integration/IndefiniteIntegralSolver.cs | 80 +++++++++++++++++++ .../Integration/Integration.Definition.cs | 2 + ...areInATrigonometricFunctionIntegralTest.cs | 49 ++++++++++++ 4 files changed, 144 insertions(+) create mode 100644 Sources/Tests/UnitTests/Calculus/RootOfASquareInATrigonometricFunctionIntegralTest.cs diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index dc39a1fe6..42b2ff3b9 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -634,6 +634,19 @@ inner-simplified now, and declined in seconds where the parts do not separate | `"x^4*e^(2*i*atan(a + b*x))".ToEntity().Integrate("x")` | `integral(...)` | a polynomial, logarithms and arctangents of `a + b x` | | `"sec(c + d*x)^2/(a + i*a*tan(c + d*x))".ToEntity().Integrate("x")` | `ln(i a d tan(c + d x) + a d)/(i a d)` | `ln(i tan(c + d x) + 1)/(i a d)`, the same up to a constant | +### A root of a square in a trigonometric function is its modulus + +**Answers where there were none.** `(a + b sin(x)) sqrt(b^2 + 2 a b sin(x) + a^2 sin(x)^2)` was +declined or past the budget, with the rest of Rubi's 4.7.7 half-odd powers of a perfect square in a +sine, tangent or secant. The radicand is `a^2 (f + b/a)^2`, so its root is `sqrt(a^2) |f + b/a|`, +written with the sign of `f + b/a` in front of the integral, as a root of a perfect square in `x` +already was ([#718](https://github.com/asc-community/AngouriMath/issues/718)). + +| Input | Was (2.5.0) | Now | +|---|---|---| +| `"(a + b*sin(x))*sqrt(b^2 + 2*a*b*sin(x) + a^2*sin(x)^2)".ToEntity().Integrate("x")` | `integral(...)` | `sgn(sin(x) + b/a) sqrt(a^2)` times an expression in `x`, `cos(x)` and `sin(2x)`, `provided a^2 > 0` | +| `"(a + b*tan(x))/sqrt(b^2 + 2*a*b*tan(x) + a^2*tan(x)^2)".ToEntity().Integrate("x")` | `integral(...)` | `sgn(tan(x) + b/a) a/sqrt(a^2)` times the antiderivative of `(a + b tan(x))/(a tan(x) + b)`, `provided a^2 > 0` | + ### `NaN` was returned as the antiderivative of something that has one **A wrong answer, not a missing one.** `1/(a*x^2)` came back as `NaN + C`, and `NaN` is this diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs index 875a31f1c..1effae3e1 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs @@ -17038,6 +17038,86 @@ bool MayBeARootOfASquare(Entity node, Entity.Variable x) return assumed == Entity.Boolean.True ? answer : answer.Provided(assumed); } + /// + /// A half-odd power of a perfect square in one trigonometric function of + /// is that power of the modulus: sqrt(b^2 + 2 a b sin(y) + a^2 sin(y)^2) is + /// sqrt(a^2) |sin(y) + b/a|, which is sqrt(a^2) sgn(sin(y) + b/a) (sin(y) + b/a), + /// the sign constant between the zeros of the linear in the sine. + /// + /// + /// + /// 's reading, with the function for the + /// variable: Rubi's 4.7.7 has (a + b sin(d + e x)) sqrt(b^2 + 2 a b sin(d + e x) + a^2 sin(d + e x)^2) + /// and the same with a tangent or a secant, at the half-odd powers either way, and they + /// were declined or past the budget where with the modulus written the rest is rational in + /// the function. At the top only, and only where each such root is a factor of the + /// integrand, so that its sign goes in front; a leading coefficient a number or one of + /// known sign for a real parameter, whose condition travels with the answer, as that + /// rule's does. + /// https://github.com/asc-community/AngouriMath/issues/718 + /// + /// + internal static Entity? SolveByTakingARootOfAPerfectSquareInATrigonometricFunction(Entity expr, Entity.Variable x, bool integrateByParts) + { + if (!Integration.AnsweringTheQuestionAsked) + return null; + var factors = FactorsOfTheIntegrand(expr).Select(pair => pair.Factor).ToList(); + var changed = false; + var declined = false; + Entity assumed = Entity.Boolean.True; + Entity signs = Number.Integer.One; + var written = expr.Replace(node => + { + if (declined || node is not Powf(var radicand, Number.Rational r) || r is Number.Integer + || !r.ERational.Denominator.Equals(EInteger.FromInt32(2)) || !radicand.ContainsNode(x) + || radicand.Complexity > 40 || TreeAnalyzer.TryGetPolynomial(radicand, x, out _)) + return node; + var functions = radicand.Nodes.Where(inner => inner.ContainsNode(x) + && inner is Sinf or Cosf or Tanf or Cotanf or Secantf or Cosecantf).Distinct().ToList(); + if (functions.Count != 1) + return node; + var function = functions[0]; + var w = Variable.CreateUnique(expr, "w_sq"); + var inW = radicand.Replace(inner => inner == function ? w : inner); + if (inW.ContainsNode(x) || !TreeAnalyzer.TryGetPolynomial(inW, w, out var monomials) + || monomials.Keys.Max() is not { } degree || !degree.Equals(EInteger.FromInt32(2)) + || monomials.Keys.Any(k => k.Sign < 0)) + return node; + var a = monomials[degree]; + var b = monomials.TryGetValue(EInteger.One, out var b1) ? b1 : Number.Integer.Zero; + var c = monomials.TryGetValue(EInteger.Zero, out var c0) ? c0 : Number.Integer.Zero; + if (b.Evaled is Number.Complex and not Number.Real || c.Evaled is Number.Complex and not Number.Real + || !IsAPerfectSquareDiscriminant(a, b, c)) + return node; + if (a.Evaled is not Number.Real { IsPositive: true }) + { + if (!IsPositiveForARealParameter(a, x, out var leadingAssumed, assumed)) + return node; + assumed = leadingAssumed; + } + // The sign in front of the integral, which needs the root to be a factor of it. + if (factors.Count(factor => factor == node) < expr.Nodes.Count(inner => inner == node)) + { + declined = true; + return node; + } + var h = (b / (Number.Integer.Create(2) * a)).InnerSimplified; + if (h.Vars.Any()) + h = Functions.PartialFractions.Bare(h.Simplify()); + var linear = h == Number.Integer.Zero ? function : (function + h).InnerSimplified; + var twoR = Number.Integer.Create(r.ERational.Numerator); + signs = signs * MathS.Signum(linear); + changed = true; + return MathS.Pow(a, r) * (twoR == Number.Integer.One ? linear : MathS.Pow(linear, twoR)); + }); + if (declined || !changed) + return null; + if (Integration.ComputeAsTheSameQuestion(written, x, integrateByParts) is not { } answer) + return null; + answer = signs * answer; + return assumed == Entity.Boolean.True ? answer : answer.Provided(assumed); + } + /// /// A fractional power of a sum whose every term has x to a power in it, /// (a x^j + b x^n)^p with 0 < j < n, as K x^(j p) (a + b x^(n - j))^p: diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs b/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs index 15f973f6b..691fd3658 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs @@ -702,6 +702,8 @@ private static Entity Normalized(Entity expr, Entity.Variable x) => if ((answer = IndefiniteIntegralSolver.SolveAReciprocalOfAnInverseTrigonometricFunction(expr, x, integrateByParts)) is { }) return answer; // A fractional power of a perfect square is the power of the modulus, sgn(P) P^(2r). if ((answer = IndefiniteIntegralSolver.SolveByTakingARootOfAPerfectSquare(expr, x, integrateByParts)) is { }) return answer; + // And a square in one trigonometric function, at the top: the modulus of the linear in it. + if ((answer = IndefiniteIntegralSolver.SolveByTakingARootOfAPerfectSquareInATrigonometricFunction(expr, x, integrateByParts)) is { }) return answer; // And any power of a square in any power of x, as the power of its root times a factor // constant where the root is not zero: `x^2 (a^2 + 2 a b x^3 + b^2 x^6)^p`. if ((answer = IndefiniteIntegralSolver.SolveByWritingAPowerOfASquareAsAPowerOfItsRoot(expr, x, integrateByParts)) is { }) return answer; diff --git a/Sources/Tests/UnitTests/Calculus/RootOfASquareInATrigonometricFunctionIntegralTest.cs b/Sources/Tests/UnitTests/Calculus/RootOfASquareInATrigonometricFunctionIntegralTest.cs new file mode 100644 index 000000000..2d6ad6238 --- /dev/null +++ b/Sources/Tests/UnitTests/Calculus/RootOfASquareInATrigonometricFunctionIntegralTest.cs @@ -0,0 +1,49 @@ +// +// 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 half-odd power of b^2 + 2 a b f + a^2 f^2, a perfect square in one trigonometric + /// function f, is that power of sqrt(a^2) |f + b/a|, the sign in front of the + /// integral. Rubi's 4.7.7. + /// #718 + /// + /// + /// Checked by differentiating back with a = 1.3, b = 0.7, on both sides of the + /// zeros of f + b/a and of zero. + /// + [Trait("Area", "Calculus")] + public sealed class RootOfASquareInATrigonometricFunctionIntegralTest + { + [Theory] + [InlineData("(a + b*sin(x))*sqrt(b^2 + 2*a*b*sin(x) + a^2*sin(x)^2)")] + [InlineData("(a + b*tan(x))/sqrt(b^2 + 2*a*b*tan(x) + a^2*tan(x)^2)")] + [InlineData("(a + b*sec(x))*(b^2 + 2*a*b*sec(x) + a^2*sec(x)^2)^(3/2)")] + [InlineData("(a + b*sin(g + f*x))/(b^2 + 2*a*b*sin(g + f*x) + a^2*sin(g + f*x)^2)^(3/2)")] + public void IsAPowerOfTheModulusOfTheLinear(string integrand) + { + var integral = integrand.ToEntity().Integrate("x"); + Assert.DoesNotContain("integral(", integral.Stringize()); + Entity Pinned(Entity e) => e.Substitute("a", 1.3).Substitute("b", 0.7).Substitute("g", 0.4).Substitute("f", 1.1); + var derivative = Pinned(integral.Substitute("C", 0)).Differentiate("x"); + var original = Pinned(integrand.ToEntity()); + foreach (var at in new[] { -2.3, -1.1, -0.3, 0.4, 1.1, 2.3 }) + { + var want = original.Substitute("x", at).EvalNumerical(); + var got = derivative.Substitute("x", at).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) + Math.Abs((double)want.ImaginaryPart)), + $"d/dx of the antiderivative of {integrand} is {got} at x = {at}, where the integrand is {want}"); + } + } + } +}