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.
arccotanhere isarctan(1/x)extended byarccotan(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:arccotan(1)0.7853...=pi/4arccotan(-1)-0.7853...=-pi/4arccotan(0)1.5707...=pi/21.
arctan(x) + arccotan(x) -> pi/2, unconditionallyPatterns.Trigonometry.cs, in the block commented// arc1({}) + arc2({}) = pi/2.With this range the sum is
pi/2for non-negativexand-pi/2for negativex. Theneighbouring
arcsin(x) + arccos(x) -> pi/2in the same block is unconditional and correct,because
arccos(x)ispi/2 - arcsin(x)by definition over the whole plane — so the block againhas 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: atx = 0the sum ispi/2while
sgn(0)is0. APiecewisewould be total, butCompilethrowsUncompilableNodeExceptionon one, so producing it here would break expressions that compiletoday.
2.
arccotan(cotan(x)) -> xis guarded with the wrong intervalThis one is mine, from #884: I guarded it with
[0, pi]on the assumption thatarccotan's rangewas
(0, pi). It is not, so the guard is wrong in both directions —— 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 becausecotanhas novalue there, so the composition has none either and rewriting to
xwould invent one.The other three of #884 check out against the same measurements:
arcsinis[-pi/2, pi/2],arccosis[0, pi](arccos(-1)ispi),arctanis(-pi/2, pi/2). Onlyarccotanwaswrong.
How it was found
By
work/boundcheck, a harness written to check exactly this class: it composes every unaryfunction 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. Atx = -3the value is1, sinceln(-3)/ln(-3)is1, and the simplification is undefined. Itneeds
ln xto be non-zero and defined, notxto be positive. This is the mirror of the usualdefect: 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 itcosts: at
x = -3the original is1.7918 + 6.2832iand the gathered form is real. Alreadyhandled downstream by verifying roots against the original equation, and still formally unsound.