Skip to content

Commit 62935ca

Browse files
Do not answer an exhausted search with the empty set (#1036, #746 tier 4) (#1046)
`Solve` returned an empty `FiniteSet` both for an equation shown to have no roots and for one every solver declined. The empty set is a positive claim, so the second was a wrong answer: `x^6 + x*y + 1 = 0` has six roots for every `y` and came back as `{ }`. The two exits that mean "nothing settled this" now answer with the equation as a set builder — the spelling `AnalyticalEquationSolver` already uses for #278 and #964 — while an equation whose emptiness was established keeps `{ }`. Newton's method is included: a search from finitely many starting points inside a bounded region finding nothing is a fact about the search. 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 `x^6 + x*y + 1 = 0 and x - 1 = 0` would have become `{ 1 }` — one false claim in place of another, since 1 solves the first only at `y = -2`. Four theory rows recorded the old answer as correct. Each pinned an equation the solver gives up on and that has roots, so they move to the new test file with the honest assertion rather than being loosened. Claude-Session: https://claude.ai/code/session_01Bjumi5K7fg8yx6UK1mZTQd Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent 72c74ca commit 62935ca

7 files changed

Lines changed: 335 additions & 12 deletions

File tree

‎BREAKING-CHANGES.md‎

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ read first.
2121

2222
| Silent? | What | Was | Is |
2323
|---|---|---|---|
24+
| **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 |
25+
| **Silent** | `"(x - 1) * (x6 + x y + 1) = 0".ToEntity().Solve("x")` | `{ 1 }` | `{ 1 } \/ { x : 1 + x ^ 6 + x * y = 0 }` |
26+
| **Silent** | `"x6 + x y + 1 = 0 and x - 1 = 0".ToEntity().Solve("x")` | `{ }` | `{ x : x ^ 6 + x * y + 1 = 0 and x - 1 = 0 }` |
2427
| **Silent** | `"sum(k, k, 1, n)".ToEntity().FreeVariables`, and `product` | `{ k, n }` — the bound index counted as free | `{ n }` |
2528
| **Silent** | `"integral(t * b, t, 0, 1)".ToEntity().FreeVariables`, and every integral with limits | `{ b, t }` | `{ b }` |
2629
| | `"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 |
@@ -37,6 +40,105 @@ read first.
3740
| | `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 |
3841
| **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` |
3942

43+
### An equation nothing settled is no longer answered with the empty set
44+
45+
`Solve` and `SolveEquation` returned an empty `FiniteSet` for two different things: an equation
46+
shown to have no roots, and an equation every solver in the chain declined. The empty set is a
47+
positive claim — *no x satisfies this* — so the second of those was a wrong answer rather than a
48+
graceful failure ([#1036](https://github.com/asc-community/AngouriMath/issues/1036),
49+
[#746](https://github.com/asc-community/AngouriMath/issues/746) tier 4).
50+
51+
**Was** — indistinguishable, and false for the second column:
52+
53+
```
54+
"x6 + x y + 1 = 0".ToEntity().Solve("x") { } six roots for every y
55+
"sin(x) + x + y = 0".ToEntity().Solve("x") { }
56+
"e^x + x + y = 0".ToEntity().Solve("x") { }
57+
"x6 + x y + 1".ToEntity().SolveEquation("x") { }
58+
"e^x = 0".ToEntity().Solve("x") { } genuinely none
59+
"abs(x) = -1".ToEntity().Solve("x") { } genuinely none
60+
```
61+
62+
**Is** — the equation itself, as the set of the x that satisfy it. It names the same set and
63+
asserts of it only what was established:
64+
65+
```
66+
"x6 + x y + 1 = 0".ToEntity().Solve("x") { x : 1 + x ^ 6 + x * y = 0 }
67+
"sin(x) + x + y = 0".ToEntity().Solve("x") { x : sin(x) + x = -y }
68+
"e^x + x + y = 0".ToEntity().Solve("x") { x : e ^ x + x = -y }
69+
"x6 + x y + 1".ToEntity().SolveEquation("x") { x : 1 + x ^ 6 + x * y = 0 }
70+
"e^x = 0".ToEntity().Solve("x") { } unchanged
71+
"abs(x) = -1".ToEntity().Solve("x") { } unchanged
72+
```
73+
74+
Turning Newton's method off leaves nothing numerical either, and those answers move the same way.
75+
A search over finitely many starting points inside a bounded region finding nothing is a fact about
76+
the search:
77+
78+
```
79+
using var _ = MathS.Settings.AllowNewton.Set(false);
80+
81+
"x5 + 3x + 1 = 0".ToEntity().Solve("x") was { } is { x : x ^ 5 + 3 * x = -1 }
82+
"sin(x) * x - 3 = 0".ToEntity().Solve("x") was { } is { x : sin(x) * x = 3 }
83+
84+
"x + sqrt(x^0.1 + a) + c".ToEntity().SolveEquation("x")
85+
was { } is { x : sqrt(a + x ^ (1/10)) + c + x = 0 }
86+
"(x + 6)^(1/6) + x + x3 + a".ToEntity().SolveEquation("x")
87+
was { } is { x : (6 + x) ^ (1/6) + x + x ^ 3 = -a }
88+
"2 ^ (x sin(x)) + 4 ^ (x sin(x)) + c".ToEntity().SolveEquation("x")
89+
was { }
90+
is { x : sin(x) * x = ln(((-1 - sqrt(1 - 4 * c)) / 2) ^ (1 / ln(2)))
91+
or sin(x) * x = ln(((-1 + sqrt(1 - 4 * c)) / 2) ^ (1 / ln(2))) }
92+
```
93+
94+
The last of those is the shape of it: the exponential solver did settle the equation in
95+
`2 ^ (x sin(x))`, and it was the inversion of `x * sin(x)` that had nothing to say. What comes back
96+
now is the half that was solved, with the half that was not left standing as a condition.
97+
98+
An answer that is partly settled keeps the part that is, so a product one of whose factors was
99+
solved comes back as a union rather than as the factor's roots alone:
100+
101+
```
102+
"(x - 1) * (x6 + x y + 1) = 0".ToEntity().Solve("x")
103+
was { 1 }
104+
is { 1 } \/ { x : 1 + x ^ 6 + x * y = 0 }
105+
106+
"x6 + x y + 1 = 0 or x - 1 = 0".ToEntity().Solve("x")
107+
was { 1 }
108+
is { 1 } \/ { x : 1 + x ^ 6 + x * y = 0 }
109+
110+
"x6 + x y + 1 = 0 implies x - 1 = 0".ToEntity().Solve("x")
111+
was { 1 } \/ BB
112+
is { 1 } \/ (BB \ { x : 1 + x ^ 6 + x * y = 0 })
113+
```
114+
115+
A conjunction with an unsettled side is answered as the conjunction. Intersecting a finite set with
116+
a condition keeps the elements whose membership could not be decided, so taking the intersection
117+
here would have replaced one false claim with another — `{ 1 }` says 1 solves
118+
`x^6 + x*y + 1 = 0`, and it does so only at `y = -2`:
119+
120+
```
121+
"x6 + x y + 1 = 0 and x - 1 = 0".ToEntity().Solve("x")
122+
was { }
123+
is { x : x ^ 6 + x * y + 1 = 0 and x - 1 = 0 }
124+
```
125+
126+
**What this means for callers.** `Solve` has always been typed `Set` and has always been able to
127+
return an `Interval`, a `ConditionalSet` or a union; what changes is how often it does. Code that
128+
casts the result to `FiniteSet`, or that reads an empty result as "no solutions", has to say which
129+
of the two it means:
130+
131+
```csharp
132+
var answer = equation.Solve(x);
133+
if (answer is FiniteSet roots) { /* these are all of them */ }
134+
else if (answer.IsSetEmpty) { /* shown to have none */ }
135+
else { /* not settled, or not finite */ }
136+
```
137+
138+
Nothing that solved before stops solving, and no equation that was shown to have no roots gains
139+
any. Solving `x2 - 4 = 0`, `sin(x) = 1/2`, `x2 + 1 = 0` and the system solver's answers are
140+
unchanged, as is `Solve` on an inequality.
141+
40142
### A bound index is not a free variable
41143

42144
`FreeVariables` knew about two binders — `Lambda` and the set builder — and about no others. A

‎Sources/AngouriMath/Convenience/MathS.cs‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5652,7 +5652,9 @@ internal static EDecimal DowncastingTolerance
56525652
}
56535653

56545654
/// <summary>
5655-
/// If you only need analytical solutions and an empty set if no analytical solutions were found, disable Newton's method
5655+
/// If you only need analytical solutions, disable Newton's method. What comes back
5656+
/// where there are none is the equation itself as a set builder: the empty set
5657+
/// would say the equation has no roots, which is a claim nothing established.
56565658
/// </summary>
56575659
/// <example>
56585660
/// <code>
@@ -5671,7 +5673,7 @@ internal static EDecimal DowncastingTolerance
56715673
/// { 1.0050669478588620808778841819730587303638458251953125 + 0.93725915669289194820379407246946357190608978271484375i,
56725674
/// ... omitting here most of the output, because it's huge
56735675
/// }
5674-
/// { } // nothing was found for 5-degree polynomial without numeric solution
5676+
/// { x : x ^ 5 + 3 * x = -1 } // no analytical solution was found for this quintic
56755677
/// </code>
56765678
/// </example>
56775679
public static Setting<bool> AllowNewton { get; } = true;

‎Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver/AnalyticalEquationSolver.cs‎

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -330,11 +330,43 @@ bool ProductIsDefinedAt(Entity root)
330330
// TODO: Solve factorials (Needs Lambert W function)
331331
// https://mathoverflow.net/a/28977
332332

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

337-
return Enumerable.Empty<Entity>().ToSet();
341+
return Unsolved(expr, x);
338342
}
343+
344+
/// <summary>
345+
/// The answer when every solver above has declined: the equation itself, as the set
346+
/// of the <paramref name="x"/> that satisfy it.
347+
/// </summary>
348+
/// <remarks>
349+
/// The empty set is a positive claim -- that no <paramref name="x"/> satisfies
350+
/// <paramref name="equation"/> -- and returning it from an exhausted search asserts
351+
/// something nothing here established. <c>x^6 + x*y + 1 = 0</c> has six roots for
352+
/// every <c>y</c> and was answered <c>{ }</c>. A condition asserts only what was
353+
/// established, which is that these are the roots, whichever they are; the set is
354+
/// still exactly the solution set, so nothing downstream is told anything false.
355+
///
356+
/// The spelling is the one <see cref="StatementSolver.UnsolvedWhereIndependenceIsDenied"/>
357+
/// and the <a href="https://github.com/asc-community/AngouriMath/issues/278">#278</a>
358+
/// case above already use, rather than a second way of saying the same thing.
359+
/// <a href="https://github.com/asc-community/AngouriMath/issues/1036">#1036</a>,
360+
/// <a href="https://github.com/asc-community/AngouriMath/issues/746">#746</a>
361+
/// </remarks>
362+
private static Set Unsolved(Entity equation, Variable x)
363+
// Written back as an equation between the two sides where there are two, rather
364+
// than as the residual the solvers pass around. A compensated call is handed its
365+
// equation unsimplified on purpose, so the difference survives for them to read,
366+
// and stating it as it stands leaves the reader `x^5 + 3*x - (-1) = 0` where the
367+
// question was whether `x^5 + 3*x` is -1. The two admit the same x.
368+
=> new ConditionalSet(x, equation is Minusf(var lhs, var rhs)
369+
? lhs.Equalizes(rhs)
370+
: equation.Equalizes(0));
339371
}
340372
}

‎Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver/SolveStatement.cs‎

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,23 @@ private static EDecimal LargestTerm(Entity substituted)
148148
return largest;
149149
}
150150

151+
/// <summary>
152+
/// Where both sides of a conjunction were settled, its solution set is the
153+
/// intersection of theirs.
154+
/// </summary>
155+
/// <remarks>
156+
/// Where one of them is a condition nothing settled, it is not: intersecting a
157+
/// finite set with one keeps an element whose membership could not be decided, so
158+
/// <c>x^6 + x*y + 1 = 0 and x - 1 = 0</c> comes back as <c>{ 1 }</c> — and 1 is a
159+
/// root of the first only when <c>y</c> is -2. The conjunction as written asserts
160+
/// exactly what is known about it and nothing more.
161+
/// <a href="https://github.com/asc-community/AngouriMath/issues/1036">#1036</a>
162+
/// </remarks>
163+
private static Set Conjunction(Set left, Set right, Entity statement, Variable x)
164+
=> (left, right) is (ConditionalSet, FiniteSet) or (FiniteSet, ConditionalSet)
165+
? new ConditionalSet(x, statement)
166+
: (Set)MathS.Intersection(left, right);
167+
151168
internal static Set Solve(Entity expr, Variable x)
152169
=> expr switch
153170
{
@@ -161,8 +178,8 @@ internal static Set Solve(Entity expr, Variable x)
161178

162179
Equalsf => Empty,
163180

164-
Andf(var left, var right) =>
165-
MathS.Intersection(Solve(left, x), Solve(right, x)),
181+
Andf(var left, var right) =>
182+
Conjunction(Solve(left, x), Solve(right, x), expr, x),
166183
Orf(var left, var right) =>
167184
MathS.Union(Solve(left, x), Solve(right, x)),
168185
Impliesf(var left, var right) =>

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ partial record Entity
8484
/// { 9.08837698687877... }
8585
/// -----------------
8686
/// sin(x) * x - 3 = 0
87-
/// { }
87+
/// { x : sin(x) * x = 3 }
8888
/// </code>
8989
/// </example>
9090
public Set Solve(Variable var)

‎Sources/Tests/UnitTests/Algebra/SolveTest/SolveOneEquation.cs‎

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -189,9 +189,6 @@ public void InvertedFunctions(string func, int rootAmount)
189189
public void CDSolver(string expr, int rootCount, string? verifyRoots) => TestSolver(expr, rootCount, verifyRoots: verifyRoots);
190190

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

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

0 commit comments

Comments
 (0)