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.