packages feed

penrose-0.1.1.1: test/Transforms/Tests.hs

module Transforms.Tests
  ( tests
  ) where

import           Data.Fixed
import           Debug.Trace
import           Test.Tasty
import           Test.Tasty.HUnit
import           Test.Tasty.QuickCheck as QC
import           Test.Tasty.SmallCheck as SC

import           Penrose.Functions
import           Penrose.Shapes
import           Penrose.Transforms
import           Penrose.Util

tests :: TestTree
tests = testGroup "Functions tests" [properties, unitTests]

properties :: TestTree
properties = testGroup "Properties" [scProps, qcProps]

eps :: Float
eps = 10 ** (-3) -- Kind of arbitrarily picked so the tests pass. Not sure about the right precision

hmEq :: HMatrix Float -> HMatrix Float -> Bool
hmEq t1 t2 = hmDiff t1 t2 <= eps

ptsEq :: Pt2 Float -> Pt2 Float -> Bool
ptsEq (x1, y1) (x2, y2) = sqrt ((x1 - x2) ^ 2 + (y1 - y2) ^ 2) < eps -- Should use a different eps

id_compose_id :: Assertion
id_compose_id = idH # idH @?= idH

rotate_2pi_id :: Assertion
rotate_2pi_id = hmEq idH (rotationM (2 * pi)) @?= True

rotate_rev :: Float -> Bool
rotate_rev radians = hmEq idH (rotationM radians # rotationM (-radians)) == True

rotate_twice :: Float -> Bool
rotate_twice radians =
  hmEq (rotationM (2 * radians)) (rotationM radians # rotationM (radians)) ==
  True

translate_id :: Float -> Float -> Bool
translate_id x y =
  hmEq idH (translationM (x, y) # translationM (-x, -y)) == True

scale_id :: Float -> Float -> Bool
scale_id cx cy =
  let res = (scalingM (1 / cx, 1 / cy) # scalingM (cx, cy))
  in if cx /= 0.0 && cy /= 0.0
       then hmEq idH res == True
       else True -- Ignore scaling by 0

id_mul :: (Float, Float, Float, Float, Float, Float) -> Bool
id_mul (x1, x2, x3, x4, x5, x6) =
  let m = listToHm [x1, x2, x3, x4, x5, x6]
  in hmEq (idH # m) (m # idH) && hmEq (m # idH) m

-- Not true in general
rot_tr :: Float -> Bool
rot_tr x =
  if x == 0.0
    then True
    else ptsEq
           ((rotationM (pi / 2) # translationM (x, 0)) ## (0, 0))
           (translationM (0, x) ## (0, 0)) ==
         True

trs1 :: Assertion
trs1 =
  ptsEq
    ((translationM (-1, 1) # rotationM (pi / 2) # scalingM (2, 1)) ## (1, 0))
    (-1, 3.0) @?=
  True

-- Starting with some parameters, we show we can extract the same (?) parameters from the composed matrix
-- It's lossy though, so some tests might fail when the angle can't be reconstructed
params_matrix_id' :: (Float, Float, Float, Float, Float) -> Bool
params_matrix_id' p@(sx, sy, theta, dx, dy) =
  let p0 = [sx, sy, theta `mod'` (2.0 * pi), dx, dy]
           -- might not work with theta out of range [-2pi, 2pi]
      m = paramsToMatrix p
      (sx', sy', theta', dx', dy') = paramsOf m
      p' = [sx', sy', theta', dx', dy']
  in if sx < eps || sy < eps
       then True -- don't scale by 0
       else norm (p0 -. p') < eps

-- This also can't test it fully though, for example if the matrix didn't result from
-- the canonical scale-then-rotate-then-translate
-- e.g. if it consists only of a shear: m = listToHm [1.0,0.0,0.0,1.0,1.0,0.0]
params_matrix_eq :: (Float, Float, Float, Float, Float, Float) -> Bool
params_matrix_eq (x1, x2, x3, x4, x5, x6) =
  let m = listToHm [x1, x2, x3, x4, x5, x6]
  in if x1 < eps || x5 < eps
       then True -- don't scale by 0
       else hmEq m (paramsToMatrix $ paramsOf m)

-- TODO
-- do we need any checks for numerical stability?
-- test the canonical order: scale(w, h) then rotate(theta) then translate(x, y) = ???
-- test skewing
-- test things that commute
-- test things that don't commute
transformsGroup :: TestTree
transformsGroup =
  testGroup
    "Transforms"
    [ testCase "idH * idH = idH" id_compose_id
    , testCase "rotate by 2pi = idH" rotate_2pi_id
    , testCase "translate, rotate, scale example" trs1
    ]

--------
scProps = testGroup "(checked by SmallCheck)" []

qcProps =
  testGroup
    "(checked by QuickCheck)"
    [ QC.testProperty "forall x, rotate(x) . rotate (-x) = idH" rotate_rev
    , QC.testProperty "rotate t * rotate -t = id" rotate_rev
    , QC.testProperty "rotate 2t = rotate t * rotate t" rotate_twice
    , QC.testProperty "translate v * translate -v = idH" translate_id
    , QC.testProperty "scale (cx,cy) * scale (-cx, -cy) = idH" scale_id
    , QC.testProperty "id * A = A * id = A" id_mul
            -- This test isn't true in general
            -- QC.testProperty "translate then rotate CCW can equal a translation up" rot_tr
            -- TODO: These tests don't pass reliably, because starting with base DOF, converting them to a matrix, and trying to solve for the original params does not have a unique solution
            -- (i.e. might find satisfying params that are not the original params).
            -- Although I'm not sure why the second test isn't passing. Perhaps floating-point equality checks?
            -- Anyway, we don't actually solve for any of the original DOF in the Penrose systems, so I'm commenting these tests out. (-kye)
            -- QC.testProperty "(params -> matrix -> solve for params) ~ params" params_matrix_id',
            -- QC.testProperty "matrix ~ (matrix -> params -> matrix)" params_matrix_eq
    ]

-- Module: topic: function: property
unitTests :: TestTree
unitTests = testGroup "Unit tests" [transformsGroup]