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); + } } }