penrose-0.1.1.0: test/Transforms/Tests.hs
module Transforms.Tests (tests) where
import Test.Tasty
import Test.Tasty.SmallCheck as SC
import Test.Tasty.QuickCheck as QC
import Test.Tasty.HUnit
import Debug.Trace
import Data.Fixed
import Utils
import Transforms
import Functions
import Shapes
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
]