diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 57f7237a3..c04b0f2d3 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -351,6 +351,24 @@ rule that does the same for a polynomial under a square root, whose answers are | `"1/sqrt(a*cot(x)^2)".Integrate("x")` | `-sgn(tan(x)) ln(1/sqrt(1 + abs(tan(x))^2))/sqrt(a)` | `sgn(cot(x)) ln(1 + tan(x)^2)/(2 sqrt(a))` | | `"(b*tan(x)^2)^(5/2)".Integrate("x")` | `sgn(tan(x)) b^(5/2) ((abs(tan(x))^2)^2/2 - abs(tan(x))^2 + ln(abs(tan(x))^2 + 1))/2` | the same with `tan(x)` for `abs(tan(x))` | +### A shared symbol among a denominator sum's coefficients comes out in front of it + +**Improvement, not silent.** `tan(x)/(a + a csc(x))` came back unevaluated while +`tan(x)/(1 + csc(x))` was answered: under `u = sin(x)` the denominator is `(a u + a)(1 - u^2)`, +a symbolic linear beside a rational quadratic that shares its root, which the symbolic split +declines, and under the half-angle it is a sextic with `a` in every coefficient, which nothing +factors. A written sum below the bar whose coefficients share a symbol is now that symbol times +a polynomial over the rationals — `(a u + a)` is `a (u + 1)` — before the denominator is +refactored, so the repeated factor is found and the rational machinery answers; and the lowest +power of the variable every term holds comes out the same way. + +| | Was | Is | +|---|---|---| +| `"tan(x)/(a + a*csc(x))".Integrate("x")` | unevaluated | `(-1/(2 (sin(x) + 1)) + 3/4 ln(sin(x) + 1) + 1/4 ln(sin(x) - 1))/(-a)` | +| `"sec(x)^2/(a + a*csc(x))".Integrate("x")` | unevaluated | in `tan(x/2)`, over `a` | +| `"x^2/((a*x + a)*(1 - x^2))".Integrate("x")` | unevaluated | the partial fractions over `(x + 1)^2 (1 - x)`, over `a` | +| `"cot(x)^3/(a + b*csc(x))".Integrate("x")` | unevaluated | `-1/(b sin(x)) - a ln(sin(x))/b^2 + (b^2 - a^2) ln(a sin(x) + b)/(a b^2)`, once `a u^4 + b u^3` is read as `u^3 (a u + b)` | + ### A condition the answer states on its own is no longer repeated beside it **Improvement, not silent.** `Simplify` returned `(1 - ln(x))/x^2 provided not x = 0` for the diff --git a/Sources/.editorconfig b/Sources/.editorconfig index 08ff276bc..77950d116 100644 --- a/Sources/.editorconfig +++ b/Sources/.editorconfig @@ -442,6 +442,9 @@ file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed [Tests/UnitTests/Calculus/CombinedRadicalsTest.cs] file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed under MIT.\nDetails: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.\nWebsite: https://am.angouri.org.\n +[Tests/UnitTests/Calculus/DenominatorContentTest.cs] +file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed under MIT.\nDetails: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.\nWebsite: https://am.angouri.org.\n + [Tests/UnitTests/Calculus/SineOverASymbolicQuadraticTest.cs] file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed under MIT.\nDetails: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.\nWebsite: https://am.angouri.org.\n diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs index 86f9236bd..34cf23b18 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs @@ -621,6 +621,16 @@ is var (multiple, leftover) // under a second this way, with the chain behind it for a spelling this rule does // not answer on its own -- one irreducible factor to the first power is the table's. // Once: the spelling this produces refactors to itself. + // A written sum with a symbol among its coefficients that they all share is that + // symbol times a polynomial over the rationals, and is written so first: `(a u + a)` + // beside `1 - u^2` shares a root with it, which the symbolic split declines, and a + // symbolic sextic is nothing the refactoring below reads, where `a` times a rational + // one is. Once: a primitive factor has no content to take out. + if (WithTheContentOutOfEachSumFactor(denominator, x) is { } primitive + && (SolveByPartialFractions(numerator / primitive, x, integrateByParts) + ?? Integration.ComputeIndefiniteIntegral(numerator / primitive, x, integrateByParts)) is { } overPrimitives) + return overPrimitives; + if (TryWriteInIrreducibleFactors(denominator, x) is { } refactored && (SolveByPartialFractions(numerator / refactored, x, integrateByParts) ?? Integration.ComputeIndefiniteIntegral(numerator / refactored, x, integrateByParts)) is { } overIrreducibles) @@ -7535,6 +7545,79 @@ private static bool IsASumOfMonomials(Entity expr, Entity.Variable x) return true; } + /// + /// with the content of every written sum among its + /// factors taken out in front of it: (a u + a)(1 - u^2) is a (u + 1)(1 - u^2), + /// and -a t^6 - 2a t^5 - a t^4 + a t^2 + 2a t + a is a times the + /// polynomial over the rationals. Null where no factor has one. + /// + /// + /// The content is the gcd of the coefficients as polynomials in the symbols, and a + /// factor whose coefficients share one is a constant times a smaller polynomial: with + /// the constant written inside, (a u + a) was a symbolic linear whose root the + /// factor 1 - u^2 beside it shares, which the symbolic split declines, and the + /// sextic the half-angle makes of tan(x)/(a + a csc(x)) was a polynomial with a + /// symbol in every coefficient, which nothing factors -- where over the rationals it is + /// (t - 1)(t + 1)^3 (t^2 + 1). Rubi's a + a csc(x) and the like are what + /// this is for. + /// https://github.com/asc-community/AngouriMath/issues/718 + /// + private static Entity? WithTheContentOutOfEachSumFactor(Entity denominator, Entity.Variable x) + { + var changed = false; + Entity product = Number.Integer.One; + foreach (var factor in Mulf.LinearChildren(denominator)) + { + // Whole powers only: `(c a + d a x)^(1/3)` is `a^(1/3) (c + d x)^(1/3)` for a + // positive `a` and not for a negative one, and a symbol is neither. + var (@base, power) = factor is Powf(var b, Number.Integer p) ? (b, p) : (factor, Number.Integer.One); + if (@base is not (Sumf or Minusf) || !@base.ContainsNode(x) || !@base.Vars.Any(v => v != x) + || !TreeAnalyzer.TryGetPolynomial(@base, x, out var read) || read.Count < 2 + || read.Keys.Any(degree => degree.Sign < 0) || read.Values.Any(coefficient => coefficient.ContainsNode(x))) + { + product *= factor; + continue; + } + var variables = @base.Vars.OrderBy(v => v.Name, System.StringComparer.Ordinal).ToList(); + if (variables.Count > MultivariatePolynomial.MaxVariables) + { + product *= factor; + continue; + } + var indices = new Dictionary(); + for (var i = 0; i < variables.Count; i++) + indices[variables[i]] = i; + var rest = Enumerable.Range(0, variables.Count).Where(i => variables[i] != x).ToList(); + // The lowest power of x every term holds comes out too: `a u^4 + b u^3` is + // `u^3 (a u + b)`, which is what the sine substitution makes of `cot^3/(a + b csc)`, + // and neither the refactoring over the rationals nor the symbolic split reads + // a symbolic polynomial for the monomial it is a multiple of. + var lowest = read.Keys.Min()!; + if (MultivariatePolynomial.TryParse(@base, indices) is not { } polynomial + || Functions.PolynomialGcd.ContentIn(polynomial, indices[x], rest, 0) is not { } content + || content.IsConstant && lowest.IsZero + || polynomial.DivideExact(content) is not { } primitive) + { + product *= factor; + continue; + } + var monomial = lowest.IsZero ? Number.Integer.One : lowest.Equals(EInteger.One) ? x : MathS.Pow(x, Number.Integer.Create(lowest)); + var primitivePart = primitive.ToEntity(variables); + if (!lowest.IsZero && TreeAnalyzer.PolynomialLongDivision(primitivePart, monomial, genericCase: true, inTermsOf: x) is var (divided, remainder) + && (remainder is Divf(var top, _) ? top : remainder).Evaled is Number.Complex { IsZero: true }) + primitivePart = divided.InnerSimplified; + else if (!lowest.IsZero) + { + product *= factor; + continue; + } + Entity constantPart = content.IsConstant ? monomial : lowest.IsZero ? content.ToEntity(variables) : content.ToEntity(variables) * monomial; + product *= power == Number.Integer.One ? constantPart * primitivePart : MathS.Pow(constantPart, power) * MathS.Pow(primitivePart, power); + changed = true; + } + return changed ? product : null; + } + private static Entity? TryWriteInIrreducibleFactors(Entity denominator, Entity.Variable x) { // Only where the written bases, each taken once, share a factor among them or diff --git a/Sources/Tests/UnitTests/Calculus/DenominatorContentTest.cs b/Sources/Tests/UnitTests/Calculus/DenominatorContentTest.cs new file mode 100644 index 000000000..27d1e3884 --- /dev/null +++ b/Sources/Tests/UnitTests/Calculus/DenominatorContentTest.cs @@ -0,0 +1,64 @@ +// +// 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 written sum below the bar whose coefficients share a symbol is that symbol times a + /// polynomial over the rationals, and is read so: (a u + a)(1 - u^2) is + /// a (u + 1)(1 - u^2), whose repeated factor the refactoring over the rationals then + /// finds; and the lowest power of the variable every term holds comes out the same way, + /// a u^4 + 2 u^3 being u^3 (a u + 2). Rubi's tan(x)/(a + a csc(x)) and + /// cot(x)^3/(a + b csc(x)), declined for the shared root under u = sin(x), for + /// a symbolic sextic under the half-angle, and for a symbolic quartic that is a multiple of + /// u^3. + /// #718 + /// + [Trait("Area", "Calculus")] + public sealed class DenominatorContentTest + { + private static readonly double[] Points = { 0.4, 1.1, 2.3, 3.6, 5.2 }; + + private static void DifferentiatesBack(string integrand, double constant) + { + var integral = integrand.ToEntity().Integrate("x"); + Assert.DoesNotContain("integral(", integral.Stringize()); + Assert.DoesNotContain("NaN", integral.Stringize()); + + var derivative = integral.Substitute("C", 0).Substitute("a", constant).Differentiate("x"); + var original = integrand.ToEntity().Substitute("a", constant); + foreach (var at in Points) + { + var got = derivative.Substitute("x", at).EvalNumerical(); + var want = original.Substitute("x", at).EvalNumerical(); + 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} at a = {constant} is {got} at x = {at}, " + + $"where the integrand is {want}"); + } + } + + [Theory] + [InlineData("tan(x)/(a + a*csc(x))")] + [InlineData("sec(x)^2/(a + a*csc(x))")] + [InlineData("x^2/((a*x + a)*(1 - x^2))")] + [InlineData("1/((2*a*x + 4*a)*(x + 2)^2)")] + [InlineData("cot(x)^3/(2 + a*csc(x))")] + [InlineData("(1 - x^2)/(a*x^4 + 2*x^3)")] + public void TheContentComesOutOfTheSum(string integrand) + { + DifferentiatesBack(integrand, 2); + DifferentiatesBack(integrand, -3); + } + } +}