packages feed

math-programming-glpk-0.5.0: src/Math/Programming/Glpk/Internal.hs

{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}

-- | This module contains the full definitions backing the simplified API
-- exposed in 'Math.Programming.Glpk'.
module Math.Programming.Glpk.Internal where

import Control.Monad
import Control.Monad.Trans
import Control.Monad.Trans.Reader
import Control.Monad.Trans.State
import Data.Functor
import qualified Data.Text as T
import Data.Typeable
import Data.Void
import Foreign.C.String
import Foreign.C.Types
import Foreign.Marshal.Alloc
import Foreign.Ptr
import Foreign.Storable
import Math.Programming
import Math.Programming.Glpk.Header
import UnliftIO
import UnliftIO.Concurrent

-- | A reference to a GLPK variable.
type GlpkVariable = GlpkPtr Column

-- | A reference to a GLPK constraint.
type GlpkConstraint = GlpkPtr Row

-- | A placeholder for an objective.
--
-- GLPK supports only single-objective problems, and so no indices
-- need to be stored.
newtype GlpkObjective = GlpkObjective ()

class (MonadLP GlpkVariable GlpkConstraint GlpkObjective m, MonadIP GlpkVariable GlpkConstraint GlpkObjective m) => MonadGlpk m where
  writeFormulation :: FilePath -> m ()

-- | An interface to the low-level GLPK API.
--
-- High-level solver settings can be modified by altering the
-- 'SimplexMethodControlParameters' and 'MIPControlParameters' values
-- for LP and IP solves, respectively.
data GlpkEnv = GlpkEnv
  { -- | A pointer to the Problem object. Most GLPK routines take this
    -- as the first argument.
    _glpkEnvProblem :: Ptr Problem,
    -- | The variables in the model
    _glpkVariables :: IORef [GlpkVariable],
    -- | The next unique ID to assign to a variable.
    _glpkNextVariableId :: IORef Integer,
    -- | The constraints in the model
    _glpkConstraints :: IORef [GlpkConstraint],
    -- | The next unique ID to assign to a variable.
    _glpkNextConstraintId :: IORef Integer,
    -- | The control parameters for the simplex method
    _glpkSimplexControl :: IORef SimplexMethodControlParameters,
    -- | The control parameters for the MIP solver
    _glpkMIPControl :: IORef (MIPControlParameters Void),
    -- | The type of the last solve. This is needed to know whether to
    -- retrieve simplex, interior point, or MIP solutions.
    _glpkLastSolveType :: IORef (Maybe SolveType)
  }

-- | A pointer to a GLPK row or column.
--
-- We assign an immutable unique value to each 'GlpkPtr' we create.
--
-- Internally, GLPK refers to variables and constraints by their
-- column and row indices, respectively. These indices can change when
-- rows and columns are deleted, so we update this value as necessary.
data GlpkPtr a = GlpkPtr
  { -- | An immutable, unique value associated with this pointer.
    _glpkPtrId :: Integer,
    -- | The referenced object.
    _glpkPtrRef :: IORef a,
    -- | Whether this reference has been deleted from the problem.
    _glpkPtrDeleted :: IORef Bool
  }

instance Eq (GlpkPtr a) where
  (GlpkPtr x _ _) == (GlpkPtr y _ _) = x == y

instance Ord (GlpkPtr a) where
  compare (GlpkPtr x _ _) (GlpkPtr y _ _) = compare x y

-- | An error that GLPK can encounter.
data GlpkException
  = UnknownVariable
  | UnknownCode T.Text CInt
  | GlpkFailure T.Text
  deriving
    ( Show,
      Typeable
    )

instance Exception GlpkException

-- | An environment to solve math programs using GLPK.
newtype GlpkT m a = GlpkT {_runGlpk :: ReaderT GlpkEnv m a}
  deriving
    ( Functor,
      Applicative,
      Monad,
      MonadIO,
      MonadUnliftIO,
      MonadTrans
    )

type Glpk = GlpkT IO

instance MonadLP GlpkVariable GlpkConstraint GlpkObjective Glpk where
  addVariable = addVariable'
  deleteVariable = deleteVariable'
  getVariableName = getVariableName'
  setVariableName = setVariableName'
  getVariableValue = getVariableValue'
  getVariableBounds = getVariableBounds'
  setVariableBounds = setVariableBounds'

  addConstraint = addConstraint'
  deleteConstraint = deleteConstraint'
  getConstraintName = getConstraintName'
  setConstraintName = setConstraintName'
  getConstraintValue = getDualValue

  addObjective = addObjective'
  deleteObjective = deleteObjective'
  getObjectiveName = getObjectiveName'
  setObjectiveName = setObjectiveName'
  getObjectiveValue = getObjectiveValue'
  getObjectiveSense = getSense'
  setObjectiveSense = setSense'

  getTimeout = getTimeout'
  setTimeout = setTimeout'
  optimizeLP = optimizeLP'

instance MonadIP GlpkVariable GlpkConstraint GlpkObjective Glpk where
  getVariableDomain = getVariableDomain'
  setVariableDomain = setVariableDomain'
  getRelativeMIPGap = getRelativeMIPGap'
  setRelativeMIPGap = setRelativeMIPGap'
  optimizeIP = optimizeIP'

instance MonadGlpk Glpk where
  writeFormulation = writeFormulation'

instance MonadGlpk m => MonadGlpk (ReaderT r m) where
  writeFormulation = lift . writeFormulation

instance MonadGlpk m => MonadGlpk (StateT s m) where
  writeFormulation = lift . writeFormulation

withGlpkErrorHook :: (Ptr a -> IO CInt) -> Ptr a -> IO b -> IO b
withGlpkErrorHook hook ptr actions =
  bracket (mkHaskellErrorHook hook) freeHaskellFunPtr $ \hookPtr -> do
    glp_error_hook hookPtr ptr
    actions

removeGlpkErrorHook :: IO ()
removeGlpkErrorHook = glp_error_hook nullFunPtr nullPtr

runGlpk :: Glpk a -> IO a
runGlpk program =
  let withGlpkEnv actions =
        bracket glp_init_env (const glp_free_env) $ \case
          0 -> actions
          1 -> throwIO (GlpkFailure "GLPK already initialized")
          2 -> throwIO (GlpkFailure "GLPK failed to initialize; not enough memory")
          3 -> throwIO (GlpkFailure "GLPK failed to initialize; unsupported programming model")
          r -> throwIO (GlpkFailure ("GLPK failed to initialize; unknown status code " <> T.pack (show r)))
   in runInBoundThread $
        withGlpkEnv $
          flip finally removeGlpkErrorHook $
            withGlpkErrorHook (const glp_free_env) nullPtr $
              runGlpk' program

getDefaultSimplexControlParameters :: IO SimplexMethodControlParameters
getDefaultSimplexControlParameters = do
  params <- alloca $ \simplexControlPtr -> do
    glp_init_smcp simplexControlPtr
    peek simplexControlPtr

  -- Turn on presolve. Users can simply turn this off before the first
  -- optimization call if they desire.
  pure (params {smcpPresolve = glpkPresolveOn})

getDefaultMIPControlParameters :: IO (MIPControlParameters Void)
getDefaultMIPControlParameters = do
  params <- alloca $ \mipControlPtr -> do
    glp_init_iocp mipControlPtr
    peek mipControlPtr

  -- Turn on presolve. Users can simply turn this off before the first
  -- optimization call if they desire.
  pure (params {iocpPresolve = glpkPresolveOn})

runGlpk' :: Glpk a -> IO a
runGlpk' glpk = do
  -- Turn off terminal output. If we don't, users won't be able to
  -- inhibit terminal output generated from our setup.
  _ <- glp_term_out glpkOff

  bracket glp_create_prob glp_delete_prob $ \problem -> do
    env <-
      GlpkEnv problem
        <$> newIORef []
        <*> newIORef 0
        <*> newIORef []
        <*> newIORef 0
        <*> (getDefaultSimplexControlParameters >>= newIORef)
        <*> (getDefaultMIPControlParameters >>= newIORef)
        <*> newIORef Nothing

    runReaderT (_runGlpk glpk) env

data SolveType = LP | MIP | InteriorPoint

-- | Retrieve a component of the Glpk context
askGlpk :: Monad m => (GlpkEnv -> a) -> GlpkT m a
askGlpk = flip fmap (GlpkT ask)

-- | The underlying Glpk problem pointer
askProblem :: Monad m => GlpkT m (Ptr Problem)
askProblem = askGlpk _glpkEnvProblem

-- | All registered variables
askVariablesRef :: Glpk (IORef [GlpkVariable])
askVariablesRef = askGlpk _glpkVariables

-- | All registered constraints
askConstraintsRef :: Glpk (IORef [GlpkConstraint])
askConstraintsRef = askGlpk _glpkConstraints

-- | Note that a new row or column has been added to the to the problem.
register :: GlpkPtr a -> IORef [GlpkPtr a] -> Glpk ()
register newPtr ptrRefs = do
  liftIO $ modifyIORef' ptrRefs (newPtr :)

-- | Note that a row or column has been deleted from the problem, and
-- update row or column indices accordingly.
unregister :: Integral a => GlpkPtr a -> IORef [GlpkPtr a] -> Glpk ()
unregister deletedPtr ptrsRef =
  let update removed (GlpkPtr _ ptr _) = do
        z <- readIORef ptr
        when (z > removed) $
          modifyIORef' ptr pred
   in liftIO $ do
        -- If the reference was already deleted, do nothing
        deleted <- readIORef (_glpkPtrDeleted deletedPtr)
        unless deleted $ do
          -- Mark deletion
          writeIORef (_glpkPtrDeleted deletedPtr) True

          -- Remove the element to be unregistered
          modifyIORef' ptrsRef (filter ((/= _glpkPtrId deletedPtr) . _glpkPtrId))

          -- Modify the referenced values that were greater than the
          -- referenced element.
          deletedId <- readIORef (_glpkPtrRef deletedPtr)
          ptrs <- readIORef ptrsRef
          mapM_ (update deletedId) ptrs

readColumn :: GlpkVariable -> Glpk Column
readColumn = liftIO . readIORef . _glpkPtrRef

readRow :: GlpkConstraint -> Glpk Row
readRow = liftIO . readIORef . _glpkPtrRef

addVariable' :: Glpk GlpkVariable
addVariable' = do
  problem <- askProblem
  variable <- liftIO $ do
    column <- glp_add_cols problem 1
    glp_set_col_bnds problem column glpkFree 0 0
    GlpkPtr (fromIntegral column)
      <$> newIORef column
      <*> newIORef False

  askVariablesRef >>= register variable
  setVariableName' variable (defaultVariableName variable)
  pure variable

defaultVariableName :: GlpkVariable -> T.Text
defaultVariableName (GlpkPtr x _ _) = "x" <> T.pack (show x)

defaultConstraintName :: GlpkConstraint -> T.Text
defaultConstraintName (GlpkPtr x _ _) = "c" <> T.pack (show x)

setVariableName' :: GlpkVariable -> T.Text -> Glpk ()
setVariableName' variable name = do
  problem <- askProblem
  column <- readColumn variable
  liftIO $ withCText name (glp_set_col_name problem column)

getVariableName' :: GlpkVariable -> Glpk T.Text
getVariableName' variable = do
  problem <- askProblem
  column <- readColumn variable
  name <- liftIO $ glp_get_col_name problem column >>= peekCString
  pure (T.pack name)

deleteVariable' :: GlpkVariable -> Glpk ()
deleteVariable' variable = do
  problem <- askProblem
  column <- readColumn variable
  liftIO $ allocaGlpkArray [column] (glp_del_cols problem 1)
  askVariablesRef >>= unregister variable

addConstraint' :: Inequality (Expr GlpkVariable) -> Glpk GlpkConstraint
addConstraint' (Inequality ordering lhs rhs) =
  let LinExpr terms constant = simplify (lhs .-. rhs) :: Expr GlpkVariable

      constraintType :: GlpkConstraintType
      constraintType = case ordering of
        LT -> glpkLT
        GT -> glpkGT
        EQ -> glpkFixed

      constraintRhs :: CDouble
      constraintRhs = realToFrac (negate constant)

      numVars :: CInt
      numVars = fromIntegral (length terms)

      variables :: [GlpkVariable]
      variables = map snd terms

      coefficients :: [CDouble]
      coefficients = map (realToFrac . fst) terms
   in do
        problem <- askProblem
        columns <- mapM readColumn variables
        constraintPtr <- liftIO $ do
          row <- glp_add_rows problem 1
          allocaGlpkArray columns $ \columnArr ->
            allocaGlpkArray coefficients $ \coefficientArr -> do
              glp_set_row_bnds problem row constraintType constraintRhs constraintRhs
              glp_set_mat_row problem row numVars columnArr coefficientArr

          GlpkPtr (fromIntegral row)
            <$> newIORef row
            <*> newIORef False

        askConstraintsRef >>= register constraintPtr
        setConstraintName' constraintPtr (defaultConstraintName constraintPtr)
        pure constraintPtr

setConstraintName' :: GlpkConstraint -> T.Text -> Glpk ()
setConstraintName' constraintId name = do
  problem <- askProblem
  row <- readRow constraintId
  liftIO $ withCText name (glp_set_row_name problem row)

getConstraintName' :: GlpkConstraint -> Glpk T.Text
getConstraintName' constraint = do
  problem <- askProblem
  row <- readRow constraint
  name <- liftIO $ glp_get_row_name problem row >>= peekCString
  pure (T.pack name)

getDualValue :: GlpkConstraint -> Glpk Double
getDualValue constraint = do
  problem <- askProblem
  row <- readRow constraint
  fmap realToFrac . liftIO $ glp_get_row_dual problem row

deleteConstraint' :: GlpkConstraint -> Glpk ()
deleteConstraint' constraint = do
  problem <- askProblem
  row <- readRow constraint
  liftIO $ allocaGlpkArray [row] (glp_del_rows problem 1)
  askConstraintsRef >>= unregister constraint

addObjective' :: Expr GlpkVariable -> Glpk GlpkObjective
addObjective' expr =
  let LinExpr terms constant = simplify expr
   in do
        problem <- askProblem

        -- Set the constant term
        liftIO $ glp_set_obj_coef problem (GlpkInt 0) (realToFrac constant)

        -- Set the variable terms
        forM_ terms $ \(coef, variable) -> do
          column <- readColumn variable
          liftIO $ glp_set_obj_coef problem column (realToFrac coef)

        pure (GlpkObjective ())

-- | Delete an objective
--
-- There is nothing to actually delete, so we just set a zero objective
deleteObjective' :: GlpkObjective -> Glpk ()
deleteObjective' _ = void (addObjective' mempty)

getObjectiveName' :: GlpkObjective -> Glpk T.Text
getObjectiveName' _ = do
  problem <- askProblem
  name <- liftIO $ glp_get_obj_name problem >>= peekCString
  pure (T.pack name)

setObjectiveName' :: GlpkObjective -> T.Text -> Glpk ()
setObjectiveName' _ name = do
  problem <- askProblem
  liftIO $ withCText name (glp_set_obj_name problem)

getSense' :: GlpkObjective -> Glpk Sense
getSense' _ = do
  problem <- askProblem
  direction <- liftIO $ glp_get_obj_dir problem
  if direction == glpkMin
    then pure Minimization
    else pure Maximization

setSense' :: GlpkObjective -> Sense -> Glpk ()
setSense' _ sense =
  let direction = case sense of
        Minimization -> glpkMin
        Maximization -> glpkMax
   in do
        problem <- askProblem
        liftIO $ glp_set_obj_dir problem direction

getObjectiveValue' :: GlpkObjective -> Glpk Double
getObjectiveValue' _ = do
  problem <- askProblem
  lastSolveRef <- askGlpk _glpkLastSolveType
  lastSolve <- (liftIO . readIORef) lastSolveRef
  fmap realToFrac . liftIO $ case lastSolve of
    Just MIP -> glp_mip_obj_val problem
    Just LP -> glp_get_obj_val problem
    Just InteriorPoint -> glp_ipt_obj_val problem
    Nothing -> glp_get_obj_val problem -- There's been no solve, so who cares

optimizeLP' :: Glpk SolutionStatus
optimizeLP' = do
  -- Note that we've run an LP solve
  solveTypeRef <- askGlpk _glpkLastSolveType
  liftIO $ writeIORef solveTypeRef (Just LP)

  -- Run Simplex
  problem <- askProblem
  controlRef <- askGlpk _glpkSimplexControl
  liftIO $ do
    control <- readIORef controlRef
    alloca $ \controlPtr -> do
      poke controlPtr control
      _ <- glp_simplex problem controlPtr
      glp_get_status problem Data.Functor.<&> solutionStatus

optimizeIP' :: Glpk SolutionStatus
optimizeIP' = do
  -- Note that we've run a MIP solve
  solveTypeRef <- askGlpk _glpkLastSolveType
  liftIO $ writeIORef solveTypeRef (Just MIP)

  problem <- askProblem
  controlRef <- askGlpk _glpkMIPControl
  liftIO $ do
    control <- readIORef controlRef
    alloca $ \controlPtr -> do
      poke controlPtr control
      _ <- glp_intopt problem controlPtr
      glp_mip_status problem Data.Functor.<&> solutionStatus

setVariableBounds' :: GlpkVariable -> Bounds -> Glpk ()
setVariableBounds' variable bounds =
  let (boundType, cLow, cHigh) = case bounds of
        Free -> (glpkFree, 0, 0)
        NonNegativeReals -> (glpkGT, 0, 0)
        NonPositiveReals -> (glpkLT, 0, 0)
        Interval low high -> (glpkBounded, realToFrac low, realToFrac high)
   in do
        problem <- askProblem
        column <- readColumn variable
        liftIO $ glp_set_col_bnds problem column boundType cLow cHigh

getVariableBounds' :: GlpkVariable -> Glpk Bounds
getVariableBounds' variable =
  let boundsFor lb ub
        | lb == - maxCDouble && ub == maxCDouble = Free
        | lb == - maxCDouble && ub == 0.0 = NonPositiveReals
        | lb == 0.0 && ub == maxCDouble = NonNegativeReals
        | otherwise = Interval lb' ub'
        where
          lb' = realToFrac lb
          ub' = realToFrac ub
   in do
        problem <- askProblem
        column <- readColumn variable
        lb <- liftIO (glp_get_col_lb problem column)
        ub <- liftIO (glp_get_col_ub problem column)
        return (boundsFor lb ub)

setVariableDomain' :: GlpkVariable -> Domain -> Glpk ()
setVariableDomain' variable domain =
  let vType = case domain of
        Continuous -> glpkContinuous
        Integer -> glpkInteger
        Binary -> glpkBinary
   in do
        problem <- askProblem
        column <- readColumn variable
        liftIO $ glp_set_col_kind problem column vType

getVariableDomain' :: GlpkVariable -> Glpk Domain
getVariableDomain' variable =
  let getDomain' :: GlpkVariableType -> Glpk Domain
      getDomain' vType | vType == glpkContinuous = return Continuous
      getDomain' vType | vType == glpkInteger = return Integer
      getDomain' vType
        | vType == glpkBinary = return Binary
        | otherwise = throwIO unknownCode
        where
          typeName = T.pack . show . typeOf $ vType
          GlpkVariableType code = vType
          unknownCode = UnknownCode typeName code
   in do
        problem <- askProblem
        column <- readColumn variable
        getDomain' =<< liftIO (glp_get_col_kind problem column)

getVariableValue' :: GlpkVariable -> Glpk Double
getVariableValue' variable = do
  lastSolveRef <- askGlpk _glpkLastSolveType
  lastSolve <- (liftIO . readIORef) lastSolveRef

  let method = case lastSolve of
        Nothing -> glp_get_col_prim
        Just LP -> glp_get_col_prim
        Just MIP -> glp_mip_col_val
        Just InteriorPoint -> glp_ipt_col_prim

  problem <- askProblem
  column <- readColumn variable
  liftIO $ realToFrac <$> method problem column

getTimeout' :: RealFrac a => Glpk a
getTimeout' =
  let fromMillis :: RealFrac a => CInt -> a
      fromMillis millis = realToFrac millis / 1000
   in do
        controlRef <- askGlpk _glpkSimplexControl
        control <- liftIO (readIORef controlRef)
        return $ fromMillis (smcpTimeLimitMillis control)

setTimeout' :: RealFrac a => a -> Glpk ()
setTimeout' seconds =
  let millis :: Integer
      millis = round (seconds * 1000)
   in do
        controlRef <- askGlpk _glpkSimplexControl
        control <- liftIO (readIORef controlRef)
        let control' = control {smcpTimeLimitMillis = fromIntegral millis}
        liftIO (writeIORef controlRef control')

setRelativeMIPGap' :: RealFrac a => a -> Glpk ()
setRelativeMIPGap' gap = do
  controlRef <- askGlpk _glpkMIPControl
  control <- liftIO (readIORef controlRef)
  let control' = control {iocpRelativeMIPGap = realToFrac gap}
  liftIO (writeIORef controlRef control')

getRelativeMIPGap' :: RealFrac a => Glpk a
getRelativeMIPGap' = do
  controlRef <- askGlpk _glpkMIPControl
  control <- liftIO (readIORef controlRef)
  return $ realToFrac (iocpRelativeMIPGap control)

solutionStatus :: GlpkSolutionStatus -> SolutionStatus
solutionStatus status
  | status == glpkOptimal = Optimal
  | status == glpkFeasible = Feasible
  | status == glpkInfeasible = Infeasible
  | status == glpkNoFeasible = Infeasible
  | status == glpkUnbounded = Unbounded
  | status == glpkUndefined = Infeasible
  | otherwise = Error

-- | Write out the current formulation to a file.
writeFormulation' :: FilePath -> Glpk ()
writeFormulation' fileName = do
  problem <- askProblem
  _ <- liftIO $ withCString fileName (glp_write_lp problem nullPtr)
  return ()

maxCDouble :: CDouble
maxCDouble = encodeFloat significand' exponent'
  where
    base = floatRadix (undefined :: CDouble)
    precision = floatDigits (undefined :: CDouble)
    (_, maxExponent) = floatRange (undefined :: CDouble)
    significand' = base ^ precision - 1
    exponent' = maxExponent - precision

withCText :: T.Text -> (CString -> IO a) -> IO a
withCText = withCString . T.unpack