diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index c75e5a311..4d7813533 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -3100,6 +3100,24 @@ leaves are read as quotients of the sine and the cosine | `"1/(a+b*asin(c*x))".Integrate("x")` | `integral(1 / (a + b * arcsin(c * x)), x)` | `Si` and `Ci` of `(a + b arcsin(c x))/b` | | `"x/((c+a^2*c*x^2)^2*atan(a*x))".Integrate("x")` | `integral(x / ((c + a ^ 2 * c * x ^ 2) ^ 2 * arctan(a * x)), x)` | `1/2 * Si(2 * arctan(a * x)) / (a ^ 2 * c ^ 2) + C` | +### The arcsecant beside a root of `x^2 - 1` is integrated right below `-1` too + +**Answers where there were none.** Under `x = sec(u)`, the substitution that undoes the arcsecant, +`sqrt(x^2 - 1)` is `|tan(u)|`, and on the arcsecant's range the tangent has the sign of `x`. It was +written `tan(u)`, so that on the unreleased master `x arcsec(x)/sqrt(x^2 - 1)` and its kin were +answered right for `x > 1` and with the integrand's negative for a derivative at every `x < -1`, +where they are as real; 2.5.0 declined them. The root is `tan(u) sgn(x)` now, as the cosecant's and +the cotangent's carry their signs, and a first power of the arcsecant beside it is by parts. The sign +written back is the argument's, which is that of `x` only for a positive slope and no constant: +`arcsec(x + 3)` has the sign of `x + 3` ([#718](https://github.com/asc-community/AngouriMath/issues/718)). + +| Input | Was (2.5.0) | Now | +|---|---|---| +| `"x*asec(x)/sqrt(-1 + x^2)".ToEntity().Integrate("x")` | `integral(...)` | right for `x > 1` and for `x < -1` | +| `"asec(x)/(x^2*sqrt(-1 + x^2))".ToEntity().Integrate("x")` | `integral(...)` | the same | +| `"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` | + ### A perfect square in a power of the variable is read as one `sqrt(a^2 + 2 a b x^2 + b^2 x^4) sqrt(c + e x + d x^2)` was left unevaluated. The radicand is diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs index 2ac4d83e4..b3e52e602 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs @@ -7157,10 +7157,12 @@ private static bool TheInverseIsBelowTheBar(Entity expr, Entity inverse, Entity. /// and the cosine is not negative; substituting and simplifying instead would leave /// sqrt(1 - sin(u)^2), which the simplifier is right not to call cos(u). /// So (1 - x^2)^(k/2) becomes cos(u)^k directly, and likewise - /// (1 + x^2)^(k/2) into sec(u)^k for the tangent and (x^2 - 1)^(k/2) - /// into tan(u)^k for the secant, each on its principal branch. That branch is the - /// one the inverse function is defined on, so the answer holds wherever the integrand - /// is read through it. + /// (1 + x^2)^(k/2) into sec(u)^k for the tangent, each on its principal + /// branch. That branch is the one the inverse function is defined on, so the answer holds + /// wherever the integrand is read through it. For the secant (x^2 - 1)^(k/2) is + /// |tan(u)|^k, and on the arcsecant's range the tangent has the sign of the + /// argument, so it becomes tan(u)^k times that sign to the k, as the + /// cosecant's and the cotangent's roots carry theirs. /// /// /// Exactly one inverse function, of the variable or of a linear c x + d in it, @@ -7213,9 +7215,10 @@ private static bool TheInverseIsBelowTheBar(Entity expr, Entity inverse, Entity. } var u = Variable.CreateUnique(expr, "u_inv"); - // The sign of u, where the root's sign is that: a constant on each half of the - // range, carried through the integration as a symbol and written back as the sign - // of x, which it is for the cosecant and the cotangent. + // The sign the root has, where it has one: a constant on each half of the range, + // carried through the integration as a symbol and written back as the sign of the + // argument, which it is -- the sign of u for the cosecant and the cotangent, and of + // tan(u) for the secant. var signOfU = Variable.CreateUnique(expr, "s_inv"); // x in terms of u, dx/du, the quadratic whose root goes by construction and what it // becomes, and the way back. @@ -7245,7 +7248,11 @@ private static bool TheInverseIsBelowTheBar(Entity expr, Entity inverse, Entity. (xInU, dxdu, radicandBase, root) = (MathS.Cosec(u), -MathS.Cosec(u) * MathS.Cotan(u), MathS.Sqr(argument) - 1, MathS.Cotan(u) * signOfU); break; default: - (xInU, dxdu, radicandBase, root) = (MathS.Sec(u), MathS.Sec(u) * MathS.Tan(u), MathS.Sqr(argument) - 1, MathS.Tan(u)); + // arcsec has the range [0, pi/2) ∪ (pi/2, pi], where the tangent has the sign + // of the argument: `sqrt(x^2 - 1)` is `tan(u) sgn(x)`. Taken as `tan(u)`, the + // answer was right for x > 1 and its derivative the integrand's negative for + // every x < -1, where the integrand is as real: `x arcsec(x)/sqrt(x^2 - 1)`. + (xInU, dxdu, radicandBase, root) = (MathS.Sec(u), MathS.Sec(u) * MathS.Tan(u), MathS.Sqr(argument) - 1, MathS.Tan(u) * signOfU); break; } if (linear) @@ -7318,10 +7325,10 @@ node is Powf(var @base, Number.Integer power) && power.EInteger.CompareTo(EInteg { if (radicalsRemoved == 0 && !exponentialOfTheInverse && !powerOfTheInverse) return null; - // The cosecant and the cotangent carry the sign of u into the root, and a first - // power of either beside a root is parts' -- `arccot(x)/(1 + x^2)^(3/2)` is - // `x arccot(x)/sqrt(1 + x^2) + 1/sqrt(1 + x^2)` there, with no sign in it. - if (inverse is Arccosecantf or Arccotanf && !exponentialOfTheInverse && !powerOfTheInverse) + // The cosecant, the cotangent and the secant carry a sign into the root, and a + // first power of any of them beside a root is parts' -- `arccot(x)/(1 + x^2)^(3/2)` + // is `x arccot(x)/sqrt(1 + x^2) + 1/sqrt(1 + x^2)` there, with no sign in it. + if (inverse is Arccosecantf or Arccotanf or Arcsecantf && !exponentialOfTheInverse && !powerOfTheInverse) return null; } // And nothing else of `x` under a root, which the construction did not reach: @@ -7374,10 +7381,11 @@ node is Powf(var @base, Number.Rational power) && power is not Number.Integer && if (result is null) return null; if (result.ContainsNode(signOfU)) - // The sign squared is one, and the sign itself is the sign of x. + // The sign squared is one, and the sign itself is the argument's: `arccsc(c x + d)` + // has the sign of `c x + d`, which is the sign of x only for a positive c and no d. result = result .Replace(node => node is Powf(var b, Number.Integer k) && b == signOfU ? (k.EInteger.IsEven ? Number.Integer.One : signOfU) : node) - .Substitute(signOfU, MathS.Signum(x)); + .Substitute(signOfU, MathS.Signum(argument)); return TrigonometryOfTheInverseInX(result.Substitute(u, inverse), inverse, argument); } diff --git a/Sources/Tests/UnitTests/Calculus/InverseSecantIntegralTest.cs b/Sources/Tests/UnitTests/Calculus/InverseSecantIntegralTest.cs index f632da64c..cfb4c7d1b 100644 --- a/Sources/Tests/UnitTests/Calculus/InverseSecantIntegralTest.cs +++ b/Sources/Tests/UnitTests/Calculus/InverseSecantIntegralTest.cs @@ -109,6 +109,26 @@ public void TheFourThatWereAlreadyThere(string integrand, double[] points) [InlineData("x^3*asec(x)/sqrt(x^4 - 1)", new[] { 1.4, 2.6, 5.1, -1.9, -3.7 })] public void ByPartsAgainstARootOfAQuartic(string integrand, double[] points) => DifferentiatesBack(integrand, points); + /// + /// The arcsecant beside a root of x^2 - 1, under x = sec(u): the root is + /// tan(u) sgn(x) on the arcsecant's range, and taken as tan(u) the answers + /// were right for x > 1 and their derivatives the integrands' negatives for every + /// x < -1, where the integrands are as real. Timofeev's and Charlwood's. With a linear + /// argument the sign is the argument's: at x = -1.5 the sign of x + 3 is + /// not the sign of x. + /// + [Theory] + [InlineData("x*asec(x)/sqrt(-1 + x^2)", new[] { 1.3, 2.6, -1.3, -1.9, -2.7 })] + [InlineData("asec(x)*sqrt(-1 + x^2)/x^4", new[] { 1.3, 2.6, -1.3, -1.9, -2.7 })] + [InlineData("asec(x)/(-1 + x^2)^(5/2)", new[] { 1.3, 2.6, -1.3, -1.9, -2.7 })] + [InlineData("x^3*asec(x)/(-1 + x^2)^(5/2)", new[] { 1.3, 2.6, -1.3, -1.9, -2.7 })] + [InlineData("asec(x)/(x^2*sqrt(-1 + x^2))", new[] { 1.3, 2.6, -1.3, -1.9, -2.7 })] + [InlineData("(-1 + x^2)^(3/2)*asec(x)^2/x^5", new[] { 1.3, 2.6, -1.3, -1.9, -2.7 })] + [InlineData("asec(x)^3*sqrt(-1 + x^2)/x^4", new[] { 1.3, 2.6, -1.3, -1.9, -2.7 })] + [InlineData("asec(x)^2/(x*sqrt(x^2 - 1))", new[] { 1.3, 2.6, -1.3, -1.9, -2.7 })] + [InlineData("asec(x + 3)^2/((x + 3)*sqrt((x + 3)^2 - 1))", new[] { 0.5, -1.5, -0.5, -4.5, -6.0 })] + public void BesideARootOnBothIntervals(string integrand, double[] points) => DifferentiatesBack(integrand, points); + /// /// A non-linear argument is not this row's: arcsec(x^2) would want the chain rule /// undone first, and the table reads a linear argument only.