haga/src/Seminar.hs

164 lines
4.0 KiB
Haskell
Raw Normal View History

{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedStrings #-}
module Seminar where
import Data.List ((!!), lookup, nub, zipWith3)
import Data.Random
import Data.Random.Distribution.Uniform
import qualified Data.Text as T
import GA
import Pretty
import Protolude
import Test.QuickCheck hiding (sample, shuffle)
import Test.QuickCheck.Instances
data Name
= N
{ firstName :: Text,
lastName :: Text
}
deriving (Eq, Show)
instance Arbitrary Name where
arbitrary = N <$> arbitrary <*> arbitrary
instance Pretty Name where
pretty (N f l) = f <> " " <> l
type EMail = Text
data Student
= S
{ name :: Name,
email :: EMail
}
deriving (Eq, Show)
instance Arbitrary Student where
arbitrary = S <$> arbitrary <*> arbitrary
instance Pretty Student where
pretty (S n e) = pretty n <> " <" <> e <> ">"
{-|
The seminar GA is about giving topics to students. If there are not enough
topics, some students might get assigned 'NoTopic'.
-}
data T
= T Text
| NoT
deriving (Show)
{-|
'Topic' is not 'Maybe' because this 'Eq' instance is different ('Nothing' @==@
'Nothing' but 'NoTopic' @/=@ 'NoTopic').
-}
instance Eq T where
T x == T y = x == y
-- NoT == _ = False
-- _ == NoT = False
NoT == NoT = True
NoT == _ = False
_ == NoT = False
instance Arbitrary T where
arbitrary = oneof [T <$> arbitrary, return NoT]
instance Pretty T where
pretty (T s) = s
pretty NoT = "Kein Thema"
topicToMaybe (T x) = Just x
topicToMaybe NoT = Nothing
type Priorities = [(Student, [(T, Int)])]
students :: Priorities -> [Student]
students = fmap fst
topics :: Priorities -> [T]
topics p = topics' ++ padding
where
padding = replicate (length (students p) - length topics') NoT
topics' = nub . join $ fmap fst . snd <$> p
{-|
The sum of all priorities given by all students.
-}
sumOfAll p = (1 +) . sum $ (sum . fmap snd) . snd <$> p
{-|
The priority of 'NoTopic' is 101. The priority of topics that have not been
assigned a priority by a student is one less than this value.
-}
prioOf :: Priorities -> Student -> T -> Int
prioOf _ _ NoT = 101
prioOf p s t = maybe (prioOf p s NoT - 1) identity $ lookup s p >>= lookup t
type Assignment = [(Student, T)]
data I = I Priorities Assignment
deriving (Eq, Show)
instance Pretty I where
pretty i@(I p a) =
T.unlines (gene <$> a)
where
gene :: (Student, T) -> Text
gene (s, t) =
pretty s <> ": " <> pretty t <> prio s t
prio :: Student -> T -> Text
prio s t = " (" <> show (prioOf p s t) <> ")"
instance Individual I where
new (I p _) =
sample $ I p . zip (nub $ students p) <$> shuffle (topics p)
fitness (I p a) =
return . negate . sum
$ fromIntegral . uncurry (prioOf p) <$> a
mutate (I p a) = do
x <- sample $ Uniform 0 (length a - 1)
y <- sample $ Uniform 0 (length a - 1)
return . I p $ switch x y a
{-|
Borrowed from TSP: Crossover cuts the parents in two and swaps them (if this
does not create an invalid offspring).
TODO Assumes that both individuals are based on the same priorities.
TODO Require type-wise that both Assignments are of the same length.
-}
crossover1 (I p a1) (I _ a2) = do
let l = fromIntegral $ min (length a1) (length a2) :: Double
x <- sample $ Uniform 0 l
let a1' = zipWith3 (f x) a1 a2 [0 ..]
let a2' = zipWith3 (f x) a2 a1 [0 ..]
if valid a1' && valid a2'
then return . Just $ (I p a1', I p a2')
else return Nothing
where
f x v1 v2 i = if i <= x then v1 else v2
switch :: Int -> Int -> Assignment -> Assignment
switch i' j' xs
| 0 <= i' && i' < length xs && 0 <= j' && j' < length xs =
let i = min i' j'
j = max i' j'
ei = xs !! i
ej = xs !! j
left = take i xs
middle = take (j - i - 1) $ drop (i + 1) xs
right = drop (j + 1) xs
in left ++ [(fst ei, snd ej)] ++ middle ++ [(fst ej, snd ei)] ++ right
| otherwise = xs
valid :: Assignment -> Bool
valid a =
length a == length (nub $ fst <$> a)
&& length a == length (nub $ snd <$> a)