Skip to content

arccotan's range is (-pi/2, pi/2], and two rewrites assume (0, pi) #887

Description

@Rafael-SOWNet

arccotan here is arctan(1/x) extended by arccotan(0) = pi/2, so its range is (-pi/2, pi/2] and not the (0, pi) that many texts use. Two rewrites assume the latter, and both return wrong answers for a negative argument.

Measured on master (f2594259), .NET 10. The range first, since everything follows from it:

value
arccotan(1) 0.7853... = pi/4
arccotan(-1) -0.7853... = -pi/4
arccotan(0) 1.5707... = pi/2

1. arctan(x) + arccotan(x) -> pi/2, unconditionally

Patterns.Trigonometry.cs, in the block commented // arc1({}) + arc2({}) = pi/2.

"arctan(-3) + arccotan(-3)".Simplify()   =>  pi / 2      the value is -pi/2

With this range the sum is pi/2 for non-negative x and -pi/2 for negative x. The
neighbouring arcsin(x) + arccos(x) -> pi/2 in the same block is unconditional and correct,
because arccos(x) is pi/2 - arcsin(x) by definition over the whole plane — so the block again
has a sound half and an unsound half written as though they were symmetric, which is what
#884 was.

pi/2 * sgn(x) is the closed form and is wrong at exactly one point: at x = 0 the sum is pi/2
while sgn(0) is 0. A Piecewise would be total, but Compile throws
UncompilableNodeException on one, so producing it here would break expressions that compile
today.

2. arccotan(cotan(x)) -> x is guarded with the wrong interval

This one is mine, from #884: I guarded it with [0, pi] on the assumption that arccotan's range
was (0, pi). It is not, so the guard is wrong in both directions —

"arccotan(cotan(2))".Simplify()      =>  2       the value is -1.1416   (= 2 - pi)
"arccotan(cotan(-1/2))".Simplify()   =>  left alone, though -1/2 is in the range

— admitting (pi/2, pi) where the rewrite is false, and refusing (-pi/2, 0) where it is true.
The correct interval is (-pi/2, pi/2] without zero, zero excluded because cotan has no
value there, so the composition has none either and rewriting to x would invent one.

The other three of #884 check out against the same measurements: arcsin is [-pi/2, pi/2],
arccos is [0, pi] (arccos(-1) is pi), arctan is (-pi/2, pi/2). Only arccotan was
wrong.

How it was found

By work/boundcheck, a harness written to check exactly this class: it composes every unary
function node with every other, found by reflection, and compares the simplification against the
original at points chosen to sit where an assumption fails rather than at sampled points. It found
this on its first run, together with the already-filed logarithm half of #884 — including the fact
that my own #884 guard was wrong, which is the useful part.

Two further findings from the same run are separate and not fixed here:

  • log(x, x) -> 1 provided x > 0 (Patterns.Power.cs) — the condition is too strong. At
    x = -3 the value is 1, since ln(-3)/ln(-3) is 1, and the simplification is undefined. It
    needs ln x to be non-zero and defined, not x to be positive. This is the mirror of the usual
    defect: it turns a value into undefined rather than the other way round.
  • ln(x) + ln(x+1) -> ln(x*(1+x)) — the known domain-widening gathering, and this is what it
    costs: at x = -3 the original is 1.7918 + 6.2832i and the gathered form is real. Already
    handled downstream by verifying roots against the original equation, and still formally unsound.

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