From e32afd0f092f6a415eaa065dd4ae0dfea190140d Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Wed, 21 Oct 2026 20:01:24 +0000 Subject: [PATCH] Remember a decline a sum over roots would answer as one, so a later question still gets the second pass The integrator keeps the answers it found per thread, across top-level questions, for as long as the settings hold. A rational function whose residues want a sum over roots is declined in the first pass, which sets ASumOverRootsWouldAnswer, and the second pass answers it; afterwards only the top-level question's entry was replaced with that answer. The first pass's declines of its sub-questions stayed in the memo as plain declines, and a later question that asked one of them got it from the memo, so the rule never ran to set the flag and no second pass ran: the question was declined. Integrating (1 + x^4)/(1 - x^4 + x^8) and then (1 + (1 + sqrt(3)) x^4)/(1 - x^4 + x^8) on one thread declined the second in 63 ms, which answers alone. Rubi's 1.2.3.3:53 was counted unsolved in the harness's family-1 sample for exactly this reason: the worker integrates the file's rows in order, and row 33 comes first. Bisected over the 1,202 rows before it to that one. The memo now records, with a decline, whether a sum over roots was wanted in computing it, and a hit on such a decline sets the flag again. Whether one was wanted is read from a count of the flag's raises around the computation; the flag itself is still one per top-level question, which the rules that read it mid-computation rely on. 2.5.0 declines both integrals in either order, having no sums over roots, so no released answer changes. Part of #233. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura --- .../Integration/Integration.Definition.cs | 42 +++++++++++--- ...erRootsWouldAnswerIsRememberedAsOneTest.cs | 57 +++++++++++++++++++ 2 files changed, 92 insertions(+), 7 deletions(-) create mode 100644 Sources/Tests/UnitTests/Calculus/ADeclineASumOverRootsWouldAnswerIsRememberedAsOneTest.cs diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs b/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs index 80893e834..2f4504f36 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs @@ -290,7 +290,7 @@ private static Entity WithItsPiecewiseSumsCombined(Entity expr) /// wherever it is asked for. /// /// - [System.ThreadStatic] private static Dictionary<(Entity, Entity.Variable, bool, int), Entity?>? answered; + [System.ThreadStatic] private static Dictionary<(Entity, Entity.Variable, bool, int), (Entity? Answer, bool ASumOverRootsWould)>? answered; /// The settings was filled under. [System.ThreadStatic] private static object?[]? answeredUnder; @@ -539,7 +539,7 @@ private static Entity WithItsPiecewiseSumsCombined(Entity expr) // would otherwise be answered with, without the rule ever being reached to say that a // second pass could answer it. if (answer is not null && memo is not null && stamp is not null && SettingsState.StillHolds(stamp)) - memo[(Normalized(expr, x), x, integrateByParts, 1)] = answer; + memo[(Normalized(expr, x), x, integrateByParts, 1)] = (answer, false); return answer; } @@ -553,7 +553,25 @@ private static Entity WithItsPiecewiseSumsCombined(Entity expr) /// Set where the rational integrator declined only because the residues lie in a field of /// degree above two, which a sum over roots would have written. /// - [System.ThreadStatic] internal static bool ASumOverRootsWouldAnswer; + internal static bool ASumOverRootsWouldAnswer + { + get => aSumOverRootsWouldAnswer; + set + { + if (value) + sumsOverRootsWanted++; + aSumOverRootsWouldAnswer = value; + } + } + + [System.ThreadStatic] private static bool aSumOverRootsWouldAnswer; + + /// + /// How many times was set, which is how a computation + /// tells whether it set it itself: the flag is one per question asked, and already set by + /// whatever was asked before. + /// + [System.ThreadStatic] private static int sumsOverRootsWanted; /// /// past its entry check: the cycle guard, the @@ -606,10 +624,19 @@ private static Entity WithItsPiecewiseSumsCombined(Entity expr) var depth = ScopeDepth; var key = (expr, x, integrateByParts, depth); if (into.TryGetValue(key, out var already)) - return already; + { + // A decline remembered from a first pass that a sum over roots would have answered says + // so again: the memo outlives the question, and without this the one asked next on the + // thread was declined where the second pass answers it. Integrating + // `(1 + x^4)/(1 - x^4 + x^8)` and then `(1 + (1 + sqrt(3)) x^4)/(1 - x^4 + x^8)` declined + // the second, which answered alone. + if (already.Answer is null && already.ASumOverRootsWould) + ASumOverRootsWouldAnswer = true; + return already.Answer; + } for (var other = 1; other <= DeepestScope; other++) - if (other != depth && into.TryGetValue((expr, x, integrateByParts, other), out var elsewhere) && elsewhere is not null) - return elsewhere; + if (other != depth && into.TryGetValue((expr, x, integrateByParts, other), out var elsewhere) && elsewhere.Answer is not null) + return elsewhere.Answer; // Whether the descent ran out *inside this computation*, which is the only truncation // that says anything about this key. The flag is one per thread, so it is cleared @@ -619,6 +646,7 @@ private static Entity WithItsPiecewiseSumsCombined(Entity expr) // branch -- a term of a top-level sum re-did its declined sub-integrals for that. var truncatedBefore = descentTruncated; descentTruncated = false; + var wantedBefore = sumsOverRootsWanted; Entity? computed; bool truncatedHere; try @@ -648,7 +676,7 @@ private static Entity WithItsPiecewiseSumsCombined(Entity expr) // again, which needs the recent answers and not all of them. if (into.Count >= MostAnswersKept) into.Clear(); - into[key] = computed; + into[key] = (computed, computed is null && sumsOverRootsWanted != wantedBefore); } return computed; } diff --git a/Sources/Tests/UnitTests/Calculus/ADeclineASumOverRootsWouldAnswerIsRememberedAsOneTest.cs b/Sources/Tests/UnitTests/Calculus/ADeclineASumOverRootsWouldAnswerIsRememberedAsOneTest.cs new file mode 100644 index 000000000..46c2dde86 --- /dev/null +++ b/Sources/Tests/UnitTests/Calculus/ADeclineASumOverRootsWouldAnswerIsRememberedAsOneTest.cs @@ -0,0 +1,57 @@ +// +// 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.Threading; +using System.Threading.Tasks; +using AngouriMath.Extensions; +using Xunit; + +namespace AngouriMath.Tests.Calculus +{ + /// + /// The answers already found are kept per thread across questions, and a rational function whose + /// residues want a sum over roots is declined in the first pass and answered in the second. The + /// first pass's declines were kept as declines, so a later question that asked one of them again + /// was declined at once: the memo answered for the rule, the rule never said that a second pass + /// would answer, and none ran. (1 + (1 + sqrt(3)) x^4)/(1 - x^4 + x^8) was declined after + /// (1 + x^4)/(1 - x^4 + x^8), and answered alone -- Rubi's 1.2.3.3:53, declined in the + /// harness's sample and solved when run on its own. + /// #233 + /// + [Trait("Area", "Calculus")] + public sealed class ADeclineASumOverRootsWouldAnswerIsRememberedAsOneTest + { + [Fact] + public void IsAnsweredAfterTheQuestionThatAskedItsPartsFirst() + { + const string first = "(1 + x^4)/(1 - x^4 + x^8)"; + const string then = "(1 + (1 + sqrt(3))*x^4)/(1 - x^4 + x^8)"; + Entity? answer = null; + // A thread of its own, so that what is remembered is what these two leave and nothing a + // test before them did. + var task = Task.Factory.StartNew(() => + { + Assert.DoesNotContain("integral(", first.ToEntity().Integrate("x").Stringize()); + answer = then.ToEntity().Integrate("x"); + }, CancellationToken.None, TaskCreationOptions.LongRunning, TaskScheduler.Default); + Assert.True(task.Wait(TimeSpan.FromMinutes(2)), "the two did not finish within two minutes"); + task.GetAwaiter().GetResult(); + Assert.DoesNotContain("integral(", answer!.Stringize()); + var derivative = answer.Substitute("C", 0).Differentiate("x"); + var original = then.ToEntity(); + foreach (var at in new[] { -1.7, -0.6, 0.3, 0.9, 1.4 }) + { + var want = original.Substitute("x", at).EvalNumerical(); + var got = derivative.Substitute("x", at).EvalNumerical(); + 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)), + $"d/dx of the antiderivative of {then} is {got} at x = {at}, where the integrand is {want}"); + } + } + } +}