From ea153700f565d9733a78df108f07a2eb063d1f80 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Tue, 29 Sep 2026 04:48:29 +0000 Subject: [PATCH] A linear over a quadratic beside the root of another, in closed form (g + h x)/(A sqrt(B)) with two different quadratics was declined wherever a coefficient was a symbol: the rational function beside a root was taken apart over linears only, and Euler's substitution leaves a quartic in a field the rational integrator does not stay in. For L = lambda + mu x, u = L/sqrt(B) turns N/(A sqrt(B)) into 2 rho/(alpha + beta u^2) where alpha B + beta L^2 = rho A, and that holds for the two roots of E lambda^2 - 2 P lambda mu + F mu^2 = 0, P = A2 B0 - A0 B2, E = A2 B1 - A1 B2, F = A1 B0 - A0 B1. Their two numerators span every linear, so the answer is two arctangents of a linear over the root with sqrt(P^2 - E F) in their coefficients, Rubi's q for 1.2.1.6, and a hyperbolic arctangent where the ratio under a piece's root is a negative number. The rational function beside the root is taken apart over a quadratic, a power of a linear and a power of the radicand as well, and each piece is closed: over a power of a linear by its reciprocal, over a power of the radicand as one half-odd power. Powers of linears only beside a quadratic: over squared linears alone the reciprocal writes a sign of each, sgn(sqrt(1 + x) - 1) under u = sqrt(1 + x), whose derivative is not read, and Euler's substitution answers those without one. Measured with work/intbench against a8a2b7e7, 3 s a problem: Rubi's 1.2.1.6, 4.3.9 and 4.4.9 in full, 66 -> 201 of 217 solved, none lost and none wrong, every gain made by this alone; and 12 more gained over the families sample (912 problems) and family 0 (1814), none lost. Part of #718. Co-Authored-By: Claude Opus 5.5 --- BREAKING-CHANGES.md | 21 ++ .../Integration/IndefiniteIntegralSolver.cs | 297 +++++++++++++++++- .../Integration/Integration.Definition.cs | 3 + .../MixedTrigonometricArgumentsTest.cs | 11 +- .../QuadraticBesideARootIntegralTest.cs | 99 ++++++ 5 files changed, 418 insertions(+), 13 deletions(-) create mode 100644 Sources/Tests/UnitTests/Calculus/QuadraticBesideARootIntegralTest.cs diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 1877c36da..28db39331 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -329,6 +329,27 @@ poles of `tan(x/2)`, as every half-angle answer. | `"csc(x)^2/(a+b*sin(x)+c*sin(x)^2)".Integrate("x")`, `"sec(x)^2/…"` | unevaluated | `-cot(x)/a + …`, the blocks over `sin^2` and `1 - sin^2` beside the two roots | | `"1/(a+b*cos(x)+c*cos(x)^2)".Integrate("x")` | unevaluated | the cosine form | +### A linear over a quadratic beside the root of another quadratic is integrated + +`(g + h x)/(A sqrt(B))`, `A` and `B` two different quadratics, was left unevaluated wherever a +coefficient was a symbol, and so was what the tangent substitution makes of Rubi's +`trig^m (a + b tan + c tan^2)^(p/2)`: `1/sqrt(a + b tan(x) + c tan(x)^2)` is +`1/((1 + t^2) sqrt(a + b t + c t^2))`. It is closed now, as two arctangents of a linear over +`sqrt(B)` whose coefficients hold `sqrt(P^2 - E F)`, `P = A2 B0 - A0 B2`, `E = A2 B1 - A1 B2` and +`F = A1 B0 - A0 B1` -- the root Rubi's 1.2.1.6 calls `q`; where the ratio under a piece's root is a +negative number the piece is written as the hyperbolic arctangent it is. A rational function +beside the root with such a quadratic, a power of a linear or a power of `B` below the bar is +taken apart into pieces each closed the same way +([#718](https://github.com/asc-community/AngouriMath/issues/718)). + +| Input | Was (2.5.0) | Now | +|---|---|---| +| `"1/sqrt(a + b*tan(x) + c*tan(x)^2)".ToEntity().Integrate("x")` | `integral(...)` | two arctangents of a linear in `tan(x)` over the root | +| `"(g + h*x)/((d + k*x + f*x^2)*sqrt(a + b*x + c*x^2))".ToEntity().Integrate("x")` | `integral(...)` | two arctangents of a linear over the root | +| `"cot(x)^3/(a + b*tan(x) + c*tan(x)^2)^(3/2)".ToEntity().Integrate("x")` | `integral(...)` | an antiderivative | +| `"(2 + x)/((2 + 4*x - 3*x^2)*(1 + 3*x + 2*x^2)^(3/2))".ToEntity().Integrate("x")` | `integral(...)` | an antiderivative | +| `"1/((x^2 + 1)*sqrt(x^2 + x + 1))".ToEntity().Integrate("x")` | `integral(...)` | a logarithm and an arctangent | + ### A function comes out of a fractional power of its even power with its sign **Improvement, not silent.** The entry two above made `(sin(x)^2)^(3/2)` the modulus `|sin(x)|^3` diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs index cfbedaece..e0c4e94cb 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs @@ -10776,13 +10776,223 @@ Dictionary PowerOfQ(int j) return answer.Nodes.Any(node => node is Number.Complex { IsNaN: true }) ? null : answer; } + /// + /// A linear over a quadratic beside the square root of another quadratic, + /// (g + h x)/(A sqrt(B)), in closed form, symbols in the coefficients included: + /// two arctangents of L/sqrt(B), each for a linear L for which A is + /// a sum of multiples of B and L^2. + /// + /// + /// + /// For L = lambda + mu x, u = L/sqrt(B) has du = N dx/(2 B^(3/2)) + /// with N = (2 mu B0 - lambda B1) + (mu B1 - 2 lambda B2) x, and + /// alpha + beta u^2 is (alpha B + beta L^2)/B. Where + /// alpha B + beta L^2 = rho A, then, N dx/(A sqrt(B)) is + /// 2 rho du/(alpha + beta u^2). The three equations that says, one for each power + /// of x, have a solution where their determinant is zero, which is a quadratic in + /// lambda : mu, E lambda^2 - 2 P lambda mu + F mu^2 with + /// P = A2 B0 - A0 B2, E = A2 B1 - A1 B2 and F = A1 B0 - A0 B1. Its two + /// roots give two such numerators, independent unless q = sqrt(P^2 - E F) is zero + /// or B is a square, and g + h x is a sum of the two. + /// + /// + /// Each piece is 2 rho arctan(k u)/(alpha k) with k = sqrt(beta/alpha). Its + /// derivative is 2 rho/(alpha + alpha k^2 u^2), and alpha k^2 is beta + /// on either branch of the root, so the piece holds where beta/alpha is negative + /// too, as the hyperbolic arctangent it is there. + /// + /// + /// Rubi's 1.2.1.6 closes these the same way, and its 4.3.9 and 4.4.9 come to them under + /// the tangent: 1/sqrt(a + b tan(x) + c tan(x)^2) is + /// 1/((1 + t^2) sqrt(a + b t + c t^2)), for which q is + /// sqrt((a - c)^2 + b^2). That is the nested surd the rotation of the tangent + /// takes only as a number, since the rotation hands the rational integrator a quotient + /// over it; here it is one root in a closed form. + /// https://github.com/asc-community/AngouriMath/issues/718 + /// + /// + internal static Entity? SolveALinearOverAQuadraticBesideTheRootOfAnother(Entity expr, Entity.Variable x) + { + var (numerator, denominator) = Functions.SingleQuotient.Of(expr); + Entity? radicand = null; + Entity? quadratic = null; + Entity constant = Number.Integer.One; + foreach (var factor in Mulf.LinearChildren(denominator)) + { + if (!factor.ContainsNode(x)) + { + constant = constant * factor; + continue; + } + if (factor is Powf(var @base, Number.Rational half) && half == Number.Rational.Create(1, 2) && radicand is null) + radicand = @base; + // A power of a linear is a repeated linear, the reciprocal substitution's. + else if (quadratic is null && factor is not Powf) + quadratic = factor; + else + return null; + } + if (radicand is null || quadratic is null + || !TreeAnalyzer.TryGetPolyQuadratic(quadratic, x, out var a2, out var a1, out var a0) + || !TreeAnalyzer.TryGetPolyQuadratic(radicand, x, out var b2, out var b1, out var b0) + || !TreeAnalyzer.TryGetPolyLinear(numerator, x, out var h, out var g) + || VanishesIdentically(a2) || VanishesIdentically(b2)) + return null; + // A coefficient that is a number is a real one, as in every rule about a real root. + foreach (var coefficient in new[] { a0, a1, a2, b0, b1, b2, g, h, constant }) + if (coefficient.Evaled is Number.Complex and not Number.Real) + return null; + // Two distinct roots below the bar, and none repeated under the root. + if (VanishesIdentically(a1 * a1 - 4 * a0 * a2) || VanishesIdentically(b1 * b1 - 4 * b0 * b2)) + return null; + + var p = (a2 * b0 - a0 * b2).InnerSimplified; + var e = (a2 * b1 - a1 * b2).InnerSimplified; + var f = (a1 * b0 - a0 * b1).InnerSimplified; + // Zero where the two quadratics are proportional, which is one power of one base. + // Expanded, so that the root reads as Rubi's does, sqrt(a^2 - 2 a c + b^2 + c^2) for + // 1 + t^2 beside a + b t + c t^2, and not with `- b (-b)` in it. + var qSquared = (p * p - e * f).Expand().InnerSimplified; + if (VanishesIdentically(qSquared)) + return null; + var q = MathS.Sqrt(qSquared).InnerSimplified; + var roots = !VanishesIdentically(e) ? new[] { (Lambda: p + q, Mu: e), (Lambda: p - q, Mu: e) } + : !VanishesIdentically(f) ? new[] { (Lambda: f, Mu: p + q), (Lambda: f, Mu: p - q) } + // Both even, and the substitutions are u = 1/sqrt(B) and u = x/sqrt(B). + : new[] { (Lambda: (Entity)Number.Integer.One, Mu: (Entity)Number.Integer.Zero), (Lambda: (Entity)Number.Integer.Zero, Mu: (Entity)Number.Integer.One) }; + + var root = MathS.Sqrt(radicand); + var numerators = new (Entity Constant, Entity Slope)[2]; + var pieces = new Entity[2]; + for (var i = 0; i < 2; i++) + { + var (lambda, mu) = roots[i]; + numerators[i] = ((2 * mu * b0 - lambda * b1).InnerSimplified, (mu * b1 - 2 * lambda * b2).InnerSimplified); + // alpha B + beta L^2 = rho A, from the first two of its three equations that are + // independent. + (Entity Alpha, Entity Beta, Entity Rho)? found = null; + foreach (var (alpha, beta, rho) in new[] + { + (a0 * mu * mu - a2 * lambda * lambda, p, b0 * mu * mu - b2 * lambda * lambda), + (a1 * lambda * lambda - 2 * a0 * lambda * mu, a0 * b1 - a1 * b0, b1 * lambda * lambda - 2 * b0 * lambda * mu), + (2 * a2 * lambda * mu - a1 * mu * mu, a1 * b2 - a2 * b1, 2 * b2 * lambda * mu - b1 * mu * mu), + }) + if (!VanishesIdentically(alpha) && !VanishesIdentically(beta) && !VanishesIdentically(rho)) + { + found = (alpha.InnerSimplified, beta.InnerSimplified, rho.InnerSimplified); + break; + } + if (found is not { } triple) + return null; + var u = (lambda + mu * x) / root; + // Where beta/alpha is a negative number, the hyperbolic arctangent it is, written + // so: arctan(i kappa u)/(i kappa) is artanh(kappa u)/kappa. + var ratio = (triple.Beta / triple.Alpha).InnerSimplified; + if (ratio.Evaled is Number.Real { IsNegative: true }) + { + var kappa = MathS.Sqrt((-ratio).InnerSimplified).InnerSimplified; + pieces[i] = 2 * triple.Rho * MathS.Hyperbolic.Artanh(kappa * u) / (triple.Alpha * kappa); + } + else + { + var k = MathS.Sqrt(ratio).InnerSimplified; + pieces[i] = 2 * triple.Rho * MathS.Arctan(k * u) / (triple.Alpha * k); + } + } + // g + h x as a sum of the two numerators; the determinant is 2 q E disc(B) up to its + // sign, or disc(B) where both are even, none of them zero here. + var ((c0, s0), (c1, s1)) = (numerators[0], numerators[1]); + var determinant = c0 * s1 - c1 * s0; + Entity answer = Number.Integer.Zero; + foreach (var (share, piece) in new[] { ((g * s1 - c1 * h) / determinant, pieces[0]), ((c0 * h - g * s0) / determinant, pieces[1]) }) + { + var simplified = share.InnerSimplified; + if (simplified.Evaled is not Number.Complex { IsZero: true }) + answer = answer + simplified * piece; + } + answer = (answer / constant).InnerSimplified; + if (answer.Nodes.Any(node => node is Number.Complex { IsNaN: true }) + || !Functions.PartialFractions.DerivativeHoldsAtSampledPoints(answer, expr, x)) + return null; + return answer; + } + + /// + /// A polynomial over a power of a linear beside the square root of a quadratic, + /// P/((x - p)^k sqrt(Q)) with P of degree below k, by the reciprocal + /// of the linear as takes it for a + /// constant over the first power: under t = 1/(x - p) it is + /// -sgn(t) P(p + 1/t) t^(k - 1)/sqrt(R(t)) with + /// R(t) = Q(p) t^2 + Q'(p) t + a, a polynomial over the root of a quadratic, which + /// the reduction for those closes. The pieces the partial fractions leave over + /// t^3 when cot(x)^3 sqrt(a + b tan(x) + c tan(x)^2) is taken under the + /// tangent. + /// https://github.com/asc-community/AngouriMath/issues/718 + /// + private static Entity? SolveAPolynomialOverAPowerOfALinearBesideTheRoot(Entity expr, Entity.Variable x) + { + var (numerator, denominator) = Functions.SingleQuotient.Of(expr); + Entity? radicand = null; + Entity? linear = null; + var power = 0; + Entity constant = Number.Integer.One; + foreach (var factor in Mulf.LinearChildren(denominator)) + { + if (!factor.ContainsNode(x)) + { + constant = constant * factor; + continue; + } + if (factor is Powf(var @base, Number.Rational half) && half == Number.Rational.Create(1, 2) && radicand is null) + radicand = @base; + else if (linear is null && factor is Powf(var repeated, Number.Integer { EInteger.Sign: > 0 } whole) && whole.EInteger.CanFitInInt32()) + (linear, power) = (repeated, whole.EInteger.ToInt32Unchecked()); + else + return null; + } + if (radicand is null || linear is null + || !TreeAnalyzer.TryGetPolyLinear(linear, x, out var m, out var n) || VanishesIdentically(m) + || !TreeAnalyzer.TryGetPolyQuadratic(radicand, x, out var a, out var b, out var c) || VanishesIdentically(a) + || !TreeAnalyzer.TryGetPolynomial(numerator, x, out var read) + || read.Keys.Any(degree => degree.Sign < 0 || degree.CompareTo(EInteger.FromInt32(power)) >= 0)) + return null; + foreach (var coefficient in new[] { a, b, c, m, n, constant }.Concat(read.Values)) + if (coefficient.Evaled is Number.Complex and not Number.Real) + return null; + var p = (-n / m).InnerSimplified; + var atP = (a * p * p + b * p + c).InnerSimplified; + // A root of the radicand leaves a linear under the root, which is not this. + if (VanishesIdentically(atP) + || Functions.PartialFractions.TaylorCoefficientsAtTheRoot(numerator, p, power, x) is not { } taylor) + return null; + var slopeAtP = (2 * a * p + b).InnerSimplified; + var t = Variable.CreateUnique(expr, "t_recip"); + // P(p + 1/t) t^(k - 1) is the sum of the Taylor coefficients times t^(k - 1 - j), and + // (x - p)^k is L^k/m^k. + Entity polynomialInT = Number.Integer.Zero; + for (var j = 0; j < power; j++) + if (taylor[j].Evaled is not Number.Complex { IsZero: true }) + polynomialInT += j == power - 1 ? taylor[j] : taylor[j] * MathS.Pow(t, Number.Integer.Create(power - 1 - j)); + var inT = (polynomialInT / (constant * MathS.Pow(m, Number.Integer.Create(power)))).InnerSimplified + * MathS.Pow(atP * MathS.Sqr(t) + slopeAtP * t + a, Number.Rational.Create(-1, 2)); + if (SolveAPolynomialTimesAnOddHalfPowerOfAQuadratic(inT, t) is not { } g) + return null; + var answer = (-MathS.Signum(x - p) * g.Substitute(t, 1 / (x - p))).InnerSimplified; + if (answer.Nodes.Any(node => node is Number.Complex { IsNaN: true }) + || !Functions.PartialFractions.DerivativeHoldsAtSampledPoints(answer, expr, x)) + return null; + return answer; + } + /// /// A polynomial over a product of distinct linears, beside the square root of a /// quadratic above or below the bar, taken apart as the rational function it is over /// that root: N sqrt(Q)/D is N Q/(D sqrt(Q)), and P/D is a polynomial /// plus a constant over each linear, so the integrand is a polynomial over the root /// plus one K/((x - p) sqrt(Q)) per linear -- the rule before this one's shape, - /// each. + /// each. And with a quadratic below the bar, a power of a linear or a power of the root's + /// own quadratic, a linear over the quadratic or a polynomial over the power for each, + /// every one of them closed beside the root. /// /// /// @@ -10857,6 +11067,15 @@ Dictionary PowerOfQ(int j) // numeric, and as written otherwise. var factors = new List(); Entity constant = Number.Integer.One; + // Or with a quadratic among them, a power of a linear, or a power of the radicand: + // the partial fractions split over those as well, and each piece is closed beside + // the root -- over a quadratic by the rule before this one, over a power of a linear + // by its reciprocal, over a power of the radicand by the reduction for a polynomial + // beside a half-odd power. `tan(x)^5 sqrt(a + b tan(x) + c tan(x)^2)` is + // `t^5 sqrt(a + b t + c t^2)/(1 + t^2)` under the tangent, a polynomial over the root + // and a linear over `1 + t^2` beside it. + var beyondTheLinears = false; + var aQuadratic = false; var written = Functions.PolynomialFactoring.TryFactor(below, x, out var factored) && factored is not null ? factored : below; foreach (var factor in Mulf.LinearChildren(written)) { @@ -10865,16 +11084,36 @@ Dictionary PowerOfQ(int j) constant = constant * factor; continue; } - if (!TreeAnalyzer.TryGetPolyLinear(factor, x, out var slope, out _) || slope.Evaled is Number.Complex { IsZero: true }) + if (TreeAnalyzer.TryGetPolyLinear(factor, x, out var slope, out _) && slope.Evaled is not Number.Complex { IsZero: true }) + { + factors.Add(factor); + continue; + } + var isAPowerOfALinear = factor is Powf(var repeated, Number.Integer { EInteger.Sign: > 0 }) + && TreeAnalyzer.TryGetPolyLinear(repeated, x, out var repeatedSlope, out _) && repeatedSlope.Evaled is not Number.Complex { IsZero: true }; + var isAPowerOfTheRadicand = factor is Powf(var raised, Number.Integer { EInteger.Sign: > 0 }) && raised == radicand; + var isAQuadratic = factor is not Powf && TreeAnalyzer.TryGetPolyQuadratic(factor, x, out var leading, out _, out _) && !VanishesIdentically(leading); + if (!isAPowerOfALinear && !isAPowerOfTheRadicand && !isAQuadratic) return null; factors.Add(factor); + beyondTheLinears = true; + aQuadratic |= isAQuadratic || isAPowerOfTheRadicand; } + // Powers of linears only beside a quadratic, which is what the split is for: over + // the linears alone the reciprocal writes a sign of each, `sgn(sqrt(1 + x) - 1)` once + // `sqrt(x + sqrt(1 + x))/x^2` is taken back from `u = sqrt(1 + x)`, whose derivative + // is not read where the argument is not shown real -- and Euler's substitution, after + // this, answers those without one. + if (beyondTheLinears && !aQuadratic) + return null; // Distinct: two factors with the same root are one repeated, which is not this. - for (var i = 0; i < factors.Count; i++) - for (var j = i + 1; j < factors.Count; j++) - if (!TryReadAsQuotient(factors[i] / factors[j], out var top, out var bottom) - || Functions.PolynomialGcd.TryCancel(top, bottom, out _)) - return null; + // Beyond the linears the split decides that, and merges linears with one root. + if (!beyondTheLinears) + for (var i = 0; i < factors.Count; i++) + for (var j = i + 1; j < factors.Count; j++) + if (!TryReadAsQuotient(factors[i] / factors[j], out var top, out var bottom) + || Functions.PolynomialGcd.TryCancel(top, bottom, out _)) + return null; var linears = factors.Aggregate(Number.Integer.One as Entity, (product, factor) => product * factor); // P/D as a polynomial plus a proper part, the proper part over each linear. @@ -10915,11 +11154,25 @@ Dictionary PowerOfQ(int j) var root = MathS.Pow(radicand, Number.Rational.Create(1, 2)); Entity answer = Number.Integer.Zero; - foreach (var piece in pieces) + // Beyond the linears the pieces with a denominator first, so that one no rule closes + // declines the whole before the chain is asked for the polynomial part. + var inOrder = beyondTheLinears + ? pieces.OrderBy(piece => piece.ContainsNode(x) && !TreeAnalyzer.TryGetPolynomial(piece, x, out _) ? 0 : 1).ToList() + : pieces; + foreach (var piece in inOrder) { // Written as `coefficient / sqrt(Q)`, the shape the table reads for a constant. var overTheRoot = (piece / constant).InnerSimplified / root; + // Beyond the linears each piece is closed where it can be, before the chain is + // asked for a polynomial part as it always was: the pieces are asked a level + // below a substitution already, where the scoped rules decline. var integrated = SolveALinearBesideTheRootOfAQuadratic(overTheRoot, x) + ?? (beyondTheLinears + ? SolveALinearOverAQuadraticBesideTheRootOfAnother(overTheRoot, x) + ?? SolveAPolynomialOverAPowerOfALinearBesideTheRoot(overTheRoot, x) + ?? SolveAPolynomialTimesAnOddHalfPowerOfAQuadratic( + OverAPowerOfTheRadicand((piece / constant).InnerSimplified, radicand, x) ?? overTheRoot, x) + : null) ?? (piece.ContainsNode(x) && !TreeAnalyzer.TryGetPolynomial(piece, x, out _) ? null : Integration.ComputeIndefiniteIntegral(overTheRoot, x, integrateByParts: false)); if (integrated is null) return null; @@ -10929,6 +11182,34 @@ Dictionary PowerOfQ(int j) return answer.Nodes.Any(node => node is Number.Complex { IsNaN: true }) ? null : answer; } + /// + /// A piece over a whole power of , beside its root, as the one + /// half-odd power the two are: (g + h x)/(Q^j sqrt(Q)) is + /// (g + h x) Q^(-j - 1/2), which the reduction for a polynomial beside a half-odd + /// power reads and the two factors written apart it does not; null where the piece is + /// over anything else. + /// + private static Entity? OverAPowerOfTheRadicand(Entity piece, Entity radicand, Entity.Variable x) + { + var (top, bottom) = Functions.SingleQuotient.Of(piece); + var power = 0; + Entity rest = Number.Integer.One; + var written = radicand.InnerSimplified; + foreach (var factor in Mulf.LinearChildren(bottom)) + { + if (!factor.ContainsNode(x)) + rest = rest * factor; + else if (factor == radicand || factor == written) + power += 1; + else if (factor is Powf(var @base, Number.Integer { EInteger.Sign: > 0 } whole) && (@base == radicand || @base == written) + && whole.EInteger.CanFitInInt32()) + power += whole.EInteger.ToInt32Unchecked(); + else + return null; + } + return power == 0 ? null : top / rest * MathS.Pow(radicand, Number.Rational.Create(-(2 * power + 1), 2)); + } + /// /// The product with a whole power of a constant multiple of /// written as that constant's power times the power of diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs b/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs index 87701269e..2bb3468d7 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs @@ -913,6 +913,9 @@ private static Entity Normalized(Entity expr, Entity.Variable x) => // remainder: `e^x (1 - x - x^2)/sqrt(1 - x^2)` is `(e^x sqrt(1 - x^2))'`. if ((answer = IndefiniteIntegralSolver.SolveAnExponentialTimesAnOddHalfPowerOfAQuadratic(expr, x)) is { }) return answer; if ((answer = IndefiniteIntegralSolver.SolveALinearBesideTheRootOfAQuadratic(expr, x)) is { }) return answer; + // And over a quadratic, closed as well: beside the linear's, since each of the two is + // a piece the rational function below is taken apart into. + if ((answer = IndefiniteIntegralSolver.SolveALinearOverAQuadraticBesideTheRootOfAnother(expr, x)) is { }) return answer; if ((answer = IndefiniteIntegralSolver.SolveARationalFunctionBesideTheRootOfAQuadratic(expr, x)) is { }) return answer; // A linear below the bar that divides the radicand, written over it: after the two // rules above, which read a linear beside the root as a pole and find nothing to take diff --git a/Sources/Tests/UnitTests/Calculus/MixedTrigonometricArgumentsTest.cs b/Sources/Tests/UnitTests/Calculus/MixedTrigonometricArgumentsTest.cs index 0a334d6ae..e7ebb08ff 100644 --- a/Sources/Tests/UnitTests/Calculus/MixedTrigonometricArgumentsTest.cs +++ b/Sources/Tests/UnitTests/Calculus/MixedTrigonometricArgumentsTest.cs @@ -208,17 +208,18 @@ public void AQuadraticInTheTangentIsRotatedUntilItHasNoLinearTerm(string integra /// /// With symbolic coefficients the rotation is a nested surd, the rotated quadratic is /// written in it, and what the rational integrator is handed is a quotient over that - /// field: two minutes and no answer, where the integrand is declined in a moment - /// unrotated. The rule asks for a numeric rotation, and this pins that the verdict for - /// the symbolic shape is still a quick decline rather than a search. + /// field: two minutes and no answer. The rule asks for a numeric rotation, and the + /// symbolic shape is answered instead under the tangent, in closed form, by the rule for + /// a linear over a quadratic beside the root of another; this pins that it is answered, + /// and not by a search. /// [Fact] - public void ASymbolicQuadraticInTheTangentIsDeclinedAndNotSearched() + public void ASymbolicQuadraticInTheTangentIsAnsweredAndNotSearched() { var watch = System.Diagnostics.Stopwatch.StartNew(); var integral = "1/sqrt(a + b*tan(x) + c*tan(x)^2)".ToEntity().Integrate("x"); watch.Stop(); - Assert.Contains("integral(", integral.Stringize()); + Assert.DoesNotContain("integral(", integral.Stringize()); Assert.True(watch.Elapsed < IntegrationDecline.Guard, $"the symbolic shape took {watch.Elapsed.TotalSeconds:F1} s"); } } diff --git a/Sources/Tests/UnitTests/Calculus/QuadraticBesideARootIntegralTest.cs b/Sources/Tests/UnitTests/Calculus/QuadraticBesideARootIntegralTest.cs new file mode 100644 index 000000000..842e4a9e6 --- /dev/null +++ b/Sources/Tests/UnitTests/Calculus/QuadraticBesideARootIntegralTest.cs @@ -0,0 +1,99 @@ +// +// 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 rational function beside the square root of a quadratic with another quadratic below + /// the bar: (g + h x)/(A sqrt(B)) in closed form, and what the partial fractions take + /// apart into it. Rubi's 1.2.1.6, and its 4.3.9 and 4.4.9, which the tangent brings to it + /// with A = 1 + t^2, all with symbols for coefficients, which is what the rotation of + /// the tangent could not take. + /// #718 + /// + /// + /// Checked by differentiating back with the symbols pinned, at points where the integrand is + /// real, never against a printed form: the answers are arctangents of a linear over the root + /// with a nested root in their coefficients, whose shape says nothing about whether they + /// differentiate back. + /// + [Trait("Area", "Calculus")] + public sealed class QuadraticBesideARootIntegralTest + { + private static readonly (string, double)[] Pins = + { ("a", 2), ("b", 1), ("c", 3), ("d", 5), ("k", 1), ("f", 2), ("g", 0.7), ("h", 1.3) }; + + private static void DifferentiatesBackPinned(string integrand, double[] points, params (string, double)[] pins) + { + var integral = integrand.ToEntity().Integrate("x"); + Assert.DoesNotContain("integral(", 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-9, + $"d/dx of the antiderivative of {integrand} is {got} at x = {at}, " + + $"where the integrand is {want}"); + } + Assert.True(compared >= 3, $"only {compared} points could be compared for {integrand}"); + } + + /// + /// A linear over a quadratic beside the root of another: two arctangents of a linear + /// over the root, whose coefficients hold sqrt(P^2 - E F). + /// + [Theory] + [InlineData("1/((1 + x^2)*sqrt(a + b*x + c*x^2))")] + [InlineData("x/((1 + x^2)*sqrt(a + b*x + c*x^2))")] + [InlineData("(g + h*x)/((d + k*x + f*x^2)*sqrt(a + b*x + c*x^2))")] + [InlineData("(g + h*x)/((d - f*x^2)*sqrt(a + b*x + c*x^2))")] + public void ALinearOverAQuadratic(string integrand) + => DifferentiatesBackPinned(integrand, new[] { 0.3, 0.7, 1.2, -0.5 }, Pins); + + /// + /// Under the tangent: 1/sqrt(a + b tan(x) + c tan(x)^2) is + /// 1/((1 + t^2) sqrt(a + b t + c t^2)), and the powers of the tangent and the + /// cotangent beside it leave a polynomial part, a power of t below the bar, or a + /// power of the root's own quadratic, each closed beside the root. + /// + [Theory] + [InlineData("1/sqrt(a + b*tan(x) + c*tan(x)^2)")] + [InlineData("tan(x)^5*sqrt(a + b*tan(x) + c*tan(x)^2)")] + [InlineData("cot(x)^3*sqrt(a + b*tan(x) + c*tan(x)^2)")] + [InlineData("cot(x)^2/sqrt(a + b*tan(x) + c*tan(x)^2)")] + [InlineData("cot(x)^3/(a + b*tan(x) + c*tan(x)^2)^(3/2)")] + [InlineData("cot(x)^5/sqrt(a + b*cot(x) + c*cot(x)^2)")] + public void UnderTheTangent(string integrand) + => DifferentiatesBackPinned(integrand, new[] { 0.3, 0.7, 1.1, 1.4 }, Pins); + + /// Rubi's 1.2.1.6: a polynomial part beside the linear over the quadratic. + [Theory] + [InlineData("x^2*(a + c*x^2)^(3/2)/(d + k*x + f*x^2)")] + [InlineData("x*(a + b*x + c*x^2)^(3/2)/(d - f*x^2)")] + [InlineData("x^2/((a + b*x + c*x^2)^(1/2)*(d + k*x + f*x^2))")] + public void APolynomialPartBeside(string integrand) + => DifferentiatesBackPinned(integrand, new[] { 0.3, 0.7, 1.2, -0.5 }, Pins); + } +}