Skip to content

Fix three wrong answers in the solvers and evaluation (#632, #442, #662) - #664

Merged
Happypig375 merged 7 commits into
ASC-Community:masterfrom
Rafael-SOWNet:fix/evaluation-and-solvers
Aug 3, 2026
Merged

Happypig375 merged 7 commits into
ASC-Community:masterfrom
Rafael-SOWNet:fix/evaluation-and-solvers

Conversation

@Rafael-SOWNet

Copy link
Copy Markdown
Member

Five fixes to evaluation, limits and the equation solvers. Three of them are wrong
answers rather than unsimplified ones. Each has a regression test, and each was
reproduced against a stock master build before being claimed.

#632 — a = 1/(b - c) solved to the negation of the right answer

"2 = 1 / (5 - c)".Solve("c")   →  { -4.5 }      // the root is 4.5

Open since April 2024. Minusf's inverter had its two branches the wrong way round
with respect to its own comments. The trap is in the record declaration:

public sealed partial record Minusf(Entity Subtrahend, Entity Minuend)

The names are reversed from standard terminology — the expression is
Subtrahend - Minuend. The branch was written to the names rather than to the
positions, so a subtraction containing the unknown inverted with the wrong sign. Only
the second branch is wrong; my first attempt swapped both and produced a different
wrong answer, which the numeric assertions in the new test caught.

#442 — sqrt(i) = (1 + i)/sqrt(2) answered False

Equality of two constants compared their separately evaluated values for exact digit
equality. Those two are the same number, but evaluating each rounds it differently in
the last few digits, so the comparison said they differ. It is now decided on the
difference. The test asserts both directions, so that the fix cannot collapse genuinely
different constants into equality.

#662 — EvalNumerical threw on some vectors

A 1.3 → 1.4 regression. The Euclidean norm of a vector was built and then
InnerSimplified even when the caller had asked for numeric evaluation, so sqrt(369)
survived as a Powf and EvalNumerical refused it. It only worked when the norm
happened to be a perfect square. The branch now honours isExact like its siblings.

lim x→0 (1 + x)^(1/x) returned 1 instead of e

Not in the tracker; found by a solver corpus I ran, not by reading code. The second
remarkable limit required the exponent's two-sided limit to be infinite, but 1/x at 0
tends to −∞ from the left and +∞ from the right, so the two-sided limit does not exist
even though the magnitude diverges. Both one-sided limits diverging is enough, because
the rewrite only uses the product g(x)·(f(x) − 1), which is well defined from either
side.

ln(x) + ln(x+1) = 0 returned an extraneous root, and x + ln(x) = 0 threw

Also not in the tracker.

Solving the first goes through log(a) + log(b) = log(a·b) and then x² + x − 1 = 0,
whose roots are 0.618... and −1.618... The second is not a root of the original: both
logarithms are taken off the negative reals there, and the left-hand side comes to
2πi. The rewrites that widen a domain this way cannot each carry a condition that
survives the chain of substitutions after them, so the answers are now checked once, at
the top, against the equation as the caller wrote it. A root is dropped only on
positive evidence
— anything that will not evaluate to a number, a parametric family
like pi + 2*pi*n_1 among them, is kept.

The second leaked an internal UncompilableNodeException out through the public
Solve. The Newton solver compiles the simplified expression, simplification leaves
Providedf nodes behind, and the compiler has no way to represent one. The conditions
now come off before compilation and the roots are verified after, which is where they
belonged anyway.

I also tried the more obvious fix for the extraneous root — attaching
provided a > 0 and b > 0 to the log-sum rewrite. It works, but it makes every log sum
print a condition and downgrades the exact (-1 + sqrt(5))/2 to a decimal, so I
reverted it. Verifying roots fixes the same bug, covers the whole class of
domain-widening rewrites rather than one instance, and changes no other output. The
rewrite is still formally unsound and I would rather that were tracked separately than
smuggled in here.


Testing

UnitTests 3697 passed / 0 failed, FSharpWrapperUnitTests 127 passed / 0 failed. No
existing test needed changing.

This is one of three independent PRs; the other two cover parsing and precision, and
the simplifier. They touch disjoint files and can be merged in any order.

🤖 Generated with Claude Code

{
// Careful: this node is declared Minusf(Subtrahend, Minuend), so the
// expression is `Subtrahend - Minuend` -- the two property names are the
// reverse of the usual terminology. Reading them as their names suggest is

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Rafael-SOWNet I suggest fixing this to their usual terminology instead

@Rafael-SOWNet
Rafael-SOWNet force-pushed the fix/evaluation-and-solvers branch from c94657a to 6bf23bc Compare August 3, 2026 17:52
@Happypig375

Copy link
Copy Markdown
Member

@Rafael-SOWNet Can you also submit a PR to fix the CI while you're here? Thanks

@codecov-commenter

codecov-commenter commented Aug 3, 2026 •

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 87.69231% with 8 lines in your changes missing coverage. Please review.
✅ Project coverage is 79.95%. Comparing base (90c00a8) to head (2ab53e5).
⚠️ Report is 50 commits behind head on master.

Files with missing lines Patch % Lines
...ontinuous/Solvers/NumericalSolving/NewtonSolver.cs 78.57% 2 Missing and 1 partial ⚠️
...ontinuous/Solvers/EquationSolver/SolveStatement.cs 85.71% 2 Missing ⚠️
...ath/Functions/Continuous/Limits/Transformations.cs 75.00% 0 Missing and 1 partial ⚠️
...nuous/Solvers/EquationSolver/InvertNode.Classes.cs 83.33% 0 Missing and 1 partial ⚠️
...Evaluation.Discrete/Evaluation.Discrete.Classes.cs 75.00% 0 Missing and 1 partial ⚠️
❗ Your organization needs to install the Codecov GitHub app to enable full functionality.
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #664      +/-   ##
==========================================
- Coverage   80.99%   79.95%   -1.05%     
==========================================
  Files         155      155              
  Lines       13687    12512    -1175     
  Branches     1957     2032      +75     
==========================================
- Hits        11086    10004    -1082     
+ Misses       1990     1917      -73     
+ Partials      611      591      -20     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Rafael-SOWNet and others added 7 commits August 3, 2026 18:56
`sqrt(i) = (1 + i) / sqrt(2)` simplified to False. Both sides are the same
number, but Equalsf compared their separately evaluated values for exact
digit equality, and evaluating each rounds independently -- they came out
as ...863 + ...864i versus ...861 + ...861i, differing in the last ulps.

Fall back to testing whether the difference is zero. The difference
cancels, and Real's factory already maps anything below
PrecisionErrorZeroRange onto an exact zero, so this reuses the library's
existing notion of zero rather than introducing a new tolerance. The
exact-equality fast path is kept, so infinities keep comparing as before.

Also fixes `sqrt(2) * sqrt(3) = sqrt(6)` and `sin(pi/4) = sqrt(2)/2`.
Full suite: 3688 passed, 0 failed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Solving `a = 1 / (b - c)` for c returned `1/a - b`, the negation of the
correct `b - 1/a`. Concretely, `2 = 1/(5 - c)` answered -4.5 where the
answer is 4.5, which is what the reporter saw as "the value is negative".

Minusf is declared `Minusf(Subtrahend, Minuend)`, so the expression is
`Subtrahend - Minuend` and the two property names are the reverse of the
usual terminology. Read as their names suggest, the branch for `a - x =
value` returned `value - a` instead of `a - value`. Only that branch was
wrong; the other one was already consistent with the actual declaration
order. Added a comment recording the naming trap.

Full suite: 3694 passed, 0 failed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`MathS.FromString("(|[12,15] - [0,0]|)").EvalNumerical()` threw
CannotEvalException on 1.4.0 where 1.3.0 returned a value, while
`(|[2,1] - [1,1]|)` worked -- the difference being whether the norm came
out a perfect square.

Absf's vector branch built the Euclidean norm and always finished with
InnerSimplified, ignoring the isExact flag its sibling branches respect.
sqrt(369) has no exact simplification, so it survived as a Powf and
EvalNumerical rejected it as not a simple number. Now the exact path
still returns sqrt(369) and the numeric path evaluates it.

Full suite: 3701 passed, 0 failed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`lim x->0 (1+x)^(1/x)` answered 1; the answer is e. The rewrite of
f(x)^g(x) into e^(g(x)*(f(x)-1)) required g to have an infinite two-sided
limit, but at x -> 0 the exponent 1/x goes to -oo on the left and +oo on
the right, so no two-sided limit exists even though the magnitude
diverges -- which is all the rule needs. The rewrite itself only uses the
product g(x)*(f(x)-1), which is fine from either side.

Now also accepts the case where both one-sided limits diverge. The
x -> +oo form already worked and is pinned by a test so it stays working.

Found by the solver-coverage corpus, not reported upstream.
Full suite: 3706 passed, 0 failed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two wrong answers, one root cause each, both in the path from an equation to its
roots. Neither is in the tracker; the solver corpus found them.

`ln(x) + ln(x+1) = 0` returned two roots. Solving it goes through
log(a) + log(b) = log(a * b) and then through x^2 + x - 1 = 0, whose roots are
0.618... and -1.618... The second is not a root of the original: both logarithms
are taken off the negative reals there, and the left-hand side comes to 2*pi*i.
The rewrites that widen a domain this way cannot each carry a condition that
survives the chain of substitutions after them, so the answers are checked once,
at the top, against the equation as the caller wrote it. A root is dropped only
on positive evidence -- anything that will not evaluate to a number, a parametric
family like pi + 2*pi*n_1 among them, is kept.

`x + ln(x) = 0` threw UncompilableNodeException out through the public Solve.
The Newton solver compiles the simplified expression, simplification leaves
Providedf nodes behind, and the compiler has no way to represent one. The
conditions now come off before compilation and the roots are verified after,
which is where they belonged anyway.

Measured on the 117-problem solver corpus: 78 -> 80 solved, and both the wrong
answer and the error go to zero.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
One test per issue, named for it, plus the two wrong answers that had no issue
filed, so that a future refactor that reinstates any of these fails loudly.
In `a - b`, `a` is the minuend and `b` the subtrahend. The record had them the other
way about -- `Minusf(Entity Subtrahend, Entity Minuend)` -- so the first operand, the
one on the left of the sign, was called the subtrahend.

That is what caused the sign error this branch fixes. The inverter was written to
what the names say rather than to which operand is which, so `a = 1/(b - c)` solved
to the negation of the right answer. The previous commit fixed the inversion and left
a comment warning about the names; naming them correctly is better, and removes the
need for the warning.

The swap is mechanical -- every use is positional, so each name simply exchanges with
the other across ten files, and the compiler's operand order is unchanged because the
objects are the same. The InvertNode body now reads as it means:

    Minuend.ContainsNode(x)
    ? Minuend.Invert(value + Subtrahend, x)      // x - a = value => x = value + a
    : Subtrahend.Invert(Minuend - value, x);     // a - x = value => x = a - value

These are public property names, so anyone reading them by name gets the other
operand than before. That is the point: they were wrong, and code written against
them was wrong with them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Rafael-SOWNet
Rafael-SOWNet force-pushed the fix/evaluation-and-solvers branch from 6bf23bc to 2ab53e5 Compare August 3, 2026 19:02
@Rafael-SOWNet

Copy link
Copy Markdown
Member Author

Done — the names are swapped rather than worked around, and rebased onto current master.

Minusf is now Minusf(Entity Minuend, Entity Subtrahend): in a - b, a is the minuend and b the subtrahend. Every use was positional, so each name simply exchanges with the other across ten files; the compiler's operand order is unchanged, because the objects are the same ones.

The inverter reads as it means now, and the comment warning about the naming is gone with it:

Minuend.ContainsNode(x)
? Minuend.Invert(value + Subtrahend, x)      // x - a = value => x = value + a
: Subtrahend.Invert(Minuend - value, x);     // a - x = value => x = a - value

Worth flagging since these are public property names: anyone reading them by name now gets the other operand than before. That is rather the point — they were the wrong way round, and code written against them was wrong with them. This is what caused the sign error in the first place.

3862 unit tests and 127 F# tests pass on the rebase, and both target frameworks build.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes several correctness issues across evaluation, limits, and equation solving in AngouriMath, and adds a new regression test suite to prevent the reported wrong answers and exceptions from recurring.

Changes:

  • Fix subtraction operand handling (Minusf) across substitution, formatting, evaluation, differentiation, limits, compilation, and solver inversion.
  • Make constant equality simplification robust to independent rounding by comparing evaluated differences against the library’s zero tolerance.
  • Fix numeric evaluation of vector norms, apply the second remarkable limit when the exponent diverges in magnitude, and verify solver roots against the original equation to drop demonstrably spurious roots.

Reviewed changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
Sources/Tests/UnitTests/Common/SolverRegressionTest.cs Adds regression tests covering constant equality, subtraction inversion, vector norms, remarkable limits, root verification, and Newton-solver exception leakage.
Sources/AngouriMath/Functions/Substitute.cs Aligns Minusf.Substitute child order with updated Minusf semantics.
Sources/AngouriMath/Functions/Output/ToSympy/ToSympy.Arithmetics.Classes.cs Fixes SymPy output ordering for subtraction.
Sources/AngouriMath/Functions/Output/ToString/ToString.Arithmetics.Classes.cs Fixes string output ordering for subtraction.
Sources/AngouriMath/Functions/Output/Latex/Latex.Arithmetics.Classes.cs Fixes LaTeX output ordering for subtraction.
Sources/AngouriMath/Functions/Evaluation/Evaluation.Discrete/Evaluation.Discrete.Classes.cs Updates constant equality simplification to compare evaluated differences using Number.IsZero.
Sources/AngouriMath/Functions/Evaluation/Evaluation.Continuous/Evaluation.Continuous.Arithmetics.Classes.cs Fixes subtraction simplification argument order; updates vector norm handling to respect numeric vs exact mode.
Sources/AngouriMath/Functions/Continuous/Solvers/NumericalSolving/NewtonSolver.cs Strips Providedf conditions before compilation and verifies rounded candidate roots against the original expression.
Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver/SolveStatement.cs Filters finite root sets by substituting back into the original equation and dropping demonstrably spurious roots.
Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver/InvertNode.Classes.cs Fixes subtraction inversion logic/sign when isolating a variable in a subtraction.
Sources/AngouriMath/Functions/Continuous/Limits/Transformations.cs Updates second remarkable limit trigger to accept one-sided divergence in magnitude of the exponent.
Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Limit.Classes.cs Fixes operand order when rebuilding Minusf during limit computation.
Sources/AngouriMath/Functions/Continuous/Differentiation.cs Fixes derivative rule application order for subtraction.
Sources/AngouriMath/Functions/Compilation/IntoFE/Compiler.cs Adjusts compilation operand push order to match CALL_MINUS VM evaluation order.
Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.Operators.Classes.cs Changes Minusf record parameter naming/ordering and updates linearization logic accordingly.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +67 to +70
private static bool DivergesInMagnitude(Entity power, Variable x, Entity dest)
=> IsInfiniteNode(power.Limit(x, dest))
|| (IsInfiniteNode(power.Limit(x, dest, ApproachFrom.Left))
&& IsInfiniteNode(power.Limit(x, dest, ApproachFrom.Right)));
Comment on lines +8 to +11
using AngouriMath;
using AngouriMath.Extensions;
using PeterO.Numbers;
using Xunit;
Comment on lines 60 to +67
/// <summary>
/// A node of difference
/// </summary>
public sealed partial record Minusf(Entity Subtrahend, Entity Minuend) : ContinuousNode, IBinaryNode
public sealed partial record Minusf(Entity Minuend, Entity Subtrahend) : ContinuousNode, IBinaryNode
{
/// <summary>Reuse the cache by returning the same object if possible</summary>
private Minusf New(Entity subtrahend, Entity minuend) =>
ReferenceEquals(Subtrahend, subtrahend) && ReferenceEquals(Minuend, minuend) ? this : new(subtrahend, minuend);
private Minusf New(Entity minuend, Entity subtrahend) =>
ReferenceEquals(Minuend, minuend) && ReferenceEquals(Subtrahend, subtrahend) ? this : new(minuend, subtrahend);
@Happypig375
Happypig375 merged commit d851abf into ASC-Community:master Aug 3, 2026
21 of 24 checks passed
Happypig375 pushed a commit that referenced this pull request Aug 4, 2026
Both were left by the Copilot reviewer on #664, which merged before I had read them.

SolverRegressionTest had `using PeterO.Numbers;` and never named a type from it --
the EDecimal occurrences are all property accesses on a Real, which need no using.
Removed. The library builds with TreatWarningsAsErrors, so an unused using is worth
not leaving lying about.

DivergesInMagnitude was flagged for calling Limit up to three times. The three are
distinct and the || short-circuits, so there was nothing to cache, but there was a
real waste next to it: at an infinite destination there is only one side to approach
from, so the two one-sided limits are both pointless and meaningless there. They are
now only asked for when the destination is finite.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
@Rafael-SOWNet
Rafael-SOWNet deleted the fix/evaluation-and-solvers branch August 4, 2026 20:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants