packages feed

grasp-0.1.0.0: src/AM3/Solution.hs

-- | This module implements a data type for constructing a new 'Solution'.

{-# LANGUAGE PartialTypeSignatures #-}
{-# LANGUAGE TemplateHaskell       #-}
module AM3.Solution (
  -- * Types
  Solution
  , Candidate
    -- * Construction
  , empty
    -- * Queries
  , cost
  , connections
  , correct
    -- * Other Queries
    , spaceLeft
    , dataLeft
    , connection
    -- * Modifiers
  , updateCon
  , insertCon
    -- * GRASP parameters
  , gparams
    -- * GRASP modifiers
  , appendCand
    -- * GRASP queries
  , neighborhood
  , candidates
  ) where

import           AM3.Instance
import           Control.Arrow
import           Control.Lens
import           Control.Monad.Random
import           Data.HashMap.Strict (HashMap)
import qualified Data.HashMap.Strict as H
import           Data.HashSet (HashSet)
import qualified Data.HashSet as S
import           Data.Hashable
import qualified Data.List as L
import           Data.Maybe
import           Data.Vector.Unboxed (findIndex, unsafeFreeze, (!))
import           Debug.Trace
import           GRASP
import           System.Random.Shuffle

-- | Represents a solution to an 'Instance' of the problem.
data Solution = Solution {
  _inst     :: Instance -- ^ Instance.
  , _distr  :: HashMap (CenterId, OfficeId) Int -- ^ amount of data per connection.
  , _conns :: HashMap OfficeId (HashSet CenterId) -- ^ list of connected centers per office.
  , _oUsage :: HashMap OfficeId Int -- ^ data stored per office.
  , _cUsage :: HashMap CenterId Int -- ^ storage usage per center.
  } deriving (Show, Read)
makeLenses ''Solution

-- | A candidate that can be added to a 'Solution'.
type Candidate = Connection

-- | Connection between center and office of a certain amount.
type Connection = (CenterId, OfficeId, Int)


centerTotalCost :: Instance
         -> CenterId -- ^ Id of the center.
         -> Int -- ^ Amount of data.
         -> Cost -- ^ total cost.
centerTotalCost _ _ 0 = 0
centerTotalCost i c k = centerCost i c + case findIndex (<=k) (segmentThresholds i) of
  Nothing -> error "no suitable segment exists"
  Just six -> k * segmentCost i six

-- | Tells if an office has some data stored in a center.
existsCon :: Solution -> CenterId -> OfficeId -> Bool
existsCon s c o = fromMaybe False (S.member o <$> H.lookup c (s ^. conns))

-- | List of 'Connection's in a 'Solution'.
connections :: Solution -> [Connection]
connections s = [ (c, o, k) | ((c, o), k) <- H.toList (s ^. distr)]

-- | Returns how much data from some office is assigned to a center.
connection :: Solution -> CenterId -> OfficeId -> Int
connection s c o = fromMaybe 0 (H.lookup (c, o) (s ^. distr))

-- | Strict sum
sum' :: [Cost] -> Cost
sum' = L.foldl' (+) 0

-- | Computes the total cost of a solution.
cost :: Solution -> Cost
cost sol = (sum' . map (uncurry $ centerTotalCost i)) $ H.toList (sol ^. cUsage)
  where
    i = sol ^. inst

-- | Creates an empty 'Solution'.
empty :: Instance -> Solution
empty insta = Solution
    { _inst = insta
    , _distr = H.empty
    , _cUsage = H.empty
    , _oUsage = H.empty
    , _conns = H.fromList [ (o, S.empty) | o <- offices insta ]
    }

-- | Returns (old, new, updatedMap)
update :: (Eq k, Hashable k) =>
          k -- ^ key.
          -> (Maybe a -> Maybe a) -- ^ update function.
          -> HashMap k a -- ^ The hashmap.
          -> (Maybe a, Maybe a, HashMap k a) -- ^ (old, new, updatedMap).
update k f hm =
      case f old of
          Nothing -> (old, Nothing, H.delete k hm)
          Just new -> (old, Just new, H.insert k new hm)
  where
    old = H.lookup k hm

addUsage :: CenterId -> OfficeId -> Int -> Solution -> Solution
addUsage c o k = over oUsage (H.insertWith (+) o k) . over cUsage (H.insertWith (+) c k)

assertConnExists :: CenterId -> OfficeId -> Bool -> Solution -> Solution
assertConnExists c o exists = over conns (H.adjust (fun c) o)
  where fun | exists = S.insert
            | otherwise = S.delete

-- | Returns True if the solution satisfies all the restrictions.
--
-- At the moment only checks if every office has stored its data.
correct :: Solution -> Bool
correct s = all ((== 0) . dataLeft s) (offices (s ^. inst))

-- | Updates a connection if present.
updateCon :: CenterId -> OfficeId -> (Maybe Int -> Int) -> Solution -> Solution
updateCon c o upd s =
  let (mold, mnew, hm') = update (c, o) f (s ^. distr)
      diff = new - old
      old = fromMaybe 0 mold
      new = fromMaybe 0 mnew
      s1
        | diff == 0 = s
        | otherwise = addUsage c o diff s
      s2
        | old == 0 && new > 0 = assertConnExists c o True s1
        | old > 0 && new == 0 = assertConnExists c o False s1
        | otherwise = s1
  in set distr hm' s2
  where
    f x
      | updx == 0 = Nothing
      | otherwise = Just updx
      where updx = upd x

-- | Inserts a new connection.
insertCon :: Solution -> CenterId -> OfficeId -> Int -> Solution
insertCon s c o v = assertConnExists c o True $ addUsage c o v $ over distr (H.insert (c, o) v) s

-- | Appends a 'Candidate' to the 'Solution'. Assumes that the center and the
-- office are not already connected.
appendCand :: Solution -> Candidate -> Solution
appendCand s (c, o, v) = insertCon s c o v

-- | Space left in a center.
spaceLeft :: Solution -> CenterId -> Int
spaceLeft s c = centerCapacity (s ^. inst) c - centerUsage s c

-- | Data left in an office. Replications are taken into account.
dataLeft :: Solution -> OfficeId -> Int
dataLeft s o = replications (s ^. inst) * officeData (s ^. inst) o - officeUsage s o

-- | Generates in a random order the list of neighbors.
--
-- Neighbors are generated as follows:
--
-- * Move as many data as possible from an office in one center to another center.
--
-- * Interchange two offices from two centers.
-- @(c1, o1, d1) (c2, o2, d2) --> (c1, o2, d2) (c2, o1, d1)@
neighborhood :: MonadRandom m => Solution -> m [Solution]
neighborhood s = do
  rcon <- shuffleM (connections s)
  return [ moveData c1 c2 o s | (c1, o, _) <- rcon, c2 <- centers (s ^. inst)]
    where
      -- | Moves as many data as possible from o in c1 to c2.
      moveData :: CenterId -> CenterId -> OfficeId -> Solution -> Solution
      moveData c1 c2 o s = (updateCon c1 o (\x -> fromMaybe 0 x - much)
                            >>> updateCon c2 o (\x -> fromMaybe 0 x + much)) s
        where
          much = min (spaceLeft s c2) (connection s c1 o)
      swapData c1 o1 c2 o2 = undefined

-- | How much data is stored in a center.
centerUsage :: Solution -> CenterId -> Int
centerUsage s c = fromMaybe 0 (H.lookup c (s ^. cUsage))

-- | How much data from an office is already stored.
officeUsage :: Solution -> OfficeId -> Int
officeUsage s o = fromMaybe 0 (H.lookup o (s ^. oUsage))

-- | Generates the list of candidates and their estimated cost.
--
-- Candidates are generated as follows:
--
-- * All data from an office is stored in a center.
-- * All remaining space from a center is occupied with some data from an office.
candidates :: Solution -> [(Candidate, Cost)]
candidates sol = mapMaybe (uncurry mkCandidate) (allowedConnections i)
  where
    i = view inst sol
    mkCandidate c o
      | add == 0 = Nothing
      | add > 0 && not (existsCon sol c o) =
        let addCost = centerTotalCost i c (cx + add) - centerTotalCost i c cx
        in Just ((c, o, add), addCost)
      | otherwise = Nothing
      where
        cx = centerUsage sol c
        add = minimum [officeData i o, spaceLeft sol c, dataLeft sol o]

-- | Creates the parameters 'GParams' for 'grasp' specifics for this problem.
gparams :: MonadRandom m =>
  Int -- ^ Maximum number of iterations.
  -> Maybe Int -- ^ Optional maximum number of candidates.
  -> Double -- ^ Alpha.
  -> Instance -- ^ Problem instance.
  -> GParams Solution Candidate m
gparams maxi maxCand alph inst = GParams {
  alpha = alph
  , maxitr = maxi
  , costf = return . cost
  , correctf = return . correct
  , start = return (empty inst)
  , append = \s -> return . appendCand s
  , genCandidates = \s -> let c = candidates s in return $ maybe id take maxCand c
  , neighbors = neighborhood
}