From e937f3533b3c2ce03e19677116086734ab052d7f Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Wed, 21 Oct 2026 16:04:58 +0000 Subject: [PATCH] Integrate seven trigonometric shapes: reduced powers over a + b sin, an odd tangent beside sqrt(a + b sin^2), and five more - An even power of a cosine over a power of a + b sin, reduced in the power of the binomial. - An odd power of a tangent beside a root of a + b sin^2, under v = sin^2. - From the twelfth power, a power of a tangent over a power of a + a sec, cancelled. - A root of 1 - cos beside another root, under the cosine. - Roots that are a constant times a power of x, taken out as that constant. - A power of a secant over a power of a cos + b sin, in powers of a + b tan. - An odd power of a sine over a power of a + b tan^2, under the cosine with a - b named. Chain against master f7f2a7a8: pocket of 1,267 rows, 1,250 -> 1,259 solved, 0 wrong; samples unchanged; the 10 rows that moved, alone, 2 -> 9 solved: 4.1.1.2:512, 513, 4.1.7:671 and 4.3.7:108 past the budget there, 4.2.2.1:897, 898 and 4.7.7:51 declined; 4.1.1.2:516 13.1 GB -> 0.01 GB. 4.3.7:470 is declined on both. 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 | 99 ++- .../Integration/IndefiniteIntegralSolver.cs | 828 ++++++++++++++++++ .../Integration/Integration.Definition.cs | 24 + ...sideAnotherRootUnderTheSineIntegralTest.cs | 52 ++ ...wersOfABinomialInTheTangentIntegralTest.cs | 52 ++ ...inomialInTheSineByReductionIntegralTest.cs | 57 ++ ...fOnePlusASecantByCancellingIntegralTest.cs | 52 ++ ...quaredTangentUnderTheCosineIntegralTest.cs | 52 ++ ...tionOfTheSquaredSineUnderItIntegralTest.cs | 52 ++ ...tantUpToAPowerOfTheVariableIntegralTest.cs | 51 ++ 10 files changed, 1318 insertions(+), 1 deletion(-) create mode 100644 Sources/Tests/UnitTests/Calculus/ARootOfOnePlusASineBesideAnotherRootUnderTheSineIntegralTest.cs create mode 100644 Sources/Tests/UnitTests/Calculus/ASecantPowerOverAPowerOfACosineAndASineInPowersOfABinomialInTheTangentIntegralTest.cs create mode 100644 Sources/Tests/UnitTests/Calculus/AnEvenPowerOfACosineOverAPowerOfABinomialInTheSineByReductionIntegralTest.cs create mode 100644 Sources/Tests/UnitTests/Calculus/AnEvenPowerOfATangentOverAPowerOfOnePlusASecantByCancellingIntegralTest.cs create mode 100644 Sources/Tests/UnitTests/Calculus/AnOddPowerOfASineOverAPowerOfABinomialInTheSquaredTangentUnderTheCosineIntegralTest.cs create mode 100644 Sources/Tests/UnitTests/Calculus/AnOddPowerOfATangentBesideAFunctionOfTheSquaredSineUnderItIntegralTest.cs create mode 100644 Sources/Tests/UnitTests/Calculus/ByTakingOutRootsConstantUpToAPowerOfTheVariableIntegralTest.cs diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 44d906a18..f2ef6dbd3 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -532,6 +532,27 @@ written in powers of `u = a + b f`, with coefficients `c_j = sum_i p_i C(i, j) ( | `"(a+b*sin(e+f*x))^2*(A+B*sin(e+f*x))/(c+d*sin(e+f*x))^2".ToEntity().Integrate("x")` | `integral(...)` | 3,337 characters | | `"cos(c+d*x)^4*(A+C*cos(c+d*x)^2)/(a+b*cos(c+d*x))^4".ToEntity().Integrate("x")` | `integral(...)` | 10,859 characters | +### An even power of a cosine over a power of `a + b sin` is reduced in the power of the binomial + +**Answers where there were none.** `cos(c + d x)^8/(a + b sin(c + d x))^8` ran out of time, and so did +`cos^6`, `sec^2` and `sec^4` over the same eighth power and `sec^4/(a + b sin)^3`, Rubi's 4.1.1.2: with symbols for `a` +and `b` the half-angle's partial fractions did not finish. With `u = a + b sin(y)`, `b^2 cos(y)^2 = (b^2 - a^2) + 2 a u - u^2` +lowers a power of the cosine above the bar, and read for `a^2 - b^2` it lowers one below it; what is left is the integral +`I_n` of `1/u^n`, and `(n - 1)(a^2 - b^2) I_n = b cos(y) u^(1 - n) + (2 n - 3) a I_(n - 1) - (n - 2) I_(n - 2)` takes it +down to `I_1`. A sine over a binomial in the cosine is the same with `-b sin(y)`. The coefficients are summed exactly, so +the answer holds `I_1` once. For `a^2 != b^2`, the generic case, as Rubi answers it: `a + a sin` declines. `1/(a + b sin)^n` +and a polynomial in the sine over it are answered this way too, more shortly than by the half-angle +([#233](https://github.com/asc-community/AngouriMath/issues/233)). + +| Input | Was (2.5.0) | Now | +|---|---|---| +| `"cos(c+d*x)^8/(a+b*sin(c+d*x))^8".ToEntity().Integrate("x")` | `integral(...)` | 4,788 characters | +| `"sec(c+d*x)^4/(a+b*sin(c+d*x))^8".ToEntity().Integrate("x")` | `integral(...)` | 6,910 characters | +| `"sec(c+d*x)^4/(a+b*sin(c+d*x))^3".ToEntity().Integrate("x")` | `integral(...)` | 3,030 characters | +| `"cos(c+d*x)^4/(a+b*sin(c+d*x))^8".ToEntity().Integrate("x")` | `integral(...)` | 4,092 characters | +| `"sin(x)^4/(a+b*cos(x))".ToEntity().Integrate("x")` | `integral(...)` | 1,309 characters | +| `"1/(a+b*cos(c+d*x))^4".ToEntity().Integrate("x")` | `integral(...)` | 1,851 characters | + ### A difference of a negated term is printed as a sum **A printed form.** A solution and a difference were printed with the sign of a negative factor left inside: @@ -658,6 +679,18 @@ Only where the product `T B` is in the integrand; without it the rules after thi | `"x^5*(a+b*acoth(c*x))*(d+k*ln(1-c^2*x^2))".ToEntity().Integrate("x")` | `integral(...)` | 717 characters | | `"(a+b*acoth(c*x))*(d+k*ln(1-c^2*x^2))".ToEntity().Integrate("x")` | `integral(...)` | 354 characters | +### An odd power of a tangent beside a root of `a + b sin^2` is integrated under `v = sin^2` + +**Answers where there were none.** `tan(e + f x)^5/(a + b sin(e + f x)^2)^(5/2)`, Rubi's 4.1.7, ran out of time: the +substitution in the secant wrote the root as a power of `a + b - b/u^2` and spent eight seconds writing it apart. With +`v = sin(y)^2`, `tan(y)^(2 k + 1) dy = v^k/(1 - v)^(k + 1) dv/2`, and in `v` the root is of a linear. Only where a root of a +sum with a constant in it holds the sine's square ([#233](https://github.com/asc-community/AngouriMath/issues/233)). + +| Input | Was (2.5.0) | Now | +|---|---|---| +| `"tan(k+f*x)^5/(a+b*sin(k+f*x)^2)^(5/2)".ToEntity().Integrate("x")` | `integral(...)` | 4,109 characters | +| `"tan(k+f*x)^5/sqrt(a-a*sin(k+f*x)^2)".ToEntity().Integrate("x")` | `integral(...)` | 190 characters | + ### A power of x times an inverse tangent and the logarithm of its quadratic is integrated by parts **Answers where there were none.** `x^4 (a + b atan(c x)) (d + e ln(1 + c^2 x^2))` ran out of time, and so did three more @@ -834,6 +867,17 @@ over a power of a linear. That is answered with `Si` and `Ci` | `"cos(c+d*x)^3/((pe+f*x)*(a+a*sin(c+d*x)))".ToEntity().Integrate("x")` | `integral(...)` | 246 characters | | `"cos(c+d*x)^3/((pe+f*x)^2*(a+a*sin(c+d*x)))".ToEntity().Integrate("x")` | `integral(...)` | 790 characters | +### From the twelfth power, a power of a tangent over a power of `a + a sec` is cancelled + +**Answers where there were none.** `tan(c + d x)^12/(a + a sec(c + d x))^3`, Rubi's 4.5.1.4, ran out of time. `tan^2` is +`(sec - 1)(sec + 1)`, so `tan(y)^m/(k (1 + sec(y)))^n` is `tan(y)^(m - 2 n) (sec(y) - 1)^n/k^n`, and over `k (1 - sec(y))` +the same with `sec(y) + 1` over `(-k)^n`. From the twelfth power only, where the rules after it stop answering in time: +below it they answer more shortly ([#233](https://github.com/asc-community/AngouriMath/issues/233)). + +| Input | Was (2.5.0) | Now | +|---|---|---| +| `"tan(c+d*x)^12/(a+a*sec(c+d*x))^3".ToEntity().Integrate("x")` | `integral(...)` | 1,140 characters | + ### Every linear beside a block is split off, a quadratic with rational roots among them **Answers where there were none.** `sec(c + d x)/(a + b sin(c + d x)^3)` was declined, and so were @@ -1006,6 +1050,20 @@ split. It is written `b (s + r)(s^2 - r s + r^2)` now, with `r = (a/b)^(1/3)`, w | `"sec(c + d*x)^2/(a + b*sin(c + d*x)^3)".ToEntity().Integrate("x")` | `integral(...)` | 7,010 characters | | `"sec(c + d*x)^4/(a + b*sin(c + d*x)^3)".ToEntity().Integrate("x")` | `integral(...)` | 11,646 characters | +### A root of `1 - cos` beside another root is integrated under the cosine + +**Answers where there were none.** `sqrt(1 - cos(x))/sqrt(a - cos(x))`, Rubi's 4.2.2.1, was declined and +`sqrt((1 - cos(x))/(a - cos(x)))` ran out of time: the half angle at which `1 - cos` is a square reads only roots of +that kind. With `g` the other function, `sqrt(k (1 + s f)/D)` is `K g/(sqrt(D) sqrt(1 - s f))` where `K` has the square +`k` and so is constant between the zeros of `g`, and under `u = f(y)` what is left is +`1/(sqrt(1 + u) sqrt(a - u))`. For a number `k` and another root with a symbol in it only: the rules after it answer +the rest, and more shortly ([#233](https://github.com/asc-community/AngouriMath/issues/233)). + +| Input | Was (2.5.0) | Now | +|---|---|---| +| `"sqrt(1-cos(x))/sqrt(a-cos(x))".ToEntity().Integrate("x")` | `integral(...)` | 98 characters | +| `"sqrt((1-cos(x))/(a-cos(x)))".ToEntity().Integrate("x")` | `integral(...)` | 138 characters | + ### A linear factor common to a sum below the bar is taken out of it **Answers where there were none.** `1/(a x + b x/log(c x^n))` was left unevaluated. With x taken out of the sum below the @@ -1424,6 +1482,18 @@ integral of the function ([#718](https://github.com/asc-community/AngouriMath/is | `"atan(k*x/sqrt(-a*k^2/b-k^2*x^2))^m/sqrt(a+b*x^2)".ToEntity().Integrate("x")` | `integral(...)` | 320 characters | | `"1/(atan(k*x/sqrt(-a*k^2/b-k^2*x^2))^3*sqrt(a+b*x^2))".ToEntity().Integrate("x")` | `integral(...)` | 314 characters | +### Roots that are a constant times a power of x are taken out as that constant + +**Answers where there were none.** `sin(x) sqrt(b - a/x^2)/sqrt(a - b x^2)`, Rubi's 4.7.7, was declined. The roots are +`K/x` with `K = x sqrt(b - a/x^2)/sqrt(a - b x^2)`, whose square is `-1`: constant between the zeros of the roots, where +it changes sign. With `r` the degree of the roots both at infinity and at zero, `K = A/x^r` is taken as constant where its +derivative is zero at sampled points, and the integral is `K` times that of `x^r` times the rest, here `K Si(x)`. Last +but two, where everything reading the roots as written has declined ([#233](https://github.com/asc-community/AngouriMath/issues/233)). + +| Input | Was (2.5.0) | Now | +|---|---|---| +| `"sin(x)*sqrt(b-a/x^2)/sqrt(a-b*x^2)".ToEntity().Integrate("x")` | `integral(...)` | 107 characters | + ### Half an odd power of an inverse hyperbolic cosine of `c + d x^2` is integrated in error functions **Answers where there were none.** `(a + b acosh(1 + d x^2))^(1/2)` was declined, and so were thirteen more of Rubi's @@ -1504,6 +1574,21 @@ answer these integrands; the split now ends at the first term declined. More slo | `"1/(a*coth(x)+b*csch(x))^2".ToEntity().Integrate("x")` | `integral(...)` | 1,831 characters | | `"1/(1-cosh(x)^8)".ToEntity().Integrate("x")` | `integral(...)` | 1,232 characters | +### A power of a secant over a power of `a cos + b sin` is written in powers of `a + b tan` + +**Answers where there were none.** `sec(c + d x)^4/(a cos(c + d x) + b sin(c + d x))^3` and `sec^3/(a cos + b sin)^4`, Rubi's +4.7.2, ran out of time. Where the two powers together are even the quotient is a rational function of the tangent beside +`sec^2`; odd, it went to the half-angle. It is `sec(y) (1 + tan(y)^2)^k/(a + b tan(y))^n` with `2 k + 1 = m + n`, and in +powers of `w = a + b tan(y)` a term below the bar, `sec(y)/w^i`, is `cos(y)^(i - 1)/(a cos(y) + b sin(y))^i`, and one above +it a polynomial in the tangent beside the secant. From `m + n = 7`: below it the half-angle answers in time +([#233](https://github.com/asc-community/AngouriMath/issues/233)). + +| Input | Was (2.5.0) | Now | +|---|---|---| +| `"sec(c+d*x)^4/(a*cos(c+d*x)+b*sin(c+d*x))^3".ToEntity().Integrate("x")` | `integral(...)` | 2,175 characters | +| `"sec(c+d*x)^3/(a*cos(c+d*x)+b*sin(c+d*x))^4".ToEntity().Integrate("x")` | `integral(...)` | 3,401 characters | +| `"sec(c+d*x)^6/(a*cos(c+d*x)+b*sin(c+d*x))".ToEntity().Integrate("x")` | `integral(...)` | 1,151 characters | + ### A whole power of an inverse function of `c + d x^2` is integrated by its recurrence **Answers where there were none.** `(a + b acos(1 + d x^2))^4` was declined, and so were twenty-five more of Rubi's @@ -1629,6 +1714,18 @@ twice ([#1877](https://github.com/asc-community/AngouriMath/issues/1877), | `"sin(a + b*x)/((d*e - c*f)*x^3 + f)".ToEntity().Integrate("x")` | `integral(...)` | 1,416 characters | | `"sin(x)/(a*x^2 + 2*a*x + a)".ToEntity().Integrate("x")` | `integral(...)` | 246 characters | +### An odd power of a sine over a power of `a + b tan^2` is integrated under the cosine with `a - b` named + +**Answers where there were none.** `sin(e + f x)^3/(a + b tan(e + f x)^2)^3`, Rubi's 4.3.7, ran out of time in the +substitution search before Bioche's substitution was reached, and `sin^5` over the same cube took fifteen seconds. Under +`u = cos(y)`, `a + b tan(y)^2 = ((a - b) u^2 + b)/u^2`, and the rational function in `u` is asked with `a - b` named by +a symbol of its own: written in, it took seven seconds, named a tenth of one ([#233](https://github.com/asc-community/AngouriMath/issues/233)). + +| Input | Was (2.5.0) | Now | +|---|---|---| +| `"sin(k+f*x)^3/(a+b*tan(k+f*x)^2)^3".ToEntity().Integrate("x")` | `integral(...)` | 2,667 characters | +| `"sin(k+f*x)^5/(a+b*tan(k+f*x)^2)^3".ToEntity().Integrate("x")` | `integral(...)` | 6,183 characters | + ### A trigonometric function of `a + b/x` beside a rational function is integrated in `1/x` **Answers where there were none.** `sin(c + d/x)/(e + f x)` was declined: the substitution search reads a candidate's @@ -3842,7 +3939,7 @@ of the six functions read inside a power as it is | `"(c*sin(a+b*x)^n)^(1/n)".ToEntity().Integrate("x")` | `integral(...)` | 77 characters | | `"sin(c+d*x)^6/(a+b*sin(c+d*x)^2)^2".ToEntity().Integrate("x")` | `integral(...)` | 2,169 characters | | `"cot(c+d*x)^2/(a+b*sec(c+d*x)^2)^2".ToEntity().Integrate("x")` | `integral(...)` | 1,963 characters | -| `"sqrt(1-cos(x))/sqrt(a-cos(x))".ToEntity().Integrate("x")` | `integral(...)` | 70 characters | +| `"sqrt(1-cos(x))/sqrt(a-cos(x))".ToEntity().Integrate("x")` | `integral(...)` | 98 characters | ### A product of powers of the trigonometric functions, some not whole, is integrated as sine and cosine powers diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs index c309bc8d9..741f4eff6 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs @@ -33453,6 +33453,834 @@ when two.Equals(EInteger.FromInt32(2)) && e1 == MathS.e && e2 == MathS.e return answer.Nodes.Any(node => node == MathS.NaN) ? null : answer; } + /// + /// N/((a^2 - b^2)^d b^e) with N a polynomial over the rationals in a, + /// b and a third placeholder: the coefficients the reduction formulas in a power of + /// a + b sin(y) leave, kept exact, so that summing the many paths that reach one + /// term adds numerators instead of growing an expression. + /// + private readonly record struct OverTheDifferenceOfSquares(MultivariatePolynomial Numerator, int Difference, int B) + { + /// a^2 - b^2 in the placeholders, a the first and b the second. + internal static MultivariatePolynomial DifferenceOfSquares + => MultivariatePolynomial.Monomial(3, 0).Multiply(MultivariatePolynomial.Monomial(3, 0))! + .Subtract(MultivariatePolynomial.Monomial(3, 1).Multiply(MultivariatePolynomial.Monomial(3, 1))!); + + internal static OverTheDifferenceOfSquares Of(MultivariatePolynomial numerator) => new(numerator, 0, 0); + + /// Over one bar, or null where the numerator grows past what the polynomials hold. + internal OverTheDifferenceOfSquares? Plus(OverTheDifferenceOfSquares other) + { + var difference = System.Math.Max(Difference, other.Difference); + var b = System.Math.Max(B, other.B); + return Raised(difference - Difference, b - B) is { } left && other.Raised(difference - other.Difference, b - other.B) is { } right + ? new OverTheDifferenceOfSquares(left.Add(right), difference, b) : null; + } + + internal OverTheDifferenceOfSquares? Times(MultivariatePolynomial factor) + => Numerator.Multiply(factor) is { } product ? this with { Numerator = product } : null; + + private MultivariatePolynomial? Raised(int differences, int bs) + { + MultivariatePolynomial? raised = Numerator; + for (var i = 0; i < differences && raised is not null; i++) + raised = raised.Multiply(DifferenceOfSquares); + return raised is null || bs == 0 ? raised : raised.ShiftedBy(1, bs); + } + + /// In and , what divides the numerator divided out first. + internal Entity ToEntity(IReadOnlyList placeholders, Entity a, Entity b) + { + var (numerator, difference, power) = (Numerator, Difference, B); + while (difference > 0 && numerator.DivideExact(DifferenceOfSquares) is { } lower) + (numerator, difference) = (lower, difference - 1); + while (power > 0 && numerator.DivideExact(MultivariatePolynomial.Monomial(3, 1)) is { } lower) + (numerator, power) = (lower, power - 1); + Entity value = numerator.ToEntity(placeholders).Substitute(placeholders[0], a).Substitute(placeholders[1], b); + if (difference > 0) + value /= difference == 1 ? a * a - b * b : MathS.Pow(a * a - b * b, Number.Integer.Create(difference)); + if (power > 0) + value /= power == 1 ? b : MathS.Pow(b, Number.Integer.Create(power)); + return value; + } + } + + /// + /// An even power of a cosine over a whole power of a binomial in the sine of the same + /// linear, cos(y)^p/(a + b sin(y))^n, or of a sine over one in the cosine, with + /// p of either sign -- a power of the secant is one below the bar -- by the + /// reduction formulas in the power of the binomial. With u = a + b sin(y), + /// b^2 cos(y)^2 = (b^2 - a^2) + 2 a u - u^2 lowers a power of the cosine above the + /// bar, and read for a^2 - b^2, (a^2 - b^2) = 2 a u - u^2 - b^2 cos(y)^2 + /// lowers one below it; what is left is I_n, the integral of 1/u^n, and + /// (n - 1)(a^2 - b^2) I_n = b cos(y) u^(1 - n) + (2 n - 3) a I_(n - 1) - (n - 2) I_(n - 2) + /// takes it down to I_1, the one integral asked of the rest of the integrator + /// besides the powers of the sine and the secant the reductions end on. For the cosine, + /// b sin(y) is -b sin(y) in the last. + /// + /// + /// The rules after it took the whole quotient through the half-angle, where the + /// coefficients are symbols: Rubi's 4.1.1.2 cos(c + d x)^8/(a + b sin(c + d x))^8, + /// cos^6/(a + b sin)^8, sec^2/(a + b sin)^8, sec^4/(a + b sin)^8 + /// and sec^4/(a + b sin)^3 ran out of time, and cos^4/(a + b sin)^8 and + /// cos^6/(a + b sin), answered, were 40 thousand characters each. For + /// a^2 - b^2 != 0, the generic case Rubi answers: a + a sin(y) is + /// another shape, and declines. + /// https://github.com/asc-community/AngouriMath/issues/233 + /// + internal static Entity? SolveAnEvenPowerOfACosineOverAPowerOfABinomialInTheSineByReduction(Entity expr, Entity.Variable x) + { + if (!Integration.AnsweringTheQuestionAsked) + return null; + static IEnumerable<(Entity Factor, bool Below)> Factors(Entity e, bool below) => e switch + { + Mulf(var l, var r) => Factors(l, below).Concat(Factors(r, below)), + Divf(var l, var r) => Factors(l, below).Concat(Factors(r, !below)), + _ => new[] { (e, below) } + }; + Entity constant = Number.Integer.One; + Entity? binomial = null; + var n = 0; + var trigonometric = new List<(Entity Function, int Power)>(); + foreach (var (factor, below) in Factors(expr, false)) + { + if (!factor.ContainsNode(x)) + { + constant = below ? constant / factor : constant * factor; + continue; + } + var (@base, power) = factor is Powf(var b0, Number.Integer { EInteger: var e0 }) && e0.CanFitInInt32() + ? (b0, e0.ToInt32Unchecked()) : (factor, 1); + if (below) + power = -power; + if (@base is Sinf or Cosf or Secantf or Cosecantf) + trigonometric.Add((@base, power)); + else if (@base is Sumf or Minusf && (binomial is null || binomial == @base)) + { + binomial = @base; + n -= power; + } + else + return null; + } + if (binomial is null || n < 1 || n > 12) + return null; + var functions = binomial.Nodes.Where(node => node is Sinf or Cosf && node.ContainsNode(x)).Distinct().ToList(); + if (functions.Count != 1) + return null; + var function = functions[0]; + var sine = function is Sinf; + var argument = function is Sinf(var ys) ? ys : ((Cosf)function).Argument; + // The power of the co-function, its reciprocal counted below the bar. + Entity cofunction = sine ? MathS.Cos(argument) : MathS.Sin(argument); + Entity reciprocal = sine ? MathS.Sec(argument) : MathS.Cosec(argument); + var p = 0; + foreach (var (written, power) in trigonometric) + { + if (written == cofunction) + p += power; + else if (written == reciprocal) + p -= power; + else + return null; + } + // An odd power is a rational function of the binomial's own function, and 1/u is the base case. + if (p % 2 != 0 || p == 0 && n < 2 || System.Math.Abs(p) > 12) + return null; + if (!TreeAnalyzer.TryGetPolyLinear(argument, x, out var slope, out _) || Functions.PartialFractions.IsZeroAsAValue(slope)) + return null; + var s = Variable.CreateUnique(expr, "s_red"); + if (!TreeAnalyzer.TryGetPolyLinear(binomial.Substitute(function, s), s, out var b, out var a) + || a.ContainsNode(x) || b.ContainsNode(x) || a.ContainsNode(s) || b.ContainsNode(s) + || Functions.PartialFractions.IsZeroAsAValue(a) || Functions.PartialFractions.IsZeroAsAValue(b) + || VanishesIdentically(a * a - b * b)) + return null; + + // The placeholders a, b and u, in that order. + var placeholders = new[] { Variable.CreateUnique(expr, "a_red"), Variable.CreateUnique(expr, "b_red"), Variable.CreateUnique(expr, "u_red") }; + var pa = MultivariatePolynomial.Monomial(3, 0); + var pb = MultivariatePolynomial.Monomial(3, 1); + var pu = MultivariatePolynomial.Monomial(3, 2); + MultivariatePolynomial Whole(int value) => MultivariatePolynomial.Constant(3, ERational.FromInt32(value)); + // The terms: (0, j) for cos(y) u^(-j), (1, m) for I_m, and (2, q, i) for the integral of + // cos(y)^q sin(y)^i, q zero or negative, asked of the integrator. + var combination = new Dictionary<(int Kind, int First, int Second), OverTheDifferenceOfSquares>(); + bool Add(Dictionary<(int, int, int), OverTheDifferenceOfSquares> into, (int, int, int) key, OverTheDifferenceOfSquares? value) + { + if (value is not { } added) + return false; + if (!into.TryGetValue(key, out var existing)) + { + into[key] = added; + return true; + } + if (existing.Plus(added) is not { } sum) + return false; + into[key] = sum; + return true; + } + // cos(y)^q u^k, k >= 0, in powers of the sine: u^k = sum C(k, i) a^(k - i) b^i sin(y)^i. + bool AddPowers(Dictionary<(int, int, int), OverTheDifferenceOfSquares> into, int q, int k, OverTheDifferenceOfSquares factor) + { + var choose = EInteger.One; + for (var i = 0; i <= k; i++) + { + if (i > 0) + choose = choose * (k - i + 1) / i; + var monomial = pa.Power(k - i)?.Multiply(pb.Power(i)!)?.ScaleBy(ERational.FromEInteger(choose)); + if (monomial is null || !Add(into, (2, q, i), factor.Times(monomial))) + return false; + } + return true; + } + var reduced = new Dictionary<(int, int), Dictionary<(int, int, int), OverTheDifferenceOfSquares>?>(); + // cos(y)^q/u^m, q even and at most zero, in I_m and the powers. + Dictionary<(int, int, int), OverTheDifferenceOfSquares>? Reduced(int q, int m) + { + if (reduced.TryGetValue((q, m), out var known)) + return known; + Dictionary<(int, int, int), OverTheDifferenceOfSquares>? terms = new(); + var one = OverTheDifferenceOfSquares.Of(Whole(1)); + if (m <= 0) + terms = AddPowers(terms, q, -m, one) ? terms : null; + else if (q == 0) + terms[(1, m, 0)] = one; + else + { + // (a^2 - b^2)/(cos^(-q) u^m) = 2 a/(cos^(-q) u^(m - 1)) - 1/(cos^(-q) u^(m - 2)) - b^2/(cos^(-q - 2) u^m). + foreach (var (term, factor) in new[] { ((q, m - 1), pa.ScaleBy(ERational.FromInt32(2))), ((q, m - 2), Whole(-1)), ((q + 2, m), pb.Multiply(pb)!.ScaleBy(ERational.FromInt32(-1))) }) + { + if (terms is null || Reduced(term.Item1, term.Item2) is not { } below) + { + terms = null; + break; + } + foreach (var entry in below) + if (!Add(terms, entry.Key, entry.Value.Times(factor) is { } scaled ? scaled with { Difference = scaled.Difference + 1 } : null)) + { + terms = null; + break; + } + } + } + reduced[(q, m)] = terms; + return terms; + } + if (p > 0) + { + // cos(y)^p = ((b^2 - a^2) + 2 a u - u^2)^(p/2)/b^p, in powers of u over u^n. + var square = pb.Multiply(pb)!.Subtract(pa.Multiply(pa)!).Add(pa.Multiply(pu)!.ScaleBy(ERational.FromInt32(2))).Subtract(pu.Multiply(pu)!); + if (square.Power(p / 2) is not { } expanded) + return null; + // .Key and .Value: KeyValuePair has no Deconstruct on netstandard2.0. + foreach (var power in expanded.CoefficientsIn(2)) + { + var j = power.Key; + var factor = OverTheDifferenceOfSquares.Of(power.Value) with { B = p }; + if (n - j >= 1 ? !Add(combination, (1, n - j, 0), factor) : !AddPowers(combination, 0, j - n, factor)) + return null; + } + } + else if (Reduced(p, n) is { } reducedTerms) + combination = reducedTerms; + else + return null; + // I_m, m >= 2, in cos(y) u^(-j) and I_1: (m - 1)(a^2 - b^2) I_m = sigma b cos(y) u^(1 - m) + (2 m - 3) a I_(m - 1) - (m - 2) I_(m - 2). + var sigma = sine ? 1 : -1; + var recurrences = new Dictionary?>(); + Dictionary<(int, int, int), OverTheDifferenceOfSquares>? Recurrence(int m) + { + if (recurrences.TryGetValue(m, out var known)) + return known; + Dictionary<(int, int, int), OverTheDifferenceOfSquares>? terms = new(); + if (m == 1) + terms[(1, 1, 0)] = OverTheDifferenceOfSquares.Of(Whole(1)); + else + { + var over = ERational.Create(EInteger.One, EInteger.FromInt32(m - 1)); + OverTheDifferenceOfSquares Lowered(MultivariatePolynomial numerator) => new(numerator.ScaleBy(over), 1, 0); + var ok = Add(terms, (0, m - 1, 0), Lowered(pb.ScaleBy(ERational.FromInt32(sigma)))); + foreach (var (lower, factor) in new[] { (m - 1, pa.ScaleBy(ERational.FromInt32(2 * m - 3))), (m - 2, Whole(-(m - 2))) }) + { + if (!ok || lower < 1 || factor.IsZero) + continue; + if (Recurrence(lower) is not { } below) + { + ok = false; + break; + } + foreach (var entry in below) + ok &= Add(terms, entry.Key, entry.Value.Times(factor.ScaleBy(over)) is { } scaled ? scaled with { Difference = scaled.Difference + 1 } : null); + } + if (!ok) + terms = null; + } + recurrences[m] = terms; + return terms; + } + var final = new Dictionary<(int, int, int), OverTheDifferenceOfSquares>(); + foreach (var entry in combination) + { + if (entry.Key.Item1 != 1) + { + if (!Add(final, entry.Key, entry.Value)) + return null; + continue; + } + if (Recurrence(entry.Key.Item2) is not { } expandedPower) + return null; + foreach (var term in expandedPower) + if (!Add(final, term.Key, term.Value.Numerator.Multiply(entry.Value.Numerator) is { } product + ? new OverTheDifferenceOfSquares(product, term.Value.Difference + entry.Value.Difference, term.Value.B + entry.Value.B) : null)) + return null; + } + // The algebraic terms simplified apart from the integrals, which a piecewise + // answer for I_1 would otherwise take into each of its branches. + Entity algebraic = Number.Integer.Zero; + Entity integrals = Number.Integer.Zero; + foreach (var entry in final.OrderBy(entry => entry.Key)) + { + if (entry.Value.Numerator.IsZero) + continue; + var coefficient = entry.Value.ToEntity(placeholders, a, b); + var (kind, first, second) = entry.Key; + if (kind == 0) + { + algebraic += coefficient * cofunction / (first == 1 ? binomial : MathS.Pow(binomial, Number.Integer.Create(first))); + continue; + } + Entity question = kind == 1 ? 1 / binomial + : (first == 0 ? Number.Integer.One : MathS.Pow(reciprocal, Number.Integer.Create(-first))) + * (second == 0 ? Number.Integer.One : second == 1 ? function : MathS.Pow(function, Number.Integer.Create(second))); + if (Integration.ComputeAsAQuestionOfItsOwn(question.InnerSimplified, x, integrateByParts: true) is not { } integrated) + return null; + integrals += coefficient * integrated; + } + var answer = constant * ((algebraic / slope).InnerSimplified + integrals); + return answer.Nodes.Any(node => node == MathS.NaN) ? null : answer; + } + + /// + /// A power of a secant over a whole power of a cos(y) + b sin(y), the two powers + /// together odd, sec(y)^m/(a cos(y) + b sin(y))^n, in powers of the binomial + /// w = a + b tan(y): the quotient is sec(y) (1 + tan(y)^2)^k/w^n with + /// 2 k + 1 = m + n, and with tan(y) = (w - a)/b the power + /// (1 + tan(y)^2)^k = ((w - a)^2 + b^2)^k/b^(2 k) = sum d_j w^j. A term below the + /// bar, sec(y)/w^i, is cos(y)^(i - 1)/(a cos(y) + b sin(y))^i, and one above + /// it a polynomial in the tangent beside the secant; each is its own question. + /// + /// + /// Where m + n is even the quotient is a rational function of the tangent beside + /// sec(y)^2, which the substitution answers; odd, it took the half-angle, and with + /// symbols for a and b Rubi's 4.7.2 sec(c + d x)^4/(a cos + b sin)^3 + /// and sec^3/(a cos + b sin)^4 ran out of time, and sec^6/(a cos + b sin), + /// answered, was 5.6 thousand characters. From m + n = 7, where the half-angle + /// stops answering in time. + /// https://github.com/asc-community/AngouriMath/issues/233 + /// + internal static Entity? SolveASecantPowerOverAPowerOfACosineAndASineInPowersOfABinomialInTheTangent(Entity expr, Entity.Variable x) + { + if (!Integration.AnsweringTheQuestionAsked) + return null; + static IEnumerable<(Entity Factor, bool Below)> Factors(Entity e, bool below) => e switch + { + Mulf(var l, var r) => Factors(l, below).Concat(Factors(r, below)), + Divf(var l, var r) => Factors(l, below).Concat(Factors(r, !below)), + _ => new[] { (e, below) } + }; + Entity constant = Number.Integer.One; + Entity? combination = null; + Entity? argument = null; + var n = 0; + var m = 0; + foreach (var (factor, below) in Factors(expr, false)) + { + if (!factor.ContainsNode(x)) + { + constant = below ? constant / factor : constant * factor; + continue; + } + var (@base, power) = factor is Powf(var b0, Number.Integer { EInteger: var e0 }) && e0.CanFitInInt32() + ? (b0, e0.ToInt32Unchecked()) : (factor, 1); + if (below) + power = -power; + switch (@base) + { + case Secantf(var y) when argument is null || argument == y: + argument = y; + m += power; + break; + case Cosf(var y) when argument is null || argument == y: + argument = y; + m -= power; + break; + case Sumf or Minusf when combination is null || combination == @base: + combination = @base; + n -= power; + break; + default: + return null; + } + } + // From m + n = 7: below it the half-angle answers in time, and this, asking up to five + // questions, was no faster on `sec(y)/(a cos(y) + b sin(y))^4`. + if (combination is null || argument is null || m < 1 || n < 1 || (m + n) % 2 == 0 || m + n < 7 || m + n > 13) + return null; + if (!TreeAnalyzer.TryGetPolyLinear(argument, x, out var slope, out _) || Functions.PartialFractions.IsZeroAsAValue(slope)) + return null; + // a cos(y) + b sin(y), nothing else. + var cosine = Variable.CreateUnique(expr, "c_tan"); + var sine = Variable.CreateUnique(expr, "s_tan"); + var inBoth = combination.Substitute(MathS.Cos(argument), cosine).Substitute(MathS.Sin(argument), sine); + if (inBoth.ContainsNode(x) + || !TreeAnalyzer.TryGetPolyLinear(inBoth, cosine, out var a, out var rest) + || !TreeAnalyzer.TryGetPolyLinear(rest, sine, out var b, out var free) + || a.ContainsNode(sine) || !Functions.PartialFractions.IsZeroAsAValue(free) + || Functions.PartialFractions.IsZeroAsAValue(a) || Functions.PartialFractions.IsZeroAsAValue(b)) + return null; + var k = (m + n - 1) / 2; + // The placeholders a, b and w, in that order; every coefficient is over b^(2 k). + var placeholders = new[] { Variable.CreateUnique(expr, "a_tan"), Variable.CreateUnique(expr, "b_tan"), Variable.CreateUnique(expr, "w_tan") }; + var pa = MultivariatePolynomial.Monomial(3, 0); + var pb = MultivariatePolynomial.Monomial(3, 1); + var pw = MultivariatePolynomial.Monomial(3, 2); + var shifted = pw.Subtract(pa); + if (shifted.Multiply(shifted)?.Add(pb.Multiply(pb)!).Power(k) is not { } expanded) + return null; + // The terms: (true, i) for cos(y)^(i - 1)/(a cos(y) + b sin(y))^i, (false, i) for sec(y) tan(y)^i. + var terms = new Dictionary<(bool Below, int Power), MultivariatePolynomial>(); + void Add((bool, int) key, MultivariatePolynomial value) + => terms[key] = terms.TryGetValue(key, out var existing) ? existing.Add(value) : value; + // .Key and .Value: KeyValuePair has no Deconstruct on netstandard2.0. + foreach (var power in expanded.CoefficientsIn(2)) + { + var (j, d) = (power.Key, power.Value); + if (j < n) + { + Add((true, n - j), d); + continue; + } + // sec(y) w^(j - n), w = a + b tan(y), in powers of the tangent. + var choose = EInteger.One; + for (var i = 0; i <= j - n; i++) + { + if (i > 0) + choose = choose * (j - n - i + 1) / i; + if (pa.Power(j - n - i)?.Multiply(pb.Power(i)!)?.Multiply(d)?.ScaleBy(ERational.FromEInteger(choose)) is not { } term) + return null; + Add((false, i), term); + } + } + Entity answer = Number.Integer.Zero; + foreach (var entry in terms.OrderBy(entry => entry.Key)) + { + if (entry.Value.IsZero) + continue; + // Over b^(2 k), with the powers of b the numerator holds divided out first. + var (numerator, power) = (entry.Value, 2 * k); + while (power > 0 && numerator.DivideExact(pb) is { } lower) + (numerator, power) = (lower, power - 1); + Entity coefficient = numerator.ToEntity(placeholders).Substitute(placeholders[0], a).Substitute(placeholders[1], b); + if (power > 0) + coefficient /= power == 1 ? b : MathS.Pow(b, Number.Integer.Create(power)); + var (isBelow, i) = entry.Key; + Entity question = isBelow + ? (i == 1 ? Number.Integer.One : i == 2 ? MathS.Cos(argument) : MathS.Pow(MathS.Cos(argument), Number.Integer.Create(i - 1))) + / (i == 1 ? combination : MathS.Pow(combination, Number.Integer.Create(i))) + : MathS.Sec(argument) * (i == 0 ? Number.Integer.One : i == 1 ? MathS.Tan(argument) : MathS.Pow(MathS.Tan(argument), Number.Integer.Create(i))); + if (Integration.ComputeAsAQuestionOfItsOwn(question.InnerSimplified, x, integrateByParts: true) is not { } integrated) + return null; + answer += coefficient * integrated; + } + answer = constant * answer; + return answer.Nodes.Any(node => node == MathS.NaN) ? null : answer; + } + + /// + /// An odd power of a sine over a whole power of a + b tan(y)^2, under + /// u = cos(y): a + b tan(y)^2 = ((a - b) u^2 + b)/u^2 and + /// sin(y)^m dy = -(1 - u^2)^((m - 1)/2) du, so the integrand is the rational + /// function -(1 - u^2)^((m - 1)/2) u^(2 n)/((a - b) u^2 + b)^n, asked with + /// a - b named by a symbol of its own and written back into the answer. + /// + /// + /// Bioche's substitution, which comes after the substitution search: the search ran out + /// of time on Rubi's 4.3.7 sin(e + f x)^3/(a + b tan(e + f x)^2)^3, and took + /// fifteen seconds on sin^5/(a + b tan^2)^3. The rational function with + /// a - b written in it took seven seconds, where named it takes a tenth of one. + /// The quadratic has no linear term, so the name hides no relation between its + /// coefficients; a = b is another shape, and declines. + /// https://github.com/asc-community/AngouriMath/issues/233 + /// + internal static Entity? SolveAnOddPowerOfASineOverAPowerOfABinomialInTheSquaredTangentUnderTheCosine(Entity expr, Entity.Variable x) + { + if (!Integration.AnsweringTheQuestionAsked) + return null; + static IEnumerable<(Entity Factor, bool Below)> Factors(Entity e, bool below) => e switch + { + Mulf(var l, var r) => Factors(l, below).Concat(Factors(r, below)), + Divf(var l, var r) => Factors(l, below).Concat(Factors(r, !below)), + _ => new[] { (e, below) } + }; + Entity constant = Number.Integer.One; + Entity? binomial = null; + Entity? argument = null; + var n = 0; + var m = 0; + foreach (var (factor, below) in Factors(expr, false)) + { + if (!factor.ContainsNode(x)) + { + constant = below ? constant / factor : constant * factor; + continue; + } + var (@base, power) = factor is Powf(var b0, Number.Integer { EInteger: var e0 }) && e0.CanFitInInt32() + ? (b0, e0.ToInt32Unchecked()) : (factor, 1); + if (below) + power = -power; + switch (@base) + { + case Sinf(var y) when argument is null || argument == y: + argument = y; + m += power; + break; + case Sumf or Minusf when binomial is null || binomial == @base: + binomial = @base; + n -= power; + break; + default: + return null; + } + } + if (binomial is null || argument is null || m < 1 || m % 2 == 0 || m > 15 || n < 1 || n > 8) + return null; + if (!TreeAnalyzer.TryGetPolyLinear(argument, x, out var slope, out _) || Functions.PartialFractions.IsZeroAsAValue(slope)) + return null; + var tangent = MathS.Tan(argument); + var t = Variable.CreateUnique(expr, "t_sq"); + var inT = binomial.Substitute(tangent, t); + if (inT.ContainsNode(x) || !TreeAnalyzer.TryGetPolynomial(inT, t, out var read) + || read.Count != 2 || !read.TryGetValue(EInteger.Zero, out var a) || !read.TryGetValue(EInteger.FromInt32(2), out var b) + || Functions.PartialFractions.IsZeroAsAValue(b)) + return null; + var difference = (a - b).InnerSimplified; + if (VanishesIdentically(difference)) + return null; + var named = difference is Number or Variable ? difference : Variable.CreateUnique(expr, "g_sq"); + var u = Variable.CreateUnique(expr + named, "u_sq"); + var k = (m - 1) / 2; + Entity inU = MathS.Pow(u, Number.Integer.Create(2 * n)) / (n == 1 ? named * MathS.Sqr(u) + b : MathS.Pow(named * MathS.Sqr(u) + b, Number.Integer.Create(n))); + inU = -(k == 0 ? inU : MathS.Pow(1 - MathS.Sqr(u), Number.Integer.Create(k)) * inU); + if (Integration.ComputeAsAQuestionOfItsOwn(inU, u, integrateByParts: true) is not { } integrated) + return null; + if (named is Variable name && name != difference) + integrated = integrated.Substitute(name, difference); + var answer = constant * integrated.Substitute(u, MathS.Cos(argument)) / slope; + return answer.Nodes.Any(node => node == MathS.NaN) ? null : answer; + } + + /// + /// An odd power of a tangent beside a function of the square of the sine of the same + /// linear, tan(y)^(2 k + 1) F(sin(y)^2), under v = sin(y)^2: with + /// dv = 2 sin(y) cos(y) dy, tan(y)^(2 k + 1) dy = v^k/(1 - v)^(k + 1) dv/2, + /// and v^k F(v)/(2 (1 - v)^(k + 1)) is asked of the rest of the integrator. + /// + /// + /// The substitution in the secant after it writes a + b sin(y)^2 as + /// a + b (1 - 1/u^2), and writing the power of that quotient apart took eight + /// seconds on Rubi's 4.1.7 tan(e + f x)^5/(a + b sin(e + f x)^2)^(5/2), which + /// ran out of time; in v it is a power of a linear beside a rational function, + /// answered in a tenth of a second. Only where a root holds the sine's square: a + /// rational function of it the secant's substitution answers as shortly. + /// https://github.com/asc-community/AngouriMath/issues/233 + /// + internal static Entity? SolveAnOddPowerOfATangentBesideAFunctionOfTheSquaredSineUnderIt(Entity expr, Entity.Variable x) + { + if (!Integration.AnsweringTheQuestionAsked) + return null; + static IEnumerable<(Entity Factor, bool Below)> Factors(Entity e, bool below) => e switch + { + Mulf(var l, var r) => Factors(l, below).Concat(Factors(r, below)), + Divf(var l, var r) => Factors(l, below).Concat(Factors(r, !below)), + _ => new[] { (e, below) } + }; + Entity constant = Number.Integer.One; + Entity rest = Number.Integer.One; + Entity? argument = null; + var j = 0; + foreach (var (factor, below) in Factors(expr, false)) + { + if (!factor.ContainsNode(x)) + { + constant = below ? constant / factor : constant * factor; + continue; + } + var (@base, power) = factor is Powf(var b0, Number.Integer { EInteger: var e0 }) && e0.CanFitInInt32() + ? (b0, e0.ToInt32Unchecked()) : (factor, 1); + if (@base is Tanf(var y) && (argument is null || argument == y)) + { + argument = y; + j += below ? -power : power; + } + else + rest = below ? rest / factor : rest * factor; + } + if (argument is null || j < 1 || j % 2 == 0 || j > 15 || rest == Number.Integer.One) + return null; + if (!TreeAnalyzer.TryGetPolyLinear(argument, x, out var slope, out _) || Functions.PartialFractions.IsZeroAsAValue(slope)) + return null; + var v = Variable.CreateUnique(expr, "v_sq"); + var sine = MathS.Sin(argument); + var inV = rest.Replace(node => node is Powf(var s0, Number.Integer { EInteger: var e1 }) && s0 == sine && !e1.IsZero && e1.IsEven && e1.CanFitInInt32() + ? MathS.Pow(v, Number.Integer.Create(e1.ToInt32Unchecked() / 2)) : node); + // A root of a sum in v with a constant in it, which the secant's substitution writes + // apart: a rational function of the sine's square it answers as one in the secant, and as + // shortly, and `tan(y)/sqrt(a sin(y)^2)` in 110 characters, where in v it is 627. + if (inV.ContainsNode(x) || !inV.ContainsNode(v) + || !inV.Nodes.Any(node => node is Powf(var radicand, Number.Rational and not Number.Integer) && radicand.ContainsNode(v) + && TreeAnalyzer.TryGetPolynomial(radicand, v, out var inPowers) && inPowers.Count >= 2 && inPowers.ContainsKey(EInteger.Zero))) + return null; + var k = (j - 1) / 2; + Entity measure = (k == 0 ? Number.Integer.One : k == 1 ? v : MathS.Pow(v, Number.Integer.Create(k))) + / (2 * (k == 0 ? 1 - v : MathS.Pow(1 - v, Number.Integer.Create(k + 1)))); + if (Integration.ComputeAsAQuestionOfItsOwn(measure * inV, v, integrateByParts: true) is not { } integrated) + return null; + var answer = constant * integrated.Substitute(v, MathS.Sqr(sine)) / slope; + return answer.Nodes.Any(node => node == MathS.NaN) ? null : answer; + } + + /// + /// An even power of a tangent over a power of k (1 +- sec(y)), cancelled where the + /// power above is at least twice the one below: tan^2 = (sec - 1)(sec + 1), so + /// tan(y)^m/(k (1 + sec(y)))^n is tan(y)^(m - 2 n) (sec(y) - 1)^n/k^n, and + /// over k (1 - sec(y)) it is tan(y)^(m - 2 n) (sec(y) + 1)^n/(-k)^n. + /// + /// + /// Rubi's 4.5.1.4 tan(c + d x)^12/(a + a sec(c + d x))^3 ran out of time, and + /// tan(c + d x)^6 (sec(c + d x) - 1)^3/a^3 is answered in a third of a second. + /// From the twelfth power only, where the rules after it stop answering in time -- + /// tan^12/(a + a sec) and tan^14/(a + a sec)^3 ran past a minute -- since + /// below it they answer more shortly: tan^10/(a + a sec)^3 in 507 characters + /// where cancelled it is 851. + /// https://github.com/asc-community/AngouriMath/issues/233 + /// + internal static Entity? SolveAnEvenPowerOfATangentOverAPowerOfOnePlusASecantByCancelling(Entity expr, Entity.Variable x) + { + if (!Integration.AnsweringTheQuestionAsked) + return null; + static IEnumerable<(Entity Factor, bool Below)> Factors(Entity e, bool below) => e switch + { + Mulf(var l, var r) => Factors(l, below).Concat(Factors(r, below)), + Divf(var l, var r) => Factors(l, below).Concat(Factors(r, !below)), + _ => new[] { (e, below) } + }; + Entity constant = Number.Integer.One; + Entity? binomial = null; + Entity? argument = null; + var n = 0; + var m = 0; + foreach (var (factor, below) in Factors(expr, false)) + { + if (!factor.ContainsNode(x)) + { + constant = below ? constant / factor : constant * factor; + continue; + } + var (@base, power) = factor is Powf(var b0, Number.Integer { EInteger: var e0 }) && e0.CanFitInInt32() + ? (b0, e0.ToInt32Unchecked()) : (factor, 1); + if (below) + power = -power; + switch (@base) + { + case Tanf(var y) when argument is null || argument == y: + argument = y; + m += power; + break; + case Sumf or Minusf when binomial is null || binomial == @base: + binomial = @base; + n -= power; + break; + default: + return null; + } + } + if (binomial is null || argument is null || n < 1 || m % 2 != 0 || m < 2 * n || m < 12 || m > 24) + return null; + // k (1 + sigma sec(y)). + var secant = MathS.Sec(argument); + var s = Variable.CreateUnique(expr, "s_tc"); + if (!TreeAnalyzer.TryGetPolyLinear(binomial.Substitute(secant, s), s, out var slope, out var k) + || slope.ContainsNode(x) || k.ContainsNode(x) || Functions.PartialFractions.IsZeroAsAValue(k)) + return null; + var sigma = (slope / k).InnerSimplified; + int sign; + if (Functions.PartialFractions.IsZeroAsAValue((sigma - 1).InnerSimplified)) sign = 1; + else if (Functions.PartialFractions.IsZeroAsAValue((sigma + 1).InnerSimplified)) sign = -1; + else return null; + var left = m - 2 * n; + var tangent = MathS.Tan(argument); + Entity cancelled = (left == 0 ? Number.Integer.One : MathS.Pow(tangent, Number.Integer.Create(left))) + * (n == 1 ? secant - sign : MathS.Pow(secant - sign, Number.Integer.Create(n))) + / (n == 1 ? sign * k : MathS.Pow(sign * k, Number.Integer.Create(n))); + return Integration.ComputeAsAQuestionOfItsOwn((constant * cancelled).InnerSimplified, x, integrateByParts: true); + } + + /// + /// A product of roots that is a constant times a whole power of the variable, though + /// written with it, beside something else: with A the roots and r the sum of + /// their exponents times the degrees of what is under them, K = A/x^r has the + /// derivative zero wherever it is real, and integral A T dx = K integral x^r T dx. + /// sqrt(b - a/x^2)/sqrt(a - b x^2) is K/x, with K^2 = -1. + /// + /// + /// r is the degree of A both at infinity and at zero, which a constant times + /// x^r needs, and K is taken as constant only where its derivative is zero at + /// the sampled points; the answer is checked. Rubi's 4.7.7 + /// sin(x) sqrt(b - a/x^2)/sqrt(a - b x^2), which is K Si(x), was declined. + /// Late, where everything reading the integrand as written has declined. + /// https://github.com/asc-community/AngouriMath/issues/233 + /// + internal static Entity? SolveByTakingOutRootsConstantUpToAPowerOfTheVariable(Entity expr, Entity.Variable x) + { + if (!Integration.AnsweringTheQuestionAsked) + return null; + static IEnumerable<(Entity Factor, bool Below)> Factors(Entity e, bool below) => e switch + { + Mulf(var l, var r) => Factors(l, below).Concat(Factors(r, below)), + Divf(var l, var r) => Factors(l, below).Concat(Factors(r, !below)), + _ => new[] { (e, below) } + }; + Entity roots = Number.Integer.One; + Entity rest = Number.Integer.One; + var atInfinity = ERational.Zero; + var atZero = ERational.Zero; + foreach (var (factor, below) in Factors(expr, false)) + { + if (factor is Powf(var radicand, Number.Rational { ERational: var exponent }) && radicand.ContainsNode(x) + && exponent.ToLowestTerms() is var q && !q.Denominator.Equals(EInteger.One)) + { + // The degrees of what is under the root at infinity and at zero, negative powers read. + if (!TreeAnalyzer.TryGetPolynomial(radicand, x, out var read) || read.Count == 0 + || read.Values.Any(value => value.ContainsNode(x) || Functions.PartialFractions.IsZeroAsAValue(value))) + return null; + var signed = below ? q.Negate() : q; + atInfinity += signed * ERational.FromEInteger(read.Keys.Max()!); + atZero += signed * ERational.FromEInteger(read.Keys.Min()!); + roots = below ? roots / factor : roots * factor; + } + else + rest = below ? rest / factor : rest * factor; + } + atInfinity = atInfinity.ToLowestTerms(); + if (roots == Number.Integer.One || !rest.ContainsNode(x) || atInfinity.CompareTo(atZero) != 0 + || !atInfinity.Denominator.Equals(EInteger.One) || !atInfinity.Numerator.CanFitInInt32()) + return null; + var r = atInfinity.Numerator.ToInt32Unchecked(); + if (System.Math.Abs(r) > 6) + return null; + // As written, x^(-r) beside the roots: simplified, the roots were taken apart and put back. + var constant = r == 0 ? roots : (r == -1 ? x : MathS.Pow(x, Number.Integer.Create(-r))) * roots; + // Constant where its derivative is zero at the sampled points: its value is not one + // number across them, since `sqrt(b - a/x^2) x/sqrt(a - b x^2)` is i on one side of + // sqrt(a/b), where both roots vanish, and -i on the other. + if (!constant.ContainsNode(x) + || !Functions.PartialFractions.HoldsAtSampledPoints(constant.Differentiate(x), Number.Integer.Zero, x)) + return null; + var power = r == 0 ? rest : r == 1 ? x * rest : MathS.Pow(x, Number.Integer.Create(r)) * rest; + if (Integration.ComputeAsAQuestionOfItsOwn(power.InnerSimplified, x, integrateByParts: true) is not { } integrated) + return null; + var answer = constant * integrated; + return answer.Nodes.Any(node => node == MathS.NaN) || Functions.PartialFractions.DerivativeDiffersWhereReal(answer, expr, x) ? null : answer; + } + + /// + /// The root of k (1 +- f(y)), or of it over something in f(y), beside another + /// root of something in f(y), for f a sine or a cosine, under + /// u = f(y): with g the other function, R = sqrt(k (1 + s f)/D) is + /// K g/(sqrt(D) sqrt(1 - s f)) where K = R sqrt(D) sqrt(1 - s f)/g has the square + /// k, and so is constant between the zeros of g; and g dy is + /// du for the sine and -du for the cosine. What is asked in u is the + /// rest over sqrt(D(u)) sqrt(1 - s u). + /// + /// + /// The half angle at which 1 +- f is a square reads only roots of that kind, and + /// Rubi's 4.2.2.1 sqrt(1 - cos(x))/sqrt(a - cos(x)) was declined and + /// sqrt((1 - cos(x))/(a - cos(x))) ran out of time; under u = cos(x) they are + /// 1/(sqrt(1 + u) sqrt(a - u)), answered in a tenth of a second. Only for a number + /// k, and where another root of something in the function with a symbol in it is + /// there: the rules after it answer the rest, and more shortly. + /// https://github.com/asc-community/AngouriMath/issues/233 + /// + internal static Entity? SolveARootOfOnePlusASineBesideAnotherRootUnderTheSine(Entity expr, Entity.Variable x) + { + if (!Integration.AnsweringTheQuestionAsked) + return null; + var functions = expr.Nodes.Where(node => node.ContainsNode(x) && node is Sinf or Cosf or Tanf or Cotanf or Secantf or Cosecantf).Distinct().ToList(); + if (functions.Count != 1 || functions[0] is not (Sinf or Cosf)) + return null; + var function = functions[0]; + var sine = function is Sinf; + var argument = function is Sinf(var ys) ? ys : ((Cosf)function).Argument; + if (!TreeAnalyzer.TryGetPolyLinear(argument, x, out var slope, out _) || Functions.PartialFractions.IsZeroAsAValue(slope)) + return null; + var u = Variable.CreateUnique(expr, "u_root"); + // k (1 + s f) as written, the sign s, or null. + int? Sign(Entity radicand) + { + if (!TreeAnalyzer.TryGetPolyLinear(radicand.Substitute(function, u), u, out var coefficient, out var k) + || coefficient.ContainsNode(x) || k.ContainsNode(x) || Functions.PartialFractions.IsZeroAsAValue(k)) + return null; + var ratio = (coefficient / k).InnerSimplified; + return Functions.PartialFractions.IsZeroAsAValue((ratio - 1).InnerSimplified) ? 1 + : Functions.PartialFractions.IsZeroAsAValue((ratio + 1).InnerSimplified) ? -1 : null; + } + static IEnumerable<(Entity Factor, bool Below)> Factors(Entity e, bool below) => e switch + { + Mulf(var l, var r) => Factors(l, below).Concat(Factors(r, below)), + Divf(var l, var r) => Factors(l, below).Concat(Factors(r, !below)), + _ => new[] { (e, below) } + }; + var factors = Factors(expr, false).ToList(); + for (var at = 0; at < factors.Count; at++) + { + var (root, rootBelow) = factors[at]; + if (rootBelow || root is not Powf(var radicand, Number.Rational { ERational: var half }) + || half.CompareTo(ERational.Create(1, 2)) != 0 || !radicand.ContainsNode(x)) + continue; + var (above, below) = Functions.SingleQuotient.Of(radicand); + // A number for k: `sqrt(a + a sin(y))` beside another root the rules after it answer, + // and more shortly than this, `sqrt(a + a sin(y)) sqrt(c + d sin(y))/sin(y)` in 1,071 + // characters where this wrote 1,879. + if (above.Vars.Any(v => v != x) || Sign(above) is not { } s) + continue; + // Everything else, and another root of something in the function among it or below the root. + Entity rest = Number.Integer.One; + for (var other = 0; other < factors.Count; other++) + if (other != at) + rest = factors[other].Below ? rest / factors[other].Factor : rest * factors[other].Factor; + // With a symbol in it: over numbers the half angle answers, `sqrt(1 - cos(x))/sqrt(2 - cos(x))` + // in 95 characters where this wrote over five hundred. + var anotherRoot = below.ContainsNode(x) && below.Vars.Any(v => v != x) + || rest.Nodes.Any(node => node is Powf(var r0, Number.Rational and not Number.Integer) + && r0.ContainsNode(x) && r0.Vars.Any(v => v != x) && Sign(r0) is null); + if (!anotherRoot) + continue; + Entity complement = sine ? MathS.Cos(argument) : MathS.Sin(argument); + Entity conjugate = s == 1 ? 1 - function : 1 + function; + var constant = root * (below.ContainsNode(x) ? MathS.Sqrt(below) : Number.Integer.One) * MathS.Sqrt(conjugate) / complement; + // g dy = du for the sine, -du for the cosine. + var inU = ((sine ? rest : -rest) / ((below.ContainsNode(x) ? MathS.Sqrt(below) : Number.Integer.One) * MathS.Sqrt(conjugate))) + .Substitute(function, u); + if (inU.ContainsNode(x)) + continue; + if (Integration.ComputeAsAQuestionOfItsOwn(inU.InnerSimplified, u, integrateByParts: true) is not { } integrated) + return null; + var answer = constant * integrated.Substitute(u, function) / slope; + return answer.Nodes.Any(node => node == MathS.NaN) || Functions.PartialFractions.DerivativeDiffersWhereReal(answer, expr, x) ? null : answer; + } + return null; + } + /// /// A power of x times a whole power, past the first, of a sum holding an inverse function or a /// logarithm of a power of x, x^m (a + b atanh(c x^n))^p, under t = x^n where diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs b/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs index 466ed1a97..2112f2f44 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs @@ -719,6 +719,26 @@ private static Entity WithItsPiecewiseSumsCombined(Entity expr) // A power of x times a power of a sum holding an inverse function of x^n, under t = x^n, // before the rules that timed out on `x^3 (a + b atanh(c x^2))^2`. if ((answer = IndefiniteIntegralSolver.SolveAPowerOfXTimesAFunctionOfAnotherUnderThatPower(expr, x)) is { }) return answer; + // An even power of a cosine over a whole power of a binomial in the sine, or the other way + // round, by the reduction formulas in the power of the binomial, before the half-angle ran out + // of time on `cos(c + d x)^8/(a + b sin(c + d x))^8` with symbols for coefficients. + if ((answer = IndefiniteIntegralSolver.SolveAnEvenPowerOfACosineOverAPowerOfABinomialInTheSineByReduction(expr, x)) is { }) return answer; + // A power of a secant over one of a cos(y) + b sin(y), the two together odd, in powers of + // a + b tan(y), before the half-angle ran out of time on `sec(c + d x)^4/(a cos + b sin)^3`. + if ((answer = IndefiniteIntegralSolver.SolveASecantPowerOverAPowerOfACosineAndASineInPowersOfABinomialInTheTangent(expr, x)) is { }) return answer; + // An odd power of a sine over a power of a + b tan(y)^2 under u = cos(y), a - b named, before + // the substitution search ran out of time on `sin(e + f x)^3/(a + b tan(e + f x)^2)^3`. + if ((answer = IndefiniteIntegralSolver.SolveAnOddPowerOfASineOverAPowerOfABinomialInTheSquaredTangentUnderTheCosine(expr, x)) is { }) return answer; + // An odd power of a tangent beside a function of sin(y)^2 under v = sin(y)^2, before the + // secant's substitution took eight seconds on `tan(e + f x)^5/(a + b sin(e + f x)^2)^(5/2)`. + if ((answer = IndefiniteIntegralSolver.SolveAnOddPowerOfATangentBesideAFunctionOfTheSquaredSineUnderIt(expr, x)) is { }) return answer; + // From the twelfth power, an even power of a tangent over a power of k (1 +- sec), cancelled: + // the rules after it ran out of time on `tan(c + d x)^12/(a + a sec(c + d x))^3`. + if ((answer = IndefiniteIntegralSolver.SolveAnEvenPowerOfATangentOverAPowerOfOnePlusASecantByCancelling(expr, x)) is { }) return answer; + // A root of k (1 +- sin) or of k (1 +- cos), or of it over something in the function, beside + // another root, under u = sin or u = cos: the half angle reads only roots of the first kind, + // and `sqrt((1 - cos(x))/(a - cos(x)))` ran out of time. + if ((answer = IndefiniteIntegralSolver.SolveARootOfOnePlusASineBesideAnotherRootUnderTheSine(expr, x)) is { }) return answer; // A polynomial in a sine or a cosine over a whole power of a binomial in it, in powers of the // binomial, before the half-angle wrote 30 million characters for `(A + B cos + C cos^2)/(a + b cos)^4`. if ((answer = IndefiniteIntegralSolver.SolveAPolynomialInASineOverAPowerOfABinomialInItInPowersOfTheBinomial(expr, x)) is { }) return answer; @@ -1612,6 +1632,10 @@ private static Entity WithItsPiecewiseSumsCombined(Entity expr) // Two square roots as one, up to a factor whose square is a constant, in front: // `sqrt(b - a/x^2)/sqrt(a - b x^2)` is that factor over x. if ((answer = IndefiniteIntegralSolver.SolveByWritingTwoRootsAsOneUpToAConstantFactor(expr, x, integrateByParts)) is { }) return answer; + // Roots that are a constant times a power of the variable, though written with it, taken + // out as that constant: `sin(x) sqrt(b - a/x^2)/sqrt(a - b x^2)` is K sin(x)/x. As late, + // since it fires only where everything reading the roots as written has declined. + if ((answer = IndefiniteIntegralSolver.SolveByTakingOutRootsConstantUpToAPowerOfTheVariable(expr, x)) is { }) return answer; // The sign of a real-valued factor is constant between its zeros, and goes in // front of the antiderivative of the rest. After every rule that reads the sign // where it stands: `cos(x) sgn(sin(x))` is `|sin(x)|` by the substitution, and diff --git a/Sources/Tests/UnitTests/Calculus/ARootOfOnePlusASineBesideAnotherRootUnderTheSineIntegralTest.cs b/Sources/Tests/UnitTests/Calculus/ARootOfOnePlusASineBesideAnotherRootUnderTheSineIntegralTest.cs new file mode 100644 index 000000000..97c609572 --- /dev/null +++ b/Sources/Tests/UnitTests/Calculus/ARootOfOnePlusASineBesideAnotherRootUnderTheSineIntegralTest.cs @@ -0,0 +1,52 @@ +// +// 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 +{ + /// + /// The root of k (1 +- f(y)), or of it over something in f(y), beside another root of + /// something in the function, under u = f(y), the factor constant between the zeros of the + /// other function in front. Rubi's 4.2.2.1 sqrt((1 - cos(x))/(a - cos(x))) ran out of time. + /// #233 + /// + [Trait("Area", "Calculus")] + public sealed class ARootOfOnePlusASineBesideAnotherRootUnderTheSineIntegralTest + { + [Theory] + [InlineData("sqrt(1 - cos(x))/sqrt(a - cos(x))", 1.3, 0.7)] + [InlineData("sqrt((1 - cos(x))/(a - cos(x)))", 2.9, 0.7)] + [InlineData("sqrt(1 - sin(c + d*x))/sqrt(a + b*sin(c + d*x))", 1.3, 0.7)] + [InlineData("sqrt(1 + cos(x))/sqrt(a + b*cos(x))", 1.3, 0.7)] + public void UnderTheFunction(string integrand, double a, double b) + { + var integral = integrand.ToEntity().Integrate("x"); + var text = integral.Stringize(); + Assert.DoesNotContain("integral(", text); + Assert.True(text.Length < 3000, $"{text.Length} characters of answer for {integrand}"); + Entity Pinned(Entity e) => e.Substitute("a", a).Substitute("b", b).Substitute("c", 0.4).Substitute("d", 0.3).Substitute("f", 0.6).Substitute("k", 0.2); + var derivative = Pinned(integral.Substitute("C", 0)).Differentiate("x"); + var original = Pinned(integrand.ToEntity()); + var compared = 0; + foreach (var at in new[] { 0.3, 0.8, 1.3, 2.9, 4.1, 5.3 }) + { + 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-8 * 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/ASecantPowerOverAPowerOfACosineAndASineInPowersOfABinomialInTheTangentIntegralTest.cs b/Sources/Tests/UnitTests/Calculus/ASecantPowerOverAPowerOfACosineAndASineInPowersOfABinomialInTheTangentIntegralTest.cs new file mode 100644 index 000000000..eea2d68a1 --- /dev/null +++ b/Sources/Tests/UnitTests/Calculus/ASecantPowerOverAPowerOfACosineAndASineInPowersOfABinomialInTheTangentIntegralTest.cs @@ -0,0 +1,52 @@ +// +// 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 power of a secant over a whole power of a cos(y) + b sin(y), the two powers together + /// odd and at least seven, in powers of a + b tan(y). The half-angle ran out of time on + /// Rubi's 4.7.2 sec(c + d x)^4/(a cos(c + d x) + b sin(c + d x))^3. + /// #233 + /// + [Trait("Area", "Calculus")] + public sealed class ASecantPowerOverAPowerOfACosineAndASineInPowersOfABinomialInTheTangentIntegralTest + { + [Theory] + [InlineData("sec(c + d*x)^4/(a*cos(c + d*x) + b*sin(c + d*x))^3", 1.3, 0.7)] + [InlineData("sec(c + d*x)^3/(a*cos(c + d*x) + b*sin(c + d*x))^4", 0.6, -1.9)] + [InlineData("sec(c + d*x)^6/(a*cos(c + d*x) + b*sin(c + d*x))", 1.3, 0.7)] + [InlineData("sec(c + d*x)^2/(a*cos(c + d*x) + b*sin(c + d*x))^5", -0.6, 1.9)] + public void InPowersOfTheBinomialInTheTangent(string integrand, double a, double b) + { + var integral = integrand.ToEntity().Integrate("x"); + var text = integral.Stringize(); + Assert.DoesNotContain("integral(", text); + Assert.True(text.Length < 8000, $"{text.Length} characters of answer for {integrand}"); + Entity Pinned(Entity e) => e.Substitute("a", a).Substitute("b", b).Substitute("c", 0.4).Substitute("d", 0.3).Substitute("f", 0.6).Substitute("k", 0.2); + var derivative = Pinned(integral.Substitute("C", 0)).Differentiate("x"); + var original = Pinned(integrand.ToEntity()); + var compared = 0; + foreach (var at in new[] { 0.3, 0.8, 1.3, 2.9, 4.1, 5.3 }) + { + 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-8 * 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/AnEvenPowerOfACosineOverAPowerOfABinomialInTheSineByReductionIntegralTest.cs b/Sources/Tests/UnitTests/Calculus/AnEvenPowerOfACosineOverAPowerOfABinomialInTheSineByReductionIntegralTest.cs new file mode 100644 index 000000000..78b3879cc --- /dev/null +++ b/Sources/Tests/UnitTests/Calculus/AnEvenPowerOfACosineOverAPowerOfABinomialInTheSineByReductionIntegralTest.cs @@ -0,0 +1,57 @@ +// +// 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 +{ + /// + /// An even power of a cosine over a whole power of a + b sin(y), or of a sine over one of + /// a + b cos(y), by the reduction formulas in the power of the binomial down to + /// 1/(a + b sin(y)). The half-angle ran out of time on Rubi's 4.1.1.2 + /// cos(c + d x)^8/(a + b sin(c + d x))^8 and sec^4/(a + b sin)^8. + /// #233 + /// + [Trait("Area", "Calculus")] + public sealed class AnEvenPowerOfACosineOverAPowerOfABinomialInTheSineByReductionIntegralTest + { + [Theory] + [InlineData("cos(c + d*x)^8/(a + b*sin(c + d*x))^8", 1.3, 0.7)] + [InlineData("cos(c + d*x)^6/(a + b*sin(c + d*x))^8", 0.6, 1.9)] + [InlineData("sec(c + d*x)^2/(a + b*sin(c + d*x))^8", 1.3, 0.7)] + [InlineData("sec(c + d*x)^4/(a + b*sin(c + d*x))^8", 0.6, 1.9)] + [InlineData("sec(c + d*x)^4/(a + b*sin(c + d*x))^3", 1.3, -0.7)] + [InlineData("sin(c + d*x)^4/(a + b*cos(c + d*x))^5", 1.3, 0.7)] + [InlineData("csc(c + d*x)^2/(a + b*cos(c + d*x))^3", 0.6, 1.9)] + [InlineData("1/(a + b*cos(c + d*x))^6", 1.3, 0.7)] + public void ByTheReductionFormulas(string integrand, double a, double b) + { + var integral = integrand.ToEntity().Integrate("x"); + var text = integral.Stringize(); + Assert.DoesNotContain("integral(", text); + Assert.True(text.Length < 12000, $"{text.Length} characters of answer for {integrand}"); + Entity Pinned(Entity e) => e.Substitute("a", a).Substitute("b", b).Substitute("c", 0.4).Substitute("d", 0.3).Substitute("f", 0.6).Substitute("k", 0.2); + var derivative = Pinned(integral.Substitute("C", 0)).Differentiate("x"); + var original = Pinned(integrand.ToEntity()); + var compared = 0; + foreach (var at in new[] { 0.3, 0.8, 1.3, 2.9, 4.1, 5.3 }) + { + 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-8 * 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/AnEvenPowerOfATangentOverAPowerOfOnePlusASecantByCancellingIntegralTest.cs b/Sources/Tests/UnitTests/Calculus/AnEvenPowerOfATangentOverAPowerOfOnePlusASecantByCancellingIntegralTest.cs new file mode 100644 index 000000000..97d5f6ece --- /dev/null +++ b/Sources/Tests/UnitTests/Calculus/AnEvenPowerOfATangentOverAPowerOfOnePlusASecantByCancellingIntegralTest.cs @@ -0,0 +1,52 @@ +// +// 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 +{ + /// + /// From the twelfth power, an even power of a tangent over a power of k (1 +- sec(y)), + /// cancelled by tan^2 = (sec - 1)(sec + 1). The rules after it ran out of time on Rubi's + /// 4.5.1.4 tan(c + d x)^12/(a + a sec(c + d x))^3. + /// #233 + /// + [Trait("Area", "Calculus")] + public sealed class AnEvenPowerOfATangentOverAPowerOfOnePlusASecantByCancellingIntegralTest + { + [Theory] + [InlineData("tan(c + d*x)^12/(a + a*sec(c + d*x))^3", 1.3, 0.7)] + [InlineData("tan(c + d*x)^14/(a + a*sec(c + d*x))", 1.3, 0.7)] + [InlineData("tan(c + d*x)^12/(a - a*sec(c + d*x))^3", 1.3, 0.7)] + [InlineData("tan(c + d*x)^14/(a + a*sec(c + d*x))^3", 0.6, 0.7)] + public void Cancelled(string integrand, double a, double b) + { + var integral = integrand.ToEntity().Integrate("x"); + var text = integral.Stringize(); + Assert.DoesNotContain("integral(", text); + Assert.True(text.Length < 6000, $"{text.Length} characters of answer for {integrand}"); + Entity Pinned(Entity e) => e.Substitute("a", a).Substitute("b", b).Substitute("c", 0.4).Substitute("d", 0.3).Substitute("f", 0.6).Substitute("k", 0.2); + var derivative = Pinned(integral.Substitute("C", 0)).Differentiate("x"); + var original = Pinned(integrand.ToEntity()); + var compared = 0; + foreach (var at in new[] { 0.3, 0.8, 1.3, 2.9, 4.1, 5.3 }) + { + 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-8 * 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/AnOddPowerOfASineOverAPowerOfABinomialInTheSquaredTangentUnderTheCosineIntegralTest.cs b/Sources/Tests/UnitTests/Calculus/AnOddPowerOfASineOverAPowerOfABinomialInTheSquaredTangentUnderTheCosineIntegralTest.cs new file mode 100644 index 000000000..5e5599059 --- /dev/null +++ b/Sources/Tests/UnitTests/Calculus/AnOddPowerOfASineOverAPowerOfABinomialInTheSquaredTangentUnderTheCosineIntegralTest.cs @@ -0,0 +1,52 @@ +// +// 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 +{ + /// + /// An odd power of a sine over a whole power of a + b tan(y)^2, under u = cos(y) with + /// a - b named. The substitution search ran out of time on Rubi's 4.3.7 + /// sin(e + f x)^3/(a + b tan(e + f x)^2)^3. + /// #233 + /// + [Trait("Area", "Calculus")] + public sealed class AnOddPowerOfASineOverAPowerOfABinomialInTheSquaredTangentUnderTheCosineIntegralTest + { + [Theory] + [InlineData("sin(k + f*x)^3/(a + b*tan(k + f*x)^2)^3", 1.3, 0.7)] + [InlineData("sin(k + f*x)^5/(a + b*tan(k + f*x)^2)^3", 0.6, 1.9)] + [InlineData("sin(k + f*x)/(a + b*tan(k + f*x)^2)^2", 1.3, 0.7)] + [InlineData("sin(k + f*x)^3/(a + b*tan(k + f*x)^2)", 0.6, 1.9)] + public void UnderTheCosine(string integrand, double a, double b) + { + var integral = integrand.ToEntity().Integrate("x"); + var text = integral.Stringize(); + Assert.DoesNotContain("integral(", text); + Assert.True(text.Length < 8000, $"{text.Length} characters of answer for {integrand}"); + Entity Pinned(Entity e) => e.Substitute("a", a).Substitute("b", b).Substitute("c", 0.4).Substitute("d", 0.3).Substitute("f", 0.6).Substitute("k", 0.2); + var derivative = Pinned(integral.Substitute("C", 0)).Differentiate("x"); + var original = Pinned(integrand.ToEntity()); + var compared = 0; + foreach (var at in new[] { 0.3, 0.8, 1.3, 2.9, 4.1, 5.3 }) + { + 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-8 * 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/AnOddPowerOfATangentBesideAFunctionOfTheSquaredSineUnderItIntegralTest.cs b/Sources/Tests/UnitTests/Calculus/AnOddPowerOfATangentBesideAFunctionOfTheSquaredSineUnderItIntegralTest.cs new file mode 100644 index 000000000..08975651a --- /dev/null +++ b/Sources/Tests/UnitTests/Calculus/AnOddPowerOfATangentBesideAFunctionOfTheSquaredSineUnderItIntegralTest.cs @@ -0,0 +1,52 @@ +// +// 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 +{ + /// + /// An odd power of a tangent beside a root of a function of sin(y)^2, under + /// v = sin(y)^2. The secant's substitution ran out of time on Rubi's 4.1.7 + /// tan(e + f x)^5/(a + b sin(e + f x)^2)^(5/2). + /// #233 + /// + [Trait("Area", "Calculus")] + public sealed class AnOddPowerOfATangentBesideAFunctionOfTheSquaredSineUnderItIntegralTest + { + [Theory] + [InlineData("tan(k + f*x)^5/(a + b*sin(k + f*x)^2)^(5/2)", 1.3, 0.7)] + [InlineData("tan(k + f*x)^3/(a + b*sin(k + f*x)^2)^(5/2)", 1.3, 0.7)] + [InlineData("tan(k + f*x)^5/sqrt(a + b*sin(k + f*x)^2)", 1.9, -0.6)] + [InlineData("tan(k + f*x)/(a - a*sin(k + f*x)^2)^(3/2)", 1.3, 0.7)] + public void UnderTheSquaredSine(string integrand, double a, double b) + { + var integral = integrand.ToEntity().Integrate("x"); + var text = integral.Stringize(); + Assert.DoesNotContain("integral(", text); + Assert.True(text.Length < 8000, $"{text.Length} characters of answer for {integrand}"); + Entity Pinned(Entity e) => e.Substitute("a", a).Substitute("b", b).Substitute("c", 0.4).Substitute("d", 0.3).Substitute("f", 0.6).Substitute("k", 0.2); + var derivative = Pinned(integral.Substitute("C", 0)).Differentiate("x"); + var original = Pinned(integrand.ToEntity()); + var compared = 0; + foreach (var at in new[] { 0.3, 0.8, 1.3, 2.9, 4.1, 5.3 }) + { + 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-8 * 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/ByTakingOutRootsConstantUpToAPowerOfTheVariableIntegralTest.cs b/Sources/Tests/UnitTests/Calculus/ByTakingOutRootsConstantUpToAPowerOfTheVariableIntegralTest.cs new file mode 100644 index 000000000..333cd0be5 --- /dev/null +++ b/Sources/Tests/UnitTests/Calculus/ByTakingOutRootsConstantUpToAPowerOfTheVariableIntegralTest.cs @@ -0,0 +1,51 @@ +// +// 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 +{ + /// + /// Roots that are a constant times a whole power of the variable, though written with it, taken + /// out: sqrt(b - a/x^2)/sqrt(a - b x^2) is K/x with K^2 = -1, and Rubi's 4.7.7 + /// sin(x) sqrt(b - a/x^2)/sqrt(a - b x^2) is K Si(x). + /// #233 + /// + [Trait("Area", "Calculus")] + public sealed class ByTakingOutRootsConstantUpToAPowerOfTheVariableIntegralTest + { + [Theory] + [InlineData("sin(x)*sqrt(b - a/x^2)/sqrt(a - b*x^2)", 1.3, 0.7)] + [InlineData("sin(x)*sqrt(b - a/x^2)/sqrt(a - b*x^2)", 0.6, 1.9)] + [InlineData("cos(x)*sqrt(a*x^2 + b)/sqrt(a + b/x^2)", 1.3, 0.7)] + public void TakenOut(string integrand, double a, double b) + { + var integral = integrand.ToEntity().Integrate("x"); + var text = integral.Stringize(); + Assert.DoesNotContain("integral(", text); + Assert.True(text.Length < 2000, $"{text.Length} characters of answer for {integrand}"); + Entity Pinned(Entity e) => e.Substitute("a", a).Substitute("b", b).Substitute("c", 0.4).Substitute("d", 0.3).Substitute("f", 0.6).Substitute("k", 0.2); + var derivative = Pinned(integral.Substitute("C", 0)).Differentiate("x"); + var original = Pinned(integrand.ToEntity()); + var compared = 0; + foreach (var at in new[] { -2.9, -1.2, -0.7, 0.3, 0.8, 1.3, 2.9 }) + { + 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-8 * 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}"); + } + } +}