Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions BREAKING-CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
|---|---|---|
Expand All @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5361,6 +5361,14 @@ Entity Integral(ERational power)
/// chain in t.
/// https://github.com/asc-community/AngouriMath/issues/718
/// </para>
/// <para>
/// Two roots, as in <c>cos(y)^(3/2)/sqrt(a + a sec(y))</c>, are real together where the
/// cosine is negative too, and there each written apart may have turned its sign: the answer
/// said <c>provided cos(y) >= 0</c>, 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
/// </para>
/// </remarks>
internal static Entity? SolveByTheHalfAngleTangentBesideAHalfOddPowerOfOnePlusASecant(Entity expr, Entity.Variable x, bool integrateByParts)
{
Expand Down Expand Up @@ -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 =>
Expand All @@ -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.
Expand All @@ -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);
});
Expand Down Expand Up @@ -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);
}
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,15 +158,17 @@ public void AnExponentialOfAnInverseTangentBesideTheRootSaysWhereItHolds()
/// <c>u = sqrt(t)</c> is <c>-2 u^2 (1 + 1/u^2)^(5/2)/sqrt(1 - u^4)</c>, whose power of a
/// quotient is taken apart as <c>(1 + u^2)^(5/2)/|u|^5</c>. Where the cosine is negative the
/// integrand is still real, a product of two imaginary factors, and the answer built for a
/// real <c>u</c> was not its antiderivative there. Rubi's 4.5.3.1.
/// real <c>u</c> 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
/// (<see cref="OnePlusASecantHalfPowerIntegralTest"/>); either way the answer is right where
/// the cosine is negative or says nothing there.
/// </summary>
[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 })
Expand All @@ -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}");
}

/// <summary>
Expand All @@ -190,17 +194,18 @@ public void TheSearchsEvenRootSaysWhereItHolds()
/// product of roots below the bar was written with <c>sqrt(u^4) = u^2</c>, and the answer
/// was negated wherever the cosine is negative. Rubi's 4.5.1.2. The same in <c>t</c>,
/// <c>sqrt(t)/(sqrt(1/t + 1) sqrt(1 - t^2))</c>, is written over each linear once and
/// answered on both sides (<see cref="EachLinearWrittenOnceIntegralTest"/>).
/// answered on both sides (<see cref="EachLinearWrittenOnceIntegralTest"/>), and so is it in
/// the half-angle tangent now (<see cref="OnePlusASecantHalfPowerIntegralTest"/>): the
/// answer is right where the cosine is negative or says nothing there.
/// https://github.com/asc-community/AngouriMath/issues/1581
/// </summary>
[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)
{
Expand All @@ -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}");
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 });

/// <summary>
/// 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.
/// </summary>
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}");
}

/// <summary>
/// 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.
/// </summary>
private static readonly double[] BothSides = { -2.6, -2.0, -1.2, 0.3, 1.1, 1.8, 2.6, 3.6, 4.2 };

/// <summary>
/// Two roots written apart are exact where <c>cos(y)</c> 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
/// <c>1 - sec(x)</c> 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
/// <c>cos(c + d x)^(3/2) (A + C sec(c + d x)^2)/sqrt(a + a sec(c + d x))</c> said
/// <c>provided cos(c + d x) >= 0</c>, where without it the answer was the integrand's
/// negative.
/// </summary>
[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);

/// <summary>
/// Rubi's 4.5.4.2, <c>(a + a sec)^m (d sec)^n (A + B sec + C sec^2)</c>, at
/// <c>y = 0.9 + 1.3 x</c>: from <c>-2.48</c> to <c>6.36</c>, both signs of the cosine.
/// </summary>
[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) });

/// <summary>
/// The product of the constants of <c>sqrt(sec(x)) sqrt(a - a sec(x))</c> is <c>-a</c>,
/// whose sign is not known: the integrand is real for a negative <c>a</c>, 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.
/// </summary>
[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) });
}

/// <summary>
/// Three roots leave one of <c>1 - t^2</c> in the answer in t, which is not shown to hold
/// beyond <c>t^2 < 1</c>, and the answer says so.
/// </summary>
[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 });
}
Expand Down
Loading