From cfa504fb21daa4ca8bc3f98c37fc13bd896c9865 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Wed, 21 Oct 2026 16:04:58 +0000 Subject: [PATCH] Expand a power of a long sum without visiting the combinations it does not return Combinations(min, max, cellCount) enumerates the ways to share a power among the terms of a sum, and its loop ran i up to max at every depth, so every prefix of every length was visited: about 2^(n + k - 1) of them for the C(n + k - 1, k) it returns. Squaring a sum of thirty terms, 465 terms and well inside MaxExpansionTermCount, ran past a minute. The loop now stops where too few values are left above i to fill the remaining cells; a branch past that yields nothing, so no result and no order changes. Five of Rubi's integrals whose working squares such a sum were past the budget and are answered: 3.2.1's 1/((a g + b g x)^2 (A + B ln(e (a + b x)/(c + d x)))^2) and its n-th power, 4.1.3.1:357, and 6.7.1:1178 and 1251. Master f7f2a7a8 ran past 150 s on each; 2.5.0 declined three and ran past 60 s on two. Chain against master f7f2a7a8: pocket of 571 rows, 532 -> 537 solved, 0 wrong; the samples unchanged; of the 8 rows that moved, alone, master solved none and this solves 5, the other three declined here where master declined one and ran out of time on two; 6,064 rows compared for allocation, none grew. Part of #233. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura --- BREAKING-CHANGES.md | 18 ++++ Sources/.editorconfig | 3 + .../Functions/TreeAnalyzer/Expansion.cs | 11 ++- ...plifyOverASymbolicCommonDenominatorTest.cs | 83 +++++++++++++++++++ 4 files changed, 114 insertions(+), 1 deletion(-) create mode 100644 Sources/Tests/UnitTests/Common/SimplifyOverASymbolicCommonDenominatorTest.cs diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index f59a81d1c..503ab2062 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -247,6 +247,24 @@ out of it ([#233](https://github.com/asc-community/AngouriMath/issues/233)). | `"(a*m*x^m+b*n*q*ln(c*x^n)^(q-1))/(x*(a*x^m+b*ln(c*x^n)^q))".ToEntity().Integrate("x")` | `integral(...)` | `ln(a x^m + b ln(c x^n)^q)` | | `"(a*(m-1)*x^(m-1)+b*n*q*ln(c*x^n)^(q-1))/(a*x^m+b*x*ln(c*x^n)^q)".ToEntity().Integrate("x")` | `integral(...)` | `ln(a x^(m - 1) + b ln(c x^n)^q)` | +### A power of a long sum is expanded without visiting the combinations it does not return + +**Answers where there were none.** Expanding `(t_1 + ... + t_n)^k` enumerates the ways to share `k` +among the `n` terms, and the enumeration visited every prefix of every length, about +`2^(n + k - 1)` of them for the `C(n + k - 1, k)` it returns; squaring a sum of thirty terms ran past +a minute. It now stops a branch where too few values are left to fill it, which changes no term and +no order. Five integrals whose working squares such a sum were past the budget and are answered +([#233](https://github.com/asc-community/AngouriMath/issues/233)); this branch's master ran past +150 s on each. + +| Input | Was (2.5.0) | Now | +|---|---|---| +| `"1/((a*g+b*g*x)^2*(A+B*ln(k*((a+b*x)/(c+d*x))^n))^2)".ToEntity().Integrate("x")` | `integral(...)` | 412 characters | +| `"1/((a*g+b*g*x)^2*(A+B*ln(k*(a+b*x)/(c+d*x)))^2)".ToEntity().Integrate("x")` | past 60 s | 358 characters | +| `"(a+a*sin(k+f*x))^(3/2)*(A+B*sin(k+f*x))/(c+d*sin(k+f*x))^2".ToEntity().Integrate("x")` | `integral(...)` | 5,373 characters | +| `"sech(x)^2*tanh(x)^2/(2+tanh(x)^3)^2".ToEntity().Integrate("x")` | past 60 s | 117 characters | +| `"coth(x)^3*csch(x)*sqrt(1+csch(x))".ToEntity().Integrate("x")` | `integral(...)` | 325 characters | + ### A rational evaluated at a higher precision than before has all of its digits **More digits where there were a hundred.** A rational kept its decimal value in the precision current when it was diff --git a/Sources/.editorconfig b/Sources/.editorconfig index dc1e9ea43..414ded9ab 100644 --- a/Sources/.editorconfig +++ b/Sources/.editorconfig @@ -734,3 +734,6 @@ file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed [Tests/UnitTests/Calculus/HyperbolicIntegralIntegrationTest.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/Common/SimplifyOverASymbolicCommonDenominatorTest.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/TreeAnalyzer/Expansion.cs b/Sources/AngouriMath/Functions/TreeAnalyzer/Expansion.cs index a343ae3e8..8fa78efca 100644 --- a/Sources/AngouriMath/Functions/TreeAnalyzer/Expansion.cs +++ b/Sources/AngouriMath/Functions/TreeAnalyzer/Expansion.cs @@ -63,9 +63,18 @@ internal static IEnumerable> CombinateSums(EInteger itemCount, EI // Combinations(2, 6, 3) -> // { 4, 3, 2 }, { 5, 3, 2 }, { 6, 3, 2 }, { 5, 4, 2 }, { 6, 4, 2 }, // { 6, 5, 2 }, { 5, 4, 3 }, { 6, 4, 3 }, { 6, 5, 3 }, { 6, 5, 4 } + // + // The loop stops where too few values are left above i to fill the remaining + // cells, since a branch past that yields nothing. Without the bound every prefix of + // every length was visited -- about 2^(itemCount + targetSum - 1) of them for + // C(itemCount + targetSum - 1, targetSum) results -- so squaring a sum of thirty + // terms, 465 terms and well inside MaxExpansionTermCount, ran past a minute, and two + // more terms took four times as long. The bound changes no result and no order. + // https://github.com/asc-community/AngouriMath/issues/233 static IEnumerable> Combinations(EInteger min, EInteger max, EInteger cellCount) { - for (EInteger i = min; i <= max; i++) + var last = cellCount.Sign > 0 ? max - cellCount + 1 : max; + for (EInteger i = min; i <= last; i++) if (!cellCount.Equals(EInteger.One)) foreach (var l in Combinations(i + 1, max, cellCount - 1)) { diff --git a/Sources/Tests/UnitTests/Common/SimplifyOverASymbolicCommonDenominatorTest.cs b/Sources/Tests/UnitTests/Common/SimplifyOverASymbolicCommonDenominatorTest.cs new file mode 100644 index 000000000..ee231c053 --- /dev/null +++ b/Sources/Tests/UnitTests/Common/SimplifyOverASymbolicCommonDenominatorTest.cs @@ -0,0 +1,83 @@ +// +// 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 System.Threading; +using System.Threading.Tasks; +using AngouriMath.Extensions; +using Xunit; + +namespace AngouriMath.Tests.Common +{ + /// + /// Simplify returns on quotients over a symbolic denominator. Bringing two of their + /// fractions over a common denominator re-simplifies the cross products, and reading those as + /// polynomials expands squares of sums of thirty terms and more, whose exponent splits were + /// enumerated in time exponential in the number of terms. The integrator met them after a + /// substitution. #233 + /// + [Trait("Area", "Common")] + public sealed class SimplifyOverASymbolicCommonDenominatorTest + { + [Fact] + public void TheSquareOfALongSumExpands() + { + var names = Enumerable.Range(0, 30).Select(i => "s" + (char)('a' + i / 26) + (char)('a' + i % 26)).ToArray(); + var sum = string.Join(" + ", names); + var expanded = Run(() => $"({sum})^2".ToEntity().Expand()); + // Thirty squares and 435 products of two, none of which combine. + Assert.Equal(465, Entity.Sumf.LinearChildren(expanded).Count); + var at = expanded; + for (var i = 0; i < names.Length; i++) + at = at.Substitute(names[i], i + 1); + Assert.Equal((Entity)(465 * 465), at.Evaled); + } + + // Each returns as it was written: the search finds nothing shorter. Under a second or + // three each on a desktop, where the first did not return in ten minutes and the + // third took forty-seven seconds. + [Theory] + [InlineData("u^4/((c*u + d*(2 - u))^2*(2 - u))")] + [InlineData("u^3/((c*u + d)^2*(2 - u))")] + [InlineData("((A + B*(1 - 2*c/d))*sin(h)*(1 - u^2) + B*(-1/2 + c/d - c^2/(2*d^2))*sin(h))/(c + d*(2*(1 - u^2) - 1))^2/(-sin(h))")] + public void ReturnsWithTheValueItWasGiven(string written) + { + var expression = written.ToEntity(); + var simplified = Run(() => expression.Simplify(1)); + var compared = 0; + foreach (var shift in new[] { 0.0, 0.37, 1.13 }) + { + Entity Pinned(Entity e) => e + .Substitute("A", 1.3 + shift).Substitute("B", -0.7 + shift).Substitute("c", 0.4 + shift) + .Substitute("d", 1.9 - shift).Substitute("h", 0.6 + shift).Substitute("u", 0.3 + shift); + var want = Pinned(expression).EvalNumerical(); + var got = Pinned(simplified).EvalNumerical(); + if (want.IsNaN) + continue; + compared++; + Assert.True(Math.Abs((double)(got - want).RealPart) + Math.Abs((double)(got - want).ImaginaryPart) + < 1e-9 * Math.Max(1, Math.Abs((double)want.RealPart) + Math.Abs((double)want.ImaginaryPart)), + $"{written} simplified to {simplified}, which is {got} where the expression is {want}"); + } + Assert.Equal(3, compared); + } + + /// + /// On a thread of its own, so that a test host whose pool is busy cannot hold it back, + /// and with a wall far above the run, so that the bound is reached only by a computation + /// that does not finish. + /// + private static Entity Run(Func work) + { + var task = Task.Factory.StartNew(work, CancellationToken.None, + TaskCreationOptions.LongRunning, TaskScheduler.Default); + Assert.True(task.Wait(TimeSpan.FromMinutes(2)), "did not return in two minutes"); + return task.Result; + } + } +}