{-# 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 constrs0 sh (NonEmpty.Cons obj0 objs0) =
   let go constrs curObj ((dir,obj):objs) (Right (Optimal, (opt,_))) =
         mapFst (opt:) $
         let extConstrs = (curObj==.opt) : constrs in
         go extConstrs obj objs $
         solver extConstrs
            (dir, objectiveFromTerms sh obj)
       go _ _ _ sol = ([], sol)
   in go constrs0 (snd obj0) objs0 $
      solver constrs0 $ 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) :
      []