From a8df4dd2fe6d0c83544fb1213a8c39908f3a4821 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Tue, 20 Oct 2026 15:57:26 +0000 Subject: [PATCH] Answer an odd number of secant 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). For N roots, n of them of the cosine, with s the sign of the product of their constants: where the cosine is negative the turns multiply to (-1)^(N+n) s, the integrand is real there exactly where (-1)^N s > 0, and there the turns are (-1)^n. #1959 used this for an even N. For an odd N the root of 1 - t^2 stays in the answer in t, which is kept beyond t^2 < 1 only where its derivative holds there at sampled points of both signs. With the cosine's sign in front where n is odd, it holds on the whole real domain. Where s is not known the condition is now `provided < 0 or cos(y) >= 0`. The cosecant takes the same path through the complement, with sgn(sin(y)). sqrt(-1 - sec(x)) sqrt(sec(x)) sqrt(cos(x)), real only where the cosine is negative, had no value anywhere it is real. No Rubi row has an odd root count above one, and the answers to every fair row of Rubi's 4.5 and 4.6 (2,966 rows, 1,317 of them with roots) are byte-identical. Part of #233. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura --- BREAKING-CHANGES.md | 24 +++++++- .../Algebra/Polynomials/PartialFractions.cs | 7 ++- .../Integration/IndefiniteIntegralSolver.cs | 42 +++++++++---- .../OnePlusASecantHalfPowerIntegralTest.cs | 61 ++++++++++++++++--- 4 files changed, 110 insertions(+), 24 deletions(-) diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 7d8b0e0ac..b51a69e46 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -4236,8 +4236,8 @@ where `cos(y)` is positive; beyond it each turns its sign by its constant's. Two 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)). +`provided a > 0 or cos(y) >= 0` where the product's sign is not known. Three are in the entry +after this one ([#718](https://github.com/asc-community/AngouriMath/issues/718)). | Input | Was (2.5.0) | Now | |---|---|---| @@ -4249,6 +4249,26 @@ with `sgn(cos(y))` in front for an odd number of roots of the cosine, and says | `"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 odd number of roots of `a ± a sec` and the secant or cosine holds where the cosine is negative + +**Answers where there were none.** Three half-odd powers, as in +`sqrt(a + a sec(x)) sqrt(sec(x)) sqrt(cos(x))`, leave a root of `1 - tan(x/2)^2` in the answer, and +the answer said `provided cos(x) >= 0`. An odd number of roots makes the integrand real on one side +of the cosine's sign change at most, and on the negative one just where the product of the roots' +constants is negative, so `sqrt(-1 - sec(x)) sqrt(sec(x)) sqrt(cos(x))` had no value anywhere it is +real. There each root turns its sign by its constant's, as two do, and the turns are as many as the +roots of the cosine. The answer is kept beyond the condition where the antiderivative in the +half-angle tangent differentiates back there, and says `provided a < 0 or cos(y) >= 0` where the +product's sign is not known. The cosecant's the same way through the complement +([#718](https://github.com/asc-community/AngouriMath/issues/718)). + +| Input | Was (2.5.0) | Now | +|---|---|---| +| `"sqrt(-1 - sec(x))*sqrt(sec(x))*sqrt(cos(x))".ToEntity().Integrate("x")` | `integral(...)` | an arctangent in `tan(x/2)`, times `sgn(cos(x))` | +| `"sqrt(1 - sec(x))*sqrt(sec(x))*sqrt(cos(x))".ToEntity().Integrate("x")` | `integral(...)` | a logarithm in `tan(x/2)`, times `sgn(cos(x)) sgn(tan(x/2))` | +| `"sqrt(a + a*sec(x))*sqrt(sec(x))*sqrt(cos(x))".ToEntity().Integrate("x")` | `integral(...)` | an arctangent in `tan(x/2)`, times `sgn(cos(x))`, `provided a < 0 or cos(x) >= 0` | +| `"sqrt(a + a*csc(x))*sqrt(csc(x))*sqrt(sin(x))".ToEntity().Integrate("x")` | `integral(...)` | an arctangent in `tan(pi/4 - x/2)`, times `sgn(sin(x))`, `provided a < 0 or sin(x) >= 0` | + ### An even power of the secant or the cosecant under a root is written in the tangent **Answers where there were none.** `sqrt(a + b csc(x)^2)`, `(a + b sec(x)^2)^(3/2)` and their kin, diff --git a/Sources/AngouriMath/Functions/Algebra/Polynomials/PartialFractions.cs b/Sources/AngouriMath/Functions/Algebra/Polynomials/PartialFractions.cs index c0877cbc6..e8ad086d8 100644 --- a/Sources/AngouriMath/Functions/Algebra/Polynomials/PartialFractions.cs +++ b/Sources/AngouriMath/Functions/Algebra/Polynomials/PartialFractions.cs @@ -2030,9 +2030,10 @@ internal static Entity Bare(Entity e) /// f is shown real-valued, which a symbol that might be complex prevents and a /// number does not: an answer with sgn(tanh(c + d x)) in it, right, was /// unevaluable differentiated with c and d in it and threw out of the - /// integrator. + /// integrator. The points may be the caller's, as + /// takes them. /// - internal static bool DerivativeHoldsAtSampledPoints(Entity antiderivative, Entity integrand, Variable x) + internal static bool DerivativeHoldsAtSampledPoints(Entity antiderivative, Entity integrand, Variable x, string[]? points = null) { // Off the downcasting for the simplification of the pinned answer, which folds its // numbers: downcast, a pinned 1.37 is 137/100 and every constant of the answer is @@ -2040,7 +2041,7 @@ internal static bool DerivativeHoldsAtSampledPoints(Entity antiderivative, Entit // where in decimals it is 3 ms. The comparison itself reads no setting. using var _ = MathS.Settings.DowncastingEnabled.Set(false); var (pinnedAntiderivative, pinnedIntegrand) = Pinned(antiderivative, integrand, x); - return HoldsAtSampledPoints(Bare(pinnedAntiderivative).Differentiate(x), pinnedIntegrand, x); + return HoldsAtSampledPoints(Bare(pinnedAntiderivative).Differentiate(x), pinnedIntegrand, x, points); } /// diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs index f289ef6fc..1eaad30e8 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs @@ -5687,7 +5687,11 @@ bool IsAPolynomialInU(Entity part) /// 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. + /// the cosine's sign where they leave an odd number turned. Three, as in + /// sqrt(a + a sec(y)) sqrt(sec(y)) sqrt(cos(y)), leave a root of 1 - t^2 in + /// the answer in t, which said provided cos(y) >= 0 where for a negative a the + /// integrand is real only beyond it; the answer in t is kept beyond where it differentiates + /// back there. /// https://github.com/asc-community/AngouriMath/issues/233 /// /// @@ -5841,19 +5845,24 @@ bool IsAPolynomialInU(Entity part) // 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. + // to be wrong about. A root is imaginary there just where its constant is positive, so + // for N roots, n of them of the cosine, and s the sign of the product of the constants, + // the turns multiply to (-1)^(N + n) s, and the integrand is real there just where + // (-1)^N s is positive, and then the turns are (-1)^n: `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. For an even N the root of 1 - t^2 has cancelled, so that + // the answer in t holds beyond t^2 < 1 too. For an odd one it has not, and the answer in + // t is kept beyond only where it differentiates back there. It said + // `provided cos(y) >= 0`, and `sqrt(a + a sec(x)) sqrt(sec(x)) sqrt(cos(x))` is real, for + // a negative a, only beyond that. if (roots < 2) return answer; var withinTheHalfTurn = answer.Provided(companion >= Number.Integer.Zero); - if (!exponents[0].Numerator.Remainder(exponents[0].Denominator).IsZero) + var odd = roots % 2 == 1; + if (odd == exponents[0].Numerator.Remainder(exponents[0].Denominator).IsZero + || odd && !Functions.PartialFractions.DerivativeHoldsAtSampledPoints(result, integrand, t, BeyondTheHalfTurn)) return withinTheHalfTurn; - var turns = rootsOfTheCompanion % 2 == 1; + var turns = (roots + rootsOfTheCompanion) % 2 == 1; switch (constants.Evaled) { case Number.Real { IsFinite: true } product when !product.IsZero: @@ -5863,11 +5872,20 @@ bool IsAPolynomialInU(Entity part) case Number: return withinTheHalfTurn; default: - return (turns ? MathS.Signum(companion) * answer : answer) - .Provided(constants > Number.Integer.Zero | companion >= Number.Integer.Zero); + // Where s is not known, the answer for the s that makes the integrand real. + var real = odd ? constants < Number.Integer.Zero : constants > Number.Integer.Zero; + return (rootsOfTheCompanion % 2 == 1 ? MathS.Signum(companion) * answer : answer) + .Provided(real | companion >= Number.Integer.Zero); } } + /// + /// Values of the half-angle tangent past one on either side, where the cosine is negative, + /// or for the cosecant the sine. + /// + [ConstantField] + private static readonly string[] BeyondTheHalfTurn = { "1.19", "1.43", "3.17", "7.3", "-1.61", "-2.9", "-6.1" }; + /// /// A half-odd power of a ± a sec(y) beside one of c + d sec(y), integrated in /// the half angle's sine, or its cosine for the minus: a + a sec(y) is diff --git a/Sources/Tests/UnitTests/Calculus/OnePlusASecantHalfPowerIntegralTest.cs b/Sources/Tests/UnitTests/Calculus/OnePlusASecantHalfPowerIntegralTest.cs index 54396cbdb..165408a75 100644 --- a/Sources/Tests/UnitTests/Calculus/OnePlusASecantHalfPowerIntegralTest.cs +++ b/Sources/Tests/UnitTests/Calculus/OnePlusASecantHalfPowerIntegralTest.cs @@ -178,17 +178,64 @@ public void AProductOfConstantsOfUnknownSignSaysWhereItHolds() } /// - /// 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. + /// Three roots leave one of 1 - t^2 in the answer in t, which is kept beyond + /// t^2 < 1 where it differentiates back there. An odd number of roots makes the + /// integrand real on one side of the cosine's sign change at most, the negative one for a + /// negative product of the constants, and there the answer holds as written or with the + /// cosine's sign in front. It said provided cos(x) >= 0, and + /// sqrt(-1 - sec(x)) sqrt(sec(x)) sqrt(cos(x)), real only where the cosine is + /// negative, had no value anywhere it is real. /// - [Fact] - public void ThreeRootsSayWhereTheyHold() + [Theory] + [InlineData("sqrt(cos(x))*sqrt(sec(x))*sqrt(1 + sec(x))", 3)] + [InlineData("(1 + sec(x))^(3/2)*sqrt(2*sec(x))*sqrt(3*cos(x))", 3)] + [InlineData("sqrt(-1 - sec(x))*sqrt(sec(x))*sqrt(cos(x))", 6)] + [InlineData("sqrt(1 - sec(x))*sqrt(sec(x))*sqrt(cos(x))", 6)] + [InlineData("sqrt(sin(x))*sqrt(csc(x))*sqrt(1 + csc(x))", 4)] + [InlineData("sqrt(-1 - csc(x))*sqrt(csc(x))*sqrt(sin(x))", 5)] + public void ThreeRootsHoldWhereTheIntegrandIsReal(string integrand, int compare) + { + Assert.DoesNotContain("provided", integrand.ToEntity().Integrate("x").Stringize()); + HoldsWhereverTheIntegrandIsReal(integrand, BothSides, compare, Pins); + } + + /// + /// Three roots whose product of constants is of unknown sign: the integrand is real where + /// the cosine is negative for a negative product, and the answer holds there, and where it + /// is positive for a positive one; the answer says provided a < 0 or cos(x) >= 0, or + /// -a < 0 for a - a sec(x), the integrand complex beyond it. Each at both + /// signs of the constant, and the cosecant's through the complement. + /// + [Theory] + [InlineData("sqrt(a + a*sec(x))*sqrt(sec(x))*sqrt(cos(x))", 1.7, 3)] + [InlineData("sqrt(a + a*sec(x))*sqrt(sec(x))*sqrt(cos(x))", -1.7, 6)] + [InlineData("sqrt(a + a*sec(x))*sec(x)^(3/2)*sqrt(cos(x))*(A + B*sec(x))", 1.7, 3)] + [InlineData("sqrt(a + a*sec(x))*sec(x)^(3/2)*sqrt(cos(x))*(A + B*sec(x))", -1.7, 6)] + [InlineData("sqrt(a - a*sec(x))*sqrt(sec(x))*sqrt(cos(x))*cos(x)", 1.7, 6)] + [InlineData("sqrt(a - a*sec(x))*sqrt(sec(x))*sqrt(cos(x))*cos(x)", -1.7, 3)] + [InlineData("sqrt(a + a*csc(x))*sqrt(csc(x))*sqrt(sin(x))", 1.7, 4)] + [InlineData("sqrt(a + a*csc(x))*sqrt(csc(x))*sqrt(sin(x))", -1.7, 5)] + public void ThreeRootsOfAConstantOfUnknownSignHoldWhereTheIntegrandIsReal(string integrand, double a, int compare) { - 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 }); + Assert.Contains(" < 0 or ", integrand.ToEntity().Integrate("x").Stringize()); + HoldsWhereverTheIntegrandIsReal(integrand, BothSides, compare, new[] { ("a", a), ("A", 0.6), ("B", 1.3) }); } + /// + /// The same at y = 0.9 + 1.3 x, where the points fall four where the cosine is + /// positive and five where it is negative, and with three binomials, of which the + /// integrand is real where the cosine is negative for a c k > 0 and where it is + /// positive for a c k < 0. + /// + [Theory] + [InlineData("sqrt(a + a*sec(c + d*x))*sqrt(sec(c + d*x))*sqrt(cos(c + d*x))", 1.7, 0.9, 4)] + [InlineData("sqrt(a + a*sec(c + d*x))*sqrt(sec(c + d*x))*sqrt(cos(c + d*x))", -1.7, 0.9, 5)] + [InlineData("sqrt(a + a*sec(x))*sqrt(c + c*sec(x))*sqrt(k - k*sec(x))", 1.7, 0.9, 6)] + [InlineData("sqrt(a + a*sec(x))*sqrt(c + c*sec(x))*sqrt(k - k*sec(x))", -1.7, 0.9, 3)] + public void ThreeRootsOfSymbolsHoldWhereTheIntegrandIsReal(string integrand, double a, double c, int compare) + => HoldsWhereverTheIntegrandIsReal(integrand, BothSides, compare, + new[] { ("a", a), ("c", c), ("d", 1.3), ("k", 1.3) }); + /// The cosecant's, through the complement: csc(y) = sec(pi/2 - y). [Theory] [InlineData("sqrt(1 + csc(x))")]