Skip to content

abs(sgn(x)) and sgn(abs(x)) simplify to 1, and both are 0 at x = 0 #892

Description

@Rafael-SOWNet

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

  • #884arcsin(sin(x)) -> x, true only
    on the principal interval.
  • #887arctan(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 soundsgn(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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions