diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 306a9046f..b42595191 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -72,6 +72,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` | --- @@ -383,6 +384,35 @@ passing at 23 points chosen for branch cuts and principal intervals, none of whi where a rule's own arithmetic degenerates. Issue [#892](https://github.com/asc-community/AngouriMath/issues/892). +### `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 e34d92872..3f7942eae 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 @@ -487,6 +487,24 @@ protected override Entity InnerSimplify(bool isExact) Signumf(var signOf) when ValueWithCondition(signOf) is { } known => (known.Value.IsZero ? Integer.Zero : Integer.One) .Provided(known.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 675df53fa..0bd59d2dd 100644 --- a/Sources/Tests/UnitTests/Common/SimplificationRegressionTest.cs +++ b/Sources/Tests/UnitTests/Common/SimplificationRegressionTest.cs @@ -700,6 +700,47 @@ public void ArctanPlusArccotanOfASymbolIsLeftAlone() 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); + } // https://github.com/asc-community/AngouriMath/issues/892 // |sgn(z)| and sgn(|z|) are 1 for every z except 0, where both are 0 -- sgn(0) is 0, // which the comment above Signumf.InnerSimplify already said. Both rewrites answered 1