implicit-0.4.1.0: tests/Graphics/Implicit/Test/Instances.hs
{- ORMOLU_DISABLE -}
-- Implicit CAD. Copyright (C) 2011, Christopher Olah (chris@colah.ca)
-- Copyright (C) 2014 2015 2016, Julia Longtin (julia.longtin@gmail.com)
-- Released under the GNU AGPLV3+, see LICENSE
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE DefaultSignatures #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE TypeOperators #-}
-- type (~)
{-# LANGUAGE CPP #-}
{-# LANGUAGE RecordWildCards #-}
-- Polymorphic makeTestResult
{-# LANGUAGE Rank2Types #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE TypeApplications #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}
module Graphics.Implicit.Test.Instances (Observe, observe, (=~=), arbitraryNonZeroV) where
import Prelude (Applicative, (.), not, abs, fmap, Bool(False, True), Bounded, Double, Integer, fromIntegral, (*), (/), (^), round, Enum, Show(show), unlines, Ord, compare, Eq, (==), pure, RealFloat(isNaN), Int, Double, ($), (<), div, (<*>), (<$>), (+), (<>), (<=))
#if MIN_VERSION_base(4,17,0)
import Prelude (type(~))
#endif
import Graphics.Implicit
( square,
emptySpace,
fullSpace,
sphere,
cube,
cylinder2,
cylinder,
circle,
polygon,
extrude,
rotate3,
rotate3V,
transform3,
rotate,
transform )
import Graphics.Implicit.Definitions
( ExtrudeMScale(C1,C2,Fn),
SymbolicObj2(Shared2),
SymbolicObj3(Shared3),
ℝ,
ℝ2,
ℝ3,
SharedObj(Outset, Translate, Scale, UnionR, IntersectR,
DifferenceR, Shell, WithRounding) )
import Graphics.Implicit.Primitives ( getImplicit, Object(Space) )
import qualified Test.QuickCheck
import Test.QuickCheck
(CoArbitrary(coarbitrary), discard, Arbitrary(arbitrary, shrink),
genericShrink,
choose,
oneof,
scale,
sized,
suchThat,
vectorOf,
Gen,
Positive(getPositive),
NonZero(getNonZero),
Property)
import Linear (V2(V2), V3(V3), V4(V4), M33, det33, M44, det44, Epsilon, nearZero, Quaternion, axisAngle)
data Insidedness = Inside | Outside | Surface | NaNFail
deriving (Ord, Show, Enum, Bounded)
insidedness :: (RealFloat a) => a -> Insidedness
insidedness 0 = Surface
insidedness x | isNaN x = NaNFail
insidedness x | x < 0 = Inside
insidedness _ = Outside
-- Explicitely allow matching the three cases
-- so NaNFail never passes Eq (similar to an actual NaNs
-- that are never equal)
instance Eq Insidedness where
Inside == Inside = True
Outside == Outside = True
Surface == Surface = True
_ == _ = False
data TestResult obj a = TestResult {
trInsidedness :: Insidedness
, trSampledValue :: a
, trSampledObject :: obj
, trSampledAt :: (Space obj) a
}
instance
( Show obj
, Show a
, Show (Space obj a)
)
=> Show (TestResult obj a)
where
show TestResult{..} = unlines
[ ""
, "TestResult:"
, " | " <> show trInsidedness
, " | " <> show trSampledObject
, " | Sampled at " <> show trSampledAt <> " returns " <> show trSampledValue
]
instance Eq a => Eq (TestResult obj a) where
(==) ta tb = trInsidedness ta == trInsidedness tb
instance Ord a => Ord (TestResult obj a) where
compare ta tb = trInsidedness ta `compare` trInsidedness tb
------------------------------------------------------------------------------
instance Arbitrary SymbolicObj2 where
shrink = genericShrink
arbitrary = sized $ \n ->
if n <= 1
then oneof small
else oneof $
[ rotate <$> arbitrary <*> decayArbitrary 2
, transform <$> arbitraryInvertibleM33 <*> decayArbitrary 2
, Shared2 <$> arbitrary
] <> small
where
small =
[ circle <$> arbitrary
, square <$> arbitrary <*> arbitrary
, polygon <$> do
n <- choose (3, 10)
-- TODO(srk): this is a hack until #449 is solved
vectorOf n ((*100) <$> (V2 <$> arbitraryPos <*> arbitraryPos))
, pure fullSpace
, pure emptySpace
]
-- TODO(sandy): Also generate all of the extrusion variants.
instance Arbitrary SymbolicObj3 where
shrink = genericShrink
arbitrary = sized $ \n ->
if n <= 1
then oneof small
else oneof $
[ rotate3 <$> arbitrary <*> decayArbitrary 2
, rotate3V <$> arbitrary <*> arbitrary <*> decayArbitrary 2
, transform3 <$> arbitraryInvertibleM44 <*> decayArbitrary 2
, extrude <$> decayArbitrary 2 <*> arbitraryPos
, Shared3 <$> arbitrary
] <> small
where
small =
[ sphere <$> arbitraryPos
, cylinder <$> arbitraryPos <*> arbitraryPos
, cylinder2 <$> arbitraryPos <*> arbitraryPos <*> arbitraryPos
, cube <$> arbitrary <*> arbitraryV3
, pure fullSpace
, pure emptySpace
]
instance (Arbitrary obj, Arbitrary a, Arbitrary (f a), CoArbitrary (f a)) => Arbitrary (SharedObj obj f a) where
shrink = genericShrink
arbitrary = oneof
[ Translate <$> arbitrary <*> decayArbitrary 2
, Scale <$> arbitrary <*> decayArbitrary 2
, UnionR <$> arbitraryPos <*> decayedList
, IntersectR <$> arbitraryPos <*> decayedList
, DifferenceR <$> arbitraryPos <*> decayArbitrary 2 <*> decayedList
, Shell <$> arbitraryPos <*> decayArbitrary 2
, Outset <$> arbitraryPos <*> decayArbitrary 2
, WithRounding <$> arbitraryPos <*> decayArbitrary 2
]
instance Arbitrary a => Arbitrary (V2 a) where
shrink = genericShrink
arbitrary = V2 <$> arbitrary <*> arbitrary
instance Arbitrary a => Arbitrary (V3 a) where
shrink = genericShrink
arbitrary = V3 <$> arbitrary <*> arbitrary <*> arbitrary
instance Arbitrary a => Arbitrary (V4 a) where
shrink = genericShrink
arbitrary = V4 <$> arbitrary <*> arbitrary <*> arbitrary <*> arbitrary
instance CoArbitrary a => CoArbitrary (V2 a) where
coarbitrary (V2 a b) = coarbitrary (a, b)
instance CoArbitrary a => CoArbitrary (V3 a) where
coarbitrary (V3 a b c) = coarbitrary (a, b, c)
instance CoArbitrary a => CoArbitrary (V4 a) where
coarbitrary (V4 a b c d) = coarbitrary (a, b, c, d)
instance Arbitrary ExtrudeMScale where
shrink = genericShrink
arbitrary = oneof
[ C1 <$> arbitrary
, C2 <$> arbitrary
, Fn <$> arbitrary
]
instance Arbitrary (Quaternion ℝ) where
arbitrary = do
q <- arbitrary
v <- arbitraryV3
if v == 0.0
then discard
else pure $ axisAngle v q
------------------------------------------------------------------------------
-- Minimum of quickspec(s) Observe class and instances required for implicit testsuite
-- BSD3 Copyright: 2009-2019 Nick Smallbone
class (Arbitrary test, Ord outcome) => Observe test outcome a | a -> test outcome where
-- | Make an observation on a value. Should satisfy the following law: if
-- @x /= y@, then there exists a value of @test@ such that @observe test x /= observe test y@.
observe :: test -> a -> outcome
default observe :: (outcome ~ a) => test -> a -> outcome
observe _ x = x
instance (Arbitrary a, Observe test outcome b) => Observe (a, test) outcome (a -> b) where
observe (x, obs) f = observe obs (f x)
instance Observe () Double Double
(=~=) :: (Show test, Show outcome, Observe test outcome a) => a -> a -> Property
a =~= b = Test.QuickCheck.property $ \test -> observe test a Test.QuickCheck.=== observe test b
infix 4 =~=
makeTestResult
:: forall obj f a
. ( RealFloat a
, Object obj f a
, Quantizable a
, f ~ Space obj
)
=> obj
-> Space obj a
-> TestResult obj a
makeTestResult obj sampleAt =
let
fun = getImplicit obj
sampledVal = quantize epsilon $ fun sampleAt
in
TestResult
{ trInsidedness = insidedness sampledVal
, trSampledValue = sampledVal
, trSampledObject = obj
, trSampledAt = sampleAt
}
------------------------------------------------------------------------------
-- | Two 'SymbolicObj2's are the same if their 'getImplicit' functions agree at
-- all points (up to an error term of 'epsilon')
instance Observe (ℝ2, ()) (TestResult SymbolicObj2 Double) SymbolicObj2 where
observe (sampledAt, _) obj = makeTestResult obj sampledAt
------------------------------------------------------------------------------
-- | Two 'SymbolicObj3's are the same if their 'getImplicit' functions agree at
-- all points (up to an error term of 'epsilon')
instance Observe (ℝ3, ()) (TestResult SymbolicObj3 Double) SymbolicObj3 where
observe (sampledAt, _) obj = makeTestResult obj sampledAt
------------------------------------------------------------------------------
-- | The number of decimal points we need to agree to assume two 'Double's are
-- equal.
epsilon :: Int
epsilon = 10
------------------------------------------------------------------------------
-- | Types which can truncate their decimal points to a certain number of
-- digits.
class Quantizable a where
quantize
:: Int -- ^ The number of decimal points to keep
-> a
-> a
instance Quantizable a => Quantizable [a] where
quantize n = fmap (quantize n)
instance Quantizable a => Quantizable (b -> a) where
quantize n = fmap (quantize n)
instance Quantizable Double where
quantize n r =
let pow = 10 ^ n :: Double
in fromIntegral @Integer (round (r * pow)) / pow
-- | Generate a small list of 'Arbitrary' elements, splitting the current
-- complexity budget between all of them.
decayedList :: Arbitrary a => Gen [a]
decayedList = do
n <- choose (1, 10)
vectorOf n $ decayArbitrary $ n + 1
-- | Generate an arbitrary positive 'Double'. Useful for sizes.
arbitraryPos :: Gen Double
arbitraryPos = getPositive <$> arbitrary
-- | Generate an arbitrary positive 'ℝ3'. Useful for sizes.
arbitraryV3 :: Gen ℝ3
arbitraryV3 = fmap abs <$> arbitrary
-- | Generate arbitrary vector that has no zero components
arbitraryNonZeroV
:: ( Arbitrary (f (NonZero a))
, Applicative f
)
=> Gen (f a)
arbitraryNonZeroV = fmap getNonZero <$> arbitrary
-- | Generate arbitrary invertible 3x3 matrix, representing
-- affine transformation matrix in 2D space. The last vector is fixed
-- to @V3 0 0 1@ so it doesn't result in NaNs when normalized from
-- homogeneous coordinates.
--
-- Inspired by InvertibleM33 from linear-tests package
-- https://github.com/minimapletinytools/linear-tests/blob/master/src/Linear/Matrix/Arbitrary.hs
arbitraryInvertibleM33
:: ( Arbitrary a
, Epsilon a
)
=> Gen (M33 a)
arbitraryInvertibleM33 =
(V3 <$> arbitrary <*> arbitrary <*> pure (V3 0 0 1))
`suchThat` (not . nearZero . det33)
-- | Generate arbitrary invertible 4x4 matrix, representing
-- affine transformation matrix in 3D space. The last vector is fixed
-- to @V4 0 0 0 1@ so it doesn't result in NaNs when normalized from
-- homogeneous coordinates.
arbitraryInvertibleM44
:: ( Arbitrary a
, Epsilon a
)
=> Gen (M44 a)
arbitraryInvertibleM44 =
(V4 <$> arbitrary <*> arbitrary <*> arbitrary <*> pure (V4 0 0 0 1))
`suchThat` (not . nearZero . det44)
-- | Split the complexity budget by a factor of @n@.
decayArbitrary :: Arbitrary a => Int -> Gen a
decayArbitrary n = scale (`div` n) arbitrary