From 7304b0af04495ae3b215fcd3432edbcfad6a99bc Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Sun, 4 Oct 2026 13:19:25 +0000 Subject: [PATCH 1/3] A radical of a hyperbolic function of a logarithm is integrated on both sides of zero Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura --- BREAKING-CHANGES.md | 23 ++++++------ .../Integration/IndefiniteIntegralSolver.cs | 23 ++++++++++-- .../Calculus/LogarithmSubstitutionTest.cs | 35 +++++++------------ 3 files changed, 45 insertions(+), 36 deletions(-) diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 4d47c7700..3d50e5545 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -589,21 +589,22 @@ zero-discriminant arm, which gives the right answer. Each of these was checked by differentiating it back with the parameters pinned and comparing at four points. Nothing that already had an antiderivative changes. -### A radical function of whole powers of `x` written through its logarithm says where its answer holds - -**Answers where there were none, each with the condition it holds under.** `1/csch(2 ln(x))^(1/2)` -is `sqrt(sinh(2 ln(x)))`, which is `sqrt((x^2 - x^(-2))/2)` and real on both sides of zero; 2.5.0 -declined it. On the unreleased master it was integrated under `t = ln(x)`, where the rules take -`e^t` to be positive, and the answer was right for a positive `x` and wrong for every negative one. -Such an answer is given `provided x > 0` now, and `provided c x^n > 0` for a logarithm of `c x^n` +### A radical function of whole powers of `x` written through its logarithm is integrated on both sides of zero + +**Answers where there were none.** `1/csch(2 ln(x))^(1/2)` is `sqrt(sinh(2 ln(x)))`, which is +`sqrt((x^2 - x^(-2))/2)` and real on both sides of zero; 2.5.0 declined it. Its exponentials are +folded into powers of `x`, a negative one under a root written below the bar, and the answer is in those, on +both sides of zero and on both sides of 1, where `sinh(2 ln(x))` changes sign. Under `t = ln(x)`, +where the rules take `e^t` to be positive, an answer of this kind is given `provided x > 0`, and +`provided c x^n > 0` for a logarithm of `c x^n` ([#718](https://github.com/asc-community/AngouriMath/issues/718)). | Input | Was (2.5.0) | Now | |---|---|---| -| `"1/csch(2*ln(x))^(1/2)".ToEntity().Integrate("x")` | `integral(...)` | an antiderivative, `provided x > 0` | -| `"csch(2*ln(x))^(-3/2)".ToEntity().Integrate("x")` | `integral(...)` | an antiderivative, `provided x > 0` | -| `"1/csch(2*ln(c*x))^(1/2)".ToEntity().Integrate("x")` | `integral(...)` | an antiderivative, `provided c x > 0` | -| `"1/sech(2*ln(x))^(1/2)".ToEntity().Integrate("x")` | `integral(...)` | an antiderivative, `provided x > 0` | +| `"1/csch(2*ln(x))^(1/2)".ToEntity().Integrate("x")` | `integral(...)` | an antiderivative on both sides of zero | +| `"csch(2*ln(x))^(-3/2)".ToEntity().Integrate("x")` | `integral(...)` | an antiderivative on both sides of zero | +| `"1/csch(2*ln(c*x))^(1/2)".ToEntity().Integrate("x")` | `integral(...)` | an antiderivative on both sides of zero | +| `"1/sech(2*ln(x))^(1/2)".ToEntity().Integrate("x")` | `integral(...)` | an antiderivative on both sides of zero | ### `e^(k acoth(a x))` beside a power of `c - a c x` is integrated diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs index f072a2d88..06f28c068 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs @@ -2162,9 +2162,13 @@ private static bool TryReadAsQuotient(Entity expr, out Entity numerator, out Ent // The exponent negated as a number rather than as a tree: `-power` on the // node `2` is `2 * (-1)`, and `sin(x)^(2 * (-1))` is a shape the closed rule // for a power of the sine does not read, so `c/sin(x)^2` went to the - // half-angle substitution for what is `-c cot(x)`. + // half-angle substitution for what is `-c cot(x)`. And asked as the same + // question, since `c/g^p` and `c g^(-p)` are one: a level down the rules + // scoped to the question asked did not see it, and `1/csch(2 ln(x))^(1/2)` + // reached the fold of its exponentials there and was declined, where asked + // at the top it is answered. over is Entity.Powf(var @base, var power) ? - Integration.ComputeIndefiniteIntegral(MathS.Pow(@base, (-power).InnerSimplified), x, integrateByParts)?.Pipe(i => div * i) : + Integration.ComputeAsTheSameQuestion(MathS.Pow(@base, (-power).InnerSimplified), x, integrateByParts)?.Pipe(i => div * i) : // A constant over a product is the reciprocal of the product, asked as the // same question with the constant in front, and not the product to the // power -1: `pe/(x (d + e x) sqrt(1 - c^2 x^2))` handed on as @@ -16230,7 +16234,20 @@ node is Powf(Powf(var @base, var inner), var outer) var folded = FoldTheExponent(exponent, x); return folded ?? node; }); - return folded == expr ? null : Integration.ComputeAsAQuestionOfItsOwn(folded, x, integrateByParts); + if (folded == expr) + return null; + // A radicand with its negative powers written below the bar, which is where the rules + // for a root read a denominator: `(1/((x^2 - x^(-2))/2))^(-3/2)` was declined where + // `(1/((x^2 - 1/x^2)/2))^(-3/2)` is answered. Only under a power that is not whole: a + // whole power of a sum is expanded, and the power rule reads its terms as `x^(-k)`, + // through which `sinh(a + b ln(c x^n))^4` is answered in a second and was searched for + // six with them below the bar. + folded = folded.Replace(node => node is Powf(var radicand, var exponent) && exponent.Evaled is not Number.Integer && radicand.ContainsNode(x) + ? MathS.Pow(radicand.Replace(inside => inside is Powf(var @base, Number.Integer { EInteger: var power }) && power.Sign < 0 && @base.ContainsNode(x) + ? Number.Integer.One / (power.Equals(EInteger.FromInt32(-1)) ? @base : MathS.Pow(@base, Number.Integer.Create(-power))) + : inside), exponent) + : node); + return Integration.ComputeAsAQuestionOfItsOwn(folded, x, integrateByParts); } /// diff --git a/Sources/Tests/UnitTests/Calculus/LogarithmSubstitutionTest.cs b/Sources/Tests/UnitTests/Calculus/LogarithmSubstitutionTest.cs index 10f8bf5c7..84e9ff43c 100644 --- a/Sources/Tests/UnitTests/Calculus/LogarithmSubstitutionTest.cs +++ b/Sources/Tests/UnitTests/Calculus/LogarithmSubstitutionTest.cs @@ -6,7 +6,6 @@ // using System; -using System.Linq; using AngouriMath.Extensions; using Xunit; @@ -121,38 +120,30 @@ public void ATrigonometricFunctionOfTheLogarithm(string integrand) /// /// A radical function of whole powers of x written through its logarithm, which is /// real on both sides of zero: sinh(2 ln(x)) is (x^2 - x^(-2))/2 for a - /// negative x as well. Each answer is compared at three negative points and three - /// positive ones, wherever it does not say it holds only elsewhere. Integrated under - /// t = ln(x), the first four say they hold where the logarithm's argument is - /// positive; the last two are answered in powers of x and hold on both sides. - /// Rubi's 6.5.3, 226, and 6.6.3, 188. + /// negative x as well. Each answer is compared on both sides of zero and on both + /// sides of 1, where sinh(2 ln(x)) changes sign, and holds at every point: the + /// exponentials of the logarithm are folded into powers of x, and the answer is in + /// those. Rubi's 6.5.3, 226, and 6.6.3, 188. /// [Theory] - [InlineData("1/csch(2*ln(x))^(1/2)", false)] - [InlineData("csch(2*ln(x))^(-3/2)", false)] - [InlineData("1/csch(2*ln(13/10*x))^(1/2)", false)] - [InlineData("1/sech(2*ln(x))^(1/2)", false)] - [InlineData("sqrt(sinh(2*ln(x)))", true)] - [InlineData("sqrt(cosh(2*ln(13/10*x)))", true)] - public void ARadicalFunctionOfAWholePowerOfX(string integrand, bool onBothSides) + [InlineData("1/csch(2*ln(x))^(1/2)")] + [InlineData("csch(2*ln(x))^(-3/2)")] + [InlineData("1/csch(2*ln(13/10*x))^(1/2)")] + [InlineData("1/sech(2*ln(x))^(1/2)")] + [InlineData("sqrt(sinh(2*ln(x)))")] + [InlineData("sqrt(cosh(2*ln(13/10*x)))")] + public void ARadicalFunctionOfAWholePowerOfX(string integrand) { var integral = integrand.ToEntity().Integrate("x"); Assert.DoesNotContain("integral(", integral.Stringize()); var derivative = integral.Substitute("C", 0).Differentiate("x"); var original = integrand.ToEntity(); - var conditions = integral.Nodes.OfType().Select(provided => provided.Predicate).ToList(); - foreach (var at in new[] { -2.7, -1.9, -1.3, 1.3, 1.9, 2.7 }) + foreach (var at in new[] { -2.7, -1.9, -1.3, -0.7, -0.4, 0.4, 0.7, 1.3, 1.9, 2.7 }) { - if (conditions.Any(condition => condition.Substitute("x", at).Evaled == Entity.Boolean.False)) - { - Assert.False(onBothSides, $"the antiderivative of {integrand} says it does not hold at x = {at}"); - Assert.True(at < 0, $"the antiderivative of {integrand} says it does not hold at x = {at}"); - continue; - } 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); - Assert.True(difference / Math.Max(1.0, Math.Abs((double)want.RealPart)) < 1e-9, + Assert.True(difference / Math.Max(1.0, Math.Abs((double)want.RealPart) + Math.Abs((double)want.ImaginaryPart)) < 1e-9, $"d/dx of the antiderivative of {integrand} is {got} at x = {at}, where the integrand is {want}"); } } From c951a2686e1bf5a85cb515d1dab06fdb563da9f4 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Sun, 4 Oct 2026 16:03:53 +0000 Subject: [PATCH 2/3] The even root's condition is checked on an integrand that still reaches that step Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura --- BREAKING-CHANGES.md | 9 ++++++--- .../Calculus/LinearRadicalSubstitutionIntegralTest.cs | 8 ++++---- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 3d50e5545..074d60236 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -596,8 +596,10 @@ four points. Nothing that already had an antiderivative changes. folded into powers of `x`, a negative one under a root written below the bar, and the answer is in those, on both sides of zero and on both sides of 1, where `sinh(2 ln(x))` changes sign. Under `t = ln(x)`, where the rules take `e^t` to be positive, an answer of this kind is given `provided x > 0`, and -`provided c x^n > 0` for a logarithm of `c x^n` -([#718](https://github.com/asc-community/AngouriMath/issues/718)). +`provided c x^n > 0` for a logarithm of `c x^n`. A constant over a power, `1/g^p`, is asked as +`g^(-p)` at the depth it was asked at, so that the rules scoped to the question asked see it, which +is how `1/csch(2 ln(x))^(1/2)` reaches the fold, and `1/sqrt(x + x^(3/2))` is answered on both sides +of zero as well ([#718](https://github.com/asc-community/AngouriMath/issues/718)). | Input | Was (2.5.0) | Now | |---|---|---| @@ -605,6 +607,7 @@ where the rules take `e^t` to be positive, an answer of this kind is given `prov | `"csch(2*ln(x))^(-3/2)".ToEntity().Integrate("x")` | `integral(...)` | an antiderivative on both sides of zero | | `"1/csch(2*ln(c*x))^(1/2)".ToEntity().Integrate("x")` | `integral(...)` | an antiderivative on both sides of zero | | `"1/sech(2*ln(x))^(1/2)".ToEntity().Integrate("x")` | `integral(...)` | an antiderivative on both sides of zero | +| `"1/sqrt(x + x^(3/2))".ToEntity().Integrate("x")` | `integral(...)` | `4 sqrt(1 + sqrt(x))` times a factor constant where it is continuous, on both sides of zero | ### `e^(k acoth(a x))` beside a power of `c - a c x` is integrated @@ -1578,7 +1581,7 @@ was ([#718](https://github.com/asc-community/AngouriMath/issues/718)). | Input | Was (2.5.0) | Now | |---|---|---| | `"x * sqrt(c - a*c*x) / e^(3 * atanh(a*x))".Integrate("x")` | `integral(…)` — left unevaluated, with `e` evaluated to a hundred digits inside it | an antiderivative in `sqrt(c - a c x)`, `provided c - a * c * x >= 0` | -| `"1/sqrt(x + x^(3/2))".Integrate("x")` | `integral(1 / sqrt(x + x ^ (3/2)), x)` — left unevaluated | `2 * sqrt(1 + sqrt(x)) / (1/2) + C provided x >= 0` | +| `"sqrt(x)/sqrt(x + x^2)".Integrate("x")` | `integral(...)` — left unevaluated | `2 sqrt(x + 1)` times a factor constant where it is continuous, `provided x + 1 >= 0` | ### An answer that read the sign of an even root says where the root is real diff --git a/Sources/Tests/UnitTests/Calculus/LinearRadicalSubstitutionIntegralTest.cs b/Sources/Tests/UnitTests/Calculus/LinearRadicalSubstitutionIntegralTest.cs index 910eaaa36..12682b9cf 100644 --- a/Sources/Tests/UnitTests/Calculus/LinearRadicalSubstitutionIntegralTest.cs +++ b/Sources/Tests/UnitTests/Calculus/LinearRadicalSubstitutionIntegralTest.cs @@ -79,8 +79,8 @@ private static void DifferentiatesBack(string integrand) /// /// That step reads u as a non-negative real, which it is only where the radical - /// is real, and the answer says so: 1/sqrt(x + x^(3/2)) comes back - /// provided x >= 0. Beyond the radicand's zero u is imaginary, and the + /// is real, and the answer says so: sqrt(x)/sqrt(x + x^2) comes back + /// provided x + 1 >= 0. Beyond the radicand's zero u is imaginary, and the /// integrand can still be real there -- Rubi's x sqrt(c - a c x) / e^(3 atanh(a x)) /// above a x = 1 is the product of two imaginary factors -- while the answer built /// for a real u is not its antiderivative: with a = 3.1, c = 0.7 at @@ -93,8 +93,8 @@ private static void DifferentiatesBack(string integrand) [Fact] public void TheEvenRootStepSaysWhereItHolds() { - var conditioned = "1/sqrt(x + x^(3/2))".ToEntity().Integrate("x"); - Assert.Contains("provided x >= 0", conditioned.Stringize()); + var conditioned = "sqrt(x)/sqrt(x + x^2)".ToEntity().Integrate("x"); + Assert.Contains("provided x + 1 >= 0", conditioned.Stringize()); var integrand = "x * sqrt(c - a*c*x) / e^(3 * atanh(a*x))".ToEntity(); var integral = integrand.Integrate("x"); From 1bd4d7e950bc398a87ce648748e63d3dc3dfde5e Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Sun, 4 Oct 2026 16:07:05 +0000 Subject: [PATCH 3/3] A constant over a whole power keeps its level Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura --- BREAKING-CHANGES.md | 8 ++++---- .../Integration/IndefiniteIntegralSolver.cs | 16 ++++++++++------ 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 074d60236..38def14b8 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -596,10 +596,10 @@ four points. Nothing that already had an antiderivative changes. folded into powers of `x`, a negative one under a root written below the bar, and the answer is in those, on both sides of zero and on both sides of 1, where `sinh(2 ln(x))` changes sign. Under `t = ln(x)`, where the rules take `e^t` to be positive, an answer of this kind is given `provided x > 0`, and -`provided c x^n > 0` for a logarithm of `c x^n`. A constant over a power, `1/g^p`, is asked as -`g^(-p)` at the depth it was asked at, so that the rules scoped to the question asked see it, which -is how `1/csch(2 ln(x))^(1/2)` reaches the fold, and `1/sqrt(x + x^(3/2))` is answered on both sides -of zero as well ([#718](https://github.com/asc-community/AngouriMath/issues/718)). +`provided c x^n > 0` for a logarithm of `c x^n`. A constant over a power that is not whole, `1/g^p`, +is asked as `g^(-p)` at the depth it was asked at, so that the rules scoped to the question asked see +it, which is how `1/csch(2 ln(x))^(1/2)` reaches the fold, and `1/sqrt(x + x^(3/2))` is answered on +both sides of zero as well ([#718](https://github.com/asc-community/AngouriMath/issues/718)). | Input | Was (2.5.0) | Now | |---|---|---| diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs index 06f28c068..1d8df10f2 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs @@ -2162,13 +2162,17 @@ private static bool TryReadAsQuotient(Entity expr, out Entity numerator, out Ent // The exponent negated as a number rather than as a tree: `-power` on the // node `2` is `2 * (-1)`, and `sin(x)^(2 * (-1))` is a shape the closed rule // for a power of the sine does not read, so `c/sin(x)^2` went to the - // half-angle substitution for what is `-c cot(x)`. And asked as the same - // question, since `c/g^p` and `c g^(-p)` are one: a level down the rules - // scoped to the question asked did not see it, and `1/csch(2 ln(x))^(1/2)` - // reached the fold of its exponentials there and was declined, where asked - // at the top it is answered. + // half-angle substitution for what is `-c cot(x)`. And where the power is not + // whole asked as the same question, since `c/g^p` and `c g^(-p)` are one: a + // level down the rules scoped to the question asked, the ones for a root, did + // not see it, and `1/csch(2 ln(x))^(1/2)` reached the fold of its exponentials + // there and was declined, where asked at the top it is answered. A whole power + // keeps its level: asked as the same question, `(c + d x)/(a + a tanh(e + f x))^3` + // ran past the budget where it is answered in a second. over is Entity.Powf(var @base, var power) ? - Integration.ComputeAsTheSameQuestion(MathS.Pow(@base, (-power).InnerSimplified), x, integrateByParts)?.Pipe(i => div * i) : + (power is Number.Rational and not Number.Integer + ? Integration.ComputeAsTheSameQuestion(MathS.Pow(@base, (-power).InnerSimplified), x, integrateByParts) + : Integration.ComputeIndefiniteIntegral(MathS.Pow(@base, (-power).InnerSimplified), x, integrateByParts))?.Pipe(i => div * i) : // A constant over a product is the reciprocal of the product, asked as the // same question with the constant in front, and not the product to the // power -1: `pe/(x (d + e x) sqrt(1 - c^2 x^2))` handed on as