From ef6ed0d0bee9c699eb063d0848b455fb0a9ebcfb Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Wed, 21 Oct 2026 13:59:51 +0000 Subject: [PATCH] Hold an answer through a root taken real where a second root makes the integrand real Three places take a root as a nonnegative real u and attach `radicand >= 0` to the answer: the factoring in SolveByLinearRadicalSubstitution and the "as the root" and factored branches of SolveBySubstitution. Where a second root is imaginary on the same side, the integrand is real there as a product of two imaginary roots, and the answer said nothing. Timofeev 926 and 927, asec(x) beside sqrt(x^2 - 1), are real on 0 < x < 1; Rubi's sqrt(cot) (a + b tan)^(k/2) and its kin in 4.3.2.1 and 4.3.3.1 are real where tan < -a/b. ConditionedOnTheRadicand, where the answer holds an even root besides, asks PartialFractions.WhereTheDerivativeHoldsBeyondTheRadicand. That sorts 15 points per sign pattern by interval arithmetic on the integrand and the radicand, and only where one is real beyond the zero differentiates the answer, lazily and real points first. An answer that holds at every real point there, beside exactly one other half-odd root, carries the integrand's real domain -- `tan(c + d x) >= 0 or a + b tan(c + d x) <= 0`; one that also holds at every complex point carries no condition; anything else keeps the old one, and so does an integrand real nowhere beyond the zero. 40 answers on part of the line -> whole (26 of 4.3.3.1, 12 of 4.3.2.1, Timofeev 926, 927), each checked with mpmath on both sides; the condition grows 28-60 characters. Chain against master a567c8ba (base a4e7656d): pocket of 2,104 rows, exactly those 40 Partial -> Solved, 0 wrong; samples unchanged in verdicts. The check costs 24-111 MB on the rows it changes; Bondarenko 22 and 23, which the first version re-answered at 2.4x the allocation, are master's again, byte for byte, at 0.582 and 0.505 GB. Part of #233. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura --- BREAKING-CHANGES.md | 29 ++++ .../Algebra/Polynomials/PartialFractions.cs | 155 ++++++++++++++++++ .../Integration/IndefiniteIntegralSolver.cs | 77 ++++++++- ...swerBeyondANegativeRadicandIntegralTest.cs | 97 +++++++++++ 4 files changed, 353 insertions(+), 5 deletions(-) create mode 100644 Sources/Tests/UnitTests/Calculus/AnAnswerBeyondANegativeRadicandIntegralTest.cs diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 883fc378b..250299e8d 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -7316,6 +7316,35 @@ written back is the argument's, which is that of `x` only for a positive slope a | `"asec(x)^2/(x*sqrt(x^2 - 1))".ToEntity().Integrate("x")` | `integral(...)` | the same | | `"asec(x + 3)^2/((x + 3)*sqrt((x + 3)^2 - 1))".ToEntity().Integrate("x")` | `integral(...)` | right for `x > -2` and for `x < -4` | +### An answer through a root taken real holds where a second root makes the integrand real + +**Answers where there were none.** An integrand real where a radicand is negative, a second root +being imaginary there too, was integrated through the root of that radicand as a non-negative real +`u`, and the answer said `provided` the radicand is not negative: under `u = sqrt(tan(c + d x))` for +`sqrt(cot(c + d x)) (a + b tan(c + d x))^(5/2) (A + B tan(c + d x))`, twenty-five more of Rubi's +4.3.3.1 and twelve of its 4.3.2.1, and under `u = sqrt(w - 1)`, `w = x^2`, for the remainder by parts +leaves from Timofeev's `arcsec(x) sqrt(x^2 - 1)/x^4` and `arcsec(x)/(x^2 - 1)^(5/2)`. On the +unreleased master these said nothing where `tan(c + d x) < -a/b`, or on `0 < x < 1`, though each +integrand is real there; 2.5.0 declined them. The answer is measured there now, its derivative +against the integrand at a spread of points where the radicand is negative, the symbols pinned to +four patterns of signs. These hold where the integrand is real and not where it is imaginary, so +beside one other half-odd root the condition is that either radicand has its sign, +`provided tan(c + d x) >= 0 or a + b tan(c + d x) <= 0`, the integrand's real domain; for the +arcsecants it is `x^2 - 1 >= 0 or (x^2)^2 - x^2 <= 0`, true for every real `x`. An answer that also +holds at every point measured where the integrand is imaginary carries no condition. One that differs +where the integrand is real keeps the old one, and so does one whose integrand is real nowhere beyond +the radicand's zero, which is not measured: Bondarenko's `sqrt(x + sqrt(1 + x))/(1 + x^2)` keeps +`provided 1 + x >= 0`, though its answer, a sum over the roots of an octic, holds beyond `x = -1` as a +complex function ([#233](https://github.com/asc-community/AngouriMath/issues/233)). + +| Input | Was (2.5.0) | Now | +|---|---|---| +| `"asec(x)*sqrt(-1 + x^2)/x^4".ToEntity().Integrate("x")` | `integral(...)` | 212 characters, `provided x^2 - 1 >= 0 or (x^2)^2 - x^2 <= 0` | +| `"asec(x)/(-1 + x^2)^(5/2)".ToEntity().Integrate("x")` | `integral(...)` | 506 characters, the same condition twice | +| `"sqrt(cot(c + d*x))*sqrt(a + b*tan(c + d*x))".ToEntity().Integrate("x")` | `integral(...)` | 523 characters, `provided tan(c + d x) >= 0 or a + b tan(c + d x) <= 0` | +| `"sqrt(a + b*tan(c + d*x))*(A + B*tan(c + d*x))/tan(c + d*x)^(3/2)".ToEntity().Integrate("x")` | `integral(...)` | 1,067 characters, the same and `not a = 0` | +| `"sqrt(cot(c + d*x))*(a + b*tan(c + d*x))^(5/2)*(A + B*tan(c + d*x))".ToEntity().Integrate("x")` | past twenty seconds | 2,767 characters, the same | + ### `x^(4k - 1) (a + b arcsec(c x))/sqrt(1 - c^4 x^4)` and the arccosecant's are integrated on both sides of `|c x| = 1` **Answers where there were none.** Rubi's 5.5.1 and 5.6.1 `x^(4k - 1) (a + b arcsec(c x))/sqrt(1 - c^4 x^4)` diff --git a/Sources/AngouriMath/Functions/Algebra/Polynomials/PartialFractions.cs b/Sources/AngouriMath/Functions/Algebra/Polynomials/PartialFractions.cs index 03f8669c5..1ff2f3dc2 100644 --- a/Sources/AngouriMath/Functions/Algebra/Polynomials/PartialFractions.cs +++ b/Sources/AngouriMath/Functions/Algebra/Polynomials/PartialFractions.cs @@ -3063,6 +3063,161 @@ internal static bool DerivativeDiffersWhereReal(Entity antiderivative, Entity in return DiffersWhereReal(Bare(pinnedAntiderivative).Differentiate(x), pinnedIntegrand, x); } + /// + /// Where the derivative of is shown to be + /// on the part of the line where is + /// negative: at every point there of a spread along the line, compared as complex numbers, + /// or at every point there where the integrand is real; at two points or more where it is + /// real, and nowhere that it is real does it differ or have no value. An answer built + /// through the root of the radicand as a real u -- a sign of u read as one, + /// sqrt(u^2 w) as u sqrt(w) -- was conditioned on the radicand for that, and + /// beside a second root imaginary there the integrand is real there too: + /// sqrt(a + b t)/t^(3/2) for t < -a/b, a product of two imaginary roots. + /// Whether the identities still hold there depends on the signs, so it is measured rather + /// than assumed: (1 + sec(x))^(5/2) sqrt(cos(x)) under u = sqrt(cos(x)) was the + /// integrand's negative wherever the cosine is negative, and sqrt(a + b t)/t^(3/2) + /// under u = sqrt(t) is its antiderivative beyond t = -a/b and not between + /// -a/b and 0, where the integrand is imaginary. + /// + /// + /// + /// The other symbols are pinned as pins them, and + /// again with each of three patterns of signs, since the region and the identities both move + /// with them. A pattern counts only where the answer holds at a point where the radicand is + /// positive: an answer that is not one for those signs at all says nothing of this region. + /// + /// + /// Nothing is differentiated where the integrand is real at no point beyond the radicand's + /// zero: the condition is then the integrand's real domain already, or more, and measuring + /// the answer off it buys nothing on the real line. Bondarenko's + /// sqrt(x + sqrt(1 + x))/(1 + x^2) is such a one, its answer a sum over the roots of + /// an octic, and measured at the fifteen points it cost twice the integral's allocation to + /// show it holds beyond x = -1 as a complex function. Off the real line the points are + /// compared until one differs. + /// + /// + internal static BeyondTheRadicand WhereTheDerivativeHoldsBeyondTheRadicand(Entity antiderivative, Entity integrand, Entity radicand, Variable x) + { + // Differentiated and evaluated at fifteen points for each of four patterns, an answer of + // a hundred thousand characters is a search of its own; it keeps its condition. + if (antiderivative.Complexity > LargestAnswerMeasuredBeyondTheRadicand) + return BeyondTheRadicand.NotShown; + using var _ = MathS.Settings.DowncastingEnabled.Set(false); + var parameters = antiderivative.Vars.Concat(integrand.Vars).Concat(radicand.Vars).Where(v => v != x).Distinct().ToList(); + // Each pattern once for the symbols there are: none, and the four are one. The points of + // each sorted first, which asks the integrand and the radicand alone. + var measured = new List<(Entity Antiderivative, Entity Integrand, List Before, List RealBeyond, List ComplexBeyond)>(); + foreach (var pattern in SignPatterns.Where(pattern => parameters.Count > 1 || pattern == 0 || parameters.Count == 1 && pattern == 1)) + { + var (pinnedAntiderivative, pinnedIntegrand, pinnedRadicand) = (Bare(antiderivative), integrand, radicand); + for (var k = 0; k < parameters.Count; k++) + { + var fraction = (k % 3) switch { 0 => "1.37", 1 => "2.71", _ => "0.83" }; + var magnitude = EDecimal.FromString(fraction).Add(EDecimal.FromInt32(k)); + var negative = pattern switch { 1 => k % 2 == 0, 2 => k % 2 == 1, 3 => true, _ => false }; + Entity value = Real.Create(negative ? magnitude.Negate() : magnitude); + pinnedAntiderivative = pinnedAntiderivative.Substitute(parameters[k], value); + pinnedIntegrand = pinnedIntegrand.Substitute(parameters[k], value); + pinnedRadicand = pinnedRadicand.Substitute(parameters[k], value); + } + // In double intervals, which are cheap beside the decimals: a point either cannot tell + // is passed over. + var (before, realBeyond, complexBeyond) = (new List(), new List(), new List()); + foreach (var at in AlongTheLine) + { + var point = Real.Create(EDecimal.FromString(at)); + if (IntervalEvaluation.Of(pinnedIntegrand.Substitute(x, point)) is not { IsFinite: true } value + || IntervalEvaluation.Of(pinnedRadicand.Substitute(x, point)) is not { IsFinite: true, IsReal: true } radicandValue + || radicandValue.Re.ContainsZero) + continue; + var isReal = value.Im.Magnitude <= 1e-9 * System.Math.Max(1, value.Magnitude); + if (radicandValue.Re.Low > 0) + { + if (isReal) + before.Add(point); + } + else + (isReal ? realBeyond : complexBeyond).Add(point); + } + measured.Add((pinnedAntiderivative, pinnedIntegrand, before, realBeyond, complexBeyond)); + } + if (!measured.Any(pattern => pattern.RealBeyond.Count > 0)) + return BeyondTheRadicand.NotShown; + + var inDecimals = new System.Lazy[] { new(() => PreciseEvaluation.In(40)), new(() => PreciseEvaluation.In(80)) }; + // The derivative of a pattern's answer, where it holds at a point before the zero. + var derivatives = new Entity?[measured.Count]; + var controlled = new bool[measured.Count]; + Entity? Controlled(int i) + { + if (controlled[i]) + return derivatives[i]; + controlled[i] = true; + var (pinnedAntiderivative, pinnedIntegrand, before, _, _) = measured[i]; + var derivative = Bare(pinnedAntiderivative).Differentiate(x); + return derivatives[i] = before.Any(point => Agree(derivative.Substitute(x, point), pinnedIntegrand.Substitute(x, point), 1e-9, inDecimals) is true) + ? derivative : null; + } + var real = 0; + for (var i = 0; i < measured.Count; i++) + { + if (measured[i].RealBeyond.Count == 0 || Controlled(i) is not { } derivative) + continue; + foreach (var point in measured[i].RealBeyond) + { + if (Agree(derivative.Substitute(x, point), measured[i].Integrand.Substitute(x, point), 1e-9, inDecimals) is not true) + return BeyondTheRadicand.NotShown; + real++; + } + } + if (real < 2) + return BeyondTheRadicand.NotShown; + for (var i = 0; i < measured.Count; i++) + { + if (measured[i].ComplexBeyond.Count == 0 || Controlled(i) is not { } derivative) + continue; + foreach (var point in measured[i].ComplexBeyond) + if (Agree(derivative.Substitute(x, point), measured[i].Integrand.Substitute(x, point), 1e-9, inDecimals) is not true) + return BeyondTheRadicand.WhereTheIntegrandIsReal; + } + return BeyondTheRadicand.Everywhere; + } + + /// + /// What shows of an answer where the + /// radicand is negative. + /// + internal enum BeyondTheRadicand + { + /// Nothing: it differs where the integrand is real, too few points compared, or the integrand is real at none beyond the zero. + NotShown, + /// It holds where the integrand is real, and differs somewhere it is not. + WhereTheIntegrandIsReal, + /// It holds at every point compared, the integrand real there or not. + Everywhere, + } + + /// + /// The largest answer, by , that + /// measures; the answers it was + /// written for are a few thousand characters. + /// + private const int LargestAnswerMeasuredBeyondTheRadicand = 20000; + + /// + /// The signs of the pinned symbols: all positive, alternating either way, all negative. + /// + [ConstantField] + private static readonly int[] SignPatterns = { 0, 1, 2, 3 }; + + /// + /// A spread of points for , on both + /// sides of zero and of one, near and far. + /// + [ConstantField] + private static readonly string[] AlongTheLine = + { "-7.3", "-3.1", "-1.61", "-0.83", "-0.47", "-0.19", "0.13", "0.29", "0.53", "0.77", "0.94", "1.43", "2.3", "3.17", "6.1" }; + /// /// Whether has no value at any of the points /// takes by default where has diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs index 6efc0fec9..4b3929ac1 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs @@ -14296,7 +14296,12 @@ node is Powf(var radical, Number.Rational e) && e is not Number.Integer if (Integration.ComputeIndefiniteIntegral(integrand, u, integrateByParts) is not { } result) return null; var back = result.Substitute(u, MathS.Pow(radicalBase, Number.Rational.Create(1, q))); - return condition is null ? back : back.Provided(condition); + // Beside a second root the integrand can be real where this radicand is negative, and + // the condition was the whole of its domain there: the remainder parts leaves from + // Timofeev's `arcsec(x) sqrt(x^2 - 1)/x^4`, in `w = x^2`, is + // `(w - 1)^(3/2)/(sqrt(w^2 - w) w^2)`, real on `0 < w < 1`, where the answer through an + // imaginary u holds. So the condition stays only where the answer does not hold there. + return condition is null ? back : ConditionedOnTheRadicand(back, expr, radicalBase, x); } /// @@ -41273,6 +41278,63 @@ private static bool HoldsAnEvenRootBesides(Entity expr, Entity radicand, Entity. return false; } + /// + /// , an antiderivative of through the root of + /// taken as a non-negative real, conditioned on the radicand only + /// as far as that is owed. Beside a second even root the integrand can be real where the + /// radicand is negative, a product of two imaginary roots, and there the answer is measured: + /// where it holds at every point the condition is not owed at all, and where it holds just + /// where the integrand is real, beside one other root of a half-odd power, the condition is + /// the radicand's or the other's negative, which is where the integrand is real. + /// + /// + /// The answers to sqrt(cot(c + d x)) (a + b tan(c + d x))^(5/2) (A + B tan(c + d x)), to + /// twenty-five more of Rubi's 4.3.3.1 and to twelve of its 4.3.2.1, as + /// sqrt(cot(c + d x)) sqrt(a + b tan(c + d x)), said provided tan(c + d x) >= 0, and those to + /// Timofeev's arcsec(x) sqrt(x^2 - 1)/x^4 and arcsec(x)/(x^2 - 1)^(5/2) + /// provided x^2 - 1 >= 0, each nothing at all on part of the line where the + /// integrand is real; each holds there as it stands. + /// https://github.com/asc-community/AngouriMath/issues/233 + /// + private static Entity ConditionedOnTheRadicand(Entity answer, Entity expr, Entity radicand, Entity.Variable x) + { + var condition = radicand >= Number.Integer.Zero; + if (!HoldsAnEvenRootBesides(expr, radicand, x)) + return answer.Provided(condition); + switch (Functions.PartialFractions.WhereTheDerivativeHoldsBeyondTheRadicand(answer, expr, radicand, x)) + { + case Functions.PartialFractions.BeyondTheRadicand.Everywhere: + return answer; + case Functions.PartialFractions.BeyondTheRadicand.WhereTheIntegrandIsReal + when TheOneOtherRadicandOfAHalfOddPower(expr, radicand, x) is { } other: + return answer.Provided(condition | other <= Number.Integer.Zero); + default: + return answer.Provided(condition); + } + } + + /// + /// The one base other than under a power of + /// that is not whole, where every such power is half-odd; else null. + /// + private static Entity? TheOneOtherRadicandOfAHalfOddPower(Entity expr, Entity radicand, Entity.Variable x) + { + Entity? other = null; + foreach (var node in expr.Nodes) + { + if (node is not Powf(var @base, Number.Rational power) || power is Number.Integer || !@base.ContainsNode(x)) + continue; + if (!power.ERational.Denominator.Equals(EInteger.FromInt32(2))) + return null; + if (@base == radicand) + continue; + if (other is not null && other != @base) + return null; + other = @base; + } + return other; + } + /// /// Whether every function of in is its tangent /// or its cotangent: sums, products, quotients and powers of those, and of anything free of @@ -41693,9 +41755,13 @@ private static bool IsAFunctionOfTheTangentAlone(Entity expr, Entity.Variable u) // `-2 u^2 (1 + 1/u^2)^(5/2)/sqrt(1 - u^4)`, whose power of a quotient was // taken apart as `(1 + u^2)^(5/2)/|u|^5`, and was answered wrongly // wherever the cosine is negative. So the answer is conditioned on the - // radicand, as the factoring below conditions its own. + // radicand, as the factoring below conditions its own -- unless the answer + // is measured to hold where the radicand is negative and the integrand real: + // `sqrt(a + b t)/(t^(1/2) (1 + t^2))` under `u = sqrt(t)` holds beyond + // `t = -a/b`, and the condition left Rubi's `sqrt(cot(c + d x)) (a + b tan(c + d x))^(5/2)` + // and the rest of its 4.3.3.1 with half its domain. if (asTheRoot != resultInU) - return Functions.PartialFractions.Bare(asTheRoot).Substitute(uSub, u).Provided(rootOf >= Number.Integer.Zero); + return ConditionedOnTheRadicand(Functions.PartialFractions.Bare(asTheRoot).Substitute(uSub, u), expr, rootOf, x); // And a rule below can take u real without reading its sign: `sqrt(u^4)` // written as `u^2` is the other root wherever u is imaginary. That matters // only where the integrand is real with the radicand negative, which takes @@ -41735,9 +41801,10 @@ private static bool IsAFunctionOfTheTangentAlone(Entity expr, Entity.Variable u) // is imaginary and the identities the factoring used do not hold, so the // answer is conditioned on the radicand, as the linear-radical substitution // conditions its own. https://github.com/asc-community/AngouriMath/issues/718 + // And where the integrand is real with the radicand negative, held beside a second + // root, the answer is kept there where it is measured to hold. if (u is Powf(var radicand, Number.Rational root) && root.ERational.Denominator.IsEven) - return Functions.PartialFractions.Bare(forPositiveU).Substitute(uSub, u) - .Provided(radicand >= Number.Integer.Zero); + return ConditionedOnTheRadicand(Functions.PartialFractions.Bare(forPositiveU).Substitute(uSub, u), expr, radicand, x); if (ExtendedByParity(integrandInU, forPositiveU, uSub) is { } onBothSides) return onBothSides.Substitute(uSub, u); } diff --git a/Sources/Tests/UnitTests/Calculus/AnAnswerBeyondANegativeRadicandIntegralTest.cs b/Sources/Tests/UnitTests/Calculus/AnAnswerBeyondANegativeRadicandIntegralTest.cs new file mode 100644 index 000000000..721849d24 --- /dev/null +++ b/Sources/Tests/UnitTests/Calculus/AnAnswerBeyondANegativeRadicandIntegralTest.cs @@ -0,0 +1,97 @@ +// +// 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 +{ + /// + /// Integrands that are real where a radicand is negative, as a product of two imaginary + /// factors, integrated through the root of that radicand taken as a non-negative real. The + /// answers said provided the radicand is not negative, and so said nothing on part of + /// the line where the integrand is real; they hold there as they stand, and the condition is + /// now the integrand's real domain or none. + /// #233 + /// + /// + /// A point where the integrand is real and the answer has no value is a failure here, not a + /// point skipped: skipping it is how a condition false on half the line passed for an answer. + /// + [Trait("Area", "Calculus")] + public sealed class AnAnswerBeyondANegativeRadicandIntegralTest + { + private static void HoldsWhereTheIntegrandIsReal(string integrand, Func pinned, double[] points) + { + var integral = integrand.ToEntity().Integrate("x"); + var text = integral.Stringize(); + Assert.DoesNotContain("integral(", text); + Assert.DoesNotContain("NaN", text); + var derivative = pinned(integral.Substitute("C", 0)).Differentiate("x"); + var original = pinned(integrand.ToEntity()); + foreach (var at in points) + { + var want = original.Substitute("x", at).EvalNumerical(); + Assert.False(want.IsNaN, $"{integrand} has no value at x = {at}"); + Assert.True(Math.Abs((double)want.ImaginaryPart) < 1e-9 * Math.Max(1, Math.Abs((double)want.RealPart)), + $"{integrand} is {want} at x = {at}, which is not a point where it is real"); + 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}"); + 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}"); + } + } + + /// + /// Timofeev's arcsecants by parts: the remainder is answered in w = x^2 through + /// sqrt(w - 1), and the integrands are real for x < -1, for x > 1 and, + /// as the arcsecant and the root are both imaginary, for 0 < x < 1. Two points on + /// each of the three intervals. + /// + [Theory] + [InlineData("asec(x)*sqrt(-1 + x^2)/x^4")] + [InlineData("asec(x)/(-1 + x^2)^(5/2)")] + public void AnArcsecantOnAllThreeIntervals(string integrand) + => HoldsWhereTheIntegrandIsReal(integrand, e => e, new[] { -2.7, -1.3, 0.23, 0.59, 1.3, 2.6 }); + + /// + /// Rubi's 4.3.2.1 and 4.3.3.1, a root of the tangent beside a root of a + b tan(c + d x), through + /// u = sqrt(tan(c + d x)): real where the tangent is positive and where it is below + /// -a/b, two imaginary roots there. With a = 1.3, b = 0.7, + /// c = 0.4, d = 0.9, the tangent is positive for -0.44 < x < 1.30 and + /// below -a/b for 1.30 < x < 1.85 and again a period on. + /// + [Theory] + [InlineData("sqrt(cot(c + d*x))*sqrt(a + b*tan(c + d*x))")] + [InlineData("1/(sqrt(cot(c + d*x))*(a + b*tan(c + d*x))^(3/2))")] + [InlineData("sqrt(a + b*tan(c + d*x))*(A + B*tan(c + d*x))/tan(c + d*x)^(3/2)")] + [InlineData("sqrt(cot(c + d*x))*(a + b*tan(c + d*x))^(5/2)*(A + B*tan(c + d*x))")] + [InlineData("sqrt(cot(c + d*x))*(A + B*tan(c + d*x))/sqrt(a + b*tan(c + d*x))")] + [InlineData("(A + B*tan(c + d*x))/(sqrt(cot(c + d*x))*(a + b*tan(c + d*x))^(3/2))")] + public void ARootOfTheTangentBesideARootOfALinearInIt(string integrand) + => HoldsWhereTheIntegrandIsReal(integrand, + e => e.Substitute("a", 1.3).Substitute("b", 0.7).Substitute("c", 0.4).Substitute("d", 0.9) + .Substitute("A", 1.1).Substitute("B", 0.6), + new[] { 0.3, 0.9, 1.45, 1.7, 5.0 }); + + /// + /// Where the integrand is imaginary the answer still claims nothing: between -a/b + /// and 0 the tangent's root and the linear's are not both imaginary, and there the answer + /// through a real-or-imaginary u is not the integrand's. + /// + [Fact] + public void NothingIsClaimedWhereTheIntegrandIsImaginary() + { + var integral = "sqrt(a + b*tan(c + d*x))*(A + B*tan(c + d*x))/tan(c + d*x)^(3/2)".ToEntity().Integrate("x"); + var pinned = integral.Substitute("C", 0).Substitute("a", 1.3).Substitute("b", 0.7).Substitute("c", 0.4) + .Substitute("d", 0.9).Substitute("A", 1.1).Substitute("B", 0.6); + Assert.True(pinned.Substitute("x", 2.3).EvalNumerical().IsNaN); + } + } +}