From beaaa008f116a28115f4fab239a327a6f986d68c Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Wed, 21 Oct 2026 16:04:59 +0000 Subject: [PATCH] Integrate four shapes: powers of exponentials of one rate, a polynomial over a power of a linear beside a logarithm, and two roots over a quartic's root - Two powers of exponentials with one logarithmic derivative are read as multiples of one exponential: Rubi's 2.3:28, (a + b (F^(e (c + d x)))^n)^p (G^(h (f + g x)))^(d e n ln F/(g h ln G)). - A polynomial over a power of a linear beside a + b ln(c x^n), by parts against its antiderivative vanishing at zero: 3.1.4:78. - A root over the root of the quartic inside it, beside a power of a linear, split over the quartic's factors: 1.3.2:1224, 1225. - A linear over the root of a quartic whose symbols are a scale of x, in the scaled variable: 1.3.2:1358. Chain against master f7f2a7a8: pocket of 664 rows, 630 -> 635 solved, 0 wrong; samples unchanged but family 3's, 222 -> 223; the 5 rows that moved, alone, 1 -> 5 solved: 2.3:28 declined after 22 s and 12.7 GB there and answered in 0.16 s, 3.1.4:78 26.7 GB -> 0.04 GB; no row's allocation grew past 1.3x. Part of #233. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura --- BREAKING-CHANGES.md | 63 +++ .../Integration/IndefiniteIntegralSolver.cs | 418 +++++++++++++++++- .../Integration/Integration.Definition.cs | 9 +- ...rticWithItsSymbolsScaledOutIntegralTest.cs | 54 +++ ...werOfALinearTimesALogarithmIntegralTest.cs | 56 +++ ...sideItBesideAPowerOfALinearIntegralTest.cs | 74 ++++ ...wersOfExponentialsOfOneRateIntegralTest.cs | 61 +++ 7 files changed, 720 insertions(+), 15 deletions(-) create mode 100644 Sources/Tests/UnitTests/Calculus/ALinearOverTheRootOfAQuarticWithItsSymbolsScaledOutIntegralTest.cs create mode 100644 Sources/Tests/UnitTests/Calculus/ARationalFunctionOverAPowerOfALinearTimesALogarithmIntegralTest.cs create mode 100644 Sources/Tests/UnitTests/Calculus/ARootOverTheRootInsideItBesideAPowerOfALinearIntegralTest.cs create mode 100644 Sources/Tests/UnitTests/Calculus/PowersOfExponentialsOfOneRateIntegralTest.cs diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 0909ba42e..05fdd6165 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -55,6 +55,21 @@ numbers for every coefficient the solve is now done in exact rationals, to degre | `"1/((3-2*x+x^2)^(11/2)*(1+x+2*x^2)^5)".ToEntity().Integrate("x")` | `integral(...)` | 1,853 characters | | `"1/((3-2*x+x^2)^(21/2)*(1+x+2*x^2)^10)".ToEntity().Integrate("x")` | `integral(...)` | 3,475 characters | +### Two powers of exponentials with one logarithmic derivative are read as multiples of one exponential + +**Answers where there were none.** Rubi's 2.3 `(a + b (F^(e (c + d x)))^n)^p (G^(h (f + g x)))^(d e n ln(F)/(g h ln(G)))` +was declined, and on unreleased master ran past its budget, and so were the power of `G` over a power of the sum and +beside its root. A power of an exponential is `K e^(r x)`, `K` constant wherever it is differentiable and `r` its logarithmic derivative; two whose `r` +is one, decided by value, are written over the one exponential, the question in it is `(a + b K e^(r x))^p e^(r x)`, and +the answer is Rubi's. A power of an exponential was written so only beside a polynomial below the bar before +([#233](https://github.com/asc-community/AngouriMath/issues/233)). + +| Input | Was (2.5.0) | Now | +|---|---|---| +| `"(a+b*(F^(k*(c+d*x)))^n)^p*(G^(h*(f+g*x)))^(d*k*n*ln(F)/(g*h*ln(G)))".ToEntity().Integrate("x")` | `integral(...)` | `(G^(h (f + g x)))^(...) (a + b (F^(k (c + d x)))^n)^(p + 1)/((p + 1) b (F^(k (c + d x)))^n n d k ln(F))` | +| `"(G^(h*(f+g*x)))^(d*k*n*ln(F)/(g*h*ln(G)))/(a+b*(F^(k*(c+d*x)))^n)^2".ToEntity().Integrate("x")` | `integral(...)` | 466 characters | +| `"(a+b*(F^(k*(c+d*x)))^n)^p*(F^(h*(f+g*x)))^(d*k*n/(g*h))".ToEntity().Integrate("x")` | `integral(...)` | 167 characters | + ### An exponential of something of the reciprocal of a linear is integrated beside another linear **Answers where there were none.** `f^(c/(a + b x)) x^2` was declined, and so were 24 more of Rubi's 2.3: @@ -246,6 +261,24 @@ It is written out again in the precision asked for now | `Rational.Create(128, 12167)`'s `EDecimal`, read at the default precision and then under 300 digits | 100 digits | 300 digits | | `sqrt(46) * 128/12167` evaluated under 300 digits, with that rational | relative error `4.7e-101` | correct to 300 digits | +### A polynomial over a power of a linear beside `a + b ln(c x^n)` is integrated by parts against its antiderivative vanishing at zero + +**Answers where there were none.** Rubi's 3.1.4 `x^5 (a + b ln(c x^n))/(d + e x)^7` was declined, and on unreleased +master ran past forty seconds, and so were `x^6 (a + b ln(c x^n))/(d + e x)^8` and `x^5 (a + b ln(x))/(d + e x)^7`. +By parts against the logarithm, the antiderivative of `P/(d + e x)^q` that vanishes at zero is `x M/(d + e x)^(q - 1)`, its coefficients read off +`N' (d + e x) - (q - 1) e N = P` from `N(0) = 0`, and beside the logarithm's derivative `b n/x` what is left is +`b n M/(d + e x)^(q - 1)`, a polynomial over a power of the linear. The quotient handed on before held every power of the +linear in the antiderivative multiplied together, of degree twenty-two for the first. Where the degree of `P` is at most +`q - 2`, and only where that quotient does not split over its written factors +([#233](https://github.com/asc-community/AngouriMath/issues/233)). + +| Input | Was (2.5.0) | Now | +|---|---|---| +| `"x^5*(a+b*ln(c*x^n))/(d+k*x)^7".ToEntity().Integrate("x")` | `integral(...)` | 387 characters | +| `"x^6*(a+b*ln(c*x^n))/(d+k*x)^8".ToEntity().Integrate("x")` | `integral(...)` | 438 characters | +| `"x^5*(a+b*ln(x))/(d+k*x)^7".ToEntity().Integrate("x")` | `integral(...)` | 375 characters | +| `"x^4*(a+b*ln(c*x^n))/(d+k*x)^7".ToEntity().Integrate("x")` | `integral(...)` | 426 characters | + ### A power of x times the logarithm of a polynomial plus a root is integrated by parts over the conjugate **Answers where there were none.** `x^3 ln(-1 + 4 x + 4 sqrt((x - 1) x))` was declined, and so were three more of Rubi's 3.5, @@ -2556,6 +2589,22 @@ answered it wrongly ([#1770](https://github.com/asc-community/AngouriMath/issues |---|---|---| | `"1/((x^2 + 3)^2*(x + sqrt(x^2 + 3)))".ToEntity().Integrate("x")` | `integral(...)` | `-2/((sqrt(x^2 + 3) + x)^2 + 3)^2` | +### A root over the root of the quartic inside it, beside a power of a linear, is split over the quartic's factors + +**Answers where there were none.** Rubi's 1.3.2 `sqrt(2 x^2 + sqrt(3 + 4 x^4))/((c + d x) sqrt(3 + 4 x^4))` and the same +over `(c + d x)^2` were declined, and so were Welz's `sqrt(x^2 + sqrt(1 + x^4))/((1 + x) sqrt(1 + x^4))` and its square. +Where `c^2 = b d^2`, `sqrt(c x^2 + d sqrt(a + b x^4))/sqrt(a + b x^4)` is `sqrt(d/(2 sqrt(b)))` times +`1/sqrt(x^2 + i beta) + 1/sqrt(x^2 - i beta)` on the real line, `beta = sqrt(a/b)`, and each over a power of a linear is +an inverse hyperbolic tangent and roots by the recurrence. The sum is real, and continuous on each side of the pole: the +logarithm of a quotient that the table writes for a real root jumps on the real line for an imaginary one. For positive +numbers `a`, `b`, `c` and `d` ([#233](https://github.com/asc-community/AngouriMath/issues/233)). + +| Input | Was (2.5.0) | Now | +|---|---|---| +| `"sqrt(2*x^2+sqrt(3+4*x^4))/((c+d*x)*sqrt(3+4*x^4))".ToEntity().Integrate("x")` | `integral(...)` | 608 characters | +| `"sqrt(2*x^2+sqrt(3+4*x^4))/((c+d*x)^2*sqrt(3+4*x^4))".ToEntity().Integrate("x")` | `integral(...)` | 928 characters | +| `"sqrt(x^2+sqrt(1+x^4))/((1+x)*sqrt(1+x^4))".ToEntity().Integrate("x")` | `integral(...)` | 273 characters | + ### A whole power of a sum of two square roots below the bar is rationalised **Answers where there were none.** `x/(sqrt(a + b x) + sqrt(c + b x))^3` was declined, while the @@ -3466,6 +3515,20 @@ arctangents and two inverse hyperbolic tangents for each sign of `c r` | `"x/((b*x^3 + 2*a*(5 - 3*sqrt(3)))*sqrt(a + b*x^3))".ToEntity().Integrate("x")` | `integral(...)` | 1,703 characters, by the sign of `a` | | `"x/((x^3 + 10 + 6*sqrt(3))*sqrt(1 + x^3))".ToEntity().Integrate("x")` | `integral(...)` | 626 characters | +### A linear over the root of a quartic whose symbols are a scale of x is integrated in the scaled variable + +**Answers where there were none.** Rubi's 1.3.2 `x/sqrt(-44375 b^4 + 576000 b^3 c x + 576000 b^2 c^2 x^2 + 5308416 c^4 x^4)` +was declined, and so was Bronstein's `x/sqrt(x^4 + 10 x^2 - 96 x - 71)` with a scale in it, while each was answered with +numbers for the symbols. Under `x = (b/c) t` the quartic is `5308416 b^4` times a monic one in `t` with rational +coefficients, and the logarithmic ansatz answers that: `ln(p + q sqrt(P))/(18432 c^2)` for `p` of degree eight. The +scaling of the variable took one symbol and did not take the square out from under the root +([#233](https://github.com/asc-community/AngouriMath/issues/233)). + +| Input | Was (2.5.0) | Now | +|---|---|---| +| `"x/sqrt(-44375*b^4+576000*b^3*c*x+576000*b^2*c^2*x^2+5308416*c^4*x^4)".ToEntity().Integrate("x")` | `integral(...)` | 520 characters | +| `"x/sqrt(x^4+10*a^2*x^2-96*a^3*x-71*a^4)".ToEntity().Integrate("x")` | `integral(...)` | 312 characters | + ### `x^2` over a three-quarter power of a quadratic binomial beside another is integrated where that is elementary **Answers where there were none.** `x^2/((A + B x^2)^(3/4) (C + D x^2))` at `B C - 2 A D = 0`, Rubi's diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs index 31dc3156e..194821e16 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs @@ -7548,6 +7548,127 @@ private static bool TryReadAsABinomialIn(Entity expr, Entity.Variable x, out Ent return !c.ContainsNode(x) && n >= 1; } + /// + /// A linear over the square root of a quartic whose symbols are a scale of the variable, + /// (alpha x + beta)/sqrt(P(x)) with P(s t) = C M(t), M monic with rational + /// coefficients and C a positive square: under x = s t it is a constant times + /// (t + nu)/sqrt(M(t)) for a rational nu, which the logarithmic ansatz answers + /// where it is elementary. Rubi's 1.3.2 + /// x/sqrt(-44375 b^4 + 576000 b^3 c x + 576000 b^2 c^2 x^2 + 5308416 c^4 x^4), which is + /// ln(p + q sqrt(P))/(18432 c^2) for p of degree eight. + /// + /// + /// The ansatz reads rational coefficients, and the scaling of the variable takes one symbol + /// and reads what is left by simplifying, which does not take C out from under the + /// root, so the row was declined with b = c = 1 answered. Here s is the + /// symbols of the ratio of two adjacent coefficients, each coefficient of M is + /// checked to be a number, and sqrt(C) is written as the monomial it is, its powers + /// multiples of four. The question in t has no symbols, so it is not scaled again. + /// https://github.com/asc-community/AngouriMath/issues/233 + /// + internal static Entity? SolveALinearOverTheRootOfAQuarticWithItsSymbolsScaledOut(Entity expr, Entity.Variable x, bool integrateByParts) + { + var (numerator, denominator) = Functions.SingleQuotient.Of(expr); + Entity? radicand = null; + Entity constantBelow = Number.Integer.One; + foreach (var factor in Mulf.LinearChildren(denominator)) + { + if (!factor.ContainsNode(x)) + { + constantBelow = constantBelow == Number.Integer.One ? factor : constantBelow * factor; + continue; + } + if (radicand is not null || factor is not Powf(var @base, Number.Rational half) + || !half.ERational.Equals(ERational.Create(1, 2))) + return null; + radicand = @base; + } + if (radicand is null || !radicand.Vars.Any(symbol => symbol != x) + || !TreeAnalyzer.TryGetPolyLinear(numerator, x, out var alpha, out var beta) + || alpha.ContainsNode(x) || beta.ContainsNode(x) || VanishesIdentically(alpha) + || !TreeAnalyzer.TryGetPolynomial(radicand, x, out var read) + || read.Keys.Any(degree => degree.Sign < 0 || degree.CompareTo(EInteger.FromInt32(4)) > 0) + || read.Values.Any(coefficient => coefficient.ContainsNode(x))) + return null; + var coefficients = Enumerable.Range(0, 5) + .Select(degree => read.TryGetValue(EInteger.FromInt32(degree), out var c) ? c.InnerSimplified : Number.Integer.Zero).ToArray(); + if (VanishesIdentically(coefficients[4])) + return null; + Entity? ratio = null; + for (var j = 0; j < 4 && ratio is null; j++) + if (!VanishesIdentically(coefficients[j]) && !VanishesIdentically(coefficients[j + 1])) + ratio = Functions.PartialFractions.InLowestTermsOverTheSymbols(coefficients[j] / coefficients[j + 1]); + if (ratio is null) + return null; + // Its symbols alone: the number in it would only rescale t. + var (ratioAbove, ratioBelow) = Functions.SingleQuotient.Of(ratio); + Entity Symbolic(Entity side) => Mulf.LinearChildren(side).Where(factor => factor is not Number) + .Aggregate(Number.Integer.One as Entity, (product, factor) => product == Number.Integer.One ? factor : product * factor); + var (symbolsAbove, symbolsBelow) = (Symbolic(ratioAbove), Symbolic(ratioBelow)); + if (symbolsAbove == Number.Integer.One && symbolsBelow == Number.Integer.One) + return null; + var scale = symbolsBelow == Number.Integer.One ? symbolsAbove : symbolsAbove / symbolsBelow; + var leading = Functions.PartialFractions.InLowestTermsOverTheSymbols(coefficients[4] * MathS.Pow(scale, 4)); + if (RootOfAPositiveSquare(leading) is not { } rootOfLeading) + return null; + var t = Variable.CreateUnique(expr, "t_scale"); + Entity monic = MathS.Pow(t, 4); + for (var j = 3; j >= 0; j--) + { + var normalised = Functions.PartialFractions.InLowestTermsOverTheSymbols(coefficients[j] * MathS.Pow(scale, j) / leading); + // The lowest terms leave `b^4/b^4` standing past the gcd's bounds. + if (normalised.Vars.Any()) + normalised = Functions.PartialFractions.Bare(normalised.Simplify()); + if (normalised.Vars.Any() || normalised.Evaled is not Number.Rational rational) + return null; + if (!rational.IsZero) + monic += j == 0 ? rational : rational * (j == 1 ? t : MathS.Pow(t, j)); + } + // (alpha s t + beta) s dt/(K sqrt(C) sqrt(M)) = alpha s^2/(K sqrt(C)) (t + nu) dt/sqrt(M). + var shift = Functions.PartialFractions.InLowestTermsOverTheSymbols(beta / (alpha * scale)); + if (shift.Vars.Any()) + shift = Functions.PartialFractions.Bare(shift.Simplify()); + if (shift.Vars.Any() || shift.Evaled is not Number.Rational) + return null; + var inT = (shift.Evaled is Number.Complex { IsZero: true } ? (Entity)t : t + shift.Evaled) / MathS.Sqrt(monic); + if (Integration.ComputeAsAQuestionOfItsOwn(inT, t, integrateByParts) is not { } integrated + || integrated.Nodes.Any(node => node is Integralf)) + return null; + var multiple = Functions.PartialFractions.InLowestTermsOverTheSymbols(alpha * MathS.Pow(scale, 2) / (constantBelow * rootOfLeading)); + // The root as the integrand's, over the monomial, where the answer kept it as written. + var root = MathS.Sqrt(monic); + var answer = multiple * integrated.Replace(node => node == root ? MathS.Sqrt(radicand) / rootOfLeading : node) + .Substitute(t, symbolsBelow == Number.Integer.One ? x / symbolsAbove : symbolsBelow * x / symbolsAbove); + return Functions.PartialFractions.DerivativeHoldsAtSampledPoints(answer, expr, x) ? answer : null; + } + + /// + /// The square root of , a positive rational times powers of + /// symbols, each power a multiple of four, as the rational's root times their halves; + /// null for any other value. + /// + private static Entity? RootOfAPositiveSquare(Entity value) + { + var (top, bottom) = Functions.SingleQuotient.Of(value); + Entity root = Number.Integer.One; + var number = ERational.One; + foreach (var (side, above) in new[] { (top, true), (bottom, false) }) + foreach (var factor in Mulf.LinearChildren(side)) + switch (factor) + { + case Number.Rational rational: + number = above ? number.Multiply(rational.ERational) : number.Divide(rational.ERational); + break; + case Powf(Variable symbol, Number.Integer power) when power.EInteger.Remainder(4).IsZero && power.EInteger.Sign > 0: + var half = MathS.Pow(symbol, Number.Integer.Create(power.EInteger.Divide(2))); + root = above ? root * half : root / half; + break; + default: + return null; + } + return number.Sign > 0 ? (MathS.Sqrt(Number.Rational.Create(number)) * root).InnerSimplified : null; + } + /// /// A linear over a linear, or a quadratic over a quadratic, beside the reciprocal of the /// square root of a cubic binomial a + b x^3, at the coefficients where its @@ -26434,11 +26555,22 @@ when IsAnExponential(@base, inner) && !outer.ContainsNode(x) && outer.Evaled is /// https://github.com/asc-community/AngouriMath/issues/718 /// /// + /// /// (F^u)^n is F^(n u) only where F^u is on the principal branch, which /// it is for a positive F and need not be for a symbol; K carries the /// difference, and the answer holds for every F. Only beside a polynomial below the /// bar, where nothing else reads the power: above it, the rules for a polynomial times an /// exponential answer it as written. + /// + /// + /// And beside another power of an exponential with the same rate, decided by value, which + /// nothing reads from the two as written: Rubi's 2.3 + /// (a + b (F^(e (c + d x)))^n)^p (G^(h (f + g x)))^(d e n ln(F)/(g h ln(G))) is + /// K_2 e^(r x) (a + b K_1 e^(r x))^p, which the substitution u = e^(r x) + /// answers, and was a search past its budget. The constants come back beside the + /// exponential they multiplied and are cancelled against it, and the answer is Rubi's. + /// https://github.com/asc-community/AngouriMath/issues/233 + /// /// internal static Entity? SolveByWritingAPowerOfAnExponentialAsAMultipleOfOne(Entity expr, Entity.Variable x, bool integrateByParts) { @@ -26449,8 +26581,32 @@ when IsAnExponential(@base, inner) && !outer.ContainsNode(x) && outer.Evaled is .Distinct().ToList(); if (powers.Count == 0) return null; + var rates = new List(); + foreach (var node in powers) + { + if (node is not Powf(Powf(var @base, var inner), var outer) + || !TreeAnalyzer.TryGetPolyLinear(inner, x, out var slope, out _) || TreeAnalyzer.IsZero(slope)) + return null; + rates.Add(outer.Evaled is Number.Integer ? null + : (outer * slope * (@base == MathS.e ? Number.Integer.One : MathS.Ln(@base))).InnerSimplified); + } + // Two powers written apart whose rates are one, read as multiples of one exponential: + // Rubi's `(a + b (F^(e (c + d x)))^n)^p (G^(h (f + g x)))^(d e n ln(F)/(g h ln(G)))`, + // where the second is a constant multiple of the first on each interval and nothing + // reads that from the two as written. Decided by value, each later rate then written as + // the first that equals it. + var ofOneRate = false; + for (var later = 1; later < rates.Count; later++) + for (var earlier = 0; earlier < later; earlier++) + if (rates[later] is { } laterRate && rates[earlier] is { } earlierRate + && (laterRate == earlierRate || VanishesIdentically(laterRate - earlierRate))) + { + rates[later] = earlierRate; + ofOneRate = true; + break; + } // A polynomial below the bar, or under a negative whole power. - if (!FactorsOfTheIntegrand(expr).Any(pair => + if (!ofOneRate && !FactorsOfTheIntegrand(expr).Any(pair => pair.Factor.ContainsNode(x) && !pair.Factor.Nodes.Any(IsAnExponential) && (pair.Underneath || pair.Factor is Powf(_, Number.Integer { IsNegative: true })) && TreeAnalyzer.TryGetPolynomial(pair.Factor is Powf(var raised, Number.Integer) ? raised : pair.Factor, x, out var read) @@ -26458,18 +26614,17 @@ when IsAnExponential(@base, inner) && !outer.ContainsNode(x) && outer.Evaled is return null; var back = new List<(Entity.Variable Constant, Entity Value)>(); var rewritten = expr; - foreach (var node in powers) + for (var at = 0; at < powers.Count; at++) { - if (node is not Powf(Powf(var @base, var inner), var outer) - || !TreeAnalyzer.TryGetPolyLinear(inner, x, out var slope, out _) || TreeAnalyzer.IsZero(slope)) + var node = powers[at]; + if (node is not Powf(Powf(var @base, var inner), var outer)) return null; Entity replacement; - if (outer.Evaled is Number.Integer) + if (rates[at] is not { } rate) replacement = MathS.Pow(@base, (outer * inner).InnerSimplified); else { var constant = Variable.CreateUnique(rewritten, "k_pow"); - var rate = (outer * slope * (@base == MathS.e ? Number.Integer.One : MathS.Ln(@base))).InnerSimplified; replacement = constant * MathS.Pow(MathS.e, rate * x); back.Add((constant, node * MathS.Pow(MathS.e, -rate * x))); } @@ -26479,9 +26634,46 @@ when IsAnExponential(@base, inner) && !outer.ContainsNode(x) && outer.Evaled is return null; foreach (var (constant, value) in back) answer = answer.Substitute(constant, value); + if (!ofOneRate) + return answer; + // Each constant came back as its power times `e^(-r x)`, beside the `e^(r x)` it + // multiplied: the two are cancelled as written, a product at a time and then across + // the bar, which an exponential, never zero, allows. + foreach (var rate in rates.OfType().Distinct()) + answer = WithOppositeExponentialsCancelled(answer, MathS.Pow(MathS.e, rate * x), MathS.Pow(MathS.e, -rate * x)); return answer; } + /// + /// with and , an + /// exponential and its reciprocal, cancelled where they stand as factors of one product, + /// and then, for a product or quotient, each cancelled where it stands above and below + /// the bar. Exact, since an exponential is never zero. + /// + private static Entity WithOppositeExponentialsCancelled(Entity expr, Entity up, Entity down) + { + var cancelled = expr.Replace(node => + { + if (node is not Mulf) + return node; + var factors = Mulf.LinearChildren(node).ToList(); + var pairs = System.Math.Min(factors.Count(factor => factor == up), factors.Count(factor => factor == down)); + if (pairs == 0) + return node; + for (var removed = 0; removed < pairs; removed++) + { + factors.Remove(up); + factors.Remove(down); + } + return factors.Count == 0 ? Number.Integer.One : factors.Aggregate((product, factor) => product * factor); + }); + if (cancelled is Sumf or Minusf || !cancelled.Nodes.Any(node => node == up || node == down)) + return cancelled; + var (above, below) = Functions.SingleQuotient.Of(cancelled); + var acrossTheBar = CancelCommonFactors(above, below); + return acrossTheBar.Nodes.Any(node => node == up || node == down) ? cancelled : acrossTheBar; + } + /// /// An exponential of a multiple of a logarithm is a power of the argument: /// e^(k ln(q)) is q^k, since e^(k ln q) is the definition of the @@ -26969,7 +27161,7 @@ internal static bool IsAPerfectSquareDiscriminant(Entity a, Entity b, Entity c) /// with a^2 = b^2 c; sqrt(c x^2 + d sqrt(a + b x^4))/sqrt(a + b x^4) with /// c^2 = b d^2; 1/((a + b x^n) sqrt(c x^2 + d (a + b x^n)^(2/n))); and /// sqrt(a x^2 + b x sqrt(c + d x^2))/(x sqrt(c + d x^2)) with a^2 = b^2 d and - /// b^2 c + a = 0. + /// b^2 c + a = 0. And the second over a power of a linear, for numbers. /// /// /// @@ -26983,6 +27175,19 @@ internal static bool IsAPerfectSquareDiscriminant(Entity a, Entity b, Entity c) /// Timofeev suites declined all of them. /// https://github.com/asc-community/AngouriMath/issues/718 /// + /// + /// Over a power of a linear L the substitution leaves x as a fourth root in + /// w; there the quartic is split instead. With beta = sqrt(a/b), + /// a + b x^4 = b (x^2 + i beta)(x^2 - i beta), and on the real line the two roots of + /// the factors are conjugates whose sum is sqrt(2 x^2 + 2 sqrt(x^4 + beta^2)), so + /// sqrt(S)/R is sqrt(d/(2 sqrt(b))) times + /// 1/sqrt(x^2 + i beta) + 1/sqrt(x^2 - i beta), each over L^k an inverse + /// hyperbolic tangent and roots, and the two together real. For positive numbers + /// a, b, c, d: Rubi's 1.3.2 + /// sqrt(2 x^2 + sqrt(3 + 4 x^4))/((c + d x) sqrt(3 + 4 x^4)) and its square, and + /// Welz's 47 and 48, declined before. + /// https://github.com/asc-community/AngouriMath/issues/233 + /// /// internal static Entity? SolveANestedRootByItsClosedForm(Entity expr, Entity.Variable x, bool integrateByParts) { @@ -27013,7 +27218,7 @@ internal static bool IsAPerfectSquareDiscriminant(Entity a, Entity b, Entity c) { 1 => ARootOfAConstantPlusARootOfAQuadratic(factors[0]), 2 => ARootOverTheRootInsideIt(factors) ?? AReciprocalOfTheRootInsideIt(factors), - _ => AProductRootOverTheRootInsideIt(factors), + _ => AProductRootOverTheRootInsideIt(factors) ?? ARootOverTheRootInsideItBesideAPowerOfALinear(factors), }; if (answer is null) return null; @@ -27131,6 +27336,58 @@ internal static bool IsAPerfectSquareDiscriminant(Entity a, Entity b, Entity c) return MathS.Sqrt(2) * b / a * (q is Variable named ? inX.Substitute(named, reciprocal) : inX); } + // sqrt(c x^2 + d sqrt(P))/(sqrt(P) L^k), P = a + b x^4 with numbers, a/b > 0 and + // c = d sqrt(b) > 0: with beta = sqrt(a/b), P = b (x^2 + i beta)(x^2 - i beta), and + // on the real line sqrt(x^2 + i beta) + sqrt(x^2 - i beta) is the root of + // 2 x^2 + 2 sqrt(x^4 + beta^2), so the integrand is + // sqrt(d/(2 sqrt(b))) (1/sqrt(x^2 + i beta) + 1/sqrt(x^2 - i beta))/L^k. + Entity? ARootOverTheRootInsideItBesideAPowerOfALinear(List three) + { + if (three.Find(f => f is Powf(_, var p) && p == half) is not Powf(var sum, _) + || three.Find(f => f is Powf(_, var p) && p == -half) is not Powf(var radicand, _) + || three.Find(f => f is Powf(var l, Number.Integer { EInteger.Sign: < 0 }) && TreeAnalyzer.TryGetPolyLinear(l, x, out _, out _)) + is not Powf(var linear, Number.Integer power) + || power.EInteger.CompareTo(EInteger.FromInt32(-4)) < 0 + || !TreeAnalyzer.TryGetPolyLinear(linear, x, out var slope, out var offset) + // A power of x alone is the substitution's, which answers it shorter. + || VanishesIdentically(slope) || VanishesIdentically(offset) + || Functions.PartialFractions.HoldsTheImaginaryUnit(slope) || Functions.PartialFractions.HoldsTheImaginaryUnit(offset) + || Coefficients(radicand, 0, 4) is not [var a, var b] + || SquareOfXPlusMultipleOfRoot(sum, radicand, half) is not var (c, d) + || a.Evaled is not Number.Real { IsPositive: true } || b.Evaled is not Number.Real { IsPositive: true } + || c.Evaled is not Number.Real { IsPositive: true } || d.Evaled is not Number.Real { IsPositive: true } + || !Functions.PartialFractions.IsZeroAsAValue(c * c - b * d * d)) + return null; + var beta = MathS.Sqrt(a / b).InnerSimplified; + var multiple = MathS.Sqrt(d / (2 * MathS.Sqrt(b))).InnerSimplified; + var k = (-power.EInteger).ToInt32Checked(); + return multiple * (OverAPowerOfALinearBesideARoot(MathS.i * beta) + OverAPowerOfALinearBesideARoot(-MathS.i * beta)); + + // The integral of 1/(L^k sqrt(x^2 + q)), L = offset + slope x, by + // I_(m + 1) = ((1 - m) I_(m - 1) + (2m - 1) offset I_m - slope sqrt(x^2 + q)/L^m)/(m D), + // D = offset^2 + slope^2 q, from I_0 = ln(x + sqrt(x^2 + q)) and + // I_1 = -artanh((q slope - offset x)/(sqrt(D) sqrt(x^2 + q)))/sqrt(D). For an + // imaginary q these are continuous on each side of the pole: one minus the square + // of the artanh's argument is q L^2/(D (x^2 + q)), which is a negative number nowhere + // on the real line. The table's ln((A + B)/(A - B)) is twice the inverse hyperbolic + // cotangent of the same argument, whose cut is where that is a positive number, and + // for `2 x^2 + sqrt(3 + 4 x^4)` over `0.7 + 1.3 x` it jumped at x = -1.7. + Entity OverAPowerOfALinearBesideARoot(Entity q) + { + var root = MathS.Sqrt(MathS.Pow(x, 2) + q); + var discriminant = (offset * offset + slope * slope * q).InnerSimplified; + var rootOfIt = MathS.Sqrt(discriminant); + Entity before = MathS.Ln(x + root); + Entity current = -MathS.Hyperbolic.Artanh((q * slope - offset * x) / (rootOfIt * root)) / rootOfIt; + for (var m = 1; m < k; m++) + { + var next = ((1 - m) * before + (2 * m - 1) * offset * current - slope * root / MathS.Pow(linear, m)) / (m * discriminant); + (before, current) = (current, next); + } + return current; + } + } + // multiple times the integral of 1/(1 - q w^2) at w = at, with q named while it is asked. Entity? InW(Entity multiple, Entity coefficient, Entity at) { @@ -29084,6 +29341,12 @@ static bool AreEqualAsPolynomials(Entity left, Entity right) /// a constant has been taken out in front; against a constant alone it is by parts /// against one, asked at the top or one below it like the rest of by parts. /// https://github.com/asc-community/AngouriMath/issues/718 + /// Where the antiderivative times the derivative of A + B ln(C x^n) does not split + /// over its written factors and the rational function is a polynomial over a power of one + /// linear, the antiderivative is the one that vanishes at zero, as one quotient: Rubi's 3.1.4 + /// x^5 (a + b ln(c x^n))/(d + e x)^7 left a remainder of degree twenty-two that the + /// substitution search did not finish. + /// https://github.com/asc-community/AngouriMath/issues/233 /// internal static Entity? SolveARationalFunctionTimesAPowerOfALogarithm(Entity expr, Entity.Variable x, bool integrateByParts) { @@ -29208,8 +29471,28 @@ static bool AreEqualAsPolynomials(Entity left, Entity right) // and each on its own beside the power of the logarithm is a dilogarithm. var remainders = new List(); if (power == 1) + { + var splitEach = true; foreach (var term in derivativeTerms) - remainders.Add(AsPartialFractions(term * integralOfTheRational, x)); + { + remainders.Add(AsPartialFractions(term * integralOfTheRational, x, out var split)); + splitEach &= split; + } + // A product that does not split was handed on as one quotient over every + // denominator of the antiderivative's terms multiplied together: against + // `a + b ln(c x^n)`, `x^5/(d + k x)^7` has six, and the remainder was a quotient + // of degree twenty-two with symbols in it, which the substitution search did + // not finish in forty seconds. Where the logarithm's derivative is a constant over + // `x` and the rational function a polynomial over one power of a linear, the + // antiderivative vanishing at zero is `x M/(s + t x)^(q - 1)` with `M` a + // polynomial, and the remainder is that constant times `M/(s + t x)^(q - 1)`. + if (!splitEach && TheConstantOverX(derivativeTerms, x) is { } overX + && TheAntiderivativeVanishingAtZeroOverAPowerOfALinear(rational, x) is var (vanishing, vanishingOverX)) + { + integralOfTheRational = vanishing; + remainders = new List { overX * vanishingOverX }; + } + } else { // Keyed by the part of the denominator holding x; the constants go with the @@ -29219,7 +29502,7 @@ static bool AreEqualAsPolynomials(Entity left, Entity right) // with nothing cancelled. var collected = new List<(Entity Below, Entity Above)>(); foreach (var term in derivativeTerms) - foreach (var fraction in TermsDistributed(AsPartialFractions(term * integralOfTheRational, x))) + foreach (var fraction in TermsDistributed(AsPartialFractions(term * integralOfTheRational, x, out _))) { var (fractionAbove, fractionBelow) = Functions.SingleQuotient.Of(fraction); Entity holdingX = Number.Integer.One, constantBelow = Number.Integer.One; @@ -29289,14 +29572,17 @@ static Entity Distributed(Entity inner, Number.Integer exponent) /// brought over one bar and written as its whole part plus /// its partial fractions over the written linear factors of the denominator, the /// symbolic ones by the derivatives at the roots; over one - /// bar as it came where the denominator is not such a product. + /// bar as it came where the denominator is not such a product, and then + /// is false. /// - private static Entity AsPartialFractions(Entity product, Entity.Variable x) + private static Entity AsPartialFractions(Entity product, Entity.Variable x, out bool split) { + split = true; var combined = Functions.SingleQuotient.Combine(product).InnerSimplified; var (above, below) = Functions.SingleQuotient.Of(combined); if (below == Number.Integer.One || !below.ContainsNode(x)) return combined; + split = false; above = Functions.PartialFractions.Bare(above.Expand().InnerSimplified); if (!TreeAnalyzer.TryGetPolynomial(above, x, out var aboveRead) || !TreeAnalyzer.TryGetPolynomial(below, x, out var belowRead) || aboveRead.Count == 0 || belowRead.Count == 0) @@ -29315,7 +29601,10 @@ private static Entity AsPartialFractions(Entity product, Entity.Variable x) (proper, below) = Functions.SingleQuotient.Of(Functions.PartialFractions.Bare(remainder.InnerSimplified)); } if (proper == Number.Integer.Zero || proper.Evaled is Number.Complex { IsZero: true }) + { + split = true; return whole; + } Entity fractions; if (below == Number.Integer.One) fractions = proper; @@ -29328,7 +29617,8 @@ private static Entity AsPartialFractions(Entity product, Entity.Variable x) : decomposition; } else - fractions = proper / below; + return whole == Number.Integer.Zero ? proper / below : whole + proper / below; + split = true; return whole == Number.Integer.Zero ? fractions : whole + fractions; } @@ -29383,6 +29673,108 @@ private static Entity MatchedToAPoleOf(Entity antiderivative, List deriv return antiderivative; } + /// + /// The constant c where the sum of is + /// c/x, each of them a constant over , as the derivative of + /// A + B ln(C x^n) is; null otherwise. + /// + private static Entity? TheConstantOverX(List derivativeTerms, Entity.Variable x) + { + Entity? sum = null; + foreach (var term in derivativeTerms) + { + var (above, below) = Functions.SingleQuotient.Of(term); + if (above.ContainsNode(x)) + return null; + Entity constant = above; + var seenX = false; + foreach (var factor in Mulf.LinearChildren(below)) + if (factor == x && !seenX) + seenX = true; + else if (factor.ContainsNode(x)) + return null; + else + constant /= factor; + if (!seenX) + return null; + sum = sum is null ? constant : sum + constant; + } + return sum is null || VanishesIdentically(sum) ? null : sum; + } + + /// + /// For P/(K (s + t x)^q) with P a polynomial of + /// degree at most q - 2, K, s and t free of + /// and s not zero: its antiderivative that vanishes at zero, + /// x M/(K (s + t x)^(q - 1)) with M a polynomial, and that over + /// ; null for any other . + /// + /// + /// N = x M is read off N' (s + t x) - (q - 1) t N = P coefficient by + /// coefficient: (j + 1) s n_(j + 1) = p_j + (q - 1 - j) t n_j from n_0 = 0, + /// which is the constant that makes it vanish at zero, and the equation at + /// x^(q - 1) is p_(q - 1) = 0, the degree bound, which says that there is + /// no logarithm in it. Each coefficient is put in lowest terms over the symbols as it is + /// made, so that those of the lower powers that are zero are written as zero: for + /// x^5/(d + k x)^7 only n_6 = 1/(6 d) is not, and the antiderivative is + /// x^6/(6 d (d + k x)^6), where the integral of the rational function, matched to + /// the pole, was six powers of the linear and a constant. + /// + private static (Entity Antiderivative, Entity OverX)? TheAntiderivativeVanishingAtZeroOverAPowerOfALinear(Entity rational, Entity.Variable x) + { + var (above, below) = Functions.SingleQuotient.Of(rational); + Entity? linear = null; + var power = 0; + Entity constantBelow = Number.Integer.One; + foreach (var factor in Mulf.LinearChildren(below)) + { + if (!factor.ContainsNode(x)) + { + constantBelow = constantBelow == Number.Integer.One ? factor : constantBelow * factor; + continue; + } + if (linear is not null || factor is not Powf(var @base, Number.Integer { EInteger: var exponent }) + || exponent.CompareTo(EInteger.FromInt32(2)) < 0 || exponent.CompareTo(EInteger.FromInt32(24)) > 0) + return null; + linear = @base; + power = exponent.ToInt32Checked(); + } + if (linear is null || !TreeAnalyzer.TryGetPolyLinear(linear, x, out var slope, out var offset) + || VanishesIdentically(slope) || VanishesIdentically(offset) + || !TreeAnalyzer.TryGetPolynomial(above, x, out var read) + || read.Keys.Any(degree => degree.Sign < 0 || degree.CompareTo(EInteger.FromInt32(power - 2)) > 0) + || read.Values.Any(coefficient => coefficient.ContainsNode(x))) + return null; + var coefficients = new Entity[power]; + coefficients[0] = Number.Integer.Zero; + for (var j = 0; j < power - 1; j++) + { + var lower = read.TryGetValue(EInteger.FromInt32(j), out var p) ? p : Number.Integer.Zero; + coefficients[j + 1] = lower == Number.Integer.Zero && coefficients[j] == Number.Integer.Zero + ? Number.Integer.Zero + : Functions.PartialFractions.InLowestTermsOverTheSymbols( + (lower + Number.Integer.Create(power - 1 - j) * slope * coefficients[j]) / (Number.Integer.Create(j + 1) * offset)); + } + Entity polynomial = Number.Integer.Zero; + for (var j = 1; j < power; j++) + { + var coefficient = coefficients[j]; + if (coefficient == Number.Integer.Zero || coefficient.Evaled is Number.Complex { IsZero: true }) + continue; + var term = j == 1 ? coefficient : j == 2 ? coefficient * x : coefficient * MathS.Pow(x, j - 1); + polynomial = polynomial == Number.Integer.Zero ? term : polynomial + term; + } + if (polynomial == Number.Integer.Zero) + return null; + Entity overPower = MathS.Pow(linear, power - 1); + if (constantBelow != Number.Integer.One) + overPower = constantBelow * overPower; + var overX = polynomial / overPower; + var antiderivative = x * polynomial / overPower; + return Functions.PartialFractions.HoldsAtSampledPoints(antiderivative.Differentiate(x), rational, x) + ? (antiderivative, overX) : null; + } + /// /// The terms of with every product distributed over the sums /// among its factors: B (ln(a + b x) - ln(c + d x)) is two terms. diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs b/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs index b771c2129..c4cff24d8 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs @@ -836,8 +836,9 @@ private static Entity WithItsPiecewiseSumsCombined(Entity expr) // A power of an exponential with a positive base is the exponential of the product, // exactly, and only that spelling is one the exponential rules read. if ((answer = IndefiniteIntegralSolver.SolveByFlatteningAPowerOfAnExponential(expr, x, integrateByParts)) is { }) return answer; - // And any other power of an exponential beside a polynomial below the bar, as a constant - // multiple of an exponential wherever it is differentiable. + // And any other power of an exponential beside a polynomial below the bar, or beside + // another of the same rate, as a constant multiple of an exponential wherever it is + // differentiable. if ((answer = IndefiniteIntegralSolver.SolveByWritingAPowerOfAnExponentialAsAMultipleOfOne(expr, x, integrateByParts)) is { }) return answer; // An exponential of a quadratic in 1/L beside a power of L, onto the Gaussian under u = 1/L. if ((answer = IndefiniteIntegralSolver.SolveAGaussianInAReciprocal(expr, x)) is { }) return answer; @@ -1522,6 +1523,10 @@ private static Entity WithItsPiecewiseSumsCombined(Entity expr) // is wrong with -- it clears a parameter rather than a shape -- so everything that // can answer the problem as written gets to try first. if ((answer = IndefiniteIntegralSolver.SolveByScalingTheVariable(expr, x, integrateByParts)) is { }) return answer; + // And a linear over the root of a quartic whose symbols are a scale of x, where that + // takes them out from under the root, which the simplifier does not: the logarithmic + // ansatz answers the numeric quartic that is left. + if ((answer = IndefiniteIntegralSolver.SolveALinearOverTheRootOfAQuarticWithItsSymbolsScaledOut(expr, x, integrateByParts)) is { }) return answer; // After every rule that can answer a power in its own terms, because expanding one // throws away whatever structure it had: (1 + x^2)^2 is answered as a power and only // wants writing out if that fails. diff --git a/Sources/Tests/UnitTests/Calculus/ALinearOverTheRootOfAQuarticWithItsSymbolsScaledOutIntegralTest.cs b/Sources/Tests/UnitTests/Calculus/ALinearOverTheRootOfAQuarticWithItsSymbolsScaledOutIntegralTest.cs new file mode 100644 index 000000000..7d4748426 --- /dev/null +++ b/Sources/Tests/UnitTests/Calculus/ALinearOverTheRootOfAQuarticWithItsSymbolsScaledOutIntegralTest.cs @@ -0,0 +1,54 @@ +// +// 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 linear over the root of a quartic whose symbols are a scale of the variable, read under + /// x = s t as a numeric quartic that the logarithmic ansatz answers: Rubi's 1.3.2 + /// x/sqrt(-44375 b^4 + 576000 b^3 c x + 576000 b^2 c^2 x^2 + 5308416 c^4 x^4), and + /// Bronstein's x/sqrt(x^4 + 10 x^2 - 96 x - 71) with a scale in it, declined before. + /// #233 + /// + [Trait("Area", "Calculus")] + public sealed class ALinearOverTheRootOfAQuarticWithItsSymbolsScaledOutIntegralTest + { + [Theory] + [InlineData("x/sqrt(-44375*b^4 + 576000*b^3*c*x + 576000*b^2*c^2*x^2 + 5308416*c^4*x^4)")] + [InlineData("x/sqrt(-44375*b^4 + 576000*b^3*x + 576000*b^2*x^2 + 5308416*x^4)")] + [InlineData("x/sqrt(x^4 + 10*b^2*x^2 - 96*b^3*x - 71*b^4)")] + [InlineData("x/sqrt(c^4*x^4 + 10*b^2*c^2*x^2 - 96*b^3*c*x - 71*b^4)")] + public void IsIntegrated(string integrand) + { + var integral = integrand.ToEntity().Integrate("x"); + var text = integral.Stringize(); + Assert.DoesNotContain("integral(", text); + Assert.True(text.Length < 1500, $"{text.Length} characters of answer for {integrand}"); + Entity Pinned(Entity e) => e.Substitute("b", -0.7).Substitute("c", 1.3); + var antiderivative = Pinned(integral.Substitute("C", 0)); + var derivative = antiderivative.Differentiate("x"); + var original = Pinned(integrand.ToEntity()); + var compared = 0; + foreach (var at in new[] { -4.1, -2.3, -1.6, 1.1, 2.2, 3.7 }) + { + var want = original.Substitute("x", at).EvalNumerical(); + var got = derivative.Substitute("x", at).EvalNumerical(); + if (want.IsNaN || Math.Abs((double)want.ImaginaryPart) > 1e-12) + continue; + compared++; + Assert.True(Math.Abs((double)(got - want).RealPart) + Math.Abs((double)(got - want).ImaginaryPart) + < 1e-9 * Math.Max(1, Math.Abs((double)want.RealPart) + Math.Abs((double)want.ImaginaryPart)), + $"d/dx of the antiderivative of {integrand} is {got} at x = {at}, where the integrand is {want}"); + } + Assert.True(compared >= 4, $"only {compared} points could be compared for {integrand}"); + } + } +} diff --git a/Sources/Tests/UnitTests/Calculus/ARationalFunctionOverAPowerOfALinearTimesALogarithmIntegralTest.cs b/Sources/Tests/UnitTests/Calculus/ARationalFunctionOverAPowerOfALinearTimesALogarithmIntegralTest.cs new file mode 100644 index 000000000..50afb1657 --- /dev/null +++ b/Sources/Tests/UnitTests/Calculus/ARationalFunctionOverAPowerOfALinearTimesALogarithmIntegralTest.cs @@ -0,0 +1,56 @@ +// +// 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 polynomial over a power of a linear times A + B ln(C x^n), by parts with the + /// antiderivative of the rational function that vanishes at zero written as + /// x M(x)/(d + k x)^(q - 1): the remainder is then a polynomial over a power of the + /// linear. Rubi's 3.1.4 x^5 (a + b ln(c x^n))/(d + e x)^7 handed the substitution + /// search a quotient of degree twenty-two and ran past forty seconds. + /// #233 + /// + [Trait("Area", "Calculus")] + public sealed class ARationalFunctionOverAPowerOfALinearTimesALogarithmIntegralTest + { + [Theory] + [InlineData("x^5*(a + b*ln(c*x^n))/(d + k*x)^7")] + [InlineData("x^6*(a + b*ln(c*x^n))/(d + k*x)^8")] + [InlineData("x^5*(a + b*ln(x))/(d + k*x)^7")] + [InlineData("x^4*(a + b*ln(c*x^n))/(d + k*x)^7")] + [InlineData("x^5*(a + b*ln(c*x^n))/(d + k*x)^8")] + public void IsIntegrated(string integrand) + { + var integral = integrand.ToEntity().Integrate("x"); + var text = integral.Stringize(); + Assert.DoesNotContain("integral(", text); + Assert.True(text.Length < 1500, $"{text.Length} characters of answer for {integrand}"); + Entity Pinned(Entity e) => e.Substitute("a", 1.3).Substitute("b", 0.7).Substitute("c", 0.6) + .Substitute("d", 1.1).Substitute("k", -0.9).Substitute("n", 2); + var derivative = Pinned(integral.Substitute("C", 0)).Differentiate("x"); + var original = Pinned(integrand.ToEntity()); + var compared = 0; + foreach (var at in new[] { -2.3, -0.7, 0.3, 0.8, 2.9, 4.1 }) + { + var want = original.Substitute("x", at).EvalNumerical(); + var got = derivative.Substitute("x", at).EvalNumerical(); + if (want.IsNaN) + continue; + compared++; + Assert.True(Math.Abs((double)(got - want).RealPart) + Math.Abs((double)(got - want).ImaginaryPart) + < 1e-9 * Math.Max(1, Math.Abs((double)want.RealPart) + Math.Abs((double)want.ImaginaryPart)), + $"d/dx of the antiderivative of {integrand} is {got} at x = {at}, where the integrand is {want}"); + } + Assert.True(compared >= 5, $"only {compared} points could be compared for {integrand}"); + } + } +} diff --git a/Sources/Tests/UnitTests/Calculus/ARootOverTheRootInsideItBesideAPowerOfALinearIntegralTest.cs b/Sources/Tests/UnitTests/Calculus/ARootOverTheRootInsideItBesideAPowerOfALinearIntegralTest.cs new file mode 100644 index 000000000..59c0d92d7 --- /dev/null +++ b/Sources/Tests/UnitTests/Calculus/ARootOverTheRootInsideItBesideAPowerOfALinearIntegralTest.cs @@ -0,0 +1,74 @@ +// +// 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 +{ + /// + /// sqrt(c x^2 + d sqrt(a + b x^4))/(sqrt(a + b x^4) (p + q x)^k) with c^2 = b d^2, + /// written as sqrt(d/(2 sqrt(b))) times 1/sqrt(x^2 + i beta) + 1/sqrt(x^2 - i beta) + /// over the power, beta = sqrt(a/b): Rubi's 1.3.2 + /// sqrt(2 x^2 + sqrt(3 + 4 x^4))/((c + d x) sqrt(3 + 4 x^4)) and its square, declined before. + /// #233 + /// + /// + /// The answer is checked by differentiating back and also against Simpson's rule on an + /// interval to the left of the pole: the logarithm of a quotient, which the table writes + /// for a real root, jumps there for an imaginary one, and only its value says so. + /// + [Trait("Area", "Calculus")] + public sealed class ARootOverTheRootInsideItBesideAPowerOfALinearIntegralTest + { + [Theory] + [InlineData("sqrt(2*x^2 + sqrt(3 + 4*x^4))/((c + d*x)*sqrt(3 + 4*x^4))")] + [InlineData("sqrt(2*x^2 + sqrt(3 + 4*x^4))/((c + d*x)^2*sqrt(3 + 4*x^4))")] + [InlineData("sqrt(2*x^2 + sqrt(3 + 4*x^4))/((c + d*x)^3*sqrt(3 + 4*x^4))")] + [InlineData("sqrt(3*x^2 + sqrt(2 + 9*x^4))/((c + d*x)*sqrt(2 + 9*x^4))")] + [InlineData("sqrt(2*x^2 + sqrt(3 + 4*x^4))/((1 + 2*x)*sqrt(3 + 4*x^4))")] + public void IsIntegrated(string integrand) + { + var integral = integrand.ToEntity().Integrate("x"); + var text = integral.Stringize(); + Assert.DoesNotContain("integral(", text); + Assert.True(text.Length < 4000, $"{text.Length} characters of answer for {integrand}"); + Entity Pinned(Entity e) => e.Substitute("c", 0.7).Substitute("d", 1.3); + var antiderivative = Pinned(integral.Substitute("C", 0)); + var derivative = antiderivative.Differentiate("x"); + var original = Pinned(integrand.ToEntity()); + var compared = 0; + foreach (var at in new[] { -3.1, -1.7, -0.9, 0.2, 1.3, 4.6 }) + { + var want = original.Substitute("x", at).EvalNumerical(); + var got = derivative.Substitute("x", at).EvalNumerical(); + if (want.IsNaN) + continue; + compared++; + Assert.True(Math.Abs((double)(got - want).RealPart) + Math.Abs((double)(got - want).ImaginaryPart) + < 1e-9 * Math.Max(1, Math.Abs((double)want.RealPart) + Math.Abs((double)want.ImaginaryPart)), + $"d/dx of the antiderivative of {integrand} is {got} at x = {at}, where the integrand is {want}"); + } + Assert.True(compared >= 5, $"only {compared} points could be compared for {integrand}"); + + // On [-3, -1], left of every pole here, F(-1) - F(-3) is the integral. + double At(Entity e, double point) => (double)e.Substitute("x", point).EvalNumerical().RealPart; + const int steps = 200; + var (from, to) = (-3.0, -1.0); + var width = (to - from) / steps; + var simpson = At(original, from) + At(original, to); + for (var step = 1; step < steps; step++) + simpson += (step % 2 == 1 ? 4 : 2) * At(original, from + step * width); + simpson *= width / 3; + var difference = antiderivative.Substitute("x", to).EvalNumerical() - antiderivative.Substitute("x", from).EvalNumerical(); + Assert.True(Math.Abs((double)difference.ImaginaryPart) < 1e-9, $"F(-1) - F(-3) is {difference} for {integrand}"); + Assert.True(Math.Abs((double)difference.RealPart - simpson) < 1e-6 * Math.Max(1, Math.Abs(simpson)), + $"F(-1) - F(-3) is {difference} for {integrand}, where Simpson's rule gives {simpson}"); + } + } +} diff --git a/Sources/Tests/UnitTests/Calculus/PowersOfExponentialsOfOneRateIntegralTest.cs b/Sources/Tests/UnitTests/Calculus/PowersOfExponentialsOfOneRateIntegralTest.cs new file mode 100644 index 000000000..776709de0 --- /dev/null +++ b/Sources/Tests/UnitTests/Calculus/PowersOfExponentialsOfOneRateIntegralTest.cs @@ -0,0 +1,61 @@ +// +// 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 +{ + /// + /// Two powers of exponentials written apart whose logarithmic derivatives are one constant, + /// each read as a constant multiple of the one exponential: Rubi's 2.3 + /// (a + b (F^(e (c + d x)))^n)^p (G^(h (f + g x)))^(d e n ln(F)/(g h ln(G))), which the + /// derivative-of-a-product search ran past its budget on, is + /// (G^(h (f + g x)))^(...) (a + b (F^(e (c + d x)))^n)^(p + 1)/(b d e n (p + 1) ln(F) (F^(e (c + d x)))^n). + /// #233 + /// + /// + /// Pinned to moderate values: with every symbol near 5 the powers pass the range the + /// evaluation keeps, and both sides read as infinite. + /// + [Trait("Area", "Calculus")] + public sealed class PowersOfExponentialsOfOneRateIntegralTest + { + [Theory] + [InlineData("(a + b*(F^(k*(c + d*x)))^n)^p*(G^(h*(f + g*x)))^(d*k*n*ln(F)/(g*h*ln(G)))", false)] + [InlineData("(G^(h*(f + g*x)))^(d*k*n*ln(F)/(g*h*ln(G)))/(a + b*(F^(k*(c + d*x)))^n)^2", false)] + [InlineData("(G^(h*(f + g*x)))^(d*k*n*ln(F)/(g*h*ln(G)))*sqrt(a + b*(F^(k*(c + d*x)))^n)", false)] + [InlineData("(a + b*(F^(k*(c + d*x)))^n)^p*(F^(h*(f + g*x)))^(d*k*n/(g*h))", false)] + [InlineData("(a + b*(F^(k*(c + d*x)))^n)^p*(G^(h*(f + g*x)))^(d*k*n*ln(F)/(g*h*ln(G)))", true)] + public void IsIntegrated(string integrand, bool negativeBases) + { + var integral = integrand.ToEntity().Integrate("x"); + var text = integral.Stringize(); + Assert.DoesNotContain("integral(", text); + Assert.True(text.Length < 1500, $"{text.Length} characters of answer for {integrand}"); + Entity Pinned(Entity e) => e.Substitute("a", 1.3).Substitute("b", 0.7).Substitute("F", negativeBases ? -1.9 : 1.9) + .Substitute("k", 0.4).Substitute("c", 0.3).Substitute("d", 0.6).Substitute("n", 1.7).Substitute("p", 0.45) + .Substitute("G", negativeBases ? -2.6 : 2.6).Substitute("h", 0.35).Substitute("f", -0.2).Substitute("g", 0.8); + var derivative = Pinned(integral.Substitute("C", 0)).Differentiate("x"); + var original = Pinned(integrand.ToEntity()); + var compared = 0; + foreach (var at in new[] { -2.3, -1.1, -0.4, 0.3, 0.9, 1.7 }) + { + var want = original.Substitute("x", at).EvalNumerical(); + var got = derivative.Substitute("x", at).EvalNumerical(); + if (want.IsNaN) + continue; + compared++; + Assert.True(Math.Abs((double)(got - want).RealPart) + Math.Abs((double)(got - want).ImaginaryPart) + < 1e-9 * Math.Max(1, Math.Abs((double)want.RealPart) + Math.Abs((double)want.ImaginaryPart)), + $"d/dx of the antiderivative of {integrand} is {got} at x = {at}, where the integrand is {want}"); + } + Assert.True(compared >= 5, $"only {compared} points could be compared for {integrand}"); + } + } +}