From f94da6c11952d773c5a4f1baa18c031c8912b2b1 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Tue, 29 Sep 2026 11:18:02 +0000 Subject: [PATCH] An answer that took an even root real without reading its sign says where it holds The substitution for an even root, u = t^(1/q), conditions its answer on the radicand only where the answer below read a sign or a modulus of u. The rules below can take u real without either: SolveByCombiningRadicals writes sqrt(1 + 1/u^2) sqrt(1 - u^4) as sqrt(u^4 - u^8 + u^2 - u^6)/u^2, which reads sqrt(u^4) as u^2, the other root wherever u is imaginary. Where the radicand is negative u is imaginary, and the integrand can still be real there beside a second imaginary root: sqrt(cos(x))/sqrt(a + a sec(x)) under t = cos(x) and u = sqrt(t) came back negated wherever the cosine is negative. Where the answer below read no sign and the integrand holds a second even root, the answer is now differentiated and compared with the integrand at the default sampled points where the integrand is real; where it differs, it carries provided t >= 0, as the answers that read a sign already do. Measured with work/intbench against dd728251, 3 s a problem: a seeded twentieth of the problems holding half-odd powers of two different bases, the only ones the check reaches, 273 statable: no verdict moves, same time. Closes #1581. Co-Authored-By: Claude Opus 5.5 --- BREAKING-CHANGES.md | 17 ++++++++++ .../Algebra/Polynomials/PartialFractions.cs | 13 ++++++++ .../Integration/IndefiniteIntegralSolver.cs | 25 +++++++++++++++ .../LinearRadicalSubstitutionIntegralTest.cs | 32 +++++++++++++++++++ 4 files changed, 87 insertions(+) diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 2d8c7ac2b..de2d9f405 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -738,6 +738,23 @@ Rubi's 4.5.3.1:640 was wrong on that region |---|---|---| | `"(1+sec(x))^(5/2)*sqrt(cos(x))".Integrate("x")` | `integral((1 + sec(x)) ^ (5/2) * sqrt(cos(x)), x)` — left unevaluated | an antiderivative with `sgn(sin(x))` in it, `provided cos(x) >= 0` | +### An answer that took an even root real without reading its sign says where it holds + +The same step's answer is checked where the rules below read no sign of `u` and still took it real: +`SolveByCombiningRadicals` writes `sqrt(1 + 1/u^2) sqrt(1 - u^4)` as +`sqrt(u^4 - u^8 + u^2 - u^6)/u^2`, which reads `sqrt(u^4)` as `u^2`, the other root wherever `u` is +imaginary. Where the integrand holds a second even root, as it must to be real where the radicand +is negative, the answer is differentiated and compared with the integrand where the integrand is +real. Where it is not the antiderivative there, it now carries `provided t >= 0` as well. +`sqrt(cos(x))/sqrt(a + a sec(x))` and six of its kin in Rubi's 4.5.1.2 and 4.5.3.1 were answered +negated wherever the cosine is negative; the half-angle rule above answers those seven first now, +and this is the same answer reached through the even root +([#1581](https://github.com/asc-community/AngouriMath/issues/1581)). + +| Input | Was (2.5.0) | Now | +|---|---|---| +| `"sqrt(x)/(sqrt(1/x + 1)*sqrt(1 - x^2))".Integrate("x")` | `integral(...)` | an antiderivative in `sqrt(x)`, `provided x >= 0` | + ### The inverse hyperbolic tangent's answer is given on its domain, and says so `1/((1 - a^2 x^2)^(5/2) artanh(a x))` was left unevaluated, although the substitution `a x = tanh(u)` diff --git a/Sources/AngouriMath/Functions/Algebra/Polynomials/PartialFractions.cs b/Sources/AngouriMath/Functions/Algebra/Polynomials/PartialFractions.cs index b88f44d50..cfa49a3f3 100644 --- a/Sources/AngouriMath/Functions/Algebra/Polynomials/PartialFractions.cs +++ b/Sources/AngouriMath/Functions/Algebra/Polynomials/PartialFractions.cs @@ -1869,6 +1869,19 @@ internal static bool DerivativeHoldsAtSampledPoints(Entity antiderivative, Entit return HoldsAtSampledPoints(Bare(pinnedAntiderivative).Differentiate(x), pinnedIntegrand, x); } + /// + /// Whether the derivative of differs from + /// at one of the default points where the integrand is + /// real, as tells, with the symbols pinned before the + /// differentiation, as pins them. + /// + internal static bool DerivativeDiffersWhereReal(Entity antiderivative, Entity integrand, Variable x) + { + using var _ = MathS.Settings.DowncastingEnabled.Set(false); + var (pinnedAntiderivative, pinnedIntegrand) = Pinned(antiderivative, integrand, x); + return DiffersWhereReal(Bare(pinnedAntiderivative).Differentiate(x), pinnedIntegrand, x); + } + /// /// The two with every symbol but pinned to a fixed value: /// distinct values, off the integers, so that no two factors coincide by accident and diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs index 3abeafc79..e965d2e48 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs @@ -19089,6 +19089,20 @@ private static Entity SimplifiedWithoutTheImaginaryUnit(Entity written, Entity i /// Attempts to solve an integral using u-substitution. /// Looks for patterns where f(g(x)) * g'(x) can be integrated as F(g(x)). /// + /// + /// Whether holds an even root of something in + /// other than : the only way it can be real where the radicand is + /// negative and its own root imaginary. + /// + private static bool HoldsAnEvenRootBesides(Entity expr, Entity radicand, Entity.Variable x) + { + foreach (var node in expr.Nodes) + if (node is Powf(var @base, Number.Rational power) && power.ERational.Denominator.IsEven + && @base != radicand && @base.ContainsNode(x)) + return true; + return false; + } + internal static Entity? SolveBySubstitution(Entity expr, Entity.Variable x, bool integrateByParts = true) { // A rational function over written linear factors with symbols in their @@ -19325,6 +19339,17 @@ private static Entity SimplifiedWithoutTheImaginaryUnit(Entity written, Entity i // radicand, as the factoring below conditions its own. if (asTheRoot != resultInU) return Functions.PartialFractions.Bare(asTheRoot).Substitute(uSub, u).Provided(rootOf >= Number.Integer.Zero); + // And a rule below can take u real without reading its sign: `sqrt(u^4)` + // written as `u^2` is the other root wherever u is imaginary. That matters + // only where the integrand is real with the radicand negative, which takes + // a second even root imaginary beside this one, so there the answer is + // checked where the integrand is real: `sqrt(cos(x))/sqrt(1 + sec(x))` under + // `t = cos(x)` and `u = sqrt(t)` came back negated wherever the cosine is + // negative. https://github.com/asc-community/AngouriMath/issues/1581 + var answer = resultInU.Substitute(uSub, u); + if (HoldsAnEvenRootBesides(expr, rootOf, x) && Functions.PartialFractions.DerivativeDiffersWhereReal(answer, expr, x)) + return Functions.PartialFractions.Bare(answer).Provided(rootOf >= Number.Integer.Zero); + return answer; } // Substitute back: replace u with g(x) return resultInU.Substitute(uSub, u); diff --git a/Sources/Tests/UnitTests/Calculus/LinearRadicalSubstitutionIntegralTest.cs b/Sources/Tests/UnitTests/Calculus/LinearRadicalSubstitutionIntegralTest.cs index dff6f81d4..6a14ceae5 100644 --- a/Sources/Tests/UnitTests/Calculus/LinearRadicalSubstitutionIntegralTest.cs +++ b/Sources/Tests/UnitTests/Calculus/LinearRadicalSubstitutionIntegralTest.cs @@ -148,6 +148,38 @@ public void TheSearchsEvenRootSaysWhereItHolds() Assert.True(derivative.Substitute("x", 2.8).Evaled.IsNaN, "the answer claims a derivative outside its condition"); } + /// + /// And where the rules below read no sign at all, the answer is checked where the radicand + /// is negative. sqrt(cos(x))/sqrt(1 + sec(x)) is real where the cosine is negative, + /// two of its roots imaginary there; under t = cos(x) and u = sqrt(t) the + /// product of roots below the bar was written with sqrt(u^4) = u^2, and the answer + /// was negated wherever the cosine is negative. Rubi's 4.5.1.2, and the same in t. + /// https://github.com/asc-community/AngouriMath/issues/1581 + /// + [Theory] + [InlineData("sqrt(cos(x))/sqrt(1 + sec(x))", "provided cos(x) >= 0", new[] { 0.3, 0.9, -0.7 }, 2.4)] + [InlineData("sqrt(x)/(sqrt(1/x + 1)*sqrt(1 - x^2))", "provided x >= 0", new[] { 0.2, 0.5, 0.8 }, -0.6)] + public void AnEvenRootTakenRealWithoutASignSaysWhereItHolds(string written, string condition, double[] inside, double outsideAt) + { + var integrand = written.ToEntity(); + var integral = integrand.Integrate("x"); + Assert.DoesNotContain("integral(", integral.Stringize()); + Assert.Contains(condition, integral.Stringize()); + var derivative = integral.Substitute("C", 0).Differentiate("x"); + foreach (var at in inside) + { + var got = derivative.Substitute("x", at).EvalNumerical(); + var want = integrand.Substitute("x", at).EvalNumerical(); + Assert.False(got.IsNaN, $"no answer at x = {at}, inside the condition"); + Assert.True(Math.Abs((double)(got - want).RealPart) < 1e-9 * Math.Max(1, Math.Abs((double)want.RealPart)), + $"d/dx of the antiderivative is {got} at x = {at}, where the integrand is {want}"); + } + // Beyond it the integrand is real and the answer claims nothing. + var outside = integrand.Substitute("x", outsideAt).EvalNumerical(); + Assert.True(Math.Abs((double)outside.ImaginaryPart) < 1e-12, $"the integrand at x = {outsideAt} is {outside}, not real"); + Assert.True(derivative.Substitute("x", outsideAt).Evaled.IsNaN, "the answer claims a derivative outside its condition"); + } + /// /// A polynomial over a square root of something linear. None of these had an /// antiderivative, and each is a first-year exercise.