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
36 changes: 36 additions & 0 deletions BREAKING-CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,42 @@ keeps it true.

[#363](https://github.com/asc-community/AngouriMath/issues/363),
[#746](https://github.com/asc-community/AngouriMath/issues/746) item 79.
| **Silent** | `"derivative(y, x) + y - x".SolveEquation("y")`, and the same equation written `= 0` | `{ x }`, which is not a root of it | `{ y : derivative(y, x) + y - x = 0 }` |
| **Silent** | `"integral(y, x) + y - x".SolveEquation("y")` | `{ -(C + -x) / (x + 1) }` | `{ y : integral(y, x) + y - x = 0 }` |
| **Silent** | `"limit(y, x, 0) + y - x".SolveEquation("y")` | `{ x / 2 }` | `{ y : limit(y, x, 0) + y - x = 0 }` |
| **Silent** | `"sum(y, k, 1, 3) + y - k".SolveEquation("y")` | `{ k / 4 }` | `{ y : sum(y, k, 1, 3) + y - k = 0 }` |
| **Silent** | `"{ y : derivative(y, x) + y - x = 0 }".ToEntity().Simplify()` | `{ y : y - x = 0 }`, a different set | unchanged |

### An equation whose unknown stands under a derivative is left unsolved

`"derivative(y, x) + y - x".SolveEquation("y")` answered `{ x }`. Substituting that back —
with this library's own `Substitute` — gives `derivative(x, x) + x - x`, which is `1`. The set
named a member that is not a root.

The derivative went to zero because `y` is not `x`, and every calculus operator does the same:
`limit(y, x, 0)` is `y`, `integral(y, x)` is `x * y + C`, `sum(y, k, 1, 3)` is `3 * y`. Each is a
decision about the *name* `y`, and the root the solver then returns says that name stands for an
expression in `x` — so the answer denies the step that produced it. Every one of the operators
was affected, and so was a quadratic in the unknown: `derivative(y, x) + y ^ 2 - x` answered
`{ sqrt(x), -sqrt(x) }`, which leave `x ^ (-1/2) / 2` and `-1/2 * x ^ (-1/2)`.

The equation is not thereby unsatisfiable, so the empty set would replace one false claim with
another. What holds is the condition as written, and that is what comes back. Solving it needs a
differential-equation solver, which
[#746](https://github.com/asc-community/AngouriMath/issues/746) has as item 48 and which this
library does not yet have.

A root that does not mention the name the operator is taken over denies nothing, and is returned
as before: `"derivative(y * x, x) + y - 1".SolveEquation("y")` is `{ 1/2 }`, and
`"derivative(y ^ 2, y) - 2".SolveEquation("y")` is `{ 1 }` — there the unknown *is* the name the
derivative binds, and no independence is claimed of it.

The set builder had the same reading of its own bound name and lost it the same way:
`{ y : derivative(y, x) + y - x = 0 }` simplified to `{ y : y - x = 0 }`, which is `{ x }`. A
binder over `y` makes `y` range over values, and expressions in `x` are among them, so that
simplification settles a condition that was written to stay open. It no longer fires.

[#964](https://github.com/asc-community/AngouriMath/issues/964)

---

Expand Down
1 change: 1 addition & 0 deletions Sources/.editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -89,4 +89,5 @@ file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed
file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed under MIT.\nDetails: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.\nWebsite: https://am.angouri.org.\n

[Tests/AotSmokeTest/*.cs]
[Tests/UnitTests/Algebra/SolveTest/CalculusEquationTest.cs]
file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed under MIT.\nDetails: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.\nWebsite: https://am.angouri.org.\n
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,34 @@ public abstract partial record CalculusOperator(Entity Expression, Entity Var) :

/// <summary>The expression the operator is applied to, which may mention <see cref="Var"/>.</summary>
public Entity Expression { get; init; } = Binding.Of(Var).In(Expression);

/// <summary>
/// The names some calculus operator in <paramref name="expression"/> is taken over,
/// where <paramref name="name"/> stands inside the body that operator is applied to.
/// </summary>
/// <remarks>
/// Such an operator decides its value by taking <paramref name="name"/> to be free
/// of the name it is taken over: <c>derivative(y, x)</c> is 0 because <c>y</c> is not
/// <c>x</c>, <c>limit(y, x, 0)</c> is <c>y</c>, and <c>integral(y, x)</c> is
/// <c>x * y + C</c>. That is a decision about the name and not about a value put in
/// its place, so wherever <paramref name="name"/> stands for values rather than for
/// itself — a set builder ranges over them, a solver's answer supplies one — the
/// decision is not available and nothing may rest on it.
/// <a href="https://github.com/asc-community/AngouriMath/issues/964">#964</a>
///
/// The name an operator binds itself is not one of these: the <c>y</c> in
/// <c>derivative(y ^ 2, y)</c> is the one being differentiated over, and no
/// independence is claimed of it.
/// </remarks>
internal static IReadOnlyCollection<Variable> NamesAssumedFreeOf(Entity expression, Variable name)
{
HashSet<Variable>? found = null;
foreach (var node in expression.Nodes)
if (node is CalculusOperator { Var: Variable over } calculus
&& over != name && calculus.Expression.ContainsNode(name))
(found ??= new HashSet<Variable>()).Add(over);
return found ?? (IReadOnlyCollection<Variable>)System.Array.Empty<Variable>();
}
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ internal static Set Solve(Entity equation, Variable x)
// (2^x + 2^(2x) - 6 = 0).Solve(x) gave { 1 } while
// "2^x + 2^(2x) - 6".SolveEquation(x) gave { 1, ln((-3) ^ (1 / ln(2))) },
// and the second of those is not a root of anything.
return StatementSolver.WithoutSpuriousRoots(finite, equation, x);
return StatementSolver.UnsolvedWhereIndependenceIsDenied(
StatementSolver.WithoutSpuriousRoots(finite, equation, x), equation.Equalizes(0), x);
}
else
return solutions;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,34 @@ internal static Set WithoutSpuriousRoots(Set roots, Entity equation, Variable x)
? finite.Where(root => !IsSpurious(equation, x, root)).ToSet()
: roots;

/// <summary>
/// Answers the condition itself where a root denies the independence the calculus
/// operators in the equation were evaluated under.
/// </summary>
/// <remarks>
/// <c>derivative(y, x) + y - x</c> was answered <c>{ x }</c>: the derivative went to
/// zero because <c>y</c> is not <c>x</c>, and the root then says that it is. Putting
/// it back gives <c>derivative(x, x) + x - x</c>, which is 1 — so the set named a
/// member that is not a root. A root free of that name is untouched, because nothing
/// was assumed that it goes on to deny: <c>derivative(y * x, x) + y - 1</c> is still
/// <c>{ 1/2 }</c>.
///
/// The equation is not thereby unsatisfiable, so the empty set would be a second
/// false claim in place of the first. What holds is the condition as written, and
/// solving it needs a differential-equation solver this library does not have.
/// <a href="https://github.com/asc-community/AngouriMath/issues/964">#964</a>,
/// <a href="https://github.com/asc-community/AngouriMath/issues/746">#746</a>
/// </remarks>
internal static Set UnsolvedWhereIndependenceIsDenied(Set roots, Entity condition, Variable x)
{
if (roots is not FiniteSet finite)
return roots;
var assumed = CalculusOperator.NamesAssumedFreeOf(condition, x);
return assumed.Count > 0 && finite.Any(root => assumed.Any(root.ContainsNode))
? new ConditionalSet(x, condition)
: roots;
}

/// <summary>
/// How large the residual has to be next to the terms it is the sum of before it
/// counts as evidence against a root.
Expand Down Expand Up @@ -127,7 +155,9 @@ internal static Set Solve(Entity expr, Variable x)
=> AnalyticalSetSolver.Solve(left, right, x),

Equalsf(var left, var right) when left is not Set && right is not Set
=> WithoutSpuriousRoots(AnalyticalEquationSolver.Solve(left - right, x), left - right, x),
=> UnsolvedWhereIndependenceIsDenied(
WithoutSpuriousRoots(AnalyticalEquationSolver.Solve(left - right, x), left - right, x),
expr, x),

Equalsf => Empty,

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,17 @@ protected override Entity InnerSimplify(bool isExact)
// as `{ } provided not x = 0`, where the x named in the condition is no
// longer the x the set ranges over.
// https://github.com/asc-community/AngouriMath/issues/878

// A calculus operator taken over some other name reads this one as free of
// it -- derivative(y, x) is 0 because y is not x. Under a binder over y that
// reading is not available: y ranges over values here, and expressions in x
// are among them. Simplifying anyway settles a condition that was written to
// stay open: `{ y : derivative(y, x) + y - x = 0 }` became `{ y : y - x = 0 }`,
// which names x, and putting x back into the condition it came from gives 1.
// https://github.com/asc-community/AngouriMath/issues/964
if (Var is Variable bound && CalculusOperator.NamesAssumedFreeOf(Predicate, bound).Count > 0)
return this;

var predicate = Predicate.InnerSimplified(isExact);

// `expr provided a provided b` is what the rules build where two operands
Expand Down
92 changes: 92 additions & 0 deletions Sources/Tests/UnitTests/Algebra/SolveTest/CalculusEquationTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
//
// 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.Linq;
using AngouriMath;
using AngouriMath.Extensions;
using Xunit;

namespace AngouriMath.Tests.Algebra.SolveTest
{
/// <summary>
/// An equation whose unknown stands inside a derivative, an integral, a limit, a sum
/// or a product, where the operator is taken over some other name.
/// https://github.com/asc-community/AngouriMath/issues/964
/// </summary>
/// <remarks>
/// Such an operator is evaluated by deciding that the unknown does not move with the
/// name the operator is taken over: <c>derivative(y, x)</c> is 0 because <c>y</c> is
/// not <c>x</c>. A root that mentions <c>x</c> denies exactly that, so it cannot be
/// asserted on the strength of a derivation that assumed it.
/// </remarks>
[Trait("Area", "Algebra")]
public sealed class CalculusEquationTest
{
/// <summary>
/// The answer must not be a finite set holding something that is not a root. The
/// membership is checked by substituting back and simplifying, which is what the
/// issue did by hand.
/// </summary>
private static void AssertEveryRootSatisfies(string equation, string unknown)
{
var expr = equation.ToEntity();
if (expr.SolveEquation(unknown) is not Entity.Set.FiniteSet finite)
return;
foreach (var root in finite)
Assert.True(
expr.Substitute(unknown, root).Simplify() == 0,
$"{root.Stringize()} is not a root of {equation}; it leaves "
+ $"{expr.Substitute(unknown, root).Simplify().Stringize()}");
}

[Theory]
[InlineData("derivative(y, x) + y - x", "y")]
[InlineData("derivative(y, x) + y ^ 2 - x", "y")]
[InlineData("integral(y, x) + y - x", "y")]
[InlineData("limit(y, x, 0) + y - x", "y")]
[InlineData("sum(y, k, 1, 3) + y - k", "y")]
public void NoAssertedNonRoot(string equation, string unknown)
=> AssertEveryRootSatisfies(equation, unknown);

/// <summary>
/// The empty set says there is no such <c>y</c>, which is as false as naming one.
/// The condition itself is the only one of the three that is true.
/// </summary>
[Theory]
[InlineData("derivative(y, x) + y - x", "y")]
[InlineData("integral(y, x) + y - x", "y")]
[InlineData("limit(y, x, 0) + y - x", "y")]
[InlineData("sum(y, k, 1, 3) + y - k", "y")]
[InlineData("product(y, k, 1, 3) + y - k", "y")]
public void UnsolvedRatherThanEmpty(string equation, string unknown)
=> Assert.IsType<Entity.Set.ConditionalSet>(equation.ToEntity().SolveEquation(unknown));

/// <summary>An equation and the same equation written <c>= 0</c> answer alike.</summary>
[Theory]
[InlineData("derivative(y, x) + y - x", "y")]
[InlineData("limit(y, x, 0) + y - x", "y")]
public void StatementFormAnswersTheSame(string equation, string unknown)
=> Assert.IsType<Entity.Set.ConditionalSet>(
(equation + " = 0").ToEntity().Solve(unknown));

/// <summary>
/// Where no root mentions the name the operator is taken over, nothing was assumed
/// that the answer then denies, and the answer stands. <c>d(y*x)/dx</c> is <c>y</c>
/// for a <c>y</c> free of <c>x</c>, and <c>1/2</c> is free of <c>x</c>.
/// </summary>
[Theory]
[InlineData("derivative(y * x, x) + y - 1", "y", "1/2")]
[InlineData("derivative(y ^ 2, y) - 2", "y", "1")]
[InlineData("limit(y * x, x, 2) - 4", "y", "2")]
[InlineData("sum(y, k, 1, 3) - 6", "y", "2")]
public void AnUnthreatenedRootIsStillReturned(string equation, string unknown, string root)
{
var finite = Assert.IsType<Entity.Set.FiniteSet>(equation.ToEntity().SolveEquation(unknown));
Assert.Contains(root.ToEntity(), finite.Select(e => e.Simplify()));
}
}
}
Loading