Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ private static Entity WithItsPiecewiseSumsCombined(Entity expr)
/// wherever it is asked for.
/// </para>
/// </remarks>
[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;

/// <summary>The settings <see cref="answered"/> was filled under.</summary>
[System.ThreadStatic] private static object?[]? answeredUnder;
Expand Down Expand Up @@ -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;
}

Expand All @@ -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.
/// </summary>
[System.ThreadStatic] internal static bool ASumOverRootsWouldAnswer;
internal static bool ASumOverRootsWouldAnswer
{
get => aSumOverRootsWouldAnswer;
set
{
if (value)
sumsOverRootsWanted++;
aSumOverRootsWouldAnswer = value;
}
}

[System.ThreadStatic] private static bool aSumOverRootsWouldAnswer;

/// <summary>
/// How many times <see cref="ASumOverRootsWouldAnswer"/> 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.
/// </summary>
[System.ThreadStatic] private static int sumsOverRootsWanted;

/// <summary>
/// <see cref="ComputeIndefiniteIntegral"/> past its entry check: the cycle guard, the
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// 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. <c>(1 + (1 + sqrt(3)) x^4)/(1 - x^4 + x^8)</c> was declined after
/// <c>(1 + x^4)/(1 - x^4 + x^8)</c>, and answered alone -- Rubi's 1.2.3.3:53, declined in the
/// harness's sample and solved when run on its own.
/// <a href="https://github.com/asc-community/AngouriMath/issues/233">#233</a>
/// </summary>
[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}");
}
}
}
}
Loading