diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 883fc378b..250299e8d 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -7316,6 +7316,35 @@ written back is the argument's, which is that of `x` only for a positive slope a | `"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` | +### An answer through a root taken real holds where a second root makes the integrand real + +**Answers where there were none.** An integrand real where a radicand is negative, a second root +being imaginary there too, was integrated through the root of that radicand as a non-negative real +`u`, and the answer said `provided` the radicand is not negative: under `u = sqrt(tan(c + d x))` for +`sqrt(cot(c + d x)) (a + b tan(c + d x))^(5/2) (A + B tan(c + d x))`, twenty-five more of Rubi's +4.3.3.1 and twelve of its 4.3.2.1, and under `u = sqrt(w - 1)`, `w = x^2`, for the remainder by parts +leaves from Timofeev's `arcsec(x) sqrt(x^2 - 1)/x^4` and `arcsec(x)/(x^2 - 1)^(5/2)`. On the +unreleased master these said nothing where `tan(c + d x) < -a/b`, or on `0 < x < 1`, though each +integrand is real there; 2.5.0 declined them. The answer is measured there now, its derivative +against the integrand at a spread of points where the radicand is negative, the symbols pinned to +four patterns of signs. These hold where the integrand is real and not where it is imaginary, so +beside one other half-odd root the condition is that either radicand has its sign, +`provided tan(c + d x) >= 0 or a + b tan(c + d x) <= 0`, the integrand's real domain; for the +arcsecants it is `x^2 - 1 >= 0 or (x^2)^2 - x^2 <= 0`, true for every real `x`. An answer that also +holds at every point measured where the integrand is imaginary carries no condition. One that differs +where the integrand is real keeps the old one, and so does one whose integrand is real nowhere beyond +the radicand's zero, which is not measured: Bondarenko's `sqrt(x + sqrt(1 + x))/(1 + x^2)` keeps +`provided 1 + x >= 0`, though its answer, a sum over the roots of an octic, holds beyond `x = -1` as a +complex function ([#233](https://github.com/asc-community/AngouriMath/issues/233)). + +| Input | Was (2.5.0) | Now | +|---|---|---| +| `"asec(x)*sqrt(-1 + x^2)/x^4".ToEntity().Integrate("x")` | `integral(...)` | 212 characters, `provided x^2 - 1 >= 0 or (x^2)^2 - x^2 <= 0` | +| `"asec(x)/(-1 + x^2)^(5/2)".ToEntity().Integrate("x")` | `integral(...)` | 506 characters, the same condition twice | +| `"sqrt(cot(c + d*x))*sqrt(a + b*tan(c + d*x))".ToEntity().Integrate("x")` | `integral(...)` | 523 characters, `provided tan(c + d x) >= 0 or a + b tan(c + d x) <= 0` | +| `"sqrt(a + b*tan(c + d*x))*(A + B*tan(c + d*x))/tan(c + d*x)^(3/2)".ToEntity().Integrate("x")` | `integral(...)` | 1,067 characters, the same and `not a = 0` | +| `"sqrt(cot(c + d*x))*(a + b*tan(c + d*x))^(5/2)*(A + B*tan(c + d*x))".ToEntity().Integrate("x")` | past twenty seconds | 2,767 characters, the same | + ### `x^(4k - 1) (a + b arcsec(c x))/sqrt(1 - c^4 x^4)` and the arccosecant's are integrated on both sides of `|c x| = 1` **Answers where there were none.** Rubi's 5.5.1 and 5.6.1 `x^(4k - 1) (a + b arcsec(c x))/sqrt(1 - c^4 x^4)` diff --git a/Sources/AngouriMath/Functions/Algebra/Polynomials/PartialFractions.cs b/Sources/AngouriMath/Functions/Algebra/Polynomials/PartialFractions.cs index 03f8669c5..1ff2f3dc2 100644 --- a/Sources/AngouriMath/Functions/Algebra/Polynomials/PartialFractions.cs +++ b/Sources/AngouriMath/Functions/Algebra/Polynomials/PartialFractions.cs @@ -3063,6 +3063,161 @@ internal static bool DerivativeDiffersWhereReal(Entity antiderivative, Entity in return DiffersWhereReal(Bare(pinnedAntiderivative).Differentiate(x), pinnedIntegrand, x); } + /// + /// Where the derivative of is shown to be + /// on the part of the line where is + /// negative: at every point there of a spread along the line, compared as complex numbers, + /// or at every point there where the integrand is real; at two points or more where it is + /// real, and nowhere that it is real does it differ or have no value. An answer built + /// through the root of the radicand as a real u -- a sign of u read as one, + /// sqrt(u^2 w) as u sqrt(w) -- was conditioned on the radicand for that, and + /// beside a second root imaginary there the integrand is real there too: + /// sqrt(a + b t)/t^(3/2) for t < -a/b, a product of two imaginary roots. + /// Whether the identities still hold there depends on the signs, so it is measured rather + /// than assumed: (1 + sec(x))^(5/2) sqrt(cos(x)) under u = sqrt(cos(x)) was the + /// integrand's negative wherever the cosine is negative, and sqrt(a + b t)/t^(3/2) + /// under u = sqrt(t) is its antiderivative beyond t = -a/b and not between + /// -a/b and 0, where the integrand is imaginary. + /// + /// + /// + /// The other symbols are pinned as pins them, and + /// again with each of three patterns of signs, since the region and the identities both move + /// with them. A pattern counts only where the answer holds at a point where the radicand is + /// positive: an answer that is not one for those signs at all says nothing of this region. + /// + /// + /// Nothing is differentiated where the integrand is real at no point beyond the radicand's + /// zero: the condition is then the integrand's real domain already, or more, and measuring + /// the answer off it buys nothing on the real line. Bondarenko's + /// sqrt(x + sqrt(1 + x))/(1 + x^2) is such a one, its answer a sum over the roots of + /// an octic, and measured at the fifteen points it cost twice the integral's allocation to + /// show it holds beyond x = -1 as a complex function. Off the real line the points are + /// compared until one differs. + /// + /// + internal static BeyondTheRadicand WhereTheDerivativeHoldsBeyondTheRadicand(Entity antiderivative, Entity integrand, Entity radicand, Variable x) + { + // Differentiated and evaluated at fifteen points for each of four patterns, an answer of + // a hundred thousand characters is a search of its own; it keeps its condition. + if (antiderivative.Complexity > LargestAnswerMeasuredBeyondTheRadicand) + return BeyondTheRadicand.NotShown; + using var _ = MathS.Settings.DowncastingEnabled.Set(false); + var parameters = antiderivative.Vars.Concat(integrand.Vars).Concat(radicand.Vars).Where(v => v != x).Distinct().ToList(); + // Each pattern once for the symbols there are: none, and the four are one. The points of + // each sorted first, which asks the integrand and the radicand alone. + var measured = new List<(Entity Antiderivative, Entity Integrand, List Before, List RealBeyond, List ComplexBeyond)>(); + foreach (var pattern in SignPatterns.Where(pattern => parameters.Count > 1 || pattern == 0 || parameters.Count == 1 && pattern == 1)) + { + var (pinnedAntiderivative, pinnedIntegrand, pinnedRadicand) = (Bare(antiderivative), integrand, radicand); + for (var k = 0; k < parameters.Count; k++) + { + var fraction = (k % 3) switch { 0 => "1.37", 1 => "2.71", _ => "0.83" }; + var magnitude = EDecimal.FromString(fraction).Add(EDecimal.FromInt32(k)); + var negative = pattern switch { 1 => k % 2 == 0, 2 => k % 2 == 1, 3 => true, _ => false }; + Entity value = Real.Create(negative ? magnitude.Negate() : magnitude); + pinnedAntiderivative = pinnedAntiderivative.Substitute(parameters[k], value); + pinnedIntegrand = pinnedIntegrand.Substitute(parameters[k], value); + pinnedRadicand = pinnedRadicand.Substitute(parameters[k], value); + } + // In double intervals, which are cheap beside the decimals: a point either cannot tell + // is passed over. + var (before, realBeyond, complexBeyond) = (new List(), new List(), new List()); + foreach (var at in AlongTheLine) + { + var point = Real.Create(EDecimal.FromString(at)); + if (IntervalEvaluation.Of(pinnedIntegrand.Substitute(x, point)) is not { IsFinite: true } value + || IntervalEvaluation.Of(pinnedRadicand.Substitute(x, point)) is not { IsFinite: true, IsReal: true } radicandValue + || radicandValue.Re.ContainsZero) + continue; + var isReal = value.Im.Magnitude <= 1e-9 * System.Math.Max(1, value.Magnitude); + if (radicandValue.Re.Low > 0) + { + if (isReal) + before.Add(point); + } + else + (isReal ? realBeyond : complexBeyond).Add(point); + } + measured.Add((pinnedAntiderivative, pinnedIntegrand, before, realBeyond, complexBeyond)); + } + if (!measured.Any(pattern => pattern.RealBeyond.Count > 0)) + return BeyondTheRadicand.NotShown; + + var inDecimals = new System.Lazy[] { new(() => PreciseEvaluation.In(40)), new(() => PreciseEvaluation.In(80)) }; + // The derivative of a pattern's answer, where it holds at a point before the zero. + var derivatives = new Entity?[measured.Count]; + var controlled = new bool[measured.Count]; + Entity? Controlled(int i) + { + if (controlled[i]) + return derivatives[i]; + controlled[i] = true; + var (pinnedAntiderivative, pinnedIntegrand, before, _, _) = measured[i]; + var derivative = Bare(pinnedAntiderivative).Differentiate(x); + return derivatives[i] = before.Any(point => Agree(derivative.Substitute(x, point), pinnedIntegrand.Substitute(x, point), 1e-9, inDecimals) is true) + ? derivative : null; + } + var real = 0; + for (var i = 0; i < measured.Count; i++) + { + if (measured[i].RealBeyond.Count == 0 || Controlled(i) is not { } derivative) + continue; + foreach (var point in measured[i].RealBeyond) + { + if (Agree(derivative.Substitute(x, point), measured[i].Integrand.Substitute(x, point), 1e-9, inDecimals) is not true) + return BeyondTheRadicand.NotShown; + real++; + } + } + if (real < 2) + return BeyondTheRadicand.NotShown; + for (var i = 0; i < measured.Count; i++) + { + if (measured[i].ComplexBeyond.Count == 0 || Controlled(i) is not { } derivative) + continue; + foreach (var point in measured[i].ComplexBeyond) + if (Agree(derivative.Substitute(x, point), measured[i].Integrand.Substitute(x, point), 1e-9, inDecimals) is not true) + return BeyondTheRadicand.WhereTheIntegrandIsReal; + } + return BeyondTheRadicand.Everywhere; + } + + /// + /// What shows of an answer where the + /// radicand is negative. + /// + internal enum BeyondTheRadicand + { + /// Nothing: it differs where the integrand is real, too few points compared, or the integrand is real at none beyond the zero. + NotShown, + /// It holds where the integrand is real, and differs somewhere it is not. + WhereTheIntegrandIsReal, + /// It holds at every point compared, the integrand real there or not. + Everywhere, + } + + /// + /// The largest answer, by , that + /// measures; the answers it was + /// written for are a few thousand characters. + /// + private const int LargestAnswerMeasuredBeyondTheRadicand = 20000; + + /// + /// The signs of the pinned symbols: all positive, alternating either way, all negative. + /// + [ConstantField] + private static readonly int[] SignPatterns = { 0, 1, 2, 3 }; + + /// + /// A spread of points for , on both + /// sides of zero and of one, near and far. + /// + [ConstantField] + private static readonly string[] AlongTheLine = + { "-7.3", "-3.1", "-1.61", "-0.83", "-0.47", "-0.19", "0.13", "0.29", "0.53", "0.77", "0.94", "1.43", "2.3", "3.17", "6.1" }; + /// /// Whether has no value at any of the points /// takes by default where has diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs index 6efc0fec9..4b3929ac1 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs @@ -14296,7 +14296,12 @@ node is Powf(var radical, Number.Rational e) && e is not Number.Integer if (Integration.ComputeIndefiniteIntegral(integrand, u, integrateByParts) is not { } result) return null; var back = result.Substitute(u, MathS.Pow(radicalBase, Number.Rational.Create(1, q))); - return condition is null ? back : back.Provided(condition); + // Beside a second root the integrand can be real where this radicand is negative, and + // the condition was the whole of its domain there: the remainder parts leaves from + // Timofeev's `arcsec(x) sqrt(x^2 - 1)/x^4`, in `w = x^2`, is + // `(w - 1)^(3/2)/(sqrt(w^2 - w) w^2)`, real on `0 < w < 1`, where the answer through an + // imaginary u holds. So the condition stays only where the answer does not hold there. + return condition is null ? back : ConditionedOnTheRadicand(back, expr, radicalBase, x); } /// @@ -41273,6 +41278,63 @@ private static bool HoldsAnEvenRootBesides(Entity expr, Entity radicand, Entity. return false; } + /// + /// , an antiderivative of through the root of + /// taken as a non-negative real, conditioned on the radicand only + /// as far as that is owed. Beside a second even root the integrand can be real where the + /// radicand is negative, a product of two imaginary roots, and there the answer is measured: + /// where it holds at every point the condition is not owed at all, and where it holds just + /// where the integrand is real, beside one other root of a half-odd power, the condition is + /// the radicand's or the other's negative, which is where the integrand is real. + /// + /// + /// The answers to sqrt(cot(c + d x)) (a + b tan(c + d x))^(5/2) (A + B tan(c + d x)), to + /// twenty-five more of Rubi's 4.3.3.1 and to twelve of its 4.3.2.1, as + /// sqrt(cot(c + d x)) sqrt(a + b tan(c + d x)), said provided tan(c + d x) >= 0, and those to + /// Timofeev's arcsec(x) sqrt(x^2 - 1)/x^4 and arcsec(x)/(x^2 - 1)^(5/2) + /// provided x^2 - 1 >= 0, each nothing at all on part of the line where the + /// integrand is real; each holds there as it stands. + /// https://github.com/asc-community/AngouriMath/issues/233 + /// + private static Entity ConditionedOnTheRadicand(Entity answer, Entity expr, Entity radicand, Entity.Variable x) + { + var condition = radicand >= Number.Integer.Zero; + if (!HoldsAnEvenRootBesides(expr, radicand, x)) + return answer.Provided(condition); + switch (Functions.PartialFractions.WhereTheDerivativeHoldsBeyondTheRadicand(answer, expr, radicand, x)) + { + case Functions.PartialFractions.BeyondTheRadicand.Everywhere: + return answer; + case Functions.PartialFractions.BeyondTheRadicand.WhereTheIntegrandIsReal + when TheOneOtherRadicandOfAHalfOddPower(expr, radicand, x) is { } other: + return answer.Provided(condition | other <= Number.Integer.Zero); + default: + return answer.Provided(condition); + } + } + + /// + /// The one base other than under a power of + /// that is not whole, where every such power is half-odd; else null. + /// + private static Entity? TheOneOtherRadicandOfAHalfOddPower(Entity expr, Entity radicand, Entity.Variable x) + { + Entity? other = null; + foreach (var node in expr.Nodes) + { + if (node is not Powf(var @base, Number.Rational power) || power is Number.Integer || !@base.ContainsNode(x)) + continue; + if (!power.ERational.Denominator.Equals(EInteger.FromInt32(2))) + return null; + if (@base == radicand) + continue; + if (other is not null && other != @base) + return null; + other = @base; + } + return other; + } + /// /// Whether every function of in is its tangent /// or its cotangent: sums, products, quotients and powers of those, and of anything free of @@ -41693,9 +41755,13 @@ private static bool IsAFunctionOfTheTangentAlone(Entity expr, Entity.Variable u) // `-2 u^2 (1 + 1/u^2)^(5/2)/sqrt(1 - u^4)`, whose power of a quotient was // taken apart as `(1 + u^2)^(5/2)/|u|^5`, and was answered wrongly // wherever the cosine is negative. So the answer is conditioned on the - // radicand, as the factoring below conditions its own. + // radicand, as the factoring below conditions its own -- unless the answer + // is measured to hold where the radicand is negative and the integrand real: + // `sqrt(a + b t)/(t^(1/2) (1 + t^2))` under `u = sqrt(t)` holds beyond + // `t = -a/b`, and the condition left Rubi's `sqrt(cot(c + d x)) (a + b tan(c + d x))^(5/2)` + // and the rest of its 4.3.3.1 with half its domain. if (asTheRoot != resultInU) - return Functions.PartialFractions.Bare(asTheRoot).Substitute(uSub, u).Provided(rootOf >= Number.Integer.Zero); + return ConditionedOnTheRadicand(Functions.PartialFractions.Bare(asTheRoot).Substitute(uSub, u), expr, rootOf, x); // And a rule below can take u real without reading its sign: `sqrt(u^4)` // written as `u^2` is the other root wherever u is imaginary. That matters // only where the integrand is real with the radicand negative, which takes @@ -41735,9 +41801,10 @@ private static bool IsAFunctionOfTheTangentAlone(Entity expr, Entity.Variable u) // is imaginary and the identities the factoring used do not hold, so the // answer is conditioned on the radicand, as the linear-radical substitution // conditions its own. https://github.com/asc-community/AngouriMath/issues/718 + // And where the integrand is real with the radicand negative, held beside a second + // root, the answer is kept there where it is measured to hold. if (u is Powf(var radicand, Number.Rational root) && root.ERational.Denominator.IsEven) - return Functions.PartialFractions.Bare(forPositiveU).Substitute(uSub, u) - .Provided(radicand >= Number.Integer.Zero); + return ConditionedOnTheRadicand(Functions.PartialFractions.Bare(forPositiveU).Substitute(uSub, u), expr, radicand, x); if (ExtendedByParity(integrandInU, forPositiveU, uSub) is { } onBothSides) return onBothSides.Substitute(uSub, u); } diff --git a/Sources/Tests/UnitTests/Calculus/AnAnswerBeyondANegativeRadicandIntegralTest.cs b/Sources/Tests/UnitTests/Calculus/AnAnswerBeyondANegativeRadicandIntegralTest.cs new file mode 100644 index 000000000..721849d24 --- /dev/null +++ b/Sources/Tests/UnitTests/Calculus/AnAnswerBeyondANegativeRadicandIntegralTest.cs @@ -0,0 +1,97 @@ +// +// Copyright (c) 2019-2026 Angouri. +// AngouriMath is licensed under MIT. +// Details: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md. +// Website: https://am.angouri.org. +// + +using System; +using AngouriMath.Extensions; +using Xunit; + +namespace AngouriMath.Tests.Calculus +{ + /// + /// Integrands that are real where a radicand is negative, as a product of two imaginary + /// factors, integrated through the root of that radicand taken as a non-negative real. The + /// answers said provided the radicand is not negative, and so said nothing on part of + /// the line where the integrand is real; they hold there as they stand, and the condition is + /// now the integrand's real domain or none. + /// #233 + /// + /// + /// A point where the integrand is real and the answer has no value is a failure here, not a + /// point skipped: skipping it is how a condition false on half the line passed for an answer. + /// + [Trait("Area", "Calculus")] + public sealed class AnAnswerBeyondANegativeRadicandIntegralTest + { + private static void HoldsWhereTheIntegrandIsReal(string integrand, Func pinned, double[] points) + { + var integral = integrand.ToEntity().Integrate("x"); + var text = integral.Stringize(); + Assert.DoesNotContain("integral(", text); + Assert.DoesNotContain("NaN", text); + var derivative = pinned(integral.Substitute("C", 0)).Differentiate("x"); + var original = pinned(integrand.ToEntity()); + foreach (var at in points) + { + var want = original.Substitute("x", at).EvalNumerical(); + Assert.False(want.IsNaN, $"{integrand} has no value at x = {at}"); + Assert.True(Math.Abs((double)want.ImaginaryPart) < 1e-9 * Math.Max(1, Math.Abs((double)want.RealPart)), + $"{integrand} is {want} at x = {at}, which is not a point where it is real"); + 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}"); + Assert.True(Math.Abs((double)(got - want).RealPart) + Math.Abs((double)(got - want).ImaginaryPart) + < 1e-9 * Math.Max(1, Math.Abs((double)want.RealPart)), + $"d/dx of the antiderivative of {integrand} is {got} at x = {at}, where the integrand is {want}"); + } + } + + /// + /// Timofeev's arcsecants by parts: the remainder is answered in w = x^2 through + /// sqrt(w - 1), and the integrands are real for x < -1, for x > 1 and, + /// as the arcsecant and the root are both imaginary, for 0 < x < 1. Two points on + /// each of the three intervals. + /// + [Theory] + [InlineData("asec(x)*sqrt(-1 + x^2)/x^4")] + [InlineData("asec(x)/(-1 + x^2)^(5/2)")] + public void AnArcsecantOnAllThreeIntervals(string integrand) + => HoldsWhereTheIntegrandIsReal(integrand, e => e, new[] { -2.7, -1.3, 0.23, 0.59, 1.3, 2.6 }); + + /// + /// Rubi's 4.3.2.1 and 4.3.3.1, a root of the tangent beside a root of a + b tan(c + d x), through + /// u = sqrt(tan(c + d x)): real where the tangent is positive and where it is below + /// -a/b, two imaginary roots there. With a = 1.3, b = 0.7, + /// c = 0.4, d = 0.9, the tangent is positive for -0.44 < x < 1.30 and + /// below -a/b for 1.30 < x < 1.85 and again a period on. + /// + [Theory] + [InlineData("sqrt(cot(c + d*x))*sqrt(a + b*tan(c + d*x))")] + [InlineData("1/(sqrt(cot(c + d*x))*(a + b*tan(c + d*x))^(3/2))")] + [InlineData("sqrt(a + b*tan(c + d*x))*(A + B*tan(c + d*x))/tan(c + d*x)^(3/2)")] + [InlineData("sqrt(cot(c + d*x))*(a + b*tan(c + d*x))^(5/2)*(A + B*tan(c + d*x))")] + [InlineData("sqrt(cot(c + d*x))*(A + B*tan(c + d*x))/sqrt(a + b*tan(c + d*x))")] + [InlineData("(A + B*tan(c + d*x))/(sqrt(cot(c + d*x))*(a + b*tan(c + d*x))^(3/2))")] + public void ARootOfTheTangentBesideARootOfALinearInIt(string integrand) + => HoldsWhereTheIntegrandIsReal(integrand, + e => e.Substitute("a", 1.3).Substitute("b", 0.7).Substitute("c", 0.4).Substitute("d", 0.9) + .Substitute("A", 1.1).Substitute("B", 0.6), + new[] { 0.3, 0.9, 1.45, 1.7, 5.0 }); + + /// + /// Where the integrand is imaginary the answer still claims nothing: between -a/b + /// and 0 the tangent's root and the linear's are not both imaginary, and there the answer + /// through a real-or-imaginary u is not the integrand's. + /// + [Fact] + public void NothingIsClaimedWhereTheIntegrandIsImaginary() + { + var integral = "sqrt(a + b*tan(c + d*x))*(A + B*tan(c + d*x))/tan(c + d*x)^(3/2)".ToEntity().Integrate("x"); + var pinned = integral.Substitute("C", 0).Substitute("a", 1.3).Substitute("b", 0.7).Substitute("c", 0.4) + .Substitute("d", 0.9).Substitute("A", 1.1).Substitute("B", 0.6); + Assert.True(pinned.Substitute("x", 2.3).EvalNumerical().IsNaN); + } + } +}