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}");
+ }
+ }
+ }
+}