Skip to content

Commit 1cc2cda

Browse files
A decline is held at the depth it was made at (#1745)
The integrator remembers what it has worked out, declines included, and rules may answer the question asked and one or two levels below it and decline the same integrand deeper. The memo's key told the question asked from the rest and nothing more, so a decline made at depth three was served at depth two: the search for 1/(x^(3/2) (a + c x^4)) declines the root's substitution 2/(w^2 (a + c w^8)) at depth three and asks it again at depth two, and the integral was declined. The key carries the depth as far as a scope reaches; a decline is taken only from its own depth, and an answer from any. 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 f136324 commit 1cc2cda

3 files changed

Lines changed: 85 additions & 14 deletions

File tree

‎BREAKING-CHANGES.md‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,24 @@ improper fraction is declined before the first division rather than after the la
376376
| `"(1 - b*x^2)^3/(c*(1 - b*x^2) + a*d*x^2)^3".ToEntity().Integrate("x")` | no answer within a minute | the antiderivative |
377377
| `"(a + b*x)^(5/2)/(c + d*x)^4".ToEntity().Integrate("x")` | `integral(...)` | the antiderivative |
378378

379+
### A decline is held at the depth it was made at
380+
381+
**Answers where there were none.** The integrator remembers what it has worked out, declines
382+
included, and a rule may answer the question asked and the level or two below it and decline the
383+
same integrand deeper. A decline made at depth three was held under a key that told the question
384+
asked from the rest and nothing more, and was served at depth two: the search for
385+
`1/(x^(3/2) (a + c x^4))` declines the root's substitution `2/(w^2 (a + c w^8))` at depth three and
386+
asks it again at depth two, and the integral was declined. The memo is kept per thread, so a decline
387+
made deeper in one integral was served the same way to the next. A decline is held at the depth it
388+
was made at now, and an answer at every depth
389+
([#718](https://github.com/asc-community/AngouriMath/issues/718)).
390+
391+
| Input | Was (2.5.0) | Now |
392+
|---|---|---|
393+
| `"1/(x^(3/2)*(a + c*x^4))".ToEntity().Integrate("x")` | `integral(...)` | the antiderivative |
394+
| `"1/((a + b/x^3)*x^5)".ToEntity().Integrate("x")` | `integral(...)` | the antiderivative |
395+
| `"1/(x^(7/2)*(a + b*x^2 + c*x^4))".ToEntity().Integrate("x")` | `integral(...)` | the antiderivative |
396+
379397
### The Hermite reduction's system is solved in one order whatever the spelling
380398

381399
**Faster, and not a different value.** The reduction of a rational integrand with a repeated factor

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

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -237,17 +237,23 @@ private static Entity Normalized(Entity expr, Entity.Variable x) =>
237237
/// simplifying and a change count is therefore never still.
238238
/// </para>
239239
/// <para>
240-
/// <b>A decline is held with the scope it was made in.</b> Five rules answer only the
241-
/// question asked (<see cref="AnsweringTheQuestionAsked"/>) and decline the same
242-
/// integrand one level down, so a <see langword="null"/> computed at depth two says
243-
/// nothing about depth one — and once held without the scope it was served to the
244-
/// top-level ask: <c>sec(x)^3</c>, tried and declined inside another rule's search, then
245-
/// asked for directly and declined from the cache in two milliseconds. The key carries
246-
/// the scope, and a lookup takes a decline only from its own scope and an answer from
247-
/// either, since an antiderivative that was found is right wherever it is asked for.
240+
/// <b>A decline is held with the scope it was made in.</b> Rules answer only the question
241+
/// asked (<see cref="AnsweringTheQuestionAsked"/>), or it and one or two levels below it
242+
/// (<see cref="AnsweringTheQuestionAskedOrOneBelow"/>,
243+
/// <see cref="AnsweringTheQuestionAskedOrTwoBelow"/>), and decline the same integrand
244+
/// deeper, so a <see langword="null"/> computed at one depth says nothing about a
245+
/// shallower one. Held without the scope it was served there: <c>sec(x)^3</c>, tried and
246+
/// declined inside another rule's search, then asked for directly and declined from the
247+
/// cache in two milliseconds. And held with the top told only from the rest, a decline at
248+
/// depth three was served at depth two: the search for <c>1/(x^(3/2) (a + c x^4))</c>
249+
/// declines the root's substitution <c>2/(w^2 (a + c w^8))</c> at depth three and asks it
250+
/// again at depth two, and the integral was declined. The key carries the depth as far as
251+
/// a scope reaches, <see cref="ScopeDepth"/>, and a lookup takes a decline only from its
252+
/// own depth and an answer from any, since an antiderivative that was found is right
253+
/// wherever it is asked for.
248254
/// </para>
249255
/// </remarks>
250-
[System.ThreadStatic] private static Dictionary<(Entity, Entity.Variable, bool, bool), Entity?>? answered;
256+
[System.ThreadStatic] private static Dictionary<(Entity, Entity.Variable, bool, int), Entity?>? answered;
251257

252258
/// <summary>The settings <see cref="answered"/> was filled under.</summary>
253259
[System.ThreadStatic] private static object?[]? answeredUnder;
@@ -346,6 +352,17 @@ private static Entity Normalized(Entity expr, Entity.Variable x) =>
346352
/// </summary>
347353
internal static bool AnsweringTheQuestionAskedOrTwoBelow => descentDepth <= 3;
348354

355+
/// <summary>
356+
/// The depth as far as a scope tells one depth from another: 1 for the question asked, 2
357+
/// and 3 for the levels <see cref="AnsweringTheQuestionAskedOrOneBelow"/> and
358+
/// <see cref="AnsweringTheQuestionAskedOrTwoBelow"/> reach, and 4 for every level past
359+
/// them, where every scoped rule declines alike.
360+
/// </summary>
361+
private static int ScopeDepth => System.Math.Min(descentDepth, DeepestScope);
362+
363+
/// <summary>The deepest <see cref="ScopeDepth"/>.</summary>
364+
private const int DeepestScope = 4;
365+
349366
/// <summary>
350367
/// <paramref name="expr"/> integrated as a question in its own right rather than as a
351368
/// step in the search for the current one: the term of a sum asked at the top is asked at
@@ -485,7 +502,7 @@ private static Entity Normalized(Entity expr, Entity.Variable x) =>
485502
// would otherwise be answered with, without the rule ever being reached to say that a
486503
// second pass could answer it.
487504
if (answer is not null && memo is not null && stamp is not null && SettingsState.StillHolds(stamp))
488-
memo[(Normalized(expr, x), x, integrateByParts, true)] = answer;
505+
memo[(Normalized(expr, x), x, integrateByParts, 1)] = answer;
489506
return answer;
490507
}
491508

@@ -549,12 +566,13 @@ private static Entity Normalized(Entity expr, Entity.Variable x) =>
549566
var into = answered;
550567
var stamp = answeredUnder;
551568

552-
var key = (expr, x, integrateByParts, AnsweringTheQuestionAsked);
569+
var depth = ScopeDepth;
570+
var key = (expr, x, integrateByParts, depth);
553571
if (into.TryGetValue(key, out var already))
554572
return already;
555-
var otherScope = (expr, x, integrateByParts, !AnsweringTheQuestionAsked);
556-
if (into.TryGetValue(otherScope, out var elsewhere) && elsewhere is not null)
557-
return elsewhere;
573+
for (var other = 1; other <= DeepestScope; other++)
574+
if (other != depth && into.TryGetValue((expr, x, integrateByParts, other), out var elsewhere) && elsewhere is not null)
575+
return elsewhere;
558576

559577
// Whether the descent ran out *inside this computation*, which is the only truncation
560578
// that says anything about this key. The flag is one per thread, so it is cleared

‎Sources/Tests/UnitTests/Calculus/ScopedDeclineIsNotCachedTest.cs‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
// Website: https://am.angouri.org.
66
//
77

8+
using System;
89
using AngouriMath.Extensions;
910
using Xunit;
1011

@@ -44,5 +45,39 @@ public void AnIntegrandDeclinedInsideAnotherSearchIsStillAnsweredWhenAsked()
4445
var asked = "cos(x)^(-3)".ToEntity().Integrate("x");
4546
Assert.DoesNotContain("integral(", asked.Stringize());
4647
}
48+
49+
/// <summary>
50+
/// The same two levels further down, inside one integral's own search. A rule may answer
51+
/// the question asked and one or two levels below it and decline the same integrand
52+
/// deeper. The search for <c>1/(x^(3/2) (a + c x^4))</c> declines the root's substitution
53+
/// <c>2/(w^2 (a + c w^8))</c> at depth three and asks it again at depth two, and the
54+
/// decline was held under a key that told the top from the rest and nothing more, so it
55+
/// was served there and the integral was declined.
56+
/// </summary>
57+
/// <remarks>
58+
/// Checked by differentiating back with <c>a = 2.3</c>, <c>b = 0.7</c>, <c>c = 1.3</c>,
59+
/// for positive <c>x</c>, where the half powers are real.
60+
/// </remarks>
61+
[Theory]
62+
[InlineData("1/(x^(3/2)*(a + c*x^4))")]
63+
[InlineData("1/(x^(5/2)*(a + c*x^4))")]
64+
[InlineData("1/((a + b/x^3)*x^5)")]
65+
[InlineData("1/(x^(7/2)*(a + b*x^2 + c*x^4))")]
66+
public void AnIntegrandDeclinedThreeBelowIsStillAnsweredTwoBelow(string integrand)
67+
{
68+
var integral = integrand.ToEntity().Integrate("x");
69+
Assert.DoesNotContain("integral(", integral.Stringize());
70+
Entity Pinned(Entity e) => e.Substitute("a", 2.3).Substitute("b", 0.7).Substitute("c", 1.3);
71+
var derivative = Pinned(integral.Substitute("C", 0)).Differentiate("x");
72+
var original = Pinned(integrand.ToEntity());
73+
foreach (var at in new[] { 0.4, 0.9, 1.7 })
74+
{
75+
var got = derivative.Substitute("x", at).EvalNumerical();
76+
var want = original.Substitute("x", at).EvalNumerical();
77+
Assert.True(Math.Abs((double)(got - want).RealPart) + Math.Abs((double)(got - want).ImaginaryPart)
78+
< 1e-9 * Math.Max(1, Math.Abs((double)want.RealPart)),
79+
$"d/dx of the antiderivative of {integrand} is {got} at x = {at}, where the integrand is {want}");
80+
}
81+
}
4782
}
4883
}

0 commit comments

Comments
 (0)