From b7624814dbf0c2f49a741d8712b23afbe93a1ac0 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Sun, 27 Sep 2026 20:21:58 +0000 Subject: [PATCH 1/2] A power of x times a half-odd power of a logarithm is integrated onto the Gaussian's moments x^p F^n, with F = ln x or A + B ln(c x^r) and 2n odd: under t = sqrt(F), dx = 2 x t dt/s and x^(p + 1) e^(-(p + 1) F/s) is a constant, so the integral is that constant times 2/s times the integral of t^(2n + 1) e^((p + 1) t^2/s), an even moment of the Gaussian the table answers; beside 1/x it is F^(n + 1)/(s (n + 1)). A polynomial beside it is summed a power at a time, and the logarithm of a power of a linear is the same question under u = d + e x, as Rubi's 3.3 reads it. Part of #1501. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura --- BREAKING-CHANGES.md | 15 ++ Sources/.editorconfig | 3 + .../Integration/IndefiniteIntegralSolver.cs | 155 ++++++++++++++++++ .../Integration/Integration.Definition.cs | 2 + .../Calculus/HalfOddPowerOfALogarithmTest.cs | 86 ++++++++++ 5 files changed, 261 insertions(+) create mode 100644 Sources/Tests/UnitTests/Calculus/HalfOddPowerOfALogarithmTest.cs diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 6aa4bdb85..9b9be3dcc 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -1679,6 +1679,21 @@ where it was `0.1 * x ^ 2 / 2 + C` | `limit(sin(c*x)/x, x, 0)` | `NaN` | `c` | | `limit((1 - cos(c*x))/x^2, x, 0)` | `NaN` | `c ^ 2 / 2` | +### A half-odd power of a logarithm beside a power of `x` is integrated + +`x sqrt(ln(x))` was left unintegrated. `x^p F^n`, where `F` is `ln x` or `A + B ln(c x^r)` and +`n` is half an odd number, is now integrated onto the Gaussian's moments under `t = sqrt(F)`. +A polynomial beside it is summed a power at a time, and the logarithm of a power of a linear is +the same question under `u = d + e x` +([#1501](https://github.com/asc-community/AngouriMath/issues/1501)). + +| Input | Was (2.5.0) | Now | +|---|---|---| +| `"x*sqrt(ln(x))".Integrate("x")` | `integral(x * sqrt(ln(x)), x)` | the antiderivative, with `erfi` | +| `"sqrt(ln(a*x^n))".Integrate("x")` | `integral(sqrt(ln(a * x ^ n)), x)` | the antiderivative, with `erfi` | +| `"x^2/ln(x)^(3/2)".Integrate("x")` | `integral(x ^ 2 / ln(x) ^ (3/2), x)` | the antiderivative, with `erfi` | +| `"(f + g*x)*(a + b*ln(c*(d + k*x)^n))^(3/2)".Integrate("x")` | `integral((f + g * x) * (a + b * ln(c * (d + k * x) ^ n)) ^ (3/2), x)` | the antiderivative | + ### `binomial(n, k)` is a function **Addition, not silent.** The binomial coefficient is a node, `Entity.Binomialf`, spelled diff --git a/Sources/.editorconfig b/Sources/.editorconfig index a8e90a293..3614f5364 100644 --- a/Sources/.editorconfig +++ b/Sources/.editorconfig @@ -636,3 +636,6 @@ file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed [Tests/UnitTests/Calculus/DowncastingOffCalculusTest.cs] file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed under MIT.\nDetails: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.\nWebsite: https://am.angouri.org.\n + +[Tests/UnitTests/Calculus/HalfOddPowerOfALogarithmTest.cs] +file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed under MIT.\nDetails: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.\nWebsite: https://am.angouri.org.\n diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs index b19c03a23..9c7cba42e 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs @@ -8536,6 +8536,161 @@ private static Entity SquarefreePartAsWritten(Entity denominator, Entity.Variabl return sum.InnerSimplified; } + /// + /// A power of x times a half-odd power of a logarithm, x^p F^n with F = + /// ln x or A + B ln(c x^r) and 2n odd, onto the Gaussian's moments. + /// With t = sqrt(F) and F' = s/x, dx = 2 x t dt/s, and + /// x^(p + 1) e^(-(p + 1) F/s) is a constant, so the integral is that constant times + /// 2/s times the integral of t^(2n + 1) e^((p + 1) t^2/s): an even moment of + /// the Gaussian, which the table answers. At p = -1 it is + /// F^(n + 1)/(s (n + 1)). Rubi's 3.1.2, (d x)^m (a + b ln(c x^n))^p for a + /// half-odd p. + /// https://github.com/asc-community/AngouriMath/issues/1501 + /// + /// + /// The constant is written x^(p + 1) e^(-(p + 1) F/s), as Rubi writes it, rather + /// than with ln(c x^r) split into ln c + r ln x, which holds only where + /// c is positive. The logarithm to the right of a whole power of it is + /// 's. + /// + internal static Entity? SolveAPowerTimesAHalfOddPowerOfTheLogarithm(Entity expr, Entity.Variable x) + { + Entity? p = null; + Number.Rational? n = null; + Entity constant = Number.Integer.One; + Entity? logarithm = null; + Dictionary? polynomial = null; + foreach (var (factor, underneath) in FactorsOfTheIntegrand(expr)) + { + if (!factor.ContainsNode(x)) + { + constant = underneath ? constant / factor : constant * factor; + continue; + } + if (factor == x && p is null) + p = underneath ? Number.Integer.MinusOne : Number.Integer.One; + else if (factor is Powf(var @base, var exponent) && @base == x && !exponent.ContainsNode(x) && p is null) + p = underneath ? -exponent : exponent; + else if (factor is Powf(Mulf(var scale, var scaled), var scaledExponent) && scaled == x && !scale.ContainsNode(x) && !scaledExponent.ContainsNode(x) && p is null) + { + p = underneath ? -scaledExponent : scaledExponent; + var scalePower = MathS.Pow(scale, scaledExponent); + constant = underneath ? constant / scalePower : constant * scalePower; + } + else if (n is null && factor is Powf(var f, Number.Rational half) && half.ERational.Denominator.Equals(EInteger.FromInt32(2))) + { + if (f == MathS.Ln(x) || IsAffineInALogarithmOfAPowerOfX(f, x)) + (logarithm, n) = (f, underneath ? Number.Rational.Create(-half.ERational) : half); + // The logarithm of a power of a linear, Rubi's 3.3: the same question under + // u = d + e x. Declined, it was integrated by parts against the half-odd + // power's own antiderivative, which took (f + g x) (a + b ln(c (d + e x)^n))^(3/2) + // 37 s to give up. + else if (LinearUnderALogarithm(f, x) is { } linear) + return ByTheLinearUnderTheLogarithm(expr, x, linear); + else + return null; + } + // A polynomial beside it, which is how a linear outside the logarithm arrives once + // the linear inside it has been substituted: summed a power at a time. + else if (polynomial is null && !underneath && TreeAnalyzer.TryGetPolynomial(factor, x, out var monomials) + && monomials.Count > 1 && monomials.Keys.All(power => power.Sign >= 0 && power.CompareTo(EInteger.FromInt32(12)) <= 0)) + polynomial = monomials; + else + return null; + } + if (logarithm is null || n is null) + return null; + p = (p ?? Number.Integer.Zero).InnerSimplified; + if (p.Evaled is Number.Complex and not Number.Real) + return null; + Entity slopeOfF = Number.Integer.One; + if (logarithm != MathS.Ln(x)) + { + slopeOfF = Functions.PartialFractions.Bare((logarithm.Differentiate(x) * x).Simplify()); + if (slopeOfF.ContainsNode(x) || slopeOfF.Evaled is Number.Complex { IsZero: true }) + return null; + } + if (polynomial is null) + return HalfOddLogarithmicTerm(p, logarithm, n, slopeOfF, x) is { } single ? (constant * single).InnerSimplified : null; + Entity sum = Number.Integer.Zero; + foreach (var (power, coefficient) in polynomial) + { + if (HalfOddLogarithmicTerm((p + Number.Integer.Create(power)).InnerSimplified, logarithm, n, slopeOfF, x) is not { } term) + return null; + sum += coefficient * term; + } + return (constant * sum).InnerSimplified; + } + + /// + /// The linear d + e x, with d not zero, whose powers and constant multiples + /// make up the argument of the one logarithm in , or + /// . + /// + private static Entity? LinearUnderALogarithm(Entity expr, Entity.Variable x) + { + Entity? argument = null; + foreach (var node in expr.Nodes) + if (node is Logf(var @base, var antilogarithm) && @base == MathS.e && antilogarithm.ContainsNode(x)) + { + if (argument is not null && argument != antilogarithm) + return null; + argument = antilogarithm; + } + if (argument is null) + return null; + foreach (var node in argument.Nodes) + if (node is Sumf or Minusf && TreeAnalyzer.TryGetPolyLinear(node, x, out var slope, out var intercept) + && !TreeAnalyzer.IsZero(slope) && !TreeAnalyzer.IsZero(intercept)) + { + var placeholder = Variable.CreateUnique(argument, "u_lin"); + var inThePlaceholder = argument.Replace(child => child == node ? placeholder : child); + return !inThePlaceholder.ContainsNode(x) && IsAProductOfPowersOfTheVariable(inThePlaceholder, placeholder) ? node : null; + } + return null; + } + + /// + /// under u = d + e x, with the + /// d + e x under its logarithm, answered as a power or a polynomial of u + /// beside a half-odd power of A + B ln(c u^r) and written back in x. + /// + private static Entity? ByTheLinearUnderTheLogarithm(Entity expr, Entity.Variable x, Entity linear) + { + if (!TreeAnalyzer.TryGetPolyLinear(linear, x, out var slope, out var intercept) || slope is null || intercept is null) + return null; + var u = Variable.CreateUnique(expr, "u"); + var inU = (expr.Replace(node => node == linear ? u : node).Substitute(x, (u - intercept) / slope) / slope).InnerSimplified; + if (inU.ContainsNode(x) || LinearUnderALogarithm(inU, u) is not null) + return null; + return SolveAPowerTimesAHalfOddPowerOfTheLogarithm(inU, u)?.Substitute(u, linear).InnerSimplified; + } + + /// + /// The integral of x^q F^n for a half-odd n and F' = s/x: see + /// . + /// + private static Entity? HalfOddLogarithmicTerm(Entity q, Entity logarithm, Number.Rational n, Entity slopeOfF, Entity.Variable x) + { + var qPlusOne = (q + 1).InnerSimplified; + if (qPlusOne.Evaled is Number.Complex { IsZero: true }) + return MathS.Pow(logarithm, n + 1) / ((n + 1) * slopeOfF); + // t^(2n + 1) e^((q + 1) t^2/s) in a variable of its own, for the table's moments. + var t = Variable.CreateUnique(logarithm, "t"); + var power = (2 * n.ERational + ERational.One).ToEInteger(); + if (!power.CanFitInInt32() || power.Abs().CompareTo(EInteger.FromInt32(32)) > 0) + return null; + var gaussian = MathS.Pow(MathS.e, (qPlusOne / slopeOfF).InnerSimplified * MathS.Sqr(t)); + var inT = power.IsZero ? gaussian : MathS.Pow(t, Number.Integer.Create(power)) * gaussian; + if (IntegralPatterns.TryStandardIntegrals(inT, t) is not { } answerInT) + return null; + // x^(q + 1) e^(-(q + 1) ln(x)) is 1 wherever ln(x) is real, so for ln(x) itself it is left out. + Entity theConstant = logarithm == MathS.Ln(x) + ? Number.Integer.One + : MathS.Pow(x, qPlusOne) * MathS.Pow(MathS.e, -qPlusOne * logarithm / slopeOfF); + return 2 / slopeOfF * theConstant * answerInT.Substitute(t, MathS.Sqrt(logarithm)); + } + /// /// Whether is A + B ln(c x^r) with A, B, /// c and r free of and the logarithm present: one diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs b/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs index a3343ebcc..715a162fd 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs @@ -569,6 +569,8 @@ private static Entity Normalized(Entity expr, Entity.Variable x) => // A power of x times a power of its logarithm, by the closed reduction: exact, // and by parts n times where the exponent is a symbol was not taken. if ((answer = IndefiniteIntegralSolver.SolveAPowerTimesAPowerOfTheLogarithm(expr, x)) is { }) return answer; + // And a half-odd power of it, onto the Gaussian's moments by t = sqrt(F). + if ((answer = IndefiniteIntegralSolver.SolveAPowerTimesAHalfOddPowerOfTheLogarithm(expr, x)) is { }) return answer; if ((answer = IndefiniteIntegralSolver.SolveByFlatteningAPowerOfAnExponential(expr, x, integrateByParts)) is { }) return answer; // An exponential of a multiple of a logarithm is a power of the argument, which is // how every inverse hyperbolic function under an exponential arrives. diff --git a/Sources/Tests/UnitTests/Calculus/HalfOddPowerOfALogarithmTest.cs b/Sources/Tests/UnitTests/Calculus/HalfOddPowerOfALogarithmTest.cs new file mode 100644 index 000000000..a8e8ed9a4 --- /dev/null +++ b/Sources/Tests/UnitTests/Calculus/HalfOddPowerOfALogarithmTest.cs @@ -0,0 +1,86 @@ +// +// 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 System.Linq; +using AngouriMath.Extensions; +using Xunit; + +namespace AngouriMath.Tests.Calculus +{ + /// + /// A power of x times a half-odd power of a logarithm, x^p (A + B ln(c x^r))^n, onto the + /// Gaussian's moments by t = sqrt(F): Rubi's 3.1.2, (d x)^m (a + b ln(c x^n))^p with + /// p half an odd number, every one of which was left unevaluated. + /// https://github.com/asc-community/AngouriMath/issues/1501 + /// + [Trait("Area", "Calculus")] + public sealed class HalfOddPowerOfALogarithmTest + { + /// Past 1, where every logarithm here is real and positive for the pins below. + private static readonly double[] Points = { 1.3, 1.9, 2.6, 3.4 }; + private static readonly (string Name, string Value)[] Pins = + { ("a", "3/2"), ("b", "2/3"), ("c", "5/4"), ("n", "3"), ("m", "1/2"), ("d", "1/2"), ("k", "2"), ("f", "3/4"), ("g", "2"), ("h", "1/3"), ("p", "2"), ("q", "3/2") }; + + private static void DifferentiatesBack(string integrand) + { + var integral = integrand.ToEntity().Integrate("x"); + Assert.DoesNotContain("integral(", integral.Stringize()); + Entity Pinned(Entity e) => Pins.Aggregate(e, (current, pin) => current.Substitute(pin.Name, pin.Value.ToEntity())); + var derivative = Pinned(integral.Substitute("C", 0)).Differentiate("x"); + var original = Pinned(integrand.ToEntity()); + foreach (var at in Points) + { + var got = derivative.Substitute("x", at).EvalNumerical(); + var want = original.Substitute("x", at).EvalNumerical(); + var difference = Math.Abs((double)(got - want).RealPart) + Math.Abs((double)(got - want).ImaginaryPart); + var scale = Math.Max(1.0, Math.Abs((double)want.RealPart)); + Assert.True(difference / scale < 1e-9, $"d/dx of {integral} is {got} at x = {at}, where {integrand} is {want}"); + } + } + + [Theory] + [InlineData("sqrt(ln(x))")] + [InlineData("x*sqrt(ln(x))")] + [InlineData("x^2/sqrt(ln(x))")] + [InlineData("sqrt(ln(x))/x^2")] + [InlineData("x^3*ln(x)^(3/2)")] + [InlineData("1/(x^3*ln(x)^(3/2))")] + [InlineData("x/ln(x)^(5/2)")] + public void OfTheLogarithmItself(string integrand) => DifferentiatesBack(integrand); + + [Theory] + [InlineData("sqrt(ln(x^2))")] + [InlineData("sqrt(ln(a*x^n))")] + [InlineData("x^3*sqrt(ln(a*x^n))")] + [InlineData("x^m/sqrt(ln(a*x^n))")] + [InlineData("(a + b*ln(c*x^n))^(1/2)")] + [InlineData("x^3*ln(a*x^n)^(3/2)")] + [InlineData("x^2/ln(a*x^n)^(3/2)")] + [InlineData("x^m/ln(a*x^n)^(5/2)")] + public void OfALogarithmOfAPower(string integrand) => DifferentiatesBack(integrand); + + /// + /// The logarithm of a power of a linear, with a polynomial beside it, under + /// u = d + e x: Rubi's 3.3. Declined, these went by parts against the half-odd + /// power's own antiderivative and took up to 55 s to give up. + /// + [Theory] + [InlineData("sqrt(ln(2 + 3*x))")] + [InlineData("(f + g*x)*(a + b*ln(c*(d + k*x)^n))^(3/2)")] + [InlineData("(f + g*x)^2*sqrt(a + b*ln(c*(d + k*x)^n))")] + [InlineData("(f + g*x)^3/sqrt(a + b*ln(c*(d + k*x)^n))")] + [InlineData("(f + g*x)/(a + b*ln(c*(d + k*x)^n))^(5/2)")] + [InlineData("(g + h*x)^2/sqrt(a + b*ln(c*(d*(k + f*x)^p)^q))")] + public void OfALogarithmOfAPowerOfALinear(string integrand) => DifferentiatesBack(integrand); + + /// Beside 1/x it is elementary, F^(n + 1)/(s (n + 1)). + [Fact] + public void BesideTheReciprocalItIsAPower() + => Assert.Equal("2 * sqrt(ln(x)) + C".ToEntity(), "1/(x*sqrt(ln(x)))".ToEntity().Integrate("x")); + } +} From 34d54078384d57582a023bf27797610efe6fbe0c Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Sun, 27 Sep 2026 20:25:06 +0000 Subject: [PATCH 2/2] Read the polynomial's monomials without deconstructing a KeyValuePair netstandard2.0 has no KeyValuePair.Deconstruct, so the library did not build for it; the local builds were net10.0 only. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura --- .../Continuous/Integration/IndefiniteIntegralSolver.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs index 9c7cba42e..a3c203e66 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs @@ -8613,11 +8613,11 @@ private static Entity SquarefreePartAsWritten(Entity denominator, Entity.Variabl if (polynomial is null) return HalfOddLogarithmicTerm(p, logarithm, n, slopeOfF, x) is { } single ? (constant * single).InnerSimplified : null; Entity sum = Number.Integer.Zero; - foreach (var (power, coefficient) in polynomial) + foreach (var monomial in polynomial) { - if (HalfOddLogarithmicTerm((p + Number.Integer.Create(power)).InnerSimplified, logarithm, n, slopeOfF, x) is not { } term) + if (HalfOddLogarithmicTerm((p + Number.Integer.Create(monomial.Key)).InnerSimplified, logarithm, n, slopeOfF, x) is not { } term) return null; - sum += coefficient * term; + sum += monomial.Value * term; } return (constant * sum).InnerSimplified; }