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