packages feed

grasp-0.1.0.0: src/AM3/RandomInstance.hs

-- | Module for generating random 'Instance's.

module AM3.RandomInstance (
  -- * Random generator
  randomInstance
  , randomInstanceFile
  -- * Parameters
  , Params(..)
  , Range
    -- * Probability
  , Probability
  , fromDouble
     -- * Auxiliary functions
  , coin
  ) where

import AM3.Instance
import Control.Monad.Random
import Control.Monad (replicateM)
import Control.Monad.Trans

-- | An inclusive range of 'Int's.
type Range = (Int, Int)

-- | A real between 0 and 1.
newtype Probability = Prob Double
  deriving (Eq, Num, Ord, Show)

instance Fractional Probability where
  fromRational = fromDouble . fromRational
  (Prob a) / (Prob b) = Prob (a / b)

-- | Creates a probability from a 'Double'. Error if not between 0 and 1.
fromDouble :: Double -> Probability
fromDouble p
  | 0 <= p && p <= 1 = Prob p
  | otherwise = error $ show p ++ " is not a probability"

asDouble :: Probability -> Double
asDouble (Prob p) = p

-- | Parameters for generating a random 'Instance'.
--
-- Concrete values are picked uniformly at random within the defined 'Range's.
data Params = Params {
  _NC :: Range -- ^ Number of entities (@nOffices + nCenters@).
  , _oP :: Probability -- ^ Probability for an entity of being an office, as
                       -- opposed to being a center. In other words, proportion
                       -- @nOffices/nCenters@.
  , _kR :: Range -- ^ Capacities range.
  , _dR :: Range -- ^ Data range.
  , _pR :: Range -- ^ Number of segments range.
  , _fR :: Range -- ^ Fixed costs range.
  , _cI :: Int -- ^ Positive 'Int'. Max cost increase added to a certain lower
               -- bound. This lower bound ensures that the total cost is
               -- monotonic with respect to the stored data. See the
               -- documentation of 'randomInstance'.
  , _uP :: Probability -- ^ Probability of allowed connection.
  } deriving (Show)


-- | Flips a coin with probability of heads /p/.
coin :: MonadRandom m => Probability -> m Bool
coin (Prob p) = (< p) <$> getRandom

-- | Generates a random 'Instance' from a set of parameters 'Params'.
--
-- Ensures the cost of a center is monotonic with respect to the amount
-- of data stored in it. This can be expressed by the following inequation.
--
-- * @M[i]@: threshold of segment @i@.
-- * @C[i]@: cost of segment @i@.
--
-- @M[i]*C[i] > (M[i] - 1)*C[i-1]@.
--
-- equivalent to:
--
-- @C[i] > (C[i-1] * (1 + M[i])) \/ M[i]@.
--
-- We add an additional random positive cost @I[i]@ to add randomness.
--
-- * @I[i]@: random cost increase. In range of (1, '_cI').
--
-- Finally we define @C[i]@ as follows.
--
-- @i = 0: C[i] = I[i]@.
--
-- @i > 0: C[i] + I[i] = (C[i-1] * (1 + M[i])) \/ M[i]@.
randomInstance :: MonadRandom m => Params -> m Instance
randomInstance Params{..} = do
  nEntities <- getRandomR _NC
  nSegs <- getRandomR _pR
  xs <- take nEntities <$> getRandoms
  let nOffices = length (filter (< asDouble _oP) xs)
      nCenters = nEntities - nOffices
  allowdCons <- replicateM (nOffices*nCenters) (coin _uP)
  fixedCosts <- take nCenters <$> getRandomRs _fR
  datas <- take nOffices <$> getRandomRs _dR
  caps <- take nCenters <$> getRandomRs _kR
  spans <- take (nSegs - 1) <$> getRandomRs (incR nSegs)
  let thresh = scanl (+) 0 spans
  costs <- getCosts thresh
  return (newInstance r datas caps fixedCosts costs thresh allowdCons)
    where
      r = 2
      getCosts ms = do
        cost0 <- getRandomR (1, _cI)
        step ms cost0
          where
            step [] _ = return []
            step (m2:ms) c1 = do
               c2 <- getRandomR (lb, lb + _cI)
               cs <- step ms c2
               return (c2:cs)
              where
                lb = ceiling (((m + 1)*fromIntegral c1) / m)
                m = fromIntegral m2 :: Double
      expectedCap = let (a, b) = _kR in a + (a + b)`div`2
      incR segs = let x = expectedCap `div` segs
                  in (max 1 x, x)

-- | Generates a random 'Instance' and exports it to a @.dat@ file.
--
-- * Running the generation with a seed:
--
-- @evalRandT (randomInstanceFile params path) (mkStdGen seed)@
--
-- * Running the generation without a seed (using the @IO monad@):
--
-- @evalRandT (randomInstanceFile params path)@
randomInstanceFile ::
  Params -- ^ Parameters for the generation.
  -> FilePath -- ^ Path of the @.dat@ file.
  -> RandT StdGen IO ()
randomInstanceFile p file = randomInstance p >>= lift . toFile file