Skip to content

Commit ab7a8bd

Browse files
An integral already being worked out is a cycle, and is declined at once (#1234)
#1233 bounded the integrator's descent, which stopped it killing the process. That was necessary and it was not sufficient: the descent branches, so 32 levels of a cycle is still an enormous search. Measured on Rubi's independent test suites, problems 1400-1450 took 985 seconds under the bound alone where v2.4.0 took 44. One integrand held the run for over ten minutes. Declining at the first repeat instead: 37 seconds over the same fifty problems, which is v2.4.0's figure and slightly under it. The set is keyed on the integrand with its variable renamed to one canonical name, and that renaming is the whole point. SolveBySubstitution names each new variable with Variable.CreateUnique, so a level and the level it came from are alpha-equivalent rather than equal. A set keyed on the integrand as written would never see the same entry twice -- which is exactly why the memo added in #1157 could not stop this either, and why the first fix had to be a depth bound rather than a visited set. DeepestDescent stays as a backstop for anything this does not model, rather than as the mechanism. #1232 Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 229c9ab commit ab7a8bd

1 file changed

Lines changed: 42 additions & 0 deletions

File tree

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

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,32 @@ private static Entity Normalized(Entity expr) =>
224224
/// </summary>
225225
[System.ThreadStatic] private static bool descentTruncated;
226226

227+
/// <summary>
228+
/// The integrals this thread is part-way through, so that asking for one again while it is
229+
/// still being worked out is recognised as a cycle rather than followed round again.
230+
/// </summary>
231+
/// <remarks>
232+
/// <para>
233+
/// Keyed on the integrand with its variable renamed to <see cref="CycleVariable"/>, which
234+
/// is the whole point. <see cref="IndefiniteIntegralSolver.SolveBySubstitution"/> names
235+
/// each new variable with <c>Variable.CreateUnique</c>, so a level and the level it came
236+
/// from are alpha-equivalent rather than equal, and a set keyed on the integrand as
237+
/// written would never see the same entry twice. Neither does <see cref="answered"/>, and
238+
/// for the same reason — which is why the memo could not stop this.
239+
/// </para>
240+
/// <para>
241+
/// This is what makes <see cref="DeepestDescent"/> a backstop instead of the mechanism.
242+
/// The bound alone stops the process dying, and it is not enough on its own: the descent
243+
/// branches, so 32 levels of a cycle is still an enormous search, and one Rubi integrand
244+
/// took over ten minutes under the bound where <c>v2.4.0</c> passed the whole section it
245+
/// is in within a minute. Declining at the first repeat costs nothing and answers at once.
246+
/// </para>
247+
/// </remarks>
248+
[System.ThreadStatic] private static HashSet<(Entity, bool)>? inProgress;
249+
250+
/// <summary>The name every in-progress integrand's variable is rewritten to.</summary>
251+
[ConstantField] private static readonly Entity.Variable CycleVariable = Entity.Variable.CreateVariableOrConstant("__integration_cycle_var");
252+
227253
/// <summary>Does not add the constant of integration because this is called recursively.</summary>
228254
internal static Entity? ComputeIndefiniteIntegral(Entity expr, Entity.Variable x, bool integrateByParts = true)
229255
{
@@ -233,14 +259,30 @@ private static Entity Normalized(Entity expr) =>
233259
return null;
234260
}
235261
if (descentDepth == 0)
262+
{
236263
descentTruncated = false;
264+
inProgress?.Clear();
265+
}
266+
267+
// Renamed so that the same integral under two different variable names is one entry.
268+
var cycleKey = (expr.Substitute(x, CycleVariable), integrateByParts);
269+
var open = inProgress ??= new HashSet<(Entity, bool)>();
270+
if (!open.Add(cycleKey))
271+
{
272+
// Already on the stack: following it again cannot reach an answer this call has
273+
// not already tried, so declining is both correct and immediate.
274+
descentTruncated = true;
275+
return null;
276+
}
277+
237278
descentDepth++;
238279
try
239280
{
240281
return ComputeIndefiniteIntegralBounded(expr, x, integrateByParts);
241282
}
242283
finally
243284
{
285+
open.Remove(cycleKey);
244286
descentDepth--;
245287
}
246288
}

0 commit comments

Comments
 (0)