diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md
index f6dd61c5c..772500478 100644
--- a/BREAKING-CHANGES.md
+++ b/BREAKING-CHANGES.md
@@ -176,6 +176,22 @@ after it
|---|---|---|
| `"(a*c + b*c*x)^(-3-2*p)*(f + g*x)*(a^2 + 2*a*b*x + b^2*x^2)^p".ToEntity().Integrate("x")` | `integral(...)`; an answer with no value on the unreleased master | the antiderivative |
+### A value substituted under a binder is not captured by the name it binds
+
+**Wrong answers fixed.** An integral, a sum, a product, a derivative, a limit, a set builder and the
+other binders renamed their bound name to a temporary, substituted, and renamed it back, so a value
+holding the bound name was captured by it: `integral(t + a, t, 0, 1)` with `a := t` became
+`integral(t + t, t, 0, 1)`, which is 1 where the integral is `t + 1/2`. Where the value holds the
+bound name, the binder now takes a fresh one, `t_1`, and keeps it, as a lambda already did. A
+substitution whose target is absent from the body no longer passes over it, which made a chain of
+nested integrals cost twice as much per level
+([#1808](https://github.com/asc-community/AngouriMath/issues/1808)).
+
+| Input | Was (2.5.0) | Now |
+|---|---|---|
+| `new Integralf("t + a".ToEntity(), "t", (0, 1)).Substitute("a", "t")` | `integral(t + t, t, 0, 1)` | `integral(t_1 + t, t_1, 0, 1)` |
+| `new Summationf("k * a".ToEntity(), "k", 1, 3).Substitute("a", "k")` | `sum(k * k, k, 1, 3)`, 14 | `sum(k_1 * k, k_1, 1, 3)`, `6 k` |
+
### A power of a constant below `1e-50` is no longer simplified to zero
**Answers that were wrong.** Evaluation rounds a value within `1e-50` of an integer onto it, and
diff --git a/Sources/AngouriMath/Functions/Substitute.cs b/Sources/AngouriMath/Functions/Substitute.cs
index af6b6af44..0cbb04a1f 100644
--- a/Sources/AngouriMath/Functions/Substitute.cs
+++ b/Sources/AngouriMath/Functions/Substitute.cs
@@ -570,50 +570,63 @@ partial record Set
{
partial record ConditionalSet
{
- // TODO: it might be optimized
///
public override Entity Substitute(Entity x, Entity value)
{
if (this == x)
return value;
- var replacement = Variable.CreateTemp((x + value + Predicate + Var).Vars);
+ var (postSubs, boundName) = SubstituteUnderABinder(Predicate, Var, x, value);
- // { x | x > a } -> { temp_1 | temp_1 > a }
- var tempSubstituted = Predicate.Substitute(Var, replacement);
-
- // a = 0 -> { temp_1 | temp_1 > a } -> { temp_1 | temp_1 > 0 }
- // x = 0 -> { temp_1 | temp_1 > a } -> { temp_1 | temp_1 > a }
- var subs = tempSubstituted.Substitute(x, value);
-
- // { temp_1 | temp_1 > a } -> { x | x > a }
- var postSubs = subs.Substitute(replacement, Var);
-
- return New(Var, postSubs);
+ return New(boundName, postSubs);
}
}
}
+ ///
+ /// , bound over , with replaced
+ /// by where it occurs free, and the name the binder binds afterwards.
+ ///
+ ///
+ ///
+ /// Where holds the bound name, the binder takes a fresh one and keeps
+ /// it, as does: renamed back after the substitution, the value's
+ /// occurrence of the name was captured, and integral(t + a, t, 0, 1) with a := t
+ /// became integral(t + t, t, 0, 1), which is 1 where the integral is t + 1/2.
+ ///
+ ///
+ /// The body is passed over only where it can change. A body without , or
+ /// under a binder over itself, is returned as it is; where neither
+ /// nor holds the bound name, one pass gives what a
+ /// renaming would. Renaming it three times at every level, as every binder did, made a chain of
+ /// nested integrals cost a multiple of itself per level
+ /// (https://github.com/asc-community/AngouriMath/issues/1808).
+ ///
+ ///
+ private static (Entity Body, Entity Bound) SubstituteUnderABinder(Entity body, Entity bound, Entity x, Entity value)
+ {
+ if (x == bound || !body.ContainsNode(x))
+ return (body, bound);
+ if (value.ContainsNode(bound))
+ {
+ var fresh = Variable.CreateUnique(body + bound + x + value, bound is Variable named ? named.Name : "t");
+ return (body.Substitute(bound, fresh).Substitute(x, value), fresh);
+ }
+ if (!x.ContainsNode(bound))
+ return (body.Substitute(x, value), bound);
+ var replacement = Variable.CreateTemp((x + value + body + bound).Vars);
+ return (body.Substitute(bound, replacement).Substitute(x, value).Substitute(replacement, bound), bound);
+ }
+
partial record Integralf
{
- // TODO: it might be optimized
///
public override Entity Substitute(Entity x, Entity value)
{
if (this == x)
return value;
- var replacement = Variable.CreateTemp((x + value + Expression + Var).Vars);
-
- // integrate(x ^ 2 + a, x) -> integrate(temp_1 ^ 2 + a, temp_1)
- var tempSubstituted = Expression.Substitute(Var, replacement);
+ var (postSubs, boundName) = SubstituteUnderABinder(Expression, Var, x, value);
- // a = 0 -> integrate(temp_1 ^ 2 + a, temp_1) -> integrate(temp_1 ^ 2 + 0, temp_1)
- // x = 0 -> integrate(temp_1 ^ 2 + a, temp_1) -> integrate(temp_1 ^ 2 + a, temp_1)
- var subs = tempSubstituted.Substitute(x, value);
-
- // integrate(temp_1 ^ 2 + a, temp_1) -> integrate(x ^ 2 + a, temp_1)
- var postSubs = subs.Substitute(replacement, Var);
-
- return New(postSubs, Var, Range is var (from, to) ? (from.Substitute(x, value), to.Substitute(x, value)) : null);
+ return New(postSubs, boundName, Range is var (from, to) ? (from.Substitute(x, value), to.Substitute(x, value)) : null);
}
}
@@ -629,11 +642,8 @@ public override Entity Substitute(Entity x, Entity value)
{
if (this == x)
return value;
- var replacement = Variable.CreateTemp((x + value + Expression + Var).Vars);
- var tempSubstituted = Expression.Substitute(Var, replacement);
- var subs = tempSubstituted.Substitute(x, value);
- var postSubs = subs.Substitute(replacement, Var);
- return New(postSubs, Var, From.Substitute(x, value), To.Substitute(x, value));
+ var (postSubs, boundName) = SubstituteUnderABinder(Expression, Var, x, value);
+ return New(postSubs, boundName, From.Substitute(x, value), To.Substitute(x, value));
}
}
@@ -645,9 +655,8 @@ public override Entity Substitute(Entity x, Entity value)
{
if (this == x)
return value;
- var replacement = Variable.CreateTemp((x + value + Expression + Var).Vars);
- var renamed = Expression.Substitute(Var, replacement).Substitute(x, value).Substitute(replacement, Var);
- return New(renamed, Var, Over.Substitute(x, value));
+ var (renamed, boundName) = SubstituteUnderABinder(Expression, Var, x, value);
+ return New(renamed, boundName, Over.Substitute(x, value));
}
}
@@ -659,9 +668,8 @@ public override Entity Substitute(Entity x, Entity value)
{
if (this == x)
return value;
- var replacement = Variable.CreateTemp((x + value + Expression + Var).Vars);
- var renamed = Expression.Substitute(Var, replacement).Substitute(x, value).Substitute(replacement, Var);
- return New(renamed, Var, Over.Substitute(x, value));
+ var (renamed, boundName) = SubstituteUnderABinder(Expression, Var, x, value);
+ return New(renamed, boundName, Over.Substitute(x, value));
}
}
@@ -672,9 +680,8 @@ public override Entity Substitute(Entity x, Entity value)
{
if (this == x)
return value;
- var replacement = Variable.CreateTemp((x + value + Expression + Var).Vars);
- var renamed = Expression.Substitute(Var, replacement).Substitute(x, value).Substitute(replacement, Var);
- return New(renamed, Var, Over.Substitute(x, value));
+ var (renamed, boundName) = SubstituteUnderABinder(Expression, Var, x, value);
+ return New(renamed, boundName, Over.Substitute(x, value));
}
}
@@ -685,9 +692,8 @@ public override Entity Substitute(Entity x, Entity value)
{
if (this == x)
return value;
- var replacement = Variable.CreateTemp((x + value + Expression + Var).Vars);
- var renamed = Expression.Substitute(Var, replacement).Substitute(x, value).Substitute(replacement, Var);
- return New(renamed, Var, Over.Substitute(x, value));
+ var (renamed, boundName) = SubstituteUnderABinder(Expression, Var, x, value);
+ return New(renamed, boundName, Over.Substitute(x, value));
}
}
@@ -698,9 +704,8 @@ public override Entity Substitute(Entity x, Entity value)
{
if (this == x)
return value;
- var replacement = Variable.CreateTemp((x + value + Expression + Var).Vars);
- var renamed = Expression.Substitute(Var, replacement).Substitute(x, value).Substitute(replacement, Var);
- return New(renamed, Var, Over.Substitute(x, value));
+ var (renamed, boundName) = SubstituteUnderABinder(Expression, Var, x, value);
+ return New(renamed, boundName, Over.Substitute(x, value));
}
}
@@ -711,61 +716,36 @@ public override Entity Substitute(Entity x, Entity value)
{
if (this == x)
return value;
- var replacement = Variable.CreateTemp((x + value + Expression + Var).Vars);
- var tempSubstituted = Expression.Substitute(Var, replacement);
- var subs = tempSubstituted.Substitute(x, value);
- var postSubs = subs.Substitute(replacement, Var);
- return New(postSubs, Var, From.Substitute(x, value), To.Substitute(x, value));
+ var (postSubs, boundName) = SubstituteUnderABinder(Expression, Var, x, value);
+ return New(postSubs, boundName, From.Substitute(x, value), To.Substitute(x, value));
}
}
partial record Derivativef
{
- // TODO: it might be optimized
///
public override Entity Substitute(Entity x, Entity value)
{
if (this == x)
return value;
- var replacement = Variable.CreateTemp((x + value + Expression + Var).Vars);
-
- // derive(x ^ 2 + a, x) -> derive(temp_1 ^ 2 + a, temp_1)
- var tempSubstituted = Expression.Substitute(Var, replacement);
-
- // a = 0 -> derive(temp_1 ^ 2 + a, temp_1) -> derive(temp_1 ^ 2 + 0, temp_1)
- // x = 0 -> derive(temp_1 ^ 2 + a, temp_1) -> derive(temp_1 ^ 2 + a, temp_1)
- var subs = tempSubstituted.Substitute(x, value);
+ var (postSubs, boundName) = SubstituteUnderABinder(Expression, Var, x, value);
- // derive(temp_1 ^ 2 + a, temp_1) -> derive(x ^ 2 + a, temp_1)
- var postSubs = subs.Substitute(replacement, Var);
-
- return New(postSubs, Var);
+ return New(postSubs, boundName);
}
}
partial record Limitf
{
- // TODO: it might be optimized
///
public override Entity Substitute(Entity x, Entity value)
{
if (this == x)
return value;
- var replacement = Variable.CreateTemp((x + value + Expression + Var).Vars);
-
- // derive(x ^ 2 + a, x) -> derive(temp_1 ^ 2 + a, temp_1)
- var tempSubstituted = Expression.Substitute(Var, replacement);
-
- // a = 0 -> derive(temp_1 ^ 2 + a, temp_1) -> derive(temp_1 ^ 2 + 0, temp_1)
- // x = 0 -> derive(temp_1 ^ 2 + a, temp_1) -> derive(temp_1 ^ 2 + a, temp_1)
- var subs = tempSubstituted.Substitute(x, value);
-
- // derive(temp_1 ^ 2 + a, temp_1) -> derive(x ^ 2 + a, temp_1)
- var postSubs = subs.Substitute(replacement, Var);
+ var (postSubs, boundName) = SubstituteUnderABinder(Expression, Var, x, value);
var dst = Destination.Substitute(x, value);
- return New(postSubs, Var, dst, ApproachFrom);
+ return New(postSubs, boundName, dst, ApproachFrom);
}
}
diff --git a/Sources/Tests/UnitTests/Core/SubstitutionUnderABinderTest.cs b/Sources/Tests/UnitTests/Core/SubstitutionUnderABinderTest.cs
new file mode 100644
index 000000000..0e07e9271
--- /dev/null
+++ b/Sources/Tests/UnitTests/Core/SubstitutionUnderABinderTest.cs
@@ -0,0 +1,73 @@
+//
+// 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 AngouriMath.Extensions;
+using Xunit;
+using static AngouriMath.Entity;
+
+namespace AngouriMath.Tests.Core
+{
+ ///
+ /// Substituting under a binder: what is bound is not reached, a value holding the bound name is
+ /// not captured, and a body the target does not occur in is not traversed again at every level.
+ /// #1808
+ ///
+ [Trait("Area", "Core")]
+ public sealed class SubstitutionUnderABinderTest
+ {
+ private static Entity Nested(int depth, Entity upper)
+ {
+ if (depth == 0)
+ return 1;
+ var dummy = Variable.CreateVariableUnchecked($"u_{depth}");
+ return new Integralf(Nested(depth - 1, dummy), dummy, (0, upper));
+ }
+
+ ///
+ /// One integral per level, the target in the outermost bound only. Renaming every body three
+ /// times at every level made this take about twice as long per level, past a few minutes by
+ /// twenty; it is linear in the depth now. The answer is the chain built with the replacement.
+ ///
+ [Fact]
+ public void AChainOfIntegralsIsSubstitutedInItsOuterBoundOnly()
+ {
+ var target = Variable.CreateVariableUnchecked("target");
+ var replacement = Variable.CreateVariableUnchecked("w");
+ Assert.Equal(Nested(20, replacement), Nested(20, target).Substitute(target, replacement));
+ }
+
+ [Fact]
+ public void TheBoundNameIsNotReached()
+ {
+ var integral = new Integralf("t^2 + a".ToEntity(), "t", (0, "b".ToEntity()));
+ Assert.Equal(integral, integral.Substitute("t", 5));
+ Assert.Equal(integral, integral.Substitute("t^2".ToEntity(), 5));
+ Assert.Equal(new Integralf("t^2 + 3".ToEntity(), "t", (0, "b".ToEntity())), integral.Substitute("a", 3));
+ }
+
+ [Fact]
+ public void AValueHoldingTheBoundNameIsNotCaptured()
+ {
+ var integral = new Integralf("t + a".ToEntity(), "t", (0, 1));
+ var substituted = (Integralf)integral.Substitute("a", "t");
+ Assert.NotEqual(Variable.CreateVariableUnchecked("t"), substituted.Var);
+ Assert.Equal(1.5.ToNumber(), substituted.Substitute("t", 1).EvalNumerical());
+ var sum = (Summationf)new Summationf("k * a".ToEntity(), "k", 1, 3).Substitute("a", "k");
+ Assert.NotEqual(Variable.CreateVariableUnchecked("k"), sum.Var);
+ Assert.Equal(12.ToNumber(), sum.Substitute("k", 2).EvalNumerical());
+ }
+
+ [Fact]
+ public void ASumIsSubstitutedOutsideItsIndex()
+ {
+ var sum = new Summationf("k * a".ToEntity(), "k", 1, "n".ToEntity());
+ Assert.Equal(new Summationf("k * 2".ToEntity(), "k", 1, "n".ToEntity()), sum.Substitute("a", 2));
+ Assert.Equal(new Summationf("k * a".ToEntity(), "k", 1, 4), sum.Substitute("n", 4));
+ Assert.Equal(sum, sum.Substitute("k", 7));
+ }
+ }
+}