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