symtegration-0.6.1: test/Symtegration/Symbolic/Arbitrary.hs
{-# OPTIONS_GHC -fno-warn-orphans #-}
-- |
-- Description: QuickCheck Arbitrary instances for generating Symtegration.Symbolic values.
-- Copyright: Copyright 2024 Yoo Chung
-- License: Apache-2.0
-- Maintainer: dev@chungyc.org
module Symtegration.Symbolic.Arbitrary
( Simple (..),
Compound (..),
Complete (..),
SymbolMap (..),
SymbolText (..),
arbitraryNumber,
arbitrarySymbol,
arbitraryUnaryFunction,
arbitraryBinaryFunction,
arbitrarySymbolText,
shrinkSymbolText,
arbitrarySymbolMap,
shrinkSymbolMap,
assign,
)
where
import Data.Map (Map)
import Data.Map qualified as Map
import Data.Set (Set)
import Data.Set qualified as S
import Data.String (fromString)
import Data.Text (Text)
import Data.Text qualified as Text
import Symtegration.ErrorDouble
import Symtegration.FiniteDouble
import Symtegration.Symbolic
import Test.QuickCheck
instance Arbitrary Expression where
arbitrary = sized $ \n -> case n of
0 -> oneof [arbitraryNumber, arbitrarySymbol]
_ ->
frequency
[ (1, arbitraryNumber),
(1, arbitrarySymbol),
(4, resize (max 0 (n - 1)) arbitraryUnaryFunction),
(8, resize (n `div` 2) arbitraryBinaryFunction)
]
shrink (Number n) = Number <$> shrink n
shrink (Symbol s) = Symbol <$> shrinkSymbolText s
shrink (UnaryApply func x) = x : (UnaryApply func <$> shrink x)
shrink (BinaryApply func x y) =
x : y : [BinaryApply func x' y' | (x', y') <- shrink (x, y)]
instance Arbitrary UnaryFunction where
arbitrary = chooseEnum (minBound, maxBound)
instance Arbitrary BinaryFunction where
arbitrary = chooseEnum (minBound, maxBound)
-- | Generates simple symbolic mathematical expressions.
-- Specically, those which represent a single symbol or a single number.
newtype Simple = Simple Expression deriving (Eq, Show)
instance Arbitrary Simple where
arbitrary = Simple <$> oneof [arbitraryNumber, arbitrarySymbol]
-- | Generates a compound symbolic mathematical expression.
-- Specifically, either a unary function application or a binary function application.
newtype Compound = Compound Expression deriving (Eq, Show)
instance Arbitrary Compound where
arbitrary = Compound <$> oneof [arbitraryUnaryFunction, arbitraryBinaryFunction]
shrink (Compound e) = Compound <$> filter isCompound (shrink e)
where
isCompound (Number _) = False
isCompound (Symbol _) = False
isCompound _ = True
-- | Generates arbitrary expressions with a complete assignment of numbers to symbols.
-- The assignment of symbols to values will only contain symbols appearing in the expression.
-- Use the 'assign' function to turn the map into a function.
data Complete = Complete Expression (Map Text FiniteDouble) deriving (Eq, Show)
instance Arbitrary Complete where
arbitrary = do
expr <- arbitrary
vals <- infiniteList
let symbols = gatherSymbols expr
let assignment = Map.fromList $ zip (S.toList symbols) vals
if not (sensitiveExpression expr (assign assignment))
-- Only use expressions where slight divergences do not result in huge errors.
then return $ Complete expr (Map.map FiniteDouble assignment)
-- If we do not have such an expression, try again.
else arbitrary
shrink (Complete e m) = [Complete e' (restrict m e') | e' <- shrink e]
where
-- Keep symbol assignments still relevant to a shrinked expression.
restrict xs x = Map.restrictKeys xs $ gatherSymbols x
-- | Gather the symbols appearing in an expression.
gatherSymbols :: Expression -> Set Text
gatherSymbols (Number _) = S.empty
gatherSymbols (Symbol s) = S.singleton s
gatherSymbols (UnaryApply _ x) = gatherSymbols x
gatherSymbols (BinaryApply _ x y) = S.union (gatherSymbols x) (gatherSymbols y)
-- | Generates a random assignment from symbols to values.
-- Use the 'assign' function to turn it into a function.
newtype SymbolMap a = SymbolMap (Map Text a) deriving (Eq, Show)
instance (Arbitrary a) => Arbitrary (SymbolMap a) where
arbitrary = SymbolMap <$> arbitrarySymbolMap
shrink (SymbolMap m) = SymbolMap <$> shrinkSymbolMap m
-- | Generates random readable symbol.
newtype SymbolText = SymbolText Text deriving (Eq, Show)
instance Arbitrary SymbolText where
arbitrary = SymbolText <$> arbitrarySymbolText
shrink (SymbolText s) = SymbolText <$> shrinkSymbolText s
-- | Generate a random number.
arbitraryNumber :: Gen Expression
arbitraryNumber = Number <$> arbitrary
-- | Generate a random symbol with only letters.
arbitrarySymbol :: Gen Expression
arbitrarySymbol = Symbol <$> arbitrarySymbolText
-- | Generate a random expression with an unary function application.
arbitraryUnaryFunction :: Gen Expression
arbitraryUnaryFunction = UnaryApply <$> arbitrary <*> arbitrary
-- | Generate a random expression with a binary function application.
arbitraryBinaryFunction :: Gen Expression
arbitraryBinaryFunction = BinaryApply <$> arbitrary <*> arbitrary <*> arbitrary
-- | Generate a random map from readable symbols to values.
arbitrarySymbolMap :: (Arbitrary a) => Gen (Map Text a)
arbitrarySymbolMap = Map.fromList <$> listOf assocs
where
assocs = do
s <- arbitrarySymbolText
x <- arbitrary
return (s, x)
-- | Shrinks a map from readable symbols to values.
shrinkSymbolMap :: (Arbitrary a) => Map Text a -> [Map Text a]
shrinkSymbolMap = shrinkMapBy Map.fromList Map.toList (shrinkList shrinkAssoc)
where
shrinkAssoc (s, x) = do
s' <- shrinkSymbolText s
x' <- shrink x
return (s', x')
-- | Generate random text that is appropriate as a readable symbol.
-- They will be short, since what exactly are in the symbols is usually not important.
-- Does not generate the special symbol "pi".
arbitrarySymbolText :: Gen Text
arbitrarySymbolText = resize 3 $ fromString <$> listOf1 (choose ('a', 'z')) `suchThat` (/= "pi")
-- | Shrinks readable symbols.
shrinkSymbolText :: Text -> [Text]
shrinkSymbolText s =
-- Exclude empty text and s itself.
drop 1 $ reverse $ drop 1 $ Text.tails s
-- | For creating a function which assigns symbols to values
-- based on the given map, which are easier to generate with
-- specific properties and easier to show than a function itself.
-- Shorthand for writing @assign m@ instead of @flip Map.lookup m@.
assign :: Map Text a -> Text -> Maybe a
assign = flip Map.lookup