packages feed

iterative-forward-search-0.1.0.0: src/Data/IFS/Algorithm.hs

--------------------------------------------------------------------------------
-- Iterative Forward Search                                                   --
--------------------------------------------------------------------------------
-- This source code is licensed under the terms found in the LICENSE file in  --
-- the root directory of this source tree.                                    --
--------------------------------------------------------------------------------

module Data.IFS.Algorithm (
    defaultTermination,
    ifs
) where

--------------------------------------------------------------------------------

import           Control.Arrow              ( Arrow((&&&)) )
import           Control.Monad.Trans.Class  ( MonadTrans(lift) )
import           Control.Monad.Trans.Reader

import qualified Data.IntMap                as IM
import qualified Data.IntSet                as IS
import           Data.Maybe                 ( fromJust )

import           System.Random

import           Data.IFS.Types

--------------------------------------------------------------------------------

-- | `defaultTermination` @iterations currAssign@ determines whether to continue
-- the algorithm or terminate. It terminates if the current assignment assigns
-- all variables or the maximum number of iterations has been exceded (25 times
-- the number of variables)
defaultTermination :: Int
                   -> Assignment
                   -> CSPMonad Solution (Maybe Solution)
defaultTermination iterations currAssign = do
    -- get variables
    vars <- cspVariables <$> ask
    -- check conditions
    case (IS.size vars > IM.size currAssign, iterations <= 25 * IS.size vars) of
        (True, True)  -> pure Nothing
        (True, False) -> pure $ Just $ Incomplete currAssign
        (False, _)    -> pure $ Just $ Complete currAssign

-- | `getMostRestricted` @vars doms cons@ indexes these variables by size of
-- domain - # connected constraints. The lowest index is then the most
-- restricted variable.
getMostRestricted :: Variables
                  -> Domains
                  -> Constraints
                  -> IM.IntMap [Var]
getMostRestricted vars doms cons =
    -- TODO: Scaling one of these numbers could be better
    IM.fromListWith (++) $ flip map (IS.toList vars) $ \var ->
            (IS.size (doms IM.! var) - countConnectedCons var cons, [var])

    where
        -- counts the number of constraints connected to @var@
        countConnectedCons var = flip foldl 0 $ \conflicting (conVars, _) ->
            if var `IS.member` conVars
            then conflicting + 1
            else conflicting

-- | `selectVariable` @currAssignment@ decides which variable to change next
selectVariable :: Int -> Assignment -> CSPMonad r Var
selectVariable iterations currAssignment = do
    -- get CSP parameters
    MkCSP{..} <- ask

    -- get variables currently not assigned. We can assume this is non-empty
    -- as the algorithm terminates when all are assigned
    let unassigned = cspVariables IS.\\ IS.fromList (IM.keys currAssignment)

    -- find which of these is most restricted
    let restricted = getMostRestricted unassigned cspDomains cspConstraints

    -- if we are before the random cap then pick one of the most difficult
    -- variables
    if iterations < cspRandomCap
    then
        -- pick a random variable from the most difficult
        let toChoseFrom = snd $ IM.findMin restricted
        in (toChoseFrom !!) <$> lift (randomRIO (0, length toChoseFrom - 1))
    else
        -- pick any random variable
        let unassignedList = IS.toList unassigned
        in (unassignedList !!) <$> lift (randomRIO (0, length unassignedList - 1))

-- | `setValue` @csp currAssign var@ determines a value to assign to @var@ and
-- returns @currAssign@ with @var@ assigned to the determined value
setValue :: Assignment
         -> Var
         -> CSPMonad r Assignment
setValue currAssign var = do
    (doms, cons) <- (cspDomains &&& cspConstraints) <$> ask
    let domain = flip IS.filter (fromJust $ IM.lookup var doms) $ \val ->
            countConflicts (IM.singleton var val) cons == 0

    -- If no possible values return current assignment unchanged
    if IS.null domain
    then pure currAssign
    else do
        -- create map with key of the number of contraints violated, and the
        -- value being a list of assignments with that number of conflicts
        let conflictMap = IM.fromListWith (++) $ flip map (IS.toList domain)
                        $ \val ->
                            let assignment = IM.insert var val currAssign
                            in (countConflicts assignment cons, [assignment])

        -- TODO: Some kind of nice formula - weight the smaller conflicts more
        -- and the weighting should be heaver if the gap between nums of
        -- conflicts is larger
        -- Will chose from the 10% of assignments with the lowest number of
        -- conflicts
        let cap = ceiling $ 0.1 * fromIntegral (IS.size domain)

        -- get at least @cap@ assignments in order of conflicts
        let toChoseFrom = getToChoseFrom 0 cap [] conflictMap

        -- get a radndom assignment from this list
        (toChoseFrom !!) <$> lift (randomRIO (0, length toChoseFrom - 1))

    where
        -- counts the number of conflicts in @assignment@
        countConflicts assignment = flip foldl 0 $
            \conflicting (_, constraintF) ->
                if constraintF assignment
                then conflicting
                else conflicting + 1

        -- gets lowest number of assignments >cap possible when sorting by
        -- conflict number
        getToChoseFrom :: Int
                       -> Int
                       -> [Assignment]
                       -> IM.IntMap [Assignment]
                       -> [Assignment]
        getToChoseFrom n cap added toAdd
            | n >= cap   = added
            | otherwise = let ((_,as), toAdd') = IM.deleteFindMin toAdd
                          in getToChoseFrom (n + length as)
                                            cap
                                            (added ++ as)
                                            toAdd'

-- | `removeConflicts'` @var assign constraintF toRemove@ repeated unassigns
-- one of the least constrained variables except @var@ from @assign@ until the
-- @constraintF@ passes
removeConflicts' :: Var
                 -> Assignment
                 -> (Assignment -> Bool)
                 -> IM.IntMap [Var]
                 -> Assignment
removeConflicts' var assign constraintF toRemove
    | constraintF assign = assign
    | otherwise          =
        let
            -- get next minimum variables
            (_, x:remaining) = IM.findMax toRemove
            -- remove this variable from the map
            toRemove' = if null remaining
                        then snd $ IM.deleteFindMax toRemove
                        else IM.updateMax (const $ Just remaining) toRemove
            -- unassign this variable unless it is the variable just assigned
            newAssign = if x==var then assign else IM.delete x assign
        in removeConflicts' var newAssign constraintF toRemove'

-- | `removeConflicts` @currAssign var@ checks which constraints from @csp@
-- are violated by @currAssign@ and removes all variables involved in the
-- violated constraints except @var@
removeConflicts :: Assignment
                -> Var
                -> CSPMonad r Assignment
removeConflicts currAssignment var = do
    (doms, cons) <- (cspDomains &&& cspConstraints) <$> ask
    -- check each constraint and if it is violated unassign variables until
    -- the constraint passes
    pure $ flip (flip foldl currAssignment) cons $
            \assign (constraintVars, constraintF) ->
                if constraintF assign
                then assign
                else removeConflicts' var assign constraintF
                        $ getMostRestricted constraintVars doms cons

-- | `getBest` @newAssign bestAssign@ determines whether @newAssign@ is better
-- than @bestAssign@ and returns the best out of the two. A random is picked
-- if both are deemed equally good
getBest :: Assignment
        -> Assignment
        -> CSPMonad r Assignment
getBest newAssign bestAssign =
    case IM.size newAssign `compare` IM.size bestAssign of
        -- if more variables are assigned in the current assignment it is better
        GT -> pure newAssign
        -- if less variables are assigned it is worse
        LT -> pure bestAssign
        -- if both have an equal number of variables assigned pick randomly
        EQ -> do
            useNew <- (<= 0.5) <$> (lift randomIO :: CSPMonad r Double)
            pure $ if useNew then newAssign else bestAssign

-- | `ifs'` @iterations currAssign bestAssign@ checks whether it should continue
-- the search given @currAssign@, and if so performs the next iteration of the
-- IFS algorithm and recursively calls this function again with the new
-- assignment. If `canContinue` returns false the best assignment found so far
-- is returned
ifs' :: Int
     -> Assignment
     -> Assignment
     -> CSPMonad r r
ifs' iterations currAssign bestAssign = do
    canContinue <- cspTermination <$> ask
    continue <- canContinue iterations bestAssign
    case continue of
        Nothing -> do
            -- get variable to change
            var <- selectVariable iterations currAssign

            -- determine and set new value for @var@
            newAssignment <- setValue currAssign var

            -- find and unassign conflicting variables
            conflictsRemoved <- removeConflicts newAssignment var

            -- run ifs' with the new assignment
            nextAssignment <- getBest conflictsRemoved bestAssign
            ifs' (iterations+1) conflictsRemoved nextAssignment

        Just a -> pure a

-- | `ifs` @csp startingAssignment@ performs an iterative first search on @csp@
-- using @startingAssignment@ as the initial assignment
ifs :: CSP r
    -> Assignment
    -> IO r
ifs csp startingAssignment =
    runReaderT (ifs' 0 startingAssignment startingAssignment) csp

--------------------------------------------------------------------------------