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
102 changes: 102 additions & 0 deletions BREAKING-CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ read first.

| Silent? | What | Was | Is |
|---|---|---|---|
| **Silent** | `"x6 + x y + 1 = 0".ToEntity().Solve("x")`, and every equation no solver settles | `{ }` — there are no roots | `{ x : 1 + x ^ 6 + x * y = 0 }` — these are the roots, whichever they are |
| **Silent** | `"(x - 1) * (x6 + x y + 1) = 0".ToEntity().Solve("x")` | `{ 1 }` | `{ 1 } \/ { x : 1 + x ^ 6 + x * y = 0 }` |
| **Silent** | `"x6 + x y + 1 = 0 and x - 1 = 0".ToEntity().Solve("x")` | `{ }` | `{ x : x ^ 6 + x * y + 1 = 0 and x - 1 = 0 }` |
| **Silent** | `"sum(k, k, 1, n)".ToEntity().FreeVariables`, and `product` | `{ k, n }` — the bound index counted as free | `{ n }` |
| **Silent** | `"integral(t * b, t, 0, 1)".ToEntity().FreeVariables`, and every integral with limits | `{ b, t }` | `{ b }` |
| | `"x ^ 3 - x > 0".ToEntity().Solve("x")`, and every polynomial inequality of degree three or more | `NotSufficientlySupportedException: Only linear and quadratic polynomial inequalities are supported` | `(-1; 0) \/ (1; +oo)` — the solution set |
Expand All @@ -37,6 +40,105 @@ read first.
| | `Compile` to a nullable integral return type in a NativeAOT app | `AngouriBugException: IsNaN method expected for type System.Double`, which took the process down | the compiled function |
| **Silent** | an app publishing with `PublishTrimmed` or NativeAOT | `AngouriMath.dll` was copied in whole, being unmarked | it is trimmed with the rest, since the assembly now declares `IsTrimmable` |

### An equation nothing settled is no longer answered with the empty set

`Solve` and `SolveEquation` returned an empty `FiniteSet` for two different things: an equation
shown to have no roots, and an equation every solver in the chain declined. The empty set is a
positive claim — *no x satisfies this* — so the second of those was a wrong answer rather than a
graceful failure ([#1036](https://github.com/asc-community/AngouriMath/issues/1036),
[#746](https://github.com/asc-community/AngouriMath/issues/746) tier 4).

**Was** — indistinguishable, and false for the second column:

```
"x6 + x y + 1 = 0".ToEntity().Solve("x") { } six roots for every y
"sin(x) + x + y = 0".ToEntity().Solve("x") { }
"e^x + x + y = 0".ToEntity().Solve("x") { }
"x6 + x y + 1".ToEntity().SolveEquation("x") { }
"e^x = 0".ToEntity().Solve("x") { } genuinely none
"abs(x) = -1".ToEntity().Solve("x") { } genuinely none
```

**Is** — the equation itself, as the set of the x that satisfy it. It names the same set and
asserts of it only what was established:

```
"x6 + x y + 1 = 0".ToEntity().Solve("x") { x : 1 + x ^ 6 + x * y = 0 }
"sin(x) + x + y = 0".ToEntity().Solve("x") { x : sin(x) + x = -y }
"e^x + x + y = 0".ToEntity().Solve("x") { x : e ^ x + x = -y }
"x6 + x y + 1".ToEntity().SolveEquation("x") { x : 1 + x ^ 6 + x * y = 0 }
"e^x = 0".ToEntity().Solve("x") { } unchanged
"abs(x) = -1".ToEntity().Solve("x") { } unchanged
```

Turning Newton's method off leaves nothing numerical either, and those answers move the same way.
A search over finitely many starting points inside a bounded region finding nothing is a fact about
the search:

```
using var _ = MathS.Settings.AllowNewton.Set(false);

"x5 + 3x + 1 = 0".ToEntity().Solve("x") was { } is { x : x ^ 5 + 3 * x = -1 }
"sin(x) * x - 3 = 0".ToEntity().Solve("x") was { } is { x : sin(x) * x = 3 }

"x + sqrt(x^0.1 + a) + c".ToEntity().SolveEquation("x")
was { } is { x : sqrt(a + x ^ (1/10)) + c + x = 0 }
"(x + 6)^(1/6) + x + x3 + a".ToEntity().SolveEquation("x")
was { } is { x : (6 + x) ^ (1/6) + x + x ^ 3 = -a }
"2 ^ (x sin(x)) + 4 ^ (x sin(x)) + c".ToEntity().SolveEquation("x")
was { }
is { x : sin(x) * x = ln(((-1 - sqrt(1 - 4 * c)) / 2) ^ (1 / ln(2)))
or sin(x) * x = ln(((-1 + sqrt(1 - 4 * c)) / 2) ^ (1 / ln(2))) }
```

The last of those is the shape of it: the exponential solver did settle the equation in
`2 ^ (x sin(x))`, and it was the inversion of `x * sin(x)` that had nothing to say. What comes back
now is the half that was solved, with the half that was not left standing as a condition.

An answer that is partly settled keeps the part that is, so a product one of whose factors was
solved comes back as a union rather than as the factor's roots alone:

```
"(x - 1) * (x6 + x y + 1) = 0".ToEntity().Solve("x")
was { 1 }
is { 1 } \/ { x : 1 + x ^ 6 + x * y = 0 }

"x6 + x y + 1 = 0 or x - 1 = 0".ToEntity().Solve("x")
was { 1 }
is { 1 } \/ { x : 1 + x ^ 6 + x * y = 0 }

"x6 + x y + 1 = 0 implies x - 1 = 0".ToEntity().Solve("x")
was { 1 } \/ BB
is { 1 } \/ (BB \ { x : 1 + x ^ 6 + x * y = 0 })
```

A conjunction with an unsettled side is answered as the conjunction. Intersecting a finite set with
a condition keeps the elements whose membership could not be decided, so taking the intersection
here would have replaced one false claim with another — `{ 1 }` says 1 solves
`x^6 + x*y + 1 = 0`, and it does so only at `y = -2`:

```
"x6 + x y + 1 = 0 and x - 1 = 0".ToEntity().Solve("x")
was { }
is { x : x ^ 6 + x * y + 1 = 0 and x - 1 = 0 }
```

**What this means for callers.** `Solve` has always been typed `Set` and has always been able to
return an `Interval`, a `ConditionalSet` or a union; what changes is how often it does. Code that
casts the result to `FiniteSet`, or that reads an empty result as "no solutions", has to say which
of the two it means:

```csharp
var answer = equation.Solve(x);
if (answer is FiniteSet roots) { /* these are all of them */ }
else if (answer.IsSetEmpty) { /* shown to have none */ }
else { /* not settled, or not finite */ }
```

Nothing that solved before stops solving, and no equation that was shown to have no roots gains
any. Solving `x2 - 4 = 0`, `sin(x) = 1/2`, `x2 + 1 = 0` and the system solver's answers are
unchanged, as is `Solve` on an inequality.

### A bound index is not a free variable

`FreeVariables` knew about two binders — `Lambda` and the set builder — and about no others. A
Expand Down
6 changes: 4 additions & 2 deletions Sources/AngouriMath/Convenience/MathS.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5652,7 +5652,9 @@ internal static EDecimal DowncastingTolerance
}

/// <summary>
/// If you only need analytical solutions and an empty set if no analytical solutions were found, disable Newton's method
/// If you only need analytical solutions, disable Newton's method. What comes back
/// where there are none is the equation itself as a set builder: the empty set
/// would say the equation has no roots, which is a claim nothing established.
/// </summary>
/// <example>
/// <code>
Expand All @@ -5671,7 +5673,7 @@ internal static EDecimal DowncastingTolerance
/// { 1.0050669478588620808778841819730587303638458251953125 + 0.93725915669289194820379407246946357190608978271484375i,
/// ... omitting here most of the output, because it's huge
/// }
/// { } // nothing was found for 5-degree polynomial without numeric solution
/// { x : x ^ 5 + 3 * x = -1 } // no analytical solution was found for this quintic
/// </code>
/// </example>
public static Setting<bool> AllowNewton { get; } = true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -330,11 +330,43 @@ bool ProductIsDefinedAt(Entity root)
// TODO: Solve factorials (Needs Lambert W function)
// https://mathoverflow.net/a/28977

// if nothing has been found so far
// A numerical search that comes back with nothing has not shown there is
// nothing to find: Newton's method is started from finitely many points inside
// a bounded region, so an empty result is a fact about the search rather than
// about the equation. Where it does find roots the answer is theirs, unchanged.
if (MathS.Settings.AllowNewton && expr.Vars.Count == 1)
return expr.SolveNt(x).Select(ent => TryDowncast(expr, x, ent)).ToSet();
return expr.SolveNt(x).Select(ent => TryDowncast(expr, x, ent)).ToSet()
is FiniteSet { IsSetEmpty: false } found ? found : Unsolved(expr, x);

return Enumerable.Empty<Entity>().ToSet();
return Unsolved(expr, x);
}

/// <summary>
/// The answer when every solver above has declined: the equation itself, as the set
/// of the <paramref name="x"/> that satisfy it.
/// </summary>
/// <remarks>
/// The empty set is a positive claim -- that no <paramref name="x"/> satisfies
/// <paramref name="equation"/> -- and returning it from an exhausted search asserts
/// something nothing here established. <c>x^6 + x*y + 1 = 0</c> has six roots for
/// every <c>y</c> and was answered <c>{ }</c>. A condition asserts only what was
/// established, which is that these are the roots, whichever they are; the set is
/// still exactly the solution set, so nothing downstream is told anything false.
///
/// The spelling is the one <see cref="StatementSolver.UnsolvedWhereIndependenceIsDenied"/>
/// and the <a href="https://github.com/asc-community/AngouriMath/issues/278">#278</a>
/// case above already use, rather than a second way of saying the same thing.
/// <a href="https://github.com/asc-community/AngouriMath/issues/1036">#1036</a>,
/// <a href="https://github.com/asc-community/AngouriMath/issues/746">#746</a>
/// </remarks>
private static Set Unsolved(Entity equation, Variable x)
// Written back as an equation between the two sides where there are two, rather
// than as the residual the solvers pass around. A compensated call is handed its
// equation unsimplified on purpose, so the difference survives for them to read,
// and stating it as it stands leaves the reader `x^5 + 3*x - (-1) = 0` where the
// question was whether `x^5 + 3*x` is -1. The two admit the same x.
=> new ConditionalSet(x, equation is Minusf(var lhs, var rhs)
? lhs.Equalizes(rhs)
: equation.Equalizes(0));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,23 @@ private static EDecimal LargestTerm(Entity substituted)
return largest;
}

/// <summary>
/// Where both sides of a conjunction were settled, its solution set is the
/// intersection of theirs.
/// </summary>
/// <remarks>
/// Where one of them is a condition nothing settled, it is not: intersecting a
/// finite set with one keeps an element whose membership could not be decided, so
/// <c>x^6 + x*y + 1 = 0 and x - 1 = 0</c> comes back as <c>{ 1 }</c> — and 1 is a
/// root of the first only when <c>y</c> is -2. The conjunction as written asserts
/// exactly what is known about it and nothing more.
/// <a href="https://github.com/asc-community/AngouriMath/issues/1036">#1036</a>
/// </remarks>
private static Set Conjunction(Set left, Set right, Entity statement, Variable x)
=> (left, right) is (ConditionalSet, FiniteSet) or (FiniteSet, ConditionalSet)
? new ConditionalSet(x, statement)
: (Set)MathS.Intersection(left, right);

internal static Set Solve(Entity expr, Variable x)
=> expr switch
{
Expand All @@ -161,8 +178,8 @@ internal static Set Solve(Entity expr, Variable x)

Equalsf => Empty,

Andf(var left, var right) =>
MathS.Intersection(Solve(left, x), Solve(right, x)),
Andf(var left, var right) =>
Conjunction(Solve(left, x), Solve(right, x), expr, x),
Orf(var left, var right) =>
MathS.Union(Solve(left, x), Solve(right, x)),
Impliesf(var left, var right) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ partial record Entity
/// { 9.08837698687877... }
/// -----------------
/// sin(x) * x - 3 = 0
/// { }
/// { x : sin(x) * x = 3 }
/// </code>
/// </example>
public Set Solve(Variable var)
Expand Down
4 changes: 0 additions & 4 deletions Sources/Tests/UnitTests/Algebra/SolveTest/SolveOneEquation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -189,9 +189,6 @@ public void InvertedFunctions(string func, int rootAmount)
public void CDSolver(string expr, int rootCount, string? verifyRoots) => TestSolver(expr, rootCount, verifyRoots: verifyRoots);

[Theory]
[InlineData("x + sqrt(x^0.1 + a) + c", 0)]
[InlineData("(x + 6)^(1/6) + x + x3 + a", 0)]
[InlineData("sqrt(x + 1) + sqrt(x + 2) + a + x", 0)]
[InlineData("(x + 1)^(1/3) - x - a", 3)]
public void FractionedPoly(string expr, int rootCount) => TestSolver(expr, rootCount);

Expand Down Expand Up @@ -233,7 +230,6 @@ public void InvertedFunctions(string func, int rootAmount)
[InlineData("4^x - a", 1, "{ log(4, a) }")]
[InlineData("a^x + (a^2)^x - c", 2)]
[InlineData("e^x + (e2)^x - 1", 2)]
[InlineData("2 ^ (x sin(x)) + 4 ^ (x sin(x)) + c", 0)]
[InlineData("2^x - 4^x", 1, "{ 0 }")]
public void TestExponentialSolver(string equation, int rootCount, string? verifyRoots = null)
=> TestSolver(equation, rootCount, verifyRoots: verifyRoots);
Expand Down
Loading
Loading