abs(sgn(x)) and sgn(abs(x)) both simplify to 1, and both are 0 at x = 0.
Measured on master (f2594259), .NET 10:
"abs(sgn(x))".Simplify() => 1 "sgn(abs(x))".Simplify() => 1
eval(sgn(0)) => 0
eval(abs(sgn(0))) => 0 eval(sgn(abs(0))) => 0
eval(abs(sgn(2))) => 1
|sgn(z)| is 1 for every z != 0, since sgn(z) has unit modulus there, and sgn(|z|) likewise.
At zero both are 0, because this library's sgn(0) is 0. So the identity holds off a single point
and is applied everywhere, which makes Simplify return 1 for an expression whose value is 0.
This is the third instance of one shape
- #884 —
arcsin(sin(x)) -> x, true only
on the principal interval.
- #887 —
arctan(x) + arccotan(x) -> pi/2,
true only for non-negative x.
- this one — true only away from zero.
Each is an identity that holds on all but a thin set, written as though it held everywhere. The
Docs/Contributing/SimplificationContract.md added in
#886 is about exactly this, and its
obligation O2 — state the assumption, and a comment that states the identity without qualification
is asserting there is none — is what these three violate.
What the fix is not
Not a Providedf. abs(sgn(x)) is perfectly well defined at x = 0; its value there is 0, not
undefined. Attaching provided not x = 0 would replace a wrong value with a wrong domain, which is
§3 of the contract.
Not the obvious closed form either. abs(sgn(x)) is the indicator of x != 0, and this library
has no elementary spelling of that — sgn(abs(x)) is the same function, so rewriting one to the other
is circular. So the answer is to fire only where the argument's being zero or not can be decided, and
to leave a symbolic argument as written. That is the same treatment as #884 and #887: decide, or
decline.
Both directions of the pair need it, and it is worth grepping for Absf(Signumf and
Signumf(Absf together with the five existing abs/sgn rules in Patterns.Common.cs, since
Mulf(Signumf(a), Absf(a)) => a in that file is sound — sgn(z)|z| = z holds at zero too — so the
file has the same sound-half/unsound-half split that #884 and #887 both had.
How it was found
work/boundcheck, after adding x = 0 and x = 1 to its list of boundary points. Every one of its
366 shapes had passed at 23 points chosen for branch cuts and principal intervals; adding the two
values that make a rule's own arithmetic degenerate — rather than its branch — turned up these two
immediately. The lesson for the harness is that "boundary" includes the points where an identity's
denominator or its case analysis collapses, not only where a branch is crossed.
abs(sgn(x))andsgn(abs(x))both simplify to1, and both are0atx = 0.Measured on
master(f2594259), .NET 10:|sgn(z)|is1for everyz != 0, sincesgn(z)has unit modulus there, andsgn(|z|)likewise.At zero both are
0, because this library'ssgn(0)is0. So the identity holds off a single pointand is applied everywhere, which makes
Simplifyreturn1for an expression whose value is0.This is the third instance of one shape
arcsin(sin(x)) -> x, true onlyon the principal interval.
arctan(x) + arccotan(x) -> pi/2,true only for non-negative
x.Each is an identity that holds on all but a thin set, written as though it held everywhere. The
Docs/Contributing/SimplificationContract.mdadded in#886 is about exactly this, and its
obligation O2 — state the assumption, and a comment that states the identity without qualification
is asserting there is none — is what these three violate.
What the fix is not
Not a
Providedf.abs(sgn(x))is perfectly well defined atx = 0; its value there is0, notundefined. Attaching
provided not x = 0would replace a wrong value with a wrong domain, which is§3 of the contract.
Not the obvious closed form either.
abs(sgn(x))is the indicator ofx != 0, and this libraryhas no elementary spelling of that —
sgn(abs(x))is the same function, so rewriting one to the otheris circular. So the answer is to fire only where the argument's being zero or not can be decided, and
to leave a symbolic argument as written. That is the same treatment as #884 and #887: decide, or
decline.
Both directions of the pair need it, and it is worth grepping for
Absf(SignumfandSignumf(Absftogether with the five existingabs/sgnrules inPatterns.Common.cs, sinceMulf(Signumf(a), Absf(a)) => ain that file is sound —sgn(z)|z| = zholds at zero too — so thefile has the same sound-half/unsound-half split that #884 and #887 both had.
How it was found
work/boundcheck, after addingx = 0andx = 1to its list of boundary points. Every one of its366 shapes had passed at 23 points chosen for branch cuts and principal intervals; adding the two
values that make a rule's own arithmetic degenerate — rather than its branch — turned up these two
immediately. The lesson for the harness is that "boundary" includes the points where an identity's
denominator or its case analysis collapses, not only where a branch is crossed.