Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions BREAKING-CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
134 changes: 57 additions & 77 deletions Sources/AngouriMath/Functions/Substitute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -570,50 +570,63 @@ partial record Set
{
partial record ConditionalSet
{
// TODO: it might be optimized
/// <inheritdoc/>
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);
}
}
}

/// <summary>
/// <paramref name="body"/>, bound over <paramref name="bound"/>, with <paramref name="x"/> replaced
/// by <paramref name="value"/> where it occurs free, and the name the binder binds afterwards.
/// </summary>
/// <remarks>
/// <para>
/// Where <paramref name="value"/> holds the bound name, the binder takes a fresh one and keeps
/// it, as <see cref="Lambda"/> does: renamed back after the substitution, the value's
/// occurrence of the name was captured, and <c>integral(t + a, t, 0, 1)</c> with <c>a := t</c>
/// became <c>integral(t + t, t, 0, 1)</c>, which is 1 where the integral is <c>t + 1/2</c>.
/// </para>
/// <para>
/// The body is passed over only where it can change. A body without <paramref name="x"/>, or
/// under a binder over <paramref name="x"/> itself, is returned as it is; where neither
/// <paramref name="x"/> nor <paramref name="value"/> 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).
/// </para>
/// </remarks>
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
/// <inheritdoc/>
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);
}
}

Expand All @@ -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));
}
}

Expand All @@ -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));
}
}

Expand All @@ -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));
}
}

Expand All @@ -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));
}
}

Expand All @@ -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));
}
}

Expand All @@ -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));
}
}

Expand All @@ -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
/// <inheritdoc/>
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
/// <inheritdoc/>
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);
}
}

Expand Down
73 changes: 73 additions & 0 deletions Sources/Tests/UnitTests/Core/SubstitutionUnderABinderTest.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// 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.
/// <a href="https://github.com/asc-community/AngouriMath/issues/1808">#1808</a>
/// </summary>
[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));
}

/// <summary>
/// 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.
/// </summary>
[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));
}
}
}
Loading