diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 954ee66b5..28eb32b99 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -396,6 +396,22 @@ in Rubi's 1.1.3.4 come out shorter as well, and those that took two seconds take | `"1/(x^8*(a + b*x^3)*(c + d*x^3))".ToEntity().Integrate("x")` | `integral(...)` | 1,217 characters | | `"1/(x^6*(a + b*x^4)*(c + d*x^4))".ToEntity().Integrate("x")` | `integral(...)` | 795 characters | +### A sine or a cosine over a symbolic quadratic or binomial is split over its roots with the coefficients named + +**Answers where there were none.** A quadratic or a binomial with a compound coefficient below a sine or a cosine is +split over its roots with each compound coefficient named, and the names are written back in the answer. Carried +through the partial fractions, `d e - c f` was expanded to the seventh degree in every coefficient, and a quadratic +with a double root was split over two equal roots, so its answer was NaN everywhere. A double root is now one linear +twice ([#1877](https://github.com/asc-community/AngouriMath/issues/1877), +[#1879](https://github.com/asc-community/AngouriMath/issues/1879)). + +| Input | Was (2.5.0) | Now | +|---|---|---| +| `"sin(a + b*x)/(x*((d*e - c*f)*x^2 + f))".ToEntity().Integrate("x")` | `integral(...)` | 980 characters | +| `"sin(a + b*x)/((c + d)*x^2 + (e - f)*x + 1)".ToEntity().Integrate("x")` | `integral(...)` | 921 characters | +| `"sin(a + b*x)/((d*e - c*f)*x^3 + f)".ToEntity().Integrate("x")` | `integral(...)` | 1,416 characters | +| `"sin(x)/(a*x^2 + 2*a*x + a)".ToEntity().Integrate("x")` | `integral(...)` | 246 characters | + ### A trigonometric function of `a + b/x` beside a rational function is integrated in `1/x` **Answers where there were none.** `sin(c + d/x)/(e + f x)` was declined: the substitution search reads a candidate's @@ -404,17 +420,14 @@ partial fractions and the sine and cosine integrals answer, and `x = 1/u` is one root of a linear, the linear radical substitution gets there first ([#718](https://github.com/asc-community/AngouriMath/issues/718)). -The answers below a root are large because the sine over the linears that come out of the substitution carries -expanded coefficients ([#1877](https://github.com/asc-community/AngouriMath/issues/1877)). - | Input | Was (2.5.0) | Now | |---|---|---| | `"sin(c + d/x)/(e + f*x)".ToEntity().Integrate("x")` | `integral(...)` | 196 characters | | `"(a + b*sin(c + d/x))/(e + f*x)^2".ToEntity().Integrate("x")` | `integral(...)` | 470 characters | | `"(a + b*sin(c + d/x))^2/(e + f*x)^3".ToEntity().Integrate("x")` | `integral(...)` | 2,516 characters | | `"(c*e + d*e*x)^(1/3)*sin(a + b/(c + d*x)^(1/3))".ToEntity().Integrate("x")` | `integral(...)` | 1,453 characters | -| `"sin(a + b/sqrt(c + d*x))/(e + f*x)".ToEntity().Integrate("x")` | `integral(...)` | 12,010 characters | -| `"sin(a + b/sqrt(c + d*x))/(e + f*x)^2".ToEntity().Integrate("x")` | `integral(...)` | 32,780 characters | +| `"sin(a + b/sqrt(c + d*x))/(e + f*x)".ToEntity().Integrate("x")` | `integral(...)` | 1,114 characters | +| `"sin(a + b/sqrt(c + d*x))/(e + f*x)^2".ToEntity().Integrate("x")` | `integral(...)` | 2,288 characters | ### A power of the cosine beside a secant binomial is integrated in the cosine diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs index 7d0e7556d..d4bf126ee 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs @@ -8103,6 +8103,28 @@ private static (Entity Leading, Entity[] Roots, Entity Monic)? TheRootsOfABinomi /// private static Entity? OverAPowerOfALinear(Entity expr, Entity.Variable x, Entity argument) { + var names = new List<(Variable Name, Entity Coefficient)>(); + Entity Named(Entity coefficient) + { + if (coefficient is not (Sumf or Minusf) || !coefficient.Vars.Any()) + return coefficient; + var name = Variable.CreateUnique(expr + names.Aggregate((Entity)Number.Integer.Zero, (all, pair) => all + pair.Name), "k_quad"); + names.Add((name, coefficient)); + return name; + } + Entity WrittenBack(Entity answer) => names.Aggregate(answer, (all, pair) => all.Substitute(pair.Name, pair.Coefficient)); + // A binomial's two coefficients named the same way: its roots are distinct wherever + // its constant term is not zero, which the reading of its roots requires. + Entity WithItsCoefficientsNamed(Entity binomial) + { + if (!TreeAnalyzer.TryGetPolynomial(binomial, x, out var terms) || terms.Count != 2 + || terms.Values.Any(coefficient => coefficient.ContainsNode(x))) + return binomial; + if (!terms.TryGetValue(EInteger.Zero, out var free) || terms.Values.All(coefficient => coefficient is not (Sumf or Minusf))) + return binomial; + var top = terms.First(term => !term.Key.IsZero); + return Named(free) + Named(top.Value) * MathS.Pow(x, Number.Integer.Create(top.Key)); + } var (sine, cosine) = (MathS.Sin(argument), MathS.Cos(argument)); var (tangent, cotangent, secant, cosecant) = (MathS.Tan(argument), MathS.Cotan(argument), MathS.Sec(argument), MathS.Cosec(argument)); var s = Variable.CreateUnique(expr, "s"); @@ -8161,6 +8183,26 @@ void AddALinear(Entity linear, int power) // 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)) { + // A double root is one linear twice: split over two roots that coincide, the + // partial fractions divided by their difference, and every value was NaN. + // https://github.com/asc-community/AngouriMath/issues/1879 + if (Functions.PartialFractions.IsZeroAsAValue(middle * middle - 4 * leading * last)) + { + var twice = (x + middle / (2 * leading)).InnerSimplified; + constant /= MathS.Pow(leading, -exponent); + AddALinear(twice, -2 * exponent); + written *= MathS.Pow(twice, -2 * exponent); + continue; + } + // A compound coefficient named for the split and written back in the answer: + // `d e - c f` carried through the partial fractions over the roots came out + // expanded to the seventh degree in every coefficient, 11,876 characters for + // `sin(a + b x)/(x ((d e - c f) x^2 + f))` against 575 for `k`. Not where the + // roots coincide, which a name hides, and which are a linear twice above. + // https://github.com/asc-community/AngouriMath/issues/1877 + leading = Named(leading); + middle = Named(middle); + last = Named(last); var discriminant = MathS.Sqrt(middle * middle - 4 * leading * last); var first = ((-middle + discriminant) / (2 * leading)).InnerSimplified; var second = ((-middle - discriminant) / (2 * leading)).InnerSimplified; @@ -8170,7 +8212,7 @@ void AddALinear(Entity linear, int power) written *= MathS.Pow(Monic(leading, middle, last, x), -exponent); } // And a binomial past the quadratic the linears of its roots: `sin(c + d x)/(a + b x^3)`. - else if (TheRootsOfABinomial(@base, x) is { } binomial) + else if (TheRootsOfABinomial(WithItsCoefficientsNamed(@base), x) is { } binomial) { constant /= MathS.Pow(binomial.Leading, -exponent); foreach (var root in binomial.Roots) @@ -8193,7 +8235,7 @@ void AddALinear(Entity linear, int power) return null; if (linears.Count == 1) return AgainstAPowerOfALinear(polynomial, linears[0].Linear, linears[0].Power, angles, argument, x, needsAnIntegral: true) is var (one, _) && one is not null - ? Functions.PartialFractions.Bare((constant * one).InnerSimplified) + ? WrittenBack(Functions.PartialFractions.Bare((constant * one).InnerSimplified)) : null; // Over each linear by partial fractions, and each term the one-linear question. var denominator = linears.Aggregate((Entity)Number.Integer.One, (product, pair) => product * MathS.Pow(pair.Linear, pair.Power)); @@ -8210,7 +8252,7 @@ void AddALinear(Entity linear, int power) sum += integral; special |= integrals; } - return special ? Functions.PartialFractions.Bare((constant * sum).InnerSimplified) : null; + return special ? WrittenBack(Functions.PartialFractions.Bare((constant * sum).InnerSimplified)) : null; } /// diff --git a/Sources/Tests/UnitTests/Calculus/ATrigonometricOverAQuadraticWithCompoundCoefficientsIntegralTest.cs b/Sources/Tests/UnitTests/Calculus/ATrigonometricOverAQuadraticWithCompoundCoefficientsIntegralTest.cs new file mode 100644 index 000000000..6ed4697f5 --- /dev/null +++ b/Sources/Tests/UnitTests/Calculus/ATrigonometricOverAQuadraticWithCompoundCoefficientsIntegralTest.cs @@ -0,0 +1,75 @@ +// +// 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 +{ + /// + /// A sine or a cosine over a quadratic or a binomial with compound coefficients, split over + /// its roots with the coefficients named: sin(a + b x)/(x ((d e - c f) x^2 + f)) was + /// 11,876 characters, every coefficient expanded to the seventh degree; and one with a double + /// root is a linear twice, where split over two equal roots it was NaN everywhere. + /// #1877, + /// #1879 + /// + [Trait("Area", "Calculus")] + public sealed class ATrigonometricOverAQuadraticWithCompoundCoefficientsIntegralTest + { + private static void DifferentiatesBack(string integrand, double[] points, params (string, double)[] pins) + { + var integral = integrand.ToEntity().Integrate("x"); + Assert.DoesNotContain("integral(", integral.Stringize()); + Assert.DoesNotContain("NaN", integral.Stringize()); + + var derivative = integral.Substitute("C", 0).Differentiate("x"); + Entity original = integrand.ToEntity(); + foreach (var (name, value) in pins) + { + derivative = derivative.Substitute(name, value); + original = original.Substitute(name, value); + } + + var compared = 0; + foreach (var at in points) + { + var got = derivative.Substitute("x", at).EvalNumerical(); + var want = original.Substitute("x", at).EvalNumerical(); + if (got.IsNaN || want.IsNaN) + continue; + compared++; + var difference = Math.Abs((double)(got - want).RealPart) + + Math.Abs((double)(got - want).ImaginaryPart); + var scale = Math.Max(1.0, Math.Abs((double)want.RealPart) + Math.Abs((double)want.ImaginaryPart)); + Assert.True(difference / scale < 1e-8, + $"d/dx of the antiderivative of {integrand} is {got} at x = {at}, " + + $"where the integrand is {want}"); + } + Assert.True(compared >= 3, + $"only {compared} of {points.Length} points were comparable for {integrand}"); + } + + [Theory] + [InlineData("sin(a+b*x)/(x*((d*e-c*f)*x^2+f))", 1500)] + [InlineData("x*cos(a+b*x)/((d*e-c*f)*x^2+f)", 1500)] + [InlineData("sin(a+b*x)/((c+d)*x^2+(e-f)*x+1)", 1500)] + [InlineData("sin(a+b*x)/((c+d)*x^2+2*(c+d)*x+(c+d))", 1500)] + [InlineData("sin(x)/(a*x^2+2*a*x+a)", 1500)] + [InlineData("sin(a+b*x)/((d*e-c*f)*x^3+f)", 2000)] + [InlineData("sin(a+b/sqrt(c+d*x))/(e+f*x)", 2000)] + public void IsShortAndDifferentiatesBack(string integrand, int longest) + { + var length = integrand.ToEntity().Integrate("x").Stringize().Length; + Assert.True(length <= longest, $"the antiderivative of {integrand} is {length} characters"); + var points = new[] { -1.4, -0.3, 0.8, 2.25 }; + DifferentiatesBack(integrand, points, ("a", 1.0 / 3), ("b", 2), ("c", 1.5), ("d", 1.25), ("e", 2), ("f", 3)); + DifferentiatesBack(integrand, points, ("a", -0.4), ("b", -1.5), ("c", 2), ("d", -0.5), ("e", -1), ("f", 2)); + } + } +}