Skip to content

Commit 87be1ba

Browse files
Check for cancellation before each exact evaluation of the perfect-square collapse (#1995)
The collapse's numeric checks evaluate exactly at sample points, and over coefficients like b^335 one point is seconds of rational arithmetic. Rubi's 1.1.2.4:565 spent three and a half minutes in one Simplify of a substitution, nearly all of it there, and ran on for a minute after its budget had cancelled it, since no evaluation asked. Each evaluation now checks for cancellation first, as the other steps that can run long do. A budget now binds where it did not: 3.2.1:292 and 3.5:37, answered in 23-24 s under a 5 s budget because nothing on their path asked, time out at the budget instead. Part of #718. Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent be7e92e commit 87be1ba

2 files changed

Lines changed: 107 additions & 2 deletions

File tree

‎Sources/AngouriMath/Functions/Simplification/Patterns/Patterns.ExpandFactorize.cs‎

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
using System;
99
using AngouriMath.Core.Budgets;
10+
using AngouriMath.Core.Multithreading;
1011
using AngouriMath.Core.Transformations;
1112
using PeterO.Numbers;
1213
using static AngouriMath.Entity;
@@ -229,6 +230,7 @@ private static bool EvaluableAtSamplePoints(Entity expr)
229230
Entity at = expr;
230231
for (var v = 0; v < variables.Count; v++)
231232
at = at.Substitute(variables[v], Real.Create(PerfectSquareSamplePoints[v % PerfectSquareSamplePoints.Length]));
233+
MultithreadingFunctional.ExitIfCancelled();
232234
return at.Evaled is Complex;
233235
}
234236

@@ -238,11 +240,20 @@ private static bool EvaluableAtSamplePoints(Entity expr)
238240
/// symbolic test costs anything.
239241
/// </summary>
240242
/// <remarks>
243+
/// <para>
241244
/// The asymmetry with <see cref="AgreesNumerically"/> is deliberate and is what makes
242245
/// this safe to put in front of the symbolic test: that one answers "may I keep this",
243246
/// so it refuses whatever it cannot evaluate; this one answers "may I drop this", so it
244247
/// refuses to drop whatever it cannot evaluate. A point it cannot reach is not evidence
245248
/// of difference, and a rule may not decline on evidence it does not have.
249+
/// </para>
250+
/// <para>
251+
/// Each evaluation checks for cancellation before it starts, as the other steps that can
252+
/// run long do. An evaluation is exact, and over coefficients like <c>b^335</c> one point
253+
/// is seconds of rational arithmetic: Rubi's <c>x^(7/2)/((a + b x^2)^2 (c + d x^2)^3)</c>
254+
/// spent a minute here after its budget had cancelled it.
255+
/// https://github.com/asc-community/AngouriMath/issues/718
256+
/// </para>
246257
/// </remarks>
247258
private static bool DiffersNumerically(Entity left, Entity right)
248259
{
@@ -256,7 +267,11 @@ private static bool DiffersNumerically(Entity left, Entity right)
256267
a = a.Substitute(variables[v], Real.Create(point));
257268
b = b.Substitute(variables[v], Real.Create(point));
258269
}
259-
if (a.Evaled is not Complex left1 || b.Evaled is not Complex right1)
270+
MultithreadingFunctional.ExitIfCancelled();
271+
if (a.Evaled is not Complex left1)
272+
return false;
273+
MultithreadingFunctional.ExitIfCancelled();
274+
if (b.Evaled is not Complex right1)
260275
return false;
261276
var difference = (left1 - right1).Abs();
262277
if (difference is Real real && real.EDecimal.Abs()
@@ -280,7 +295,12 @@ private static bool AgreesNumerically(Entity original, Entity candidate)
280295
before = before.Substitute(variables[v], Real.Create(point));
281296
after = after.Substitute(variables[v], Real.Create(point));
282297
}
283-
if (before.Evaled is not Complex left || after.Evaled is not Complex right)
298+
// Checked before each evaluation, as in DiffersNumerically.
299+
MultithreadingFunctional.ExitIfCancelled();
300+
if (before.Evaled is not Complex left)
301+
return false;
302+
MultithreadingFunctional.ExitIfCancelled();
303+
if (after.Evaled is not Complex right)
284304
return false;
285305
var difference = (left - right).Abs();
286306
if (difference is not Real real || real.EDecimal.Abs()
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
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 AngouriMath.Functions;
13+
using Xunit;
14+
15+
namespace AngouriMath.Tests.Core.Multithreading
16+
{
17+
/// <summary>
18+
/// An integration stops when the budget it runs under cancels it, including inside the
19+
/// numeric checks of the perfect-square collapse, which evaluate exactly at sample points.
20+
/// <a href="https://github.com/asc-community/AngouriMath/issues/718">#718</a>
21+
/// </summary>
22+
/// <remarks>
23+
/// Rubi 1.1.2.4 #565 spends three and a half minutes in one <c>Simplify</c> of a
24+
/// substitution, nearly all of it evaluating sums with coefficients like <c>b^335</c> at
25+
/// those points, and ran on for a minute after its budget was cancelled, since no
26+
/// evaluation asked.
27+
/// </remarks>
28+
[Trait("Area", "Core")]
29+
public sealed class IntegrationCancelTest
30+
{
31+
private static readonly TimeSpan Budget = TimeSpan.FromSeconds(8);
32+
33+
/// <summary>
34+
/// The mechanism, with no clock in it: every cross term of this sum is told apart from
35+
/// its candidate at the sample points, so the collapse reaches no <c>Simplify</c> that
36+
/// would have stopped it, and answers null unless an evaluation asks first.
37+
/// </summary>
38+
[Fact]
39+
public void ACancelledPerfectSquareTestStopsBeforeItEvaluates()
40+
{
41+
using var source = new CancellationTokenSource();
42+
source.Cancel();
43+
var task = OnItsOwnThread(source.Token, () => Patterns.CollapseToPerfectSquare("x + 2 * sqrt(x * y) + y".ToEntity()));
44+
var thrown = Assert.Throws<AggregateException>(() => task.Wait());
45+
Assert.IsAssignableFrom<OperationCanceledException>(thrown.InnerException);
46+
}
47+
48+
/// <summary>
49+
/// The integrand, under a budget that lands inside those checks. A machine that answers
50+
/// it within the budget has nothing to show here.
51+
/// </summary>
52+
[Fact]
53+
public void ACancelledIntegrationStopsWithinThreeTimesItsBudget()
54+
{
55+
using var source = new CancellationTokenSource();
56+
var task = OnItsOwnThread(source.Token, () => "x^(7/2)/((a+b*x^2)^2*(c+d*x^2)^3)".ToEntity().Integrate("x"));
57+
if (task.Wait(Budget))
58+
return;
59+
source.Cancel();
60+
Assert.True(StopsWithin(task, 2 * Budget),
61+
$"still running {(2 * Budget).TotalSeconds} s after its {Budget.TotalSeconds} s budget was cancelled");
62+
}
63+
64+
// On a thread of its own, since the token is local to the thread that sets it, and a
65+
// dedicated one, so that work on the pool cannot hold the wait up.
66+
private static Task<Entity?> OnItsOwnThread(CancellationToken token, Func<Entity?> work)
67+
=> Task.Factory.StartNew(() =>
68+
{
69+
MathS.Multithreading.SetLocalCancellationToken(token);
70+
return work();
71+
}, CancellationToken.None, TaskCreationOptions.LongRunning, TaskScheduler.Default);
72+
73+
private static bool StopsWithin(Task task, TimeSpan patience)
74+
{
75+
try
76+
{
77+
return task.Wait(patience);
78+
}
79+
catch (AggregateException stopped) when (stopped.InnerException is OperationCanceledException)
80+
{
81+
return true;
82+
}
83+
}
84+
}
85+
}

0 commit comments

Comments
 (0)