From 8366e5145bf2f504a1890c209f08721c8762ade2 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Wed, 12 Aug 2026 00:02:53 +0000 Subject: [PATCH] Fold abs where the sign of its argument is known (#881) |x| is x for a non-negative real x and -x for a negative one, which is the definition of the function rather than an identity carrying a side condition. It was applied only where the argument was a Number node, so an argument whose *value* is a known real kept its abs: abs(-sqrt(6)) and abs(-pi) came back exactly as written. That is visible in answers built out of radicals. (2x^2 - 3 > 0) and (x > 0) solved to (abs(-sqrt(6)) / 2; +oo) and now solves to (sqrt(6) / 2; +oo) -- the endpoint was always the same number, printed in a form that read as unfinished work. The Solvers wiki page shows the old output and wants updating with the release; docsamples reports it as its one output mismatch until then. The sign is read off Evaled, so nothing is assumed about a symbol -- abs(-a) is left alone -- and an argument off the real line declines rather than being guessed at. sqrt(-4) evaluates to 2i, whose absolute value is 2, which is neither the argument nor its negation; a rule reading "negative, therefore negate" would be wrong there. Non-finite values decline too. #881 also asks for |-x| = |x| for a symbolic x, which is unconditionally sound and is not here. Measured why: sgn(-a) does not normalise to -sgn(a), so rewriting abs(-a) to abs(a) on its own breaks the pairing that gives sgn(-a) * abs(-a) -> -a. It wants the odd symmetry of sgn alongside it. Suite 6320 passed; casbench 117/119 with 0 wrong; rootcheck 596/596; simpsweep 10463/10463; propcheck 0 failures; crashcheck 1652 cases with 0 crashes. boundcheck reports 46 shapes rewritten and 6 disagreements both with this change and on the commit before it, measured separately, so it adds none. The report committed in the workspace said four, and was stale rather than better: two of the six are abs(sgn(x)) and sgn(abs(x)), each simplifying to 1 where both are 0 at x = 0. Those are wrong answers in master, not here, and get their own issue. Co-Authored-By: Claude Opus 5 (1M context) --- BREAKING-CHANGES.md | 30 +++++++++++++ ...aluation.Continuous.Arithmetics.Classes.cs | 18 ++++++++ .../Common/SimplificationRegressionTest.cs | 42 +++++++++++++++++++ 3 files changed, 90 insertions(+) diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 23b93cb4b..c395ecfa2 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -71,6 +71,7 @@ read first. | **silent** | `log(1, 1)` | `0` | `NaN`, since it is `0/0` | | **silent** | `log(b, 1)` | `0` for any base | `0 provided not b = 1` | | **silent** | `log(1/2, 0)` and any base below 1 | `-oo` | `+oo` | +| **silent** | `abs(-sqrt(6))`, `abs(-pi)`, `abs(1 - sqrt(2))` | left as written | `sqrt(6)`, `pi`, `sqrt(2) - 1` | --- @@ -349,6 +350,35 @@ Found by `boundcheck`, a harness that composes every unary function node with ev compares against the original at points where an assumption fails rather than at sampled points. Issue [#887](https://github.com/asc-community/AngouriMath/issues/887). +### `abs` folds where the sign of its argument is known + +`|x|` is `x` for a non-negative real `x` and `-x` for a negative one. That is the definition of the +function rather than an identity with a side condition, and it was applied only when the argument +was a *number*. An argument whose value is a known real without its node being a number was left +alone, so a radical or a constant kept its `abs`: + +| | was | is | +|---|---|---| +| `abs(-sqrt(6))` | left as written | `sqrt(6)` | +| `abs(-pi)`, `abs(-e)` | left as written | `pi`, `e` | +| `abs(1 - sqrt(2))` | left as written | `sqrt(2) - 1` | +| `abs(-2)` | `2` | `2`, unchanged | +| `abs(sqrt(-4))` | `2` | `2`, unchanged — the magnitude of `2i` | +| `abs(-a)` for symbolic `a` | left as written | left as written | + +Where this shows up is in an answer built out of radicals. `(2x^2 - 3 > 0) and (x > 0)` solved to +`(abs(-sqrt(6)) / 2; +oo)` and now solves to `(sqrt(6) / 2; +oo)`; the endpoint was always the same +number, printed in a form that looked like unfinished work. The +[Solvers wiki page](https://github.com/asc-community/AngouriMath/wiki/Solvers) shows the old output +and wants updating with the release. + +**Nothing is assumed about a symbol**, and an argument off the real line is declined rather than +guessed at: `sqrt(-4)` evaluates to `2i`, whose absolute value is `2` — neither the argument nor its +negation, so a rule that read "negative, therefore negate" would be wrong there. The sign is read +off the value, and a value that is not a finite real does not answer the question. + +Issue [#881](https://github.com/asc-community/AngouriMath/issues/881). + ### A known gap no longer presents as a bug `FutureReleaseException` is removed, and the twelve places that threw through it now throw diff --git a/Sources/AngouriMath/Functions/Evaluation/Evaluation.Continuous/Evaluation.Continuous.Arithmetics.Classes.cs b/Sources/AngouriMath/Functions/Evaluation/Evaluation.Continuous/Evaluation.Continuous.Arithmetics.Classes.cs index c5972d1f9..b9e953ee8 100644 --- a/Sources/AngouriMath/Functions/Evaluation/Evaluation.Continuous/Evaluation.Continuous.Arithmetics.Classes.cs +++ b/Sources/AngouriMath/Functions/Evaluation/Evaluation.Continuous/Evaluation.Continuous.Arithmetics.Classes.cs @@ -448,6 +448,24 @@ protected override Entity InnerSimplify(bool isExact) Complex n when !isExact => Number.Abs(n), Absf abs => abs, Signumf({ DomainCondition: var condition }) => Integer.One.Provided(condition), + + // |x| is x where the argument is a non-negative real and -x where it is + // negative, which is the definition rather than an identity needing an + // assumption. A Number folded already; what this reaches is an argument + // whose *value* is a known real without its node being a number -- + // abs(-sqrt(6)) and abs(-pi) stayed as written, so a concrete quadratic + // inequality answered with abs(-sqrt(6)) / 2 in it. + // + // An argument off the real line has to decline: sqrt(-4) evaluates to 2i, + // and |2i| is 2, which is neither the argument nor its negation. Nothing + // is assumed about the sign of a symbol either -- there the value cannot + // be read at all and the node is left alone. + // https://github.com/asc-community/AngouriMath/issues/881 + var argument when argument.Evaled is Real { EDecimal.IsFinite: true } value + => value.EDecimal.IsNegative + ? (-argument).InnerSimplified(isExact) + : argument, + _ => null }, (@this, a) => ((Absf)@this).New(a), isExact); diff --git a/Sources/Tests/UnitTests/Common/SimplificationRegressionTest.cs b/Sources/Tests/UnitTests/Common/SimplificationRegressionTest.cs index 10be70359..43655d22e 100644 --- a/Sources/Tests/UnitTests/Common/SimplificationRegressionTest.cs +++ b/Sources/Tests/UnitTests/Common/SimplificationRegressionTest.cs @@ -699,5 +699,47 @@ public void ArctanPlusArccotanOfASymbolIsLeftAlone() Assert.NotEqual(MathS.pi / 2, simplified); Assert.NotEqual(-MathS.pi / 2, simplified); } + + // https://github.com/asc-community/AngouriMath/issues/881 + // |x| is x where the argument is a non-negative real and -x where it is negative, which + // is the definition of abs rather than an identity needing an assumption. Only a Number + // folded, so abs(-sqrt(6)) and abs(-pi) came back exactly as written, and a concrete + // quadratic inequality was answered with abs(-sqrt(6)) / 2 as an endpoint. + [Theory] + [InlineData("abs(-sqrt(6))", "sqrt(6)")] + [InlineData("abs(sqrt(6))", "sqrt(6)")] + [InlineData("abs(-pi)", "pi")] + [InlineData("abs(-e)", "e")] + [InlineData("abs(1 - sqrt(2))", "sqrt(2) - 1")] + [InlineData("abs(-sqrt(6)) / 2", "sqrt(6) / 2")] + public void AbsoluteValueFoldsWhereTheSignOfTheArgumentIsDecidable(string expression, string expected) + { + var simplified = expression.ToEntity().Simplify(); + Assert.DoesNotContain(simplified.Nodes, node => node is Entity.Absf); + Assert.True(Magnitude(simplified - expected.ToEntity()) < 1e-20, + $"{expression} simplified to {simplified.Stringize()}, not {expected}"); + } + + // An argument off the real line is neither itself nor its negation under abs: sqrt(-4) + // is 2i, whose magnitude is 2. So the sign is read off the value, and a value that is + // not real does not answer the question. + [Theory] + [InlineData("abs(sqrt(-4))", "2")] + [InlineData("abs(-sqrt(-4))", "2")] + public void AbsoluteValueOffTheRealLineIsTheMagnitude(string expression, string expected) + { + var simplified = expression.ToEntity().Simplify(); + Assert.True(Magnitude(simplified - expected.ToEntity()) < 1e-20, + $"{expression} simplified to {simplified.Stringize()}, not {expected}"); + } + + // A symbol has no decidable sign, so nothing is assumed about it: abs(-a) is not a, and + // it is not -a either. It stays as written. + [Fact] + public void AbsoluteValueOfASymbolIsLeftAlone() + { + var simplified = "abs(-a)".ToEntity().Simplify(); + Assert.Contains(simplified.Nodes, node => node is Entity.Absf); + } } }