diff --git a/Sources/AngouriMath/Functions/Simplification/Patterns/Patterns.ExpandFactorize.cs b/Sources/AngouriMath/Functions/Simplification/Patterns/Patterns.ExpandFactorize.cs
index 54610c34b..aa6b36265 100644
--- a/Sources/AngouriMath/Functions/Simplification/Patterns/Patterns.ExpandFactorize.cs
+++ b/Sources/AngouriMath/Functions/Simplification/Patterns/Patterns.ExpandFactorize.cs
@@ -7,6 +7,7 @@
using System;
using AngouriMath.Core.Budgets;
+using AngouriMath.Core.Multithreading;
using AngouriMath.Core.Transformations;
using PeterO.Numbers;
using static AngouriMath.Entity;
@@ -229,6 +230,7 @@ private static bool EvaluableAtSamplePoints(Entity expr)
Entity at = expr;
for (var v = 0; v < variables.Count; v++)
at = at.Substitute(variables[v], Real.Create(PerfectSquareSamplePoints[v % PerfectSquareSamplePoints.Length]));
+ MultithreadingFunctional.ExitIfCancelled();
return at.Evaled is Complex;
}
@@ -238,11 +240,20 @@ private static bool EvaluableAtSamplePoints(Entity expr)
/// symbolic test costs anything.
///
///
+ ///
/// The asymmetry with is deliberate and is what makes
/// this safe to put in front of the symbolic test: that one answers "may I keep this",
/// so it refuses whatever it cannot evaluate; this one answers "may I drop this", so it
/// refuses to drop whatever it cannot evaluate. A point it cannot reach is not evidence
/// of difference, and a rule may not decline on evidence it does not have.
+ ///
+ ///
+ /// Each evaluation checks for cancellation before it starts, as the other steps that can
+ /// run long do. An evaluation is exact, and over coefficients like b^335 one point
+ /// is seconds of rational arithmetic: Rubi's x^(7/2)/((a + b x^2)^2 (c + d x^2)^3)
+ /// spent a minute here after its budget had cancelled it.
+ /// https://github.com/asc-community/AngouriMath/issues/718
+ ///
///
private static bool DiffersNumerically(Entity left, Entity right)
{
@@ -256,7 +267,11 @@ private static bool DiffersNumerically(Entity left, Entity right)
a = a.Substitute(variables[v], Real.Create(point));
b = b.Substitute(variables[v], Real.Create(point));
}
- if (a.Evaled is not Complex left1 || b.Evaled is not Complex right1)
+ MultithreadingFunctional.ExitIfCancelled();
+ if (a.Evaled is not Complex left1)
+ return false;
+ MultithreadingFunctional.ExitIfCancelled();
+ if (b.Evaled is not Complex right1)
return false;
var difference = (left1 - right1).Abs();
if (difference is Real real && real.EDecimal.Abs()
@@ -280,7 +295,12 @@ private static bool AgreesNumerically(Entity original, Entity candidate)
before = before.Substitute(variables[v], Real.Create(point));
after = after.Substitute(variables[v], Real.Create(point));
}
- if (before.Evaled is not Complex left || after.Evaled is not Complex right)
+ // Checked before each evaluation, as in DiffersNumerically.
+ MultithreadingFunctional.ExitIfCancelled();
+ if (before.Evaled is not Complex left)
+ return false;
+ MultithreadingFunctional.ExitIfCancelled();
+ if (after.Evaled is not Complex right)
return false;
var difference = (left - right).Abs();
if (difference is not Real real || real.EDecimal.Abs()
diff --git a/Sources/Tests/UnitTests/Core/Multithreading/IntegrationCancelTest.cs b/Sources/Tests/UnitTests/Core/Multithreading/IntegrationCancelTest.cs
new file mode 100644
index 000000000..55fbeab6b
--- /dev/null
+++ b/Sources/Tests/UnitTests/Core/Multithreading/IntegrationCancelTest.cs
@@ -0,0 +1,85 @@
+//
+// 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 AngouriMath.Functions;
+using Xunit;
+
+namespace AngouriMath.Tests.Core.Multithreading
+{
+ ///
+ /// An integration stops when the budget it runs under cancels it, including inside the
+ /// numeric checks of the perfect-square collapse, which evaluate exactly at sample points.
+ /// #718
+ ///
+ ///
+ /// Rubi 1.1.2.4 #565 spends three and a half minutes in one Simplify of a
+ /// substitution, nearly all of it evaluating sums with coefficients like b^335 at
+ /// those points, and ran on for a minute after its budget was cancelled, since no
+ /// evaluation asked.
+ ///
+ [Trait("Area", "Core")]
+ public sealed class IntegrationCancelTest
+ {
+ private static readonly TimeSpan Budget = TimeSpan.FromSeconds(8);
+
+ ///
+ /// The mechanism, with no clock in it: every cross term of this sum is told apart from
+ /// its candidate at the sample points, so the collapse reaches no Simplify that
+ /// would have stopped it, and answers null unless an evaluation asks first.
+ ///
+ [Fact]
+ public void ACancelledPerfectSquareTestStopsBeforeItEvaluates()
+ {
+ using var source = new CancellationTokenSource();
+ source.Cancel();
+ var task = OnItsOwnThread(source.Token, () => Patterns.CollapseToPerfectSquare("x + 2 * sqrt(x * y) + y".ToEntity()));
+ var thrown = Assert.Throws(() => task.Wait());
+ Assert.IsAssignableFrom(thrown.InnerException);
+ }
+
+ ///
+ /// The integrand, under a budget that lands inside those checks. A machine that answers
+ /// it within the budget has nothing to show here.
+ ///
+ [Fact]
+ public void ACancelledIntegrationStopsWithinThreeTimesItsBudget()
+ {
+ using var source = new CancellationTokenSource();
+ var task = OnItsOwnThread(source.Token, () => "x^(7/2)/((a+b*x^2)^2*(c+d*x^2)^3)".ToEntity().Integrate("x"));
+ if (task.Wait(Budget))
+ return;
+ source.Cancel();
+ Assert.True(StopsWithin(task, 2 * Budget),
+ $"still running {(2 * Budget).TotalSeconds} s after its {Budget.TotalSeconds} s budget was cancelled");
+ }
+
+ // On a thread of its own, since the token is local to the thread that sets it, and a
+ // dedicated one, so that work on the pool cannot hold the wait up.
+ private static Task OnItsOwnThread(CancellationToken token, Func work)
+ => Task.Factory.StartNew(() =>
+ {
+ MathS.Multithreading.SetLocalCancellationToken(token);
+ return work();
+ }, CancellationToken.None, TaskCreationOptions.LongRunning, TaskScheduler.Default);
+
+ private static bool StopsWithin(Task task, TimeSpan patience)
+ {
+ try
+ {
+ return task.Wait(patience);
+ }
+ catch (AggregateException stopped) when (stopped.InnerException is OperationCanceledException)
+ {
+ return true;
+ }
+ }
+ }
+}