From d115219af08977ed5ebb2c6c547a8b14004b1f6d Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Tue, 22 Sep 2026 23:28:03 +0000 Subject: [PATCH] A symbolic constant factor no longer stops the rounds of parts `F^(c (a + b x)) sin(d + pe x)^3` was left as written. The exponent's constant part becomes a factor -- `F^(a c)` -- and the by-parts loop for a polynomial times an exponential times a trigonometric stops when the factor it carries differentiates to nothing. `F^(a c)` differentiates to a *conditional* zero, `0 provided not F = 0 or a c > 0`, and the loop compared against the number: it carried on, the degree never fell, and after the step bound it declined. The derivative is read bare now. The conditions are the constant's own and travel with it in the answer -- the factor is there whatever they say -- and what the loop asks is whether the degree has fallen to nothing, which is a question about the variable. Family 4 of the Rubi suite: 286 -> 287 of 422, 0 wrong, with two rows that produced nothing now producing answers; family 6 377/417 and the 1774-problem suite 1707 unchanged. Suite 12644 passed; allocation gate passed on all 19 gated benchmarks. Part of #718. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura --- BREAKING-CHANGES.md | 15 +++++++++ .../Integration/IndefiniteIntegralSolver.cs | 9 +++-- .../Calculus/PolynomialExponentialTrigTest.cs | 33 +++++++++++++++++++ 3 files changed, 55 insertions(+), 2 deletions(-) diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index a24ce827b..4ccb886a4 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -1158,6 +1158,21 @@ rule answers, where the imaginary unit in the coefficient is read by nothing els | `"x/(2+2*i*tan(x))".Integrate("x")` | left unevaluated | the antiderivative | | `"(c+d*x)/(a+i*a*tan(pe+f*x))".Integrate("x")` | left unevaluated | the antiderivative | +### A symbolic constant factor no longer stops the rounds of parts + +`F^(c (a + b x)) sin(d + pe x)^3` was left as written. The exponent's constant part becomes a +factor -- `F^(a c)` -- and the by-parts loop stops when the factor it carries differentiates to +nothing; but `F^(a c)` differentiates to a *conditional* zero, `0 provided not F = 0 or a c > 0`, +and the loop compared against the number, so it carried on until it ran out of steps and +declined. The derivative is read bare now: the conditions are the constant's own and travel with +it in the answer, and what the loop asks is whether the degree has fallen to nothing. Rubi's +4.7.6 and 6.1.5 ([#718](https://github.com/asc-community/AngouriMath/issues/718)). + +| Input | Was (2.5.0) | Now | +|---|---|---| +| `"F^(c*(a+b*x))*sin(d+pe*x)^3".Integrate("x")` | left unevaluated | the antiderivative | +| `"F^(c*(a+b*x))*sin(d+pe*x)*cos(d+pe*x)".Integrate("x")` | left unevaluated | the antiderivative | + ### `binomial(n, k)` is a function **Addition, not silent.** The binomial coefficient is a node, `Entity.Binomialf`, spelled diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs index 9dce20aa0..599e36c75 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs @@ -3403,8 +3403,13 @@ void Add(int k, bool isSine, Entity coefficient) var antiderivative = exponential * (nextC * cosine + nextD * sine); total += carried * antiderivative; - var derivative = carried.Differentiate(x).InnerSimplified; - if (derivative == 0) + // Bare: a constant factor with symbols in it differentiates to a *conditional* + // zero -- `F^(a c)` gives `0 provided not F = 0 or a c > 0` -- and the loop, + // comparing against the number, carried on until it ran out of steps and + // declined. The conditions are the constant's own and travel with it in the + // answer; what the loop asks is whether the degree has fallen to nothing. + var derivative = Functions.PartialFractions.Bare(carried.Differentiate(x).InnerSimplified); + if (derivative == 0 || derivative.Evaled is Number.Complex { IsZero: true }) return total.InnerSimplified; // `- int P'(x) * (that) dx`, which is this loop again with the sign folded in. carried = (-derivative).InnerSimplified; diff --git a/Sources/Tests/UnitTests/Calculus/PolynomialExponentialTrigTest.cs b/Sources/Tests/UnitTests/Calculus/PolynomialExponentialTrigTest.cs index e4cd459c3..234ae44c2 100644 --- a/Sources/Tests/UnitTests/Calculus/PolynomialExponentialTrigTest.cs +++ b/Sources/Tests/UnitTests/Calculus/PolynomialExponentialTrigTest.cs @@ -262,5 +262,38 @@ public void TheResonanceIsSeenSymbolically(string integrand, string? symbol) [InlineData("x^2/(3 - 3*i*cot(x))")] [InlineData("(1 + 2*x)/(3 + 3*i*tan(1/2 + x))^2")] public void AnImaginaryTangentBelowTheBarIsAnExponential(string integrand) => DifferentiatesBack(integrand); + + /// + /// A base with symbols in it, and an exponent whose constant part has them too: + /// F^(c (a + b x)) leaves F^(a c) as a constant factor, and that factor + /// differentiates to a conditional zero -- 0 provided not F = 0 or a c > 0 -- + /// where the by-parts loop compared against the number and carried on until it ran out of + /// steps. Rubi's 4.7.6 and 6.1.5. + /// #718 + /// + [Theory] + [InlineData("F^(c*(a + b*x))*sin(d + pe*x)^3")] + [InlineData("F^(c*(a + b*x))*cos(d + pe*x)^2")] + [InlineData("F^(c*(a + b*x))*sin(d + pe*x)*cos(d + pe*x)")] + public void ASymbolicConstantFactorDoesNotStopTheParts(string integrand) + { + var integral = integrand.ToEntity().Integrate("x").Substitute("C", 0); + Assert.DoesNotContain("integral(", integral.Stringize()); + Assert.DoesNotContain("NaN", integral.Stringize()); + Entity Pin(Entity e) => e.Substitute("F", 2.3).Substitute("a", 0.4).Substitute("b", 1.1) + .Substitute("c", 0.7).Substitute("d", 0.9).Substitute("pe", 1.3); + var derivative = Pin(integral).Differentiate("x"); + var original = Pin(integrand.ToEntity()); + foreach (var at in Points) + { + var got = derivative.Substitute("x", at).EvalNumerical(); + var want = original.Substitute("x", at).EvalNumerical(); + Assert.False(got.IsNaN, $"the antiderivative of {integrand} differentiates to NaN at x = {at}"); + 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) + Math.Abs((double)want.ImaginaryPart)); + Assert.True(difference / scale < 1e-9, + $"d/dx of the antiderivative of {integrand} is {got} at x = {at}, where the integrand is {want}"); + } + } } }