From c86c0bfae53d2a8c0d3161c07ddaffc28b6e5131 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Sat, 8 Aug 2026 12:23:57 +0000 Subject: [PATCH] An absolute value cannot equal a negative (#812) "abs(x) = -1".ToEntity().Solve("x") was { -e ^ (i * r_1) provided r_1 in RR } is { } The old answer was wrong rather than merely loose. At r_1 = 0 its member is -1, and abs(-1) is 1, not -1; every member has modulus 1 and solves nothing. abs(f(x)) = value is inverted by writing f(x) as value * e ^ (i * r) over real r, which is the circle of radius |value|. That solves the equation only where |value| and value agree -- where value is real and not negative -- and the condition saying so was missing. It is now carried alongside the one on r, so a decidable negative collapses the set to empty, a decidable non-negative drops it and leaves the previous answer untouched, and a symbolic one is kept: abs(x) = 3 { 3 * e ^ (i * r_1) provided r_1 in RR } unchanged abs(x) = 0 { 0 provided r_1 in RR } unchanged abs(x) = a { a * e ^ (i * r_1) provided r_1 in RR and a >= 0 } new condition The last of those is what #318 asked this inversion to return, in the sentence that named both halves of it; the r in RR half was already implemented and the a >= 0 half is this. The tests check solutions by substituting them back with the circle's parameter given a value, not by comparing printed form. Checked for teeth against origin/master: four of the seven fail without the fix, and the three that do not are the ones asserting the previously-correct answers are unchanged. 5558 unit tests and 130 F# tests pass; casbench 113/117 with 0 wrong, propcheck 0 failures, rootcheck 596/596, simpsweep 10463/10463. Co-Authored-By: Claude Opus 5 --- BREAKING-CHANGES.md | 1 + .../EquationSolver/InvertNode.Classes.cs | 8 +++- .../UnitTests/Common/SolverRegressionTest.cs | 44 +++++++++++++++++++ 3 files changed, 52 insertions(+), 1 deletion(-) diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 218dc7ce8..62735fe1d 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -48,6 +48,7 @@ read first. | **silent** | `k/(a x^2 + c)` and `k/sqrt(a x^2 + c)` for symbolic `a` | `NaN` | a piecewise on the sign of the discriminant | | loud | `Compile` over a missing variable | `KeyNotFoundException` | `UncompilableNodeException` | | loud | the target frameworks | `net7.0;netstandard2.0` | `netstandard2.0;net8.0;net10.0` | +| **silent** | `abs(x) = c` for a negative `c` | a set of non-solutions | the empty set | --- diff --git a/Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver/InvertNode.Classes.cs b/Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver/InvertNode.Classes.cs index 00993e048..bee882dda 100644 --- a/Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver/InvertNode.Classes.cs +++ b/Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver/InvertNode.Classes.cs @@ -259,11 +259,17 @@ partial record Absf // abs(f(x)) = value // f(x) = value * e ^ (i * n) // x = f(x).InvertNode(value * e ^ (i * n), x) + // + // value * e ^ (i * r) has modulus |value|, not value, so it solves the equation + // only where the two agree -- that is, where value is real and not negative. + // Without that condition abs(x) = -1 came back as { -e ^ (i * r) provided r in RR }, + // whose members have modulus 1 and solve nothing. + // https://github.com/asc-community/AngouriMath/issues/812 private protected override IEnumerable InvertNode(Entity value, Entity x) { var @var = Variable.CreateUnique(value + Argument, "r"); return Argument.Invert(value * MathS.e.Pow(MathS.i * @var), x) - .Select(c => c.Provided(@var.In(MathS.Sets.R))); + .Select(c => c.Provided(@var.In(MathS.Sets.R) & new GreaterOrEqualf(value, 0))); } } diff --git a/Sources/Tests/UnitTests/Common/SolverRegressionTest.cs b/Sources/Tests/UnitTests/Common/SolverRegressionTest.cs index c848573a1..93f615963 100644 --- a/Sources/Tests/UnitTests/Common/SolverRegressionTest.cs +++ b/Sources/Tests/UnitTests/Common/SolverRegressionTest.cs @@ -199,5 +199,49 @@ public void NewtonSolverDoesNotLeakAnUncompilableNodeException() // The omega constant: the root of x = -ln(x). Assert.Equal(0.5671432904097838, root.EvalNumerical().RealPart.EDecimal.ToDouble(), 9); } + + // abs(x) = a was inverted to the circle a * e ^ (i * r), which has modulus |a| and + // so only solves the equation where a is real and not negative. The condition was + // missing, so a negative right-hand side came back with a set of non-solutions + // rather than with nothing. https://github.com/asc-community/AngouriMath/issues/812 + [Theory] + [InlineData("abs(x) = -1")] + [InlineData("abs(x) = -3/2")] + [InlineData("abs(x) + 1 = 0")] + public void Issue812_AnAbsoluteValueCannotEqualANegative(string equation) + { + var solutions = (Entity.Set.FiniteSet)equation.ToEntity().Solve("x"); + Assert.Empty(solutions); + } + + // The guard must not cost the answers that were right. Each solution is checked by + // substituting it back, with the free parameter of the circle given a value, rather + // than by comparing the printed form. + [Theory] + [InlineData("3", 3.0)] + [InlineData("1/2", 0.5)] + [InlineData("0", 0.0)] + public void Issue812_ANonNegativeRightHandSideStillSolves(string rhs, double modulus) + { + var solutions = (Entity.Set.FiniteSet)$"abs(x) = {rhs}".ToEntity().Solve("x"); + var solution = Assert.Single(solutions); + var parameter = solution.Vars.Single(v => v.Name != "x"); + foreach (var point in new[] { 0.0, 1.0, -2.5 }) + { + var candidate = solution.Substitute(parameter, point); + while (candidate is Entity.Providedf(var inner, _)) candidate = inner; + Assert.Equal(modulus, + MathS.Abs(candidate).EvalNumerical().RealPart.EDecimal.ToDouble(), 9); + } + } + + // A symbolic right-hand side cannot be decided either way, so the condition is + // carried rather than resolved -- which is what #318 asked this inversion to do. + [Fact] + public void Issue812_ASymbolicRightHandSideCarriesTheCondition() + { + var printed = "abs(x) = a".ToEntity().Solve("x").Stringize(); + Assert.Contains("a >= 0", printed); + } } }