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 }); }