packages feed

linear-geo-0.1.0.0: test/Main.hs

{-# LANGUAGE OverloadedStrings
           , TypeFamilies
           , FlexibleContexts
           , ConstrainedClassMethods
           , UndecidableInstances
           #-}

module Main (main) where

import Data.Reflection

import Hedgehog
import Hedgehog.Main
import qualified Hedgehog.Gen   as HG
import qualified Hedgehog.Range as HR

import qualified Linear as L
import Linear.Geo hiding (diff)

-- from the numeric-limits package:

maxValue :: (RealFloat a) => a
maxValue = x
  where n = floatDigits x
        b = floatRadix x
        (_, u) = floatRange x
        x = encodeFloat (b ^ n - 1) (u - n)

minValue :: (RealFloat a) => a
minValue = -maxValue

hugeValFRange :: Range Double
hugeValFRange = HR.exponentialFloatFrom 0 minValue maxValue

hugeValFs :: MonadGen m => m Double
hugeValFs = HG.double hugeValFRange

modBugRange :: Range Double
modBugRange =
    let l = 1e15
    in HR.exponentialFloatFrom 0 (-l) l

modBug :: MonadGen m => m Double
modBug = HG.double modBugRange

class ApproxEq a where
    type family Atom a
    (~=~) :: Given (Atom a) => a -> a -> Bool

instance ApproxEq Double where
    type Atom Double = Double
    x ~=~ y = (abs (x - y)) / ((x + y + given) / 2) <= given

instance (ApproxEq a, Given (Atom a)) => ApproxEq (Radians a) where
    type Atom (Radians a) = a
    (Radians x) ~=~ (Radians y) = x ~=~ y

instance (ApproxEq a, Given (Atom a)) => ApproxEq (Degrees a) where
    type Atom (Degrees a) = a
    (Degrees x) ~=~ (Degrees y) = x ~=~ y

instance (ApproxEq a, Given (Atom a)) => ApproxEq (DMS a) where
    type Atom (DMS a) = a
    (DMS xd xm xs) ~=~ (DMS yd ym ys) = (xd ~=~ yd)
                                     && (xm ~=~ ym)
                                     && (xs ~=~ ys)

instance (ApproxEq a, Given (Atom a)) => ApproxEq (DM a) where
    type Atom (DM a) = a
    (DM xd xm) ~=~ (DM yd ym) = (xd ~=~ yd)
                             && (xm ~=~ ym)

instance (ApproxEq a, Given (Atom a)) => ApproxEq (L.V3 a) where
    type Atom (L.V3 a) = a
    (L.V3 ax ay az) ~=~ (L.V3 bx by bz) = (ax ~=~ bx)
                                       && (ay ~=~ by)
                                       && (az ~=~ bz)

instance (ApproxEq a, Given (Atom a)) => ApproxEq (ECEF a) where
    type Atom (ECEF a) = a
    (ECEF x) ~=~ (ECEF y) = x ~=~ y

instance (ApproxEq a, Given (Atom a)) => ApproxEq (Geo a) where
    type Atom (Geo a) = a
    (Geo ap al ah) ~=~ (Geo bp bl bh) = (ap ~=~ bp)
                                     && (al ~=~ bl)
                                     && (ah ~=~ bh)

instance (RealFloat a, ApproxEq a, Given (Atom a)) => ApproxEq (ENU a) where
    type Atom (ENU a) = a
    x@(ENU _ xp) ~=~ y = let ENU _ y'p = alignOrigin x y
                         in xp ~=~ y'p

(=~=) :: (ApproxEq a, Given (Atom a), Show a, MonadTest m) => a -> a -> m ()
(=~=) a b = diff a (~=~) b

--genECEF :: MonadGen m => m Double -> m (ECEF Double)
--genECEF gd = ECEF <$> (L.V3 <$> gd <*> gd <*> gd)

--genENU :: MonadGen m => m Double -> m (ENU Double)
--genENU gd = do
--    og <- genGeo gd
--    pv <- L.V3 <$> gd <*> gd <*> gd
--    pure (ENU (geoToECEF og) pv)

genGeo :: MonadGen m => m Double -> m (Geo Double)
genGeo gd = Geo <$> (normalizeAngle <$> genRad gd)
                <*> (normalizeAngle <$> genRad gd)
                <*> gd

genRad :: MonadGen m => m Double -> m (Radians Double)
genRad = fmap Radians

genDeg :: MonadGen m => m Double -> m (Degrees Double)
genDeg = fmap Degrees

genDMS :: MonadGen m => m Double -> m (DMS Double)
genDMS gd = DMS <$> gd <*> gd <*> gd

genDM :: MonadGen m => m Double -> m (DM Double)
genDM gd = DM <$> gd <*> gd

radNormRange :: Property
radNormRange = property $ do
    r <- forAll $ genRad modBug
    let (Radians r') = normalizeAngle r
    assert ((r' >= 0) && (r' < (2 * pi)))

radNormIdemp :: Property
radNormIdemp = property $ do
    r <- forAll $ genRad modBug
    let r' = normalizeAngle r
    r' === normalizeAngle r'

radToRadFromRadIdemp :: Property
radToRadFromRadIdemp = property $ do
    r <- forAll $ genRad hugeValFs
    r === fromRadians (toRadians r)

degNormRange :: Property
degNormRange = property $ do
    d <- forAll $ genDeg modBug
    let (Degrees d') = normalizeAngle d
    assert ((d' >= 0) && (d' < 360))

degNormIdemp :: Property
degNormIdemp = property $ do
    d <- forAll $ genDeg modBug
    let d' = normalizeAngle d
    d' === normalizeAngle d'

degToRadFromRadIdemp :: Property
degToRadFromRadIdemp = property $ do
    d <- forAll $ genDeg hugeValFs
    let r = toRadians d
    give 1e-15 (d =~= fromRadians r)

dmsNormRange :: Property
dmsNormRange = property $ do
    d <- forAll $ genDMS modBug
    let dmsn = normalizeAngle d
        (Degrees d') = dmsToDegrees dmsn
    assert ((d' >= 0) && (d' < 360))

dmsNormIdemp :: Property
dmsNormIdemp = property $ do
    d <- forAll $ genDMS modBug
    let d' = normalizeAngle d
    give 1e-10 (d' =~= normalizeAngle d')

-- Stability is so bad, practically this does not hold.
--dmsToRadFromRadIdemp :: Property
--dmsToRadFromRadIdemp = property $ do
--    d <- forAll $ genDMS modBug
--    let r = toRadians d
--    give 1e-2 (normalizeAngle d =~= normalizeAngle (fromRadians r))

dmNormRange :: Property
dmNormRange = property $ do
    d <- forAll $ genDM modBug
    let dmn = normalizeAngle d
        (Degrees d') = dmToDegrees dmn
    assert ((d' >= 0) && (d' < 360))

dmNormIdemp :: Property
dmNormIdemp = property $ do
    d <- forAll $ genDM modBug
    let d' = normalizeAngle d
    d' === normalizeAngle d'

-- Stability is so bad, practically this does not hold.
--dmToRadFromRadIdemp :: Property
--dmToRadFromRadIdemp = property $ do
--    d <- forAll $ genDM modBug
--    let r = toRadians d
--    give 1e-2 (normalizeAngle d =~= normalizeAngle (fromRadians r))

radToFromLatLonIdemp :: Property
radToFromLatLonIdemp = property $ do
    g <- forAll $ genGeo hugeValFs
    let p :: Radians Double
        l :: Radians Double
        h :: Double
        (p, l, h) = toLatLonAlt g
        g' = fromLatLonAlt p l h
    g === g'

degToFromLatLonIdemp :: Property
degToFromLatLonIdemp = property $ do
    g <- forAll $ genGeo hugeValFs
    let p :: Degrees Double
        l :: Degrees Double
        h :: Double
        (p, l, h) = toLatLonAlt g
        g' = fromLatLonAlt p l h
    give 1e-8 (g =~= g')

dmsToFromLatLonIdemp :: Property
dmsToFromLatLonIdemp = property $ do
    g <- forAll $ genGeo hugeValFs
    let p :: DMS Double
        l :: DMS Double
        h :: Double
        (p, l, h) = toLatLonAlt g
        g' = fromLatLonAlt p l h
    give 1e-8 (g =~= g')

dmToFromLatLonIdemp :: Property
dmToFromLatLonIdemp = property $ do
    g <- forAll $ genGeo hugeValFs
    let p :: DM Double
        l :: DM Double
        h :: Double
        (p, l, h) = toLatLonAlt g
        g' = fromLatLonAlt p l h
    give 1e-8 (g =~= g')

-- | Not true due to NaNs, need to figure out why...
--geoToFromECEFIdemp :: Property
--geoToFromECEFIdemp = property $ do
--    g <- forAll $ genGeo hugeValFs
--    give 1e-7
--        (g =~= normalizeGeo (ecefToGeo (geoToECEF g)))

-- | Not true due to NaNs, need to figure out why...
--enuToFromECEFIdemp :: Property
--enuToFromECEFIdemp = property $ do
--    p@(ENU o _) <- forAll $ genENU hugeValFs
--    give 1e-8 (p =~= ecefToENU o (enuToECEF p))

main :: IO ()
main = defaultMain $ (:[]) $ checkParallel $ Group "Linear.Geo"
    [ ("normalizeAngle range check @Radians", radNormRange)
    , ("normalizeAngle idempotent @Radians", radNormIdemp)
    , ("(fromRadians . toRadians) == id @Radians", radToRadFromRadIdemp)
    , ("normalizeAngle range check @Degrees", degNormRange)
    , ("normalizeAngle idempotent @Degrees", degNormIdemp)
    , ("(fromRadians . toRadians) == id @Degrees", degToRadFromRadIdemp)
    , ("normalizeAngle range check @DMS", dmsNormRange)
    , ("normalizeAngle idempotent @DMS", dmsNormIdemp)
    -- Does not hold in practice due to stability
    --, ("(fromRadians . toRadians) == id @DMS", dmsToRadFromRadIdemp)
    , ("normalizeAngle range check @DM", dmNormRange)
    , ("normalizeAngle idempotent @DM", dmNormIdemp)
    -- Does not hold in practice due to stability
    --, ("(fromRadians . toRadians) == id @DM", dmToRadFromRadIdemp)
    , ("(fromLatLon . toLatLon) == id @Radians", radToFromLatLonIdemp)
    , ("(fromLatLon . toLatLon) == id @Degrees", degToFromLatLonIdemp)
    , ("(fromLatLon . toLatLon) == id @DMS", dmsToFromLatLonIdemp)
    , ("(fromLatLon . toLatLon) == id @DM", dmToFromLatLonIdemp)
    -- Not true due to NaNs, need to figure out why...
    --, ("(ecefToGeo . geoToECEF) == id", geoToFromECEFIdemp)
    --, ("(ecefToENU . enuToECEF) == id", enuToFromECEFIdemp)
    ]