{-# LANGUAGE TypeFamilies #-} {-# LANGUAGE TypeOperators #-} {- | The following LP problem maximize @4 x_1 - 3 x_2 + 2 x_3@ subject to @2 x_1 + x_2 <= 10@ @x_2 + 5 x_3 <= 20@ and @x_i >= 0@ is used as an example in the doctest comments. By default all indeterminates are non-negative. A given bound for a variable completely replaces the default, so @0 <= x_i <= b@ must be explicitly given as @i >=<. (0,b)@. Multiple bounds for a variable are not allowed, instead of @[i >=. a, i <=. b]@ use @i >=<. (a,b)@. -} module Numeric.GLPK ( Term(..), Bound(..), Inequality(..), free, (<=.), (>=.), (==.), (>=<.), NoSolutionType(..), SolutionType(..), Solution, Constraints, Direction(..), Objective, Bounds, (.*), objectiveFromTerms, simplex, simplexMulti, exact, interior, interiorMulti, ) where import qualified Math.Programming.Glpk.Header as FFI import qualified Data.Array.Comfort.Storable.Mutable as Mutable import qualified Data.Array.Comfort.Storable as Array import qualified Data.Array.Comfort.Shape as Shape import qualified Data.NonEmpty as NonEmpty import Data.Array.Comfort.Storable (Array) import Data.Tuple.HT (mapFst, mapSnd) import Data.Maybe (fromMaybe) import Data.Foldable (traverse_, for_) import Control.Monad (void) import Control.Applicative (liftA2) import Control.Exception (bracket) import Control.DeepSeq (NFData, rnf) import System.IO.Unsafe (unsafePerformIO) import qualified Foreign import Foreign.Ptr (nullPtr) import Foreign.C.Types (CDouble) {- $setup >>> import qualified Numeric.GLPK as LP >>> import Numeric.GLPK ((.*), (<=.), (==.)) >>> >>> import qualified Data.Array.Comfort.Storable as Array >>> import qualified Data.Array.Comfort.Shape as Shape >>> >>> import Data.Tuple.HT (mapPair, mapSnd) >>> >>> import qualified Test.QuickCheck as QC >>> import Test.QuickCheck ((===), (.&&.), (.||.)) >>> >>> type X = Shape.Element >>> type PairShape = Shape.NestedTuple Shape.TupleIndex (X,X) >>> type TripletShape = Shape.NestedTuple Shape.TupleIndex (X,X,X) >>> >>> pairShape :: PairShape >>> pairShape = Shape.static >>> >>> tripletShape :: TripletShape >>> tripletShape = Shape.static >>> >>> round3 :: Double -> Double >>> round3 x = fromInteger (round (1000*x)) / 1000 -} infix 7 .* (.*) :: Double -> ix -> Term ix (.*) = Term data Term ix = Term Double ix deriving (Show) infix 4 <=., >=., >=<., ==. (<=.), (>=.), (==.) :: x -> Double -> Inequality x x <=. bnd = Inequality x $ LessEqual bnd x >=. bnd = Inequality x $ GreaterEqual bnd x ==. bnd = Inequality x $ Equal bnd (>=<.) :: x -> (Double,Double) -> Inequality x x >=<. bnd = Inequality x $ uncurry Between bnd free :: x -> Inequality x free x = Inequality x Free data Inequality x = Inequality x Bound deriving Show data Bound = LessEqual Double | GreaterEqual Double | Between Double Double | Equal Double | Free deriving Show instance Functor Inequality where fmap f (Inequality x bnd) = Inequality (f x) bnd data NoSolutionType = Undefined | NoFeasible | Unbounded deriving (Eq, Show) data SolutionType = Feasible | Infeasible | Optimal deriving (Eq, Show) instance NFData NoSolutionType where rnf NoFeasible = () rnf _ = () instance NFData SolutionType where rnf Optimal = () rnf _ = () type Solution sh = Either NoSolutionType (SolutionType, (Double, Array sh Double)) type Constraints ix = [Inequality [Term ix]] data Direction = Minimize | Maximize type Objective sh = Array sh Double type Bounds ix = [Inequality ix] objectiveFromTerms :: (Shape.Indexed sh, Shape.Index sh ~ ix) => sh -> [Term ix] -> Objective sh objectiveFromTerms sh = Array.fromAssociations 0 sh . map (\(Term x ix) -> (ix,x)) prepareBounds :: Inequality a -> (a, (FFI.GlpkConstraintType, CDouble, CDouble)) prepareBounds (Inequality x bnd) = (,) x $ (\(bndType,lo,up) -> (bndType, realToFrac lo, realToFrac up)) $ case bnd of LessEqual up -> (FFI.glpkLT, 0, up) GreaterEqual lo -> (FFI.glpkGT, lo, 0) Between lo up -> (FFI.glpkBounded, lo, up) Equal y -> (FFI.glpkFixed, y, y) Free -> (FFI.glpkFree, 0, 0) columnIndex :: (Shape.Indexed sh, Shape.Index sh ~ ix) => sh -> ix -> FFI.Column columnIndex shape var = 1 + fromIntegral (Shape.offset shape var) deferredColumnIndex :: Shape.DeferredIndex ix -> FFI.Column deferredColumnIndex (Shape.DeferredIndex var) = 1 + fromIntegral var allocaArray :: (Foreign.Storable a) => Int -> (FFI.GlpkArray a -> IO b) -> IO b allocaArray n f = Foreign.allocaArray (n+1) $ f . FFI.GlpkArray pokeElem :: (Foreign.Storable a) => FFI.GlpkArray a -> Int -> a -> IO () pokeElem (FFI.GlpkArray ptr) k a = Foreign.pokeElemOff ptr k a {- | >>> case Shape.indexTupleFromShape tripletShape of (x1,x2,x3) -> mapSnd (mapSnd Array.toTuple) <$> LP.simplex [] [[2.*x1, 1.*x2] <=. 10, [1.*x2, 5.*x3] <=. 20] (LP.Maximize, Array.fromTuple (4,-3,2) :: Array.Array TripletShape Double) Right (Optimal,(28.0,(5.0,0.0,4.0))) prop> \(QC.Positive posWeight) (QC.Positive negWeight) target -> case Shape.indexTupleFromShape pairShape of (pos,neg) -> case mapSnd (mapSnd Array.toTuple) <$> LP.simplex [] [[1.*pos, (-1).*neg] ==. target] (LP.Minimize, Array.fromTuple (posWeight,negWeight) :: Array.Array PairShape Double) of (Right (LP.Optimal,(absol,(posResult,negResult)))) -> QC.property (absol>=0) .&&. (posResult === 0 .||. negResult === 0); _ -> QC.property False -} simplex :: (Shape.Indexed sh, Shape.Index sh ~ ix) => Bounds ix -> Constraints ix -> (Direction, Objective sh) -> Solution sh simplex = solve (flip FFI.glp_simplex nullPtr) {- | Optimize for one objective after another. That is, if the first optimization succeeds then the optimum is fixed as constraint and the optimization is continued with respect to the second objective and so on. The iteration fails if one optimization fails. The obtained objective values are returned as well. Their number equals the number of attempted optimizations. The last objective value is included in the Solution value. This is a bit inconsistent, but this way you have a warranty that there is an objective value if the optimization is successful. The objectives are expected as 'Term's because after successful optimization step they are used as (sparse) constraints. It's also easy to assert that the same array shape is used for all objectives. The function does not work reliably, because an added objective can make the system infeasible due to rounding errors. E.g. a non-negative objective can become very small but negative. -} simplexMulti, interiorMulti :: (Shape.Indexed sh, Shape.Index sh ~ ix) => Bounds ix -> Constraints ix -> sh -> NonEmpty.T [] (Direction, [Term ix]) -> ([Double], Solution sh) simplexMulti = solveMulti . simplex interiorMulti = solveMulti . interior solveMulti :: (Shape.Indexed sh, Shape.Index sh ~ ix) => (Constraints ix -> (Direction, Objective sh) -> Solution sh) -> Constraints ix -> sh -> NonEmpty.T [] (Direction, [Term ix]) -> ([Double], Solution sh) solveMulti solver constrs sh (NonEmpty.Cons obj0 objs0) = let go curObj ((dir,obj):objs) (Right (Optimal, (opt,_))) = mapFst (opt:) $ go obj objs $ solver ((curObj==.opt) : constrs) (dir, objectiveFromTerms sh obj) go _ _ sol = ([], sol) in go (snd obj0) objs0 $ solver constrs $ mapSnd (objectiveFromTerms sh) obj0 {- | >>> case Shape.indexTupleFromShape tripletShape of (x1,x2,x3) -> mapSnd (mapSnd Array.toTuple) <$> LP.exact [] [[2.*x1, 1.*x2] <=. 10, [1.*x2, 5.*x3] <=. 20] (LP.Maximize, Array.fromTuple (4,-3,2) :: Array.Array TripletShape Double) Right (Optimal,(28.0,(5.0,0.0,4.0))) -} exact :: (Shape.Indexed sh, Shape.Index sh ~ ix) => Bounds ix -> Constraints ix -> (Direction, Objective sh) -> Solution sh exact = solve (flip FFI.glp_exact nullPtr) {-# INLINE solve #-} solve :: (Shape.Indexed sh, Shape.Index sh ~ ix) => (Foreign.Ptr FFI.Problem -> IO FFI.GlpkSimplexStatus) -> Bounds ix -> Constraints ix -> (Direction, Objective sh) -> Solution sh solve solver bounds constrs (dir,obj) = unsafePerformIO $ bracket FFI.glp_create_prob FFI.glp_delete_prob $ \lp -> do storeProblem bounds constrs (dir,obj) lp void $ solver lp let examine = liftA2 (,) (realToFrac <$> FFI.glp_get_obj_val lp) (readGLPArray (Array.shape obj) $ \arr ix -> Mutable.write arr ix . realToFrac =<< FFI.glp_get_col_prim lp (deferredColumnIndex ix)) status <- FFI.glp_get_status lp either (return . Left) (\typ -> Right . (,) typ <$> examine) $ analyzeStatus status {- | >>> case Shape.indexTupleFromShape tripletShape of (x1,x2,x3) -> mapSnd (mapPair (round3, Array.toTuple . Array.map round3)) <$> LP.interior [] [[2.*x1, 1.*x2] <=. 10, [1.*x2, 5.*x3] <=. 20] (LP.Maximize, Array.fromTuple (4,-3,2) :: Array.Array TripletShape Double) Right (Optimal,(28.0,(5.0,0.0,4.0))) -} interior :: (Shape.Indexed sh, Shape.Index sh ~ ix) => Bounds ix -> Constraints ix -> (Direction, Objective sh) -> Solution sh interior bounds constrs (dir,obj) = unsafePerformIO $ bracket FFI.glp_create_prob FFI.glp_delete_prob $ \lp -> do storeProblem bounds constrs (dir,obj) lp void $ FFI.glp_interior lp nullPtr let examine = liftA2 (,) (realToFrac <$> FFI.glp_ipt_obj_val lp) (readGLPArray (Array.shape obj) $ \arr ix -> Mutable.write arr ix . realToFrac =<< FFI.glp_ipt_col_prim lp (deferredColumnIndex ix)) status <- FFI.glp_ipt_status lp either (return . Left) (\typ -> Right . (,) typ <$> examine) $ analyzeStatus status storeProblem :: (Shape.Indexed sh, Shape.Index sh ~ ix) => Bounds ix -> Constraints ix -> (Direction, Objective sh) -> Foreign.Ptr FFI.Problem -> IO () storeProblem bounds constrs (dir,obj) lp = do let shape = Array.shape obj void $ FFI.glp_term_out FFI.glpkOff FFI.glp_set_obj_dir lp $ case dir of Minimize -> FFI.glpkMin Maximize -> FFI.glpkMax firstRow <- FFI.glp_add_rows lp $ fromIntegral $ length constrs for_ (zip [firstRow..] $ map prepareBounds constrs) $ \(row,(_x,(bnd,lo,up))) -> FFI.glp_set_row_bnds lp row bnd lo up _firstCol <- FFI.glp_add_cols lp $ fromIntegral $ Shape.size shape for_ (Shape.indices $ Shape.Deferred shape) $ \x -> FFI.glp_set_col_bnds lp (deferredColumnIndex x) FFI.glpkGT 0 0 for_ (map prepareBounds bounds) $ \(x,(bnd,lo,up)) -> FFI.glp_set_col_bnds lp (columnIndex shape x) bnd lo up for_ (Array.toAssociations obj) $ \(x,c) -> FFI.glp_set_obj_coef lp (columnIndex shape x) (realToFrac c) let numRows = length $ concatMap (fst . prepareBounds) constrs allocaArray numRows $ \ia -> allocaArray numRows $ \ja -> allocaArray numRows $ \ar -> do for_ (zip [1..] $ concat $ zipWith (map . (,)) [firstRow..] $ map (fst . prepareBounds) constrs) $ \(k, (row, Term c x)) -> do pokeElem ia k row pokeElem ja k (columnIndex shape x) pokeElem ar k (realToFrac c) FFI.glp_load_matrix lp (fromIntegral numRows) ia ja ar {-# INLINE readGLPArray #-} readGLPArray :: (Shape.C sh, Foreign.Storable a, Num a) => sh -> (Mutable.Array IO (Shape.Deferred sh) a -> Shape.DeferredIndex sh -> IO ()) -> IO (Array sh a) readGLPArray shape act = do let defShape = Shape.Deferred shape arr <- Mutable.new defShape 0 traverse_ (act arr) (Shape.indices defShape) Array.reshape shape <$> Mutable.freeze arr analyzeStatus :: FFI.GlpkSolutionStatus -> Either NoSolutionType SolutionType analyzeStatus status = fromMaybe (error "glpk-solver: solution type unknown") $ lookup status $ (FFI.glpkUndefined, Left Undefined) : (FFI.glpkFeasible, Right Feasible) : (FFI.glpkInfeasible, Right Infeasible) : (FFI.glpkNoFeasible, Left NoFeasible) : (FFI.glpkOptimal, Right Optimal) : (FFI.glpkUnbounded, Left Unbounded) : []