Skip to content

Commit 8366e51

Browse files
Rafael-SOWNetclaude
andcommitted
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) <noreply@anthropic.com>
1 parent d8bfcd5 commit 8366e51

3 files changed

Lines changed: 90 additions & 0 deletions

File tree

BREAKING-CHANGES.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ read first.
7171
| **silent** | `log(1, 1)` | `0` | `NaN`, since it is `0/0` |
7272
| **silent** | `log(b, 1)` | `0` for any base | `0 provided not b = 1` |
7373
| **silent** | `log(1/2, 0)` and any base below 1 | `-oo` | `+oo` |
74+
| **silent** | `abs(-sqrt(6))`, `abs(-pi)`, `abs(1 - sqrt(2))` | left as written | `sqrt(6)`, `pi`, `sqrt(2) - 1` |
7475

7576
---
7677

@@ -349,6 +350,35 @@ Found by `boundcheck`, a harness that composes every unary function node with ev
349350
compares against the original at points where an assumption fails rather than at sampled points.
350351
Issue [#887](https://github.com/asc-community/AngouriMath/issues/887).
351352

353+
### `abs` folds where the sign of its argument is known
354+
355+
`|x|` is `x` for a non-negative real `x` and `-x` for a negative one. That is the definition of the
356+
function rather than an identity with a side condition, and it was applied only when the argument
357+
was a *number*. An argument whose value is a known real without its node being a number was left
358+
alone, so a radical or a constant kept its `abs`:
359+
360+
| | was | is |
361+
|---|---|---|
362+
| `abs(-sqrt(6))` | left as written | `sqrt(6)` |
363+
| `abs(-pi)`, `abs(-e)` | left as written | `pi`, `e` |
364+
| `abs(1 - sqrt(2))` | left as written | `sqrt(2) - 1` |
365+
| `abs(-2)` | `2` | `2`, unchanged |
366+
| `abs(sqrt(-4))` | `2` | `2`, unchanged — the magnitude of `2i` |
367+
| `abs(-a)` for symbolic `a` | left as written | left as written |
368+
369+
Where this shows up is in an answer built out of radicals. `(2x^2 - 3 > 0) and (x > 0)` solved to
370+
`(abs(-sqrt(6)) / 2; +oo)` and now solves to `(sqrt(6) / 2; +oo)`; the endpoint was always the same
371+
number, printed in a form that looked like unfinished work. The
372+
[Solvers wiki page](https://github.com/asc-community/AngouriMath/wiki/Solvers) shows the old output
373+
and wants updating with the release.
374+
375+
**Nothing is assumed about a symbol**, and an argument off the real line is declined rather than
376+
guessed at: `sqrt(-4)` evaluates to `2i`, whose absolute value is `2` — neither the argument nor its
377+
negation, so a rule that read "negative, therefore negate" would be wrong there. The sign is read
378+
off the value, and a value that is not a finite real does not answer the question.
379+
380+
Issue [#881](https://github.com/asc-community/AngouriMath/issues/881).
381+
352382
### A known gap no longer presents as a bug
353383

354384
`FutureReleaseException` is removed, and the twelve places that threw through it now throw

Sources/AngouriMath/Functions/Evaluation/Evaluation.Continuous/Evaluation.Continuous.Arithmetics.Classes.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -448,6 +448,24 @@ protected override Entity InnerSimplify(bool isExact)
448448
Complex n when !isExact => Number.Abs(n),
449449
Absf abs => abs,
450450
Signumf({ DomainCondition: var condition }) => Integer.One.Provided(condition),
451+
452+
// |x| is x where the argument is a non-negative real and -x where it is
453+
// negative, which is the definition rather than an identity needing an
454+
// assumption. A Number folded already; what this reaches is an argument
455+
// whose *value* is a known real without its node being a number --
456+
// abs(-sqrt(6)) and abs(-pi) stayed as written, so a concrete quadratic
457+
// inequality answered with abs(-sqrt(6)) / 2 in it.
458+
//
459+
// An argument off the real line has to decline: sqrt(-4) evaluates to 2i,
460+
// and |2i| is 2, which is neither the argument nor its negation. Nothing
461+
// is assumed about the sign of a symbol either -- there the value cannot
462+
// be read at all and the node is left alone.
463+
// https://github.com/asc-community/AngouriMath/issues/881
464+
var argument when argument.Evaled is Real { EDecimal.IsFinite: true } value
465+
=> value.EDecimal.IsNegative
466+
? (-argument).InnerSimplified(isExact)
467+
: argument,
468+
451469
_ => null
452470
},
453471
(@this, a) => ((Absf)@this).New(a), isExact);

Sources/Tests/UnitTests/Common/SimplificationRegressionTest.cs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -699,5 +699,47 @@ public void ArctanPlusArccotanOfASymbolIsLeftAlone()
699699
Assert.NotEqual(MathS.pi / 2, simplified);
700700
Assert.NotEqual(-MathS.pi / 2, simplified);
701701
}
702+
703+
// https://github.com/asc-community/AngouriMath/issues/881
704+
// |x| is x where the argument is a non-negative real and -x where it is negative, which
705+
// is the definition of abs rather than an identity needing an assumption. Only a Number
706+
// folded, so abs(-sqrt(6)) and abs(-pi) came back exactly as written, and a concrete
707+
// quadratic inequality was answered with abs(-sqrt(6)) / 2 as an endpoint.
708+
[Theory]
709+
[InlineData("abs(-sqrt(6))", "sqrt(6)")]
710+
[InlineData("abs(sqrt(6))", "sqrt(6)")]
711+
[InlineData("abs(-pi)", "pi")]
712+
[InlineData("abs(-e)", "e")]
713+
[InlineData("abs(1 - sqrt(2))", "sqrt(2) - 1")]
714+
[InlineData("abs(-sqrt(6)) / 2", "sqrt(6) / 2")]
715+
public void AbsoluteValueFoldsWhereTheSignOfTheArgumentIsDecidable(string expression, string expected)
716+
{
717+
var simplified = expression.ToEntity().Simplify();
718+
Assert.DoesNotContain(simplified.Nodes, node => node is Entity.Absf);
719+
Assert.True(Magnitude(simplified - expected.ToEntity()) < 1e-20,
720+
$"{expression} simplified to {simplified.Stringize()}, not {expected}");
721+
}
722+
723+
// An argument off the real line is neither itself nor its negation under abs: sqrt(-4)
724+
// is 2i, whose magnitude is 2. So the sign is read off the value, and a value that is
725+
// not real does not answer the question.
726+
[Theory]
727+
[InlineData("abs(sqrt(-4))", "2")]
728+
[InlineData("abs(-sqrt(-4))", "2")]
729+
public void AbsoluteValueOffTheRealLineIsTheMagnitude(string expression, string expected)
730+
{
731+
var simplified = expression.ToEntity().Simplify();
732+
Assert.True(Magnitude(simplified - expected.ToEntity()) < 1e-20,
733+
$"{expression} simplified to {simplified.Stringize()}, not {expected}");
734+
}
735+
736+
// A symbol has no decidable sign, so nothing is assumed about it: abs(-a) is not a, and
737+
// it is not -a either. It stays as written.
738+
[Fact]
739+
public void AbsoluteValueOfASymbolIsLeftAlone()
740+
{
741+
var simplified = "abs(-a)".ToEntity().Simplify();
742+
Assert.Contains(simplified.Nodes, node => node is Entity.Absf);
743+
}
702744
}
703745
}

0 commit comments

Comments
 (0)