Skip to content

Commit 64f144b

Browse files
Remember a decline a sum over roots would answer as one, so a later question still gets the second pass (#2036)
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. Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent 99fca7a commit 64f144b

2 files changed

Lines changed: 92 additions & 7 deletions

File tree

‎Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs‎

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -290,7 +290,7 @@ private static Entity WithItsPiecewiseSumsCombined(Entity expr)
290290
/// wherever it is asked for.
291291
/// </para>
292292
/// </remarks>
293-
[System.ThreadStatic] private static Dictionary<(Entity, Entity.Variable, bool, int), Entity?>? answered;
293+
[System.ThreadStatic] private static Dictionary<(Entity, Entity.Variable, bool, int), (Entity? Answer, bool ASumOverRootsWould)>? answered;
294294

295295
/// <summary>The settings <see cref="answered"/> was filled under.</summary>
296296
[System.ThreadStatic] private static object?[]? answeredUnder;
@@ -539,7 +539,7 @@ private static Entity WithItsPiecewiseSumsCombined(Entity expr)
539539
// would otherwise be answered with, without the rule ever being reached to say that a
540540
// second pass could answer it.
541541
if (answer is not null && memo is not null && stamp is not null && SettingsState.StillHolds(stamp))
542-
memo[(Normalized(expr, x), x, integrateByParts, 1)] = answer;
542+
memo[(Normalized(expr, x), x, integrateByParts, 1)] = (answer, false);
543543
return answer;
544544
}
545545

@@ -553,7 +553,25 @@ private static Entity WithItsPiecewiseSumsCombined(Entity expr)
553553
/// Set where the rational integrator declined only because the residues lie in a field of
554554
/// degree above two, which a sum over roots would have written.
555555
/// </summary>
556-
[System.ThreadStatic] internal static bool ASumOverRootsWouldAnswer;
556+
internal static bool ASumOverRootsWouldAnswer
557+
{
558+
get => aSumOverRootsWouldAnswer;
559+
set
560+
{
561+
if (value)
562+
sumsOverRootsWanted++;
563+
aSumOverRootsWouldAnswer = value;
564+
}
565+
}
566+
567+
[System.ThreadStatic] private static bool aSumOverRootsWouldAnswer;
568+
569+
/// <summary>
570+
/// How many times <see cref="ASumOverRootsWouldAnswer"/> was set, which is how a computation
571+
/// tells whether it set it itself: the flag is one per question asked, and already set by
572+
/// whatever was asked before.
573+
/// </summary>
574+
[System.ThreadStatic] private static int sumsOverRootsWanted;
557575

558576
/// <summary>
559577
/// <see cref="ComputeIndefiniteIntegral"/> past its entry check: the cycle guard, the
@@ -606,10 +624,19 @@ private static Entity WithItsPiecewiseSumsCombined(Entity expr)
606624
var depth = ScopeDepth;
607625
var key = (expr, x, integrateByParts, depth);
608626
if (into.TryGetValue(key, out var already))
609-
return already;
627+
{
628+
// A decline remembered from a first pass that a sum over roots would have answered says
629+
// so again: the memo outlives the question, and without this the one asked next on the
630+
// thread was declined where the second pass answers it. Integrating
631+
// `(1 + x^4)/(1 - x^4 + x^8)` and then `(1 + (1 + sqrt(3)) x^4)/(1 - x^4 + x^8)` declined
632+
// the second, which answered alone.
633+
if (already.Answer is null && already.ASumOverRootsWould)
634+
ASumOverRootsWouldAnswer = true;
635+
return already.Answer;
636+
}
610637
for (var other = 1; other <= DeepestScope; other++)
611-
if (other != depth && into.TryGetValue((expr, x, integrateByParts, other), out var elsewhere) && elsewhere is not null)
612-
return elsewhere;
638+
if (other != depth && into.TryGetValue((expr, x, integrateByParts, other), out var elsewhere) && elsewhere.Answer is not null)
639+
return elsewhere.Answer;
613640

614641
// Whether the descent ran out *inside this computation*, which is the only truncation
615642
// 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)
619646
// branch -- a term of a top-level sum re-did its declined sub-integrals for that.
620647
var truncatedBefore = descentTruncated;
621648
descentTruncated = false;
649+
var wantedBefore = sumsOverRootsWanted;
622650
Entity? computed;
623651
bool truncatedHere;
624652
try
@@ -648,7 +676,7 @@ private static Entity WithItsPiecewiseSumsCombined(Entity expr)
648676
// again, which needs the recent answers and not all of them.
649677
if (into.Count >= MostAnswersKept)
650678
into.Clear();
651-
into[key] = computed;
679+
into[key] = (computed, computed is null && sumsOverRootsWanted != wantedBefore);
652680
}
653681
return computed;
654682
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
//
2+
// Copyright (c) 2019-2026 Angouri.
3+
// AngouriMath is licensed under MIT.
4+
// Details: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.
5+
// Website: https://am.angouri.org.
6+
//
7+
8+
using System;
9+
using System.Threading;
10+
using System.Threading.Tasks;
11+
using AngouriMath.Extensions;
12+
using Xunit;
13+
14+
namespace AngouriMath.Tests.Calculus
15+
{
16+
/// <summary>
17+
/// The answers already found are kept per thread across questions, and a rational function whose
18+
/// residues want a sum over roots is declined in the first pass and answered in the second. The
19+
/// first pass's declines were kept as declines, so a later question that asked one of them again
20+
/// was declined at once: the memo answered for the rule, the rule never said that a second pass
21+
/// would answer, and none ran. <c>(1 + (1 + sqrt(3)) x^4)/(1 - x^4 + x^8)</c> was declined after
22+
/// <c>(1 + x^4)/(1 - x^4 + x^8)</c>, and answered alone -- Rubi's 1.2.3.3:53, declined in the
23+
/// harness's sample and solved when run on its own.
24+
/// <a href="https://github.com/asc-community/AngouriMath/issues/233">#233</a>
25+
/// </summary>
26+
[Trait("Area", "Calculus")]
27+
public sealed class ADeclineASumOverRootsWouldAnswerIsRememberedAsOneTest
28+
{
29+
[Fact]
30+
public void IsAnsweredAfterTheQuestionThatAskedItsPartsFirst()
31+
{
32+
const string first = "(1 + x^4)/(1 - x^4 + x^8)";
33+
const string then = "(1 + (1 + sqrt(3))*x^4)/(1 - x^4 + x^8)";
34+
Entity? answer = null;
35+
// A thread of its own, so that what is remembered is what these two leave and nothing a
36+
// test before them did.
37+
var task = Task.Factory.StartNew(() =>
38+
{
39+
Assert.DoesNotContain("integral(", first.ToEntity().Integrate("x").Stringize());
40+
answer = then.ToEntity().Integrate("x");
41+
}, CancellationToken.None, TaskCreationOptions.LongRunning, TaskScheduler.Default);
42+
Assert.True(task.Wait(TimeSpan.FromMinutes(2)), "the two did not finish within two minutes");
43+
task.GetAwaiter().GetResult();
44+
Assert.DoesNotContain("integral(", answer!.Stringize());
45+
var derivative = answer.Substitute("C", 0).Differentiate("x");
46+
var original = then.ToEntity();
47+
foreach (var at in new[] { -1.7, -0.6, 0.3, 0.9, 1.4 })
48+
{
49+
var want = original.Substitute("x", at).EvalNumerical();
50+
var got = derivative.Substitute("x", at).EvalNumerical();
51+
Assert.True(Math.Abs((double)(got - want).RealPart) + Math.Abs((double)(got - want).ImaginaryPart)
52+
< 1e-9 * Math.Max(1, Math.Abs((double)want.RealPart) + Math.Abs((double)want.ImaginaryPart)),
53+
$"d/dx of the antiderivative of {then} is {got} at x = {at}, where the integrand is {want}");
54+
}
55+
}
56+
}
57+
}

0 commit comments

Comments
 (0)