From edb31c5981dcc48b4211b51a3fed5e79a0351ed8 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Tue, 20 Oct 2026 03:36:43 +0000 Subject: [PATCH] Answer a half-angle integrand through two roots where the cosine is negative too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The half-angle rule beside a half-odd power of `1 ± sec` writes each root of a constant times the secant or cosine apart in t = tan(y/2). That is exact where cos(y) > 0. Beyond it each root turns its sign by its constant's, and two roots make the integrand real on both sides at once, so the answer said `provided cos(y) >= 0`: half of the integrand's domain, for every one of the 165 rows of Rubi's 4.5.4.2 that took the rule. Which roots turn is known from their constants. For an even number of roots, the turns multiply to (-1)^n s, where n is the number of roots of the cosine and s is the sign of the product of the constants, and the integrand is real just where s > 0. The answer now holds there as written, with `sgn(cos(y))` in front for an odd n, and says `provided a > 0 or cos(y) >= 0` only where s is not known. An odd number above one still says `provided cos(y) >= 0`. Part of #233. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura --- BREAKING-CHANGES.md | 11 +- .../Integration/IndefiniteIntegralSolver.cs | 51 +++++++-- .../LinearRadicalSubstitutionIntegralTest.cs | 29 +++-- .../OnePlusASecantHalfPowerIntegralTest.cs | 100 +++++++++++++++++- 4 files changed, 166 insertions(+), 25 deletions(-) diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 6386f8482..edd686f29 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -4163,9 +4163,12 @@ cosine the whole is a rational function of `t` beside one root, of `1 - t^2` or Two roots, which a half-odd power of the secant beside a whole one of `1 ± sec` makes, are elliptic and still declined. The cosecant's the same way through the complement, and a root of `1 - sec(y)` carries `sgn(tan(y/2))`, constant between its zeros. Each root written apart is exact -where `cos(y)` is positive; beyond it two roots can make the integrand real while each has turned -its sign on its own, so an answer through two says `provided cos(y) >= 0` -([#718](https://github.com/asc-community/AngouriMath/issues/718)). +where `cos(y)` is positive; beyond it each turns its sign by its constant's. Two roots make the +integrand real on both sides at once, for a positive product of their constants, and then the turns +are as many as the roots of the cosine: an answer through two holds where the cosine is negative too, +with `sgn(cos(y))` in front for an odd number of roots of the cosine, and says +`provided a > 0 or cos(y) >= 0` where the product's sign is not known. Three say +`provided cos(y) >= 0` ([#718](https://github.com/asc-community/AngouriMath/issues/718)). | Input | Was (2.5.0) | Now | |---|---|---| @@ -4174,6 +4177,8 @@ its sign on its own, so an answer through two says `provided cos(y) >= 0` | `"1/(sec(x)^(3/2)*sqrt(1 + sec(x)))".ToEntity().Integrate("x")` | `integral(...)` | an antiderivative through a root of `1 + tan(x/2)^2` | | `"sqrt(a - a*sec(x))".ToEntity().Integrate("x")` | `integral(...)` | a logarithm in `tan(x/2)`, times `sgn(tan(x/2))` | | `"sqrt(1 + csc(x))".ToEntity().Integrate("x")` | `integral(...)` | an arctangent in `tan(pi/4 - x/2)` | +| `"cos(x)^(3/2)/sqrt(1 + sec(x))".ToEntity().Integrate("x")` | `integral(...)` | a logarithm and a power in `tan(x/2)`, times `sgn(cos(x))` | +| `"cos(c+d*x)^(3/2)*(A+C*sec(c+d*x)^2)/sqrt(a+a*sec(c+d*x))".ToEntity().Integrate("x")` | `integral(...)` | the same, times `sgn(cos(c + d x))`, `provided a > 0 or cos(c + d x) >= 0` | ### An even power of the secant or the cosecant under a root is written in the tangent diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs index 5fb5c874b..c28cedfb6 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs @@ -5361,6 +5361,14 @@ Entity Integral(ERational power) /// chain in t. /// https://github.com/asc-community/AngouriMath/issues/718 /// + /// + /// Two roots, as in cos(y)^(3/2)/sqrt(a + a sec(y)), are real together where the + /// cosine is negative too, and there each written apart may have turned its sign: the answer + /// said provided cos(y) >= 0, half of the integrand's domain, for every one of 165 + /// rows of Rubi's 4.5.4.2. Which turn is known from the constants, and the answer carries + /// the cosine's sign where they leave an odd number turned. + /// https://github.com/asc-community/AngouriMath/issues/233 + /// /// internal static Entity? SolveByTheHalfAngleTangentBesideAHalfOddPowerOfOnePlusASecant(Entity expr, Entity.Variable x, bool integrateByParts) { @@ -5408,6 +5416,9 @@ Entity Integral(ERational power) var found = false; var declined = false; var roots = 0; + var rootsOfTheCompanion = 0; + // The product of the roots' constants: the sign of each radicand where t^2 < 1. + Entity constants = Number.Integer.One; // Each half-odd power, assembled in t: of a (1 ± sec(y)), or of d sec(y) or d cos(y); // for the cosecant, of a (1 ± csc(y)), d csc(y) or d sin(y). var rewritten = expr.Replace(node => @@ -5423,6 +5434,7 @@ Entity Integral(ERational power) if (ReadAsOnePlusMinusAFunction(radicand, secant, cosecant) is (var a, var plus, var isSecant) && isSecant == secantKind) { found = true; + constants = plus ? constants * a : constants * -a; if (plus) return MathS.Pow(2 * a, exponent) * MathS.Pow(oneMinus, (-exponent).InnerSimplified); // (-2a t^2/(1 - t^2))^p is (-2a)^p |t|^(2p) (1 - t^2)^(-p), and 2p is odd. @@ -5449,6 +5461,9 @@ Entity Integral(ERational power) declined = true; return node; } + if (function == companion) + rootsOfTheCompanion++; + constants = constants * coefficient; var (above, below) = function == primary ? (onePlus, oneMinus) : (oneMinus, onePlus); return MathS.Pow(coefficient, exponent) * MathS.Pow(above, exponent) * MathS.Pow(below, (-exponent).InnerSimplified); }); @@ -5500,12 +5515,36 @@ Entity Integral(ERational power) if (answer.Nodes.Any(node => node == MathS.NaN)) return null; // Each root written apart is exact inside t^2 < 1, where 1 - t^2 is positive, for any - // sign of the constants. Outside it a root whose radicand is negative there is - // imaginary, and one alone leaves the integrand complex, where the answer is nothing - // to be wrong about; but two can make it real again -- `(1 + sec(x))^(5/2) sqrt(cos(x))` - // is real where the cosine is negative -- and there each written apart may have - // turned its sign where the other did not. The answer says where it holds. - return roots < 2 ? answer : answer.Provided((secantKind ? MathS.Cos(argument) : MathS.Sin(argument)) >= Number.Integer.Zero); + // sign of the constants. Outside it `(P/(1 - t^2))^p` is `P^p (1 - t^2)^(-p)` but for a + // positive P, and `(P (1 - t^2))^p` is `P^p (1 - t^2)^p` but for a negative one, the two + // differing by e^(2 i pi p) = -1: where cos(y) < 0 a root of a ± a sec(y) or d sec(y) + // turns its sign if its constant is positive, and one of d cos(y) if its constant is + // negative. One root alone turns only where the integrand is complex, which is nothing + // to be wrong about. For an even number the turns multiply to (-1)^n s, for n roots of + // the cosine and s the sign of the product of the constants, and the integrand is real, + // on both sides at once, just where s is positive: `sqrt(sec(x)) sqrt(1 + sec(x))` holds + // as written where the cosine is negative, and `cos(x)^(3/2)/sqrt(1 + sec(x))` with the + // cosine's sign in front. The root of 1 - t^2 has cancelled then, so that the answer in + // t holds beyond t^2 < 1 too; for an odd number above one it has not, and the answer + // says where it holds, as it does where the product's sign is not known. + if (roots < 2) + return answer; + var withinTheHalfTurn = answer.Provided(companion >= Number.Integer.Zero); + if (!exponents[0].Numerator.Remainder(exponents[0].Denominator).IsZero) + return withinTheHalfTurn; + var turns = rootsOfTheCompanion % 2 == 1; + switch (constants.Evaled) + { + case Number.Real { IsFinite: true } product when !product.IsZero: + if (product < 0) + turns = !turns; + return turns ? MathS.Signum(companion) * answer : answer; + case Number: + return withinTheHalfTurn; + default: + return (turns ? MathS.Signum(companion) * answer : answer) + .Provided(constants > Number.Integer.Zero | companion >= Number.Integer.Zero); + } } /// diff --git a/Sources/Tests/UnitTests/Calculus/LinearRadicalSubstitutionIntegralTest.cs b/Sources/Tests/UnitTests/Calculus/LinearRadicalSubstitutionIntegralTest.cs index 12682b9cf..2f3732eb9 100644 --- a/Sources/Tests/UnitTests/Calculus/LinearRadicalSubstitutionIntegralTest.cs +++ b/Sources/Tests/UnitTests/Calculus/LinearRadicalSubstitutionIntegralTest.cs @@ -158,15 +158,17 @@ public void AnExponentialOfAnInverseTangentBesideTheRootSaysWhereItHolds() /// u = sqrt(t) is -2 u^2 (1 + 1/u^2)^(5/2)/sqrt(1 - u^4), whose power of a /// quotient is taken apart as (1 + u^2)^(5/2)/|u|^5. Where the cosine is negative the /// integrand is still real, a product of two imaginary factors, and the answer built for a - /// real u was not its antiderivative there. Rubi's 4.5.3.1. + /// real u was not its antiderivative there. Rubi's 4.5.3.1. The half-angle tangent + /// answers it on both sides now, with the cosine's sign in front + /// (); either way the answer is right where + /// the cosine is negative or says nothing there. /// [Fact] - public void TheSearchsEvenRootSaysWhereItHolds() + public void TheSearchsEvenRootIsRightOrSaysNothingBeyondIt() { var integrand = "(1 + sec(x))^(5/2)*sqrt(cos(x))".ToEntity(); var integral = integrand.Integrate("x"); Assert.DoesNotContain("integral(", integral.Stringize()); - Assert.Contains("provided cos(x) >= 0", integral.Stringize()); var derivative = integral.Substitute("C", 0).Differentiate("x"); // Inside the condition the derivative is the integrand. foreach (var at in new[] { 0.3, 0.9, -0.7, 5.9 }) @@ -177,10 +179,12 @@ public void TheSearchsEvenRootSaysWhereItHolds() Assert.True(Math.Abs((double)(got - want).RealPart) < 1e-9 * Math.Max(1, Math.Abs((double)want.RealPart)), $"d/dx of the antiderivative is {got} at x = {at}, where the integrand is {want}"); } - // Beyond it the integrand is real and the answer claims nothing. + // Beyond it the integrand is real, and the answer is right or claims nothing. var outside = integrand.Substitute("x", 2.8).EvalNumerical(); Assert.True(Math.Abs((double)outside.ImaginaryPart) < 1e-12, $"the integrand at x = 2.8 is {outside}, not real"); - Assert.True(derivative.Substitute("x", 2.8).Evaled.IsNaN, "the answer claims a derivative outside its condition"); + var beyond = derivative.Substitute("x", 2.8).EvalNumerical(); + Assert.True(beyond.IsNaN || Math.Abs((double)(beyond - outside).RealPart) + Math.Abs((double)(beyond - outside).ImaginaryPart) + < 1e-9 * Math.Max(1, Math.Abs((double)outside.RealPart)), $"d/dx of the antiderivative is {beyond} at x = 2.8, where the integrand is {outside}"); } /// @@ -190,17 +194,18 @@ public void TheSearchsEvenRootSaysWhereItHolds() /// product of roots below the bar was written with sqrt(u^4) = u^2, and the answer /// was negated wherever the cosine is negative. Rubi's 4.5.1.2. The same in t, /// sqrt(t)/(sqrt(1/t + 1) sqrt(1 - t^2)), is written over each linear once and - /// answered on both sides (). + /// answered on both sides (), and so is it in + /// the half-angle tangent now (): the + /// answer is right where the cosine is negative or says nothing there. /// https://github.com/asc-community/AngouriMath/issues/1581 /// [Theory] - [InlineData("sqrt(cos(x))/sqrt(1 + sec(x))", "provided cos(x) >= 0", new[] { 0.3, 0.9, -0.7 }, 2.4)] - public void AnEvenRootTakenRealWithoutASignSaysWhereItHolds(string written, string condition, double[] inside, double outsideAt) + [InlineData("sqrt(cos(x))/sqrt(1 + sec(x))", new[] { 0.3, 0.9, -0.7 }, 2.4)] + public void AnEvenRootTakenRealWithoutASignIsRightOrSaysNothingBeyondIt(string written, double[] inside, double outsideAt) { var integrand = written.ToEntity(); var integral = integrand.Integrate("x"); Assert.DoesNotContain("integral(", integral.Stringize()); - Assert.Contains(condition, integral.Stringize()); var derivative = integral.Substitute("C", 0).Differentiate("x"); foreach (var at in inside) { @@ -210,10 +215,12 @@ public void AnEvenRootTakenRealWithoutASignSaysWhereItHolds(string written, stri Assert.True(Math.Abs((double)(got - want).RealPart) < 1e-9 * Math.Max(1, Math.Abs((double)want.RealPart)), $"d/dx of the antiderivative is {got} at x = {at}, where the integrand is {want}"); } - // Beyond it the integrand is real and the answer claims nothing. + // Beyond it the integrand is real, and the answer is right or claims nothing. var outside = integrand.Substitute("x", outsideAt).EvalNumerical(); Assert.True(Math.Abs((double)outside.ImaginaryPart) < 1e-12, $"the integrand at x = {outsideAt} is {outside}, not real"); - Assert.True(derivative.Substitute("x", outsideAt).Evaled.IsNaN, "the answer claims a derivative outside its condition"); + var beyond = derivative.Substitute("x", outsideAt).EvalNumerical(); + Assert.True(beyond.IsNaN || Math.Abs((double)(beyond - outside).RealPart) + Math.Abs((double)(beyond - outside).ImaginaryPart) + < 1e-9 * Math.Max(1, Math.Abs((double)outside.RealPart)), $"d/dx of the antiderivative is {beyond} at x = {outsideAt}, where the integrand is {outside}"); } /// diff --git a/Sources/Tests/UnitTests/Calculus/OnePlusASecantHalfPowerIntegralTest.cs b/Sources/Tests/UnitTests/Calculus/OnePlusASecantHalfPowerIntegralTest.cs index 491476cf4..54396cbdb 100644 --- a/Sources/Tests/UnitTests/Calculus/OnePlusASecantHalfPowerIntegralTest.cs +++ b/Sources/Tests/UnitTests/Calculus/OnePlusASecantHalfPowerIntegralTest.cs @@ -85,16 +85,106 @@ public void OnePlusASecant(string integrand) public void OneMinusASecant(string integrand) => DifferentiatesBackPinned(integrand, new[] { 1.8, 2.2, 2.6, 3.0 }); + /// + /// d/dx of the antiderivative, the symbols pinned first, against the integrand at every + /// point where the integrand is real: each of them must have a value there. + /// + private static void HoldsWhereverTheIntegrandIsReal(string integrand, double[] points, int compare, (string, double)[] pins) + { + var integral = integrand.ToEntity().Integrate("x"); + Assert.DoesNotContain("integral(", integral.Stringize()); + + var answer = integral; + Entity original = integrand.ToEntity(); + foreach (var (name, value) in pins) + { + answer = answer.Substitute(name, value); + original = original.Substitute(name, value); + } + var derivative = answer.Differentiate("x"); + var compared = 0; + foreach (var at in points) + { + var want = original.Substitute("x", at).EvalNumerical(); + var scale = Math.Max(1.0, Math.Abs((double)want.RealPart) + Math.Abs((double)want.ImaginaryPart)); + if (want.IsNaN || Math.Abs((double)want.ImaginaryPart) > 1e-12 * scale) + continue; + compared++; + var got = derivative.Substitute("x", at).EvalNumerical(); + Assert.False(got.IsNaN, $"the antiderivative of {integrand} has no value at x = {at}, where the integrand is {want}"); + var difference = Math.Abs((double)(got - want).RealPart) + Math.Abs((double)(got - want).ImaginaryPart); + Assert.True(difference / scale < 1e-9, + $"d/dx of the antiderivative of {integrand} is {got} at x = {at}, " + + $"where the integrand is {want}"); + } + Assert.True(compared >= compare, $"only {compared} points could be compared for {integrand}"); + } + + /// + /// On either side of the cosine's sign change, two quarter turns each: (-pi, -pi/2), + /// (-pi/2, pi/2), (pi/2, pi) and (pi, 3pi/2), and the sine's the same way for the cosecant. + /// + private static readonly double[] BothSides = { -2.6, -2.0, -1.2, 0.3, 1.1, 1.8, 2.6, 3.6, 4.2 }; + /// /// Two roots written apart are exact where cos(y) is positive, for any sign of the - /// constants, and the answer says so: beyond it each may turn its sign where the other - /// does not, while the integrand is real. Checked inside, where both roots of - /// 1 - sec(x) here are imaginary and their product real. + /// constants. Where it is negative each turns its sign by its constant's, and where the + /// integrand is real -- on both sides at once, for a positive product of the constants -- + /// the turns are as many as the roots of the cosine: the answer holds there as written, or + /// with the cosine's sign in front. Rubi's + /// cos(c + d x)^(3/2) (A + C sec(c + d x)^2)/sqrt(a + a sec(c + d x)) said + /// provided cos(c + d x) >= 0, where without it the answer was the integrand's + /// negative. + /// + [Theory] + [InlineData("(c - c*sec(x))^(7/2)/sqrt(a - a*sec(x))")] + [InlineData("sqrt(sec(x))*sqrt(1 + sec(x))")] + [InlineData("cos(x)^(3/2)/sqrt(1 + sec(x))")] + [InlineData("sqrt(cos(x))*sqrt(a + a*sec(x))")] + [InlineData("sec(x)^(3/2)*(A + B*sec(x))*sqrt(a + a*sec(x))")] + [InlineData("cos(x)^(5/2)*(A + B*sec(x))*sqrt(a + a*sec(x))")] + [InlineData("sqrt(-csc(x))*sqrt(a - a*csc(x))")] + [InlineData("sin(x)^(3/2)/sqrt(1 + csc(x))")] + [InlineData("sqrt(cos(x))/sqrt(1 + sec(x))")] + [InlineData("(1 + sec(x))^(5/2)*sqrt(cos(x))")] + public void TwoRootsHoldOnBothSides(string integrand) + => HoldsWhereverTheIntegrandIsReal(integrand, BothSides, BothSides.Length, Pins); + + /// + /// Rubi's 4.5.4.2, (a + a sec)^m (d sec)^n (A + B sec + C sec^2), at + /// y = 0.9 + 1.3 x: from -2.48 to 6.36, both signs of the cosine. + /// + [Theory] + [InlineData("cos(c+d*x)^(3/2)*(A+C*sec(c+d*x)^2)/sqrt(a+a*sec(c+d*x))")] + [InlineData("sec(c+d*x)^(3/2)*(A+B*sec(c+d*x)+C*sec(c+d*x)^2)*sqrt(a+a*sec(c+d*x))")] + [InlineData("(a+a*sec(c+d*x))^(3/2)*(A+C*sec(c+d*x)^2)/sec(c+d*x)^(5/2)")] + [InlineData("cos(c+d*x)^(5/2)*(A+B*sec(c+d*x)+C*sec(c+d*x)^2)/(a+a*sec(c+d*x))^(3/2)")] + public void RubisSecantRowsHoldOnBothSides(string integrand) + => HoldsWhereverTheIntegrandIsReal(integrand, BothSides, BothSides.Length, + new[] { ("a", 1.7), ("c", 0.9), ("d", 1.3), ("A", 0.6), ("B", 1.3), ("C", 0.8) }); + + /// + /// The product of the constants of sqrt(sec(x)) sqrt(a - a sec(x)) is -a, + /// whose sign is not known: the integrand is real for a negative a, and then the + /// answer holds on both sides, and complex everywhere for a positive one, where it says it + /// holds on the cosine's positive side. + /// + [Fact] + public void AProductOfConstantsOfUnknownSignSaysWhereItHolds() + { + const string integrand = "sqrt(sec(x))*sqrt(a - a*sec(x))"; + Assert.Contains("provided", integrand.ToEntity().Integrate("x").Stringize()); + HoldsWhereverTheIntegrandIsReal(integrand, BothSides, BothSides.Length, new[] { ("a", -1.7) }); + } + + /// + /// Three roots leave one of 1 - t^2 in the answer in t, which is not shown to hold + /// beyond t^2 < 1, and the answer says so. /// [Fact] - public void TwoRootsSayWhereTheyHold() + public void ThreeRootsSayWhereTheyHold() { - const string integrand = "(c - c*sec(x))^(7/2)/sqrt(a - a*sec(x))"; + const string integrand = "sqrt(cos(x))*sqrt(sec(x))*sqrt(1 + sec(x))"; Assert.Contains("provided cos(x) >= 0", integrand.ToEntity().Integrate("x").Stringize()); DifferentiatesBackPinned(integrand, new[] { 0.3, 0.7, 1.1, 1.4 }); }