diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index ffaca613a..04d327888 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -1686,6 +1686,21 @@ was refused, so each was read as a product with an undeclared variable | `"Chi(1)".Simplify()` | `Chi` | `Chi(1)`, and `0.8378669409802082…` evaluated | | `"Si(x)".Differentiate("x")` | `Si` | `sin(x) / x` | +### A quadratic below a sine or a cosine is split over its two roots + +`sin(x)/(1 + x^2)` was left unintegrated. A quadratic below the bar, beside sines and cosines of a +linear, is its leading coefficient times the linears of its two roots, complex where its +discriminant is negative, and each term of the partial fractions over them is the one-linear +question: `Si` and `Ci` of `x - r`. Where the roots are a conjugate pair so are the two terms, and +their sum is real on the real line +([#1501](https://github.com/asc-community/AngouriMath/issues/1501)). + +| Input | Was (2.5.0) | Now | +|---|---|---| +| `"cos(x)/(x^2-1)".Integrate("x")` | `integral(cos(x) / (x ^ 2 - 1), x)` | `Si` and `Ci` of `x - 1` and of `x + 1` | +| `"sin(x)/(1+x^2)".Integrate("x")` | `integral(sin(x) / (1 + x ^ 2), x)` | `Si` and `Ci` of `x - i` and of `x + i` | +| `"sin(c+d*x)/(a+b*x^2)".Integrate("x")` | `integral(sin(c + d * x) / (a + b * x ^ 2), x)` | `Si` and `Ci` of `d (x ∓ sqrt(-4 a b)/(2 b))` | + ### The sine and cosine integrals are integrated to `sin(x)/x` was left unintegrated. A polynomial in sines and cosines of a linear, beside a diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs index 992904f4d..832f757dd 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs @@ -3947,6 +3947,18 @@ void Add(int k, bool isSine, Entity coefficient) var linears = new List<(Entity Linear, int Power)>(); Entity? polynomial = null; Entity? trigonometric = null; + void AddALinear(Entity linear, int power) + { + var at = linears.FindIndex(pair => pair.Linear == linear); + if (at < 0) + linears.Add((linear, power)); + else + linears[at] = (linear, linears[at].Power + power); + } + // x - r, written x + |r| where the root is a negative number: x + 1 for the root -1. + Entity LessTheRoot(Entity root) => + root is Number.Complex { RealPart.IsNegative: true } or Number.Complex { RealPart.EDecimal.IsZero: true, ImaginaryPart.IsNegative: true } + ? x + (-root).InnerSimplified : (x - root).InnerSimplified; foreach (var (factor, underneath) in FactorsOfTheIntegrand(expr)) { if (!factor.ContainsNode(x)) @@ -3970,13 +3982,23 @@ void Add(int k, bool isSine, Entity coefficient) // A linear, or a whole power of one, below the bar or as a negative power. if (exponent < 0) { - if (!TreeAnalyzer.TryGetPolyLinear(@base, x, out var slopeOfIt, out _) || TreeAnalyzer.IsZero(slopeOfIt)) - return null; - var at = linears.FindIndex(pair => pair.Linear == @base); - if (at < 0) - linears.Add((@base, -exponent)); + if (TreeAnalyzer.TryGetPolyLinear(@base, x, out var slopeOfIt, out _) && !TreeAnalyzer.IsZero(slopeOfIt)) + AddALinear(@base, -exponent); + // A quadratic is its leading coefficient times the linears of its two roots, + // complex where its discriminant is negative: `sin(c + d x)/(a + b x^2)` is a + // sum over `x -/+ sqrt(-a/b)`, each the one-linear question, and the sine and + // cosine integrals of the two conjugate arguments add up to a real answer. + else if (TreeAnalyzer.TryGetPolyQuadratic(@base, x, out var leading, out var middle, out var last) && !TreeAnalyzer.IsZero(leading)) + { + var discriminant = MathS.Sqrt(middle * middle - 4 * leading * last); + var first = ((-middle + discriminant) / (2 * leading)).InnerSimplified; + var second = ((-middle - discriminant) / (2 * leading)).InnerSimplified; + constant /= MathS.Pow(leading, -exponent); + AddALinear(LessTheRoot(first), -exponent); + AddALinear(LessTheRoot(second), -exponent); + } else - linears[at] = (@base, linears[at].Power - exponent); + return null; continue; } if (TreeAnalyzer.TryGetPolynomial(factor, x, out var monomials) && monomials.Keys.All(degree => degree.Sign >= 0 && degree.CompareTo(EInteger.FromInt32(12)) <= 0)) diff --git a/Sources/Tests/UnitTests/Calculus/IntegralAnswerCacheTest.cs b/Sources/Tests/UnitTests/Calculus/IntegralAnswerCacheTest.cs index 0daa68877..cf6556c5a 100644 --- a/Sources/Tests/UnitTests/Calculus/IntegralAnswerCacheTest.cs +++ b/Sources/Tests/UnitTests/Calculus/IntegralAnswerCacheTest.cs @@ -95,20 +95,22 @@ public void AskingTwiceAnswersTheSame(string integrand) } /// - /// An integrand with no elementary antiderivative, which runs every solver to exhaustion - /// and so pays the full cost of the search. It took 115 seconds before the answers - /// were kept, and about one after. + /// An integrand no rule answers, which runs every solver to exhaustion and so pays the + /// full cost of the search: a sine over a cubic, whose roots the sine and cosine integrals + /// would have to be written over. The quadratic sin(x)/(x^2 + 1)^2, which this + /// asked before the sine and cosine integrals were split over a quadratic's two roots, + /// took 115 seconds before the answers were kept, and about one after. /// /// - /// The bound is two orders of magnitude above what it now takes and a quarter of what it - /// used to, so it is a guard against the regression rather than a measurement of the - /// machine. A timing assertion tight enough to be a benchmark would only flake. + /// The bound is two orders of magnitude above what it takes and a quarter of what the + /// quadratic used to, so it is a guard against the regression rather than a measurement of + /// the machine. A timing assertion tight enough to be a benchmark would only flake. /// [Fact] public void AnIntegralWithNoAnswerStillFinishesQuickly() { var watch = Stopwatch.StartNew(); - var answer = "sin(x) / (x ^ 2 + 1) ^ 2".ToEntity().Integrate("x").Simplify(); + var answer = "sin(x) / (x ^ 3 + 1) ^ 2".ToEntity().Integrate("x").Simplify(); watch.Stop(); Assert.Contains("integral(", answer.Stringize()); diff --git a/Sources/Tests/UnitTests/Calculus/RepeatedByPartsTest.cs b/Sources/Tests/UnitTests/Calculus/RepeatedByPartsTest.cs index 7c816f837..db14b7083 100644 --- a/Sources/Tests/UnitTests/Calculus/RepeatedByPartsTest.cs +++ b/Sources/Tests/UnitTests/Calculus/RepeatedByPartsTest.cs @@ -93,8 +93,10 @@ public void ASquaredLogarithmTimesAPolynomial(string integrand) /// /// The measure that bounds the recursion, asserted where it is load-bearing. - /// sin(x)/(x^2 + 1)^2 has no elementary antiderivative, so every solver runs to - /// exhaustion on it and it pays the full cost of the search. + /// sin(x)/(x^3 + 1)^2 has no antiderivative the rules write -- the sine and cosine + /// integrals would need the cubic's roots -- so every solver runs to exhaustion on it and + /// it pays the full cost of the search. The 83 seconds below were the quadratic + /// sin(x)/(x^2 + 1)^2's, which is answered over its two roots now. /// /// /// The bound is a wall that cannot fire on a slow runner — the work is about a second — @@ -107,7 +109,7 @@ public void ASquaredLogarithmTimesAPolynomial(string integrand) public void AnUnanswerableIntegralDoesNotPayForTheRecursion() { var watch = Stopwatch.StartNew(); - var answer = "sin(x) / (x ^ 2 + 1) ^ 2".ToEntity().Integrate("x"); + var answer = "sin(x) / (x ^ 3 + 1) ^ 2".ToEntity().Integrate("x"); watch.Stop(); Assert.Contains("integral(", answer.Stringize()); diff --git a/Sources/Tests/UnitTests/Calculus/TrigonometricIntegralIntegrationTest.cs b/Sources/Tests/UnitTests/Calculus/TrigonometricIntegralIntegrationTest.cs index ae1db9b47..13b7d9dd0 100644 --- a/Sources/Tests/UnitTests/Calculus/TrigonometricIntegralIntegrationTest.cs +++ b/Sources/Tests/UnitTests/Calculus/TrigonometricIntegralIntegrationTest.cs @@ -91,6 +91,22 @@ public void APowerOfASineOrACosineOverAPowerOfALinear(string integrand) public void OverSeveralLinears(string integrand) => DifferentiatesBack(integrand, AroundZero, Pins); + /// + /// A quadratic below the bar is its leading coefficient times the linears of its two + /// roots, complex where its discriminant is negative, and the sine and cosine integrals of + /// the two conjugate arguments add up to a real answer. Rubi's 4.1.11 and 4.7.7, + /// sin(c + d x)/(a + b x^2) and sin(a + b x)/(c + d x + e x^2). + /// + [Theory] + [InlineData("sin(x)/(1 + x^2)")] + [InlineData("cos(x)/(x^2 - 4)")] + [InlineData("x*sin(2*x)/(x^2 + x + 1)")] + [InlineData("sin(c + d*x)/(a + b*x^2)")] + [InlineData("cos(x)/(x*(1 + x^2))")] + [InlineData("sin(x)/(1 + x^2)^2")] + public void OverAQuadratic(string integrand) + => DifferentiatesBack(integrand, AroundZero, Pins); + /// /// An argument a + b x^r beside a power of x is the same question under /// u = x^r, for a whole, a fractional, a negative and a symbolic r. Rubi's diff --git a/Sources/Tests/UnitTests/Calculus/TrigonometricTowerAnsatzTest.cs b/Sources/Tests/UnitTests/Calculus/TrigonometricTowerAnsatzTest.cs index a8571bc71..e9fb37e72 100644 --- a/Sources/Tests/UnitTests/Calculus/TrigonometricTowerAnsatzTest.cs +++ b/Sources/Tests/UnitTests/Calculus/TrigonometricTowerAnsatzTest.cs @@ -96,7 +96,7 @@ public void TheAnswerIsInTheSineAndCosine() [InlineData("tan(x)/x")] [InlineData("x/sin(x)")] [InlineData("x*tan(x)")] - [InlineData("x*sin(x)/(1 + x^2)")] + [InlineData("x*sin(x)/(1 + x^3)")] public void ANonElementaryOneIsDeclined(string integrand) => Assert.Contains("integral(", integrand.ToEntity().Integrate("x").Stringize()); }