ekmett / linear

Low-dimensional linear algebra primitives for Haskell.
http://hackage.haskell.org/package/linear
Other
198 stars 50 forks source link

use atan2 in V2.unangle #177

Open aavogt opened 5 months ago

aavogt commented 5 months ago

I propose unangle (V2 x y) = atan2 y x because it's shorter and numerically better:

ghci> unangle (V2 0 2.646978e-23) :: Float
NaN
ghci> unangle (V2 0 2.6469783e-23) :: Float
0.7853983
ghci> unangle (V2 0 5.4210115e-20) :: Float
1.5703081
ghci> atan2 1e-45 0 :: Float
1.5707964

But the output of unangle is on [-pi/2, 3pi/2], while the output of atan2 is on [-pi,pi]:

> do printf "unangle, atan2, angle\n" ; sequence_ [ printf "%.2f %.2f %.2f\n" (unangle v) (atan2 y x) ang | ang <- [ -2*pi, -2*pi + pi/4 .. 2*pi ], let v = angle ang, let V2 x y = v ] :: IO () 
unangle, atan2, angle
0.00 0.00 -6.28
0.79 0.79 -5.50
1.57 1.57 -4.71
2.36 2.36 -3.93
3.14 -3.14 -3.14
3.93 -2.36 -2.36
-1.57 -1.57 -1.57
-0.79 -0.79 -0.79
0.00 0.00 0.00
0.79 0.79 0.79
1.57 1.57 1.57
2.36 2.36 2.36
3.14 3.14 3.14
3.93 -2.36 3.93
4.71 -1.57 4.71
-0.79 -0.79 5.50
-0.00 -0.00 6.28

You could keep the old range but I suspect it's not necessary:

-- | output on [-pi/2, 3pi/2]
unangle (V2 x y) =  case atan2 y x of
  theta
   | theta < -pi/2 -> theta + 2*pi
   | otherwise -> theta

A bigger problem with the new definition is that the type will change. Some users of unangle need new RealFloat instances:

unangle :: (Floating a, Ord a) => V2 a -> a
atan2 :: RealFloat a => a -> a -> a
instance Floating a => Floating (V2 a) -- exists
instance RealFloat a => RealFloat (V2 a) where
  atan2 = liftA2 atan2 -- missing for good reason.. how to write encode/decodeFloat?

OverlappingInstances, TypeFamilies or maybe rewrite rules are another option to get atan2 for Float and Double, but keep asin for other types.