module ToySolver.Simplex
(
Tableau
, RowIndex
, ColIndex
, Row
, emptyTableau
, objRowIndex
, pivot
, lookupRow
, addRow
, setObjFun
, module Data.OptDir
, currentValue
, currentObjValue
, isFeasible
, isOptimal
, simplex
, dualSimplex
, phaseI
, primalDualSimplex
, isValidTableau
, toCSV
) where
import Data.Ord
import Data.List
import qualified Data.IntMap as IM
import qualified Data.IntSet as IS
import Data.OptDir
import Data.VectorSpace
import Control.Exception
import qualified ToySolver.Data.LA as LA
import ToySolver.Data.Var
type Tableau r = VarMap (Row r)
type RowIndex = Int
type ColIndex = Int
type Row r = (VarMap r, r)
data PivotResult r = PivotUnbounded | PivotFinished | PivotSuccess (Tableau r)
deriving (Show, Eq, Ord)
emptyTableau :: Tableau r
emptyTableau = IM.empty
objRowIndex :: RowIndex
objRowIndex = 1
pivot :: (Fractional r, Eq r) => RowIndex -> ColIndex -> Tableau r -> Tableau r
pivot r s tbl =
assert (isValidTableau tbl) $
assert (isValidTableau tbl') $
tbl'
where
tbl' = IM.insert s row_s $ IM.map f $ IM.delete r $ tbl
f orig@(row_i, row_i_val) =
case IM.lookup s row_i of
Nothing -> orig
Just c ->
( IM.filter (0/=) $ IM.unionWith (+) (IM.delete s row_i) (IM.map (negate c *) row_r')
, row_i_val c*row_r_val'
)
(row_r, row_r_val) = lookupRow r tbl
a_rs = row_r IM.! s
row_r' = IM.map (/ a_rs) $ IM.insert r 1 $ IM.delete s row_r
row_r_val' = row_r_val / a_rs
row_s = (row_r', row_r_val')
lookupRow :: RowIndex -> Tableau r -> Row r
lookupRow r m = m IM.! r
normalizeRow :: (Num r, Eq r) => Tableau r -> Row r -> Row r
normalizeRow a (row0,val0) = obj'
where
obj' = g $ foldl' f (IM.empty, val0) $
[ case IM.lookup j a of
Nothing -> (IM.singleton j x, 0)
Just (row,val) -> (IM.map ((x)*) (IM.delete j row), x*val)
| (j,x) <- IM.toList row0 ]
f (m1,v1) (m2,v2) = (IM.unionWith (+) m1 m2, v1+v2)
g (m,v) = (IM.filter (0/=) m, v)
setRow :: (Num r, Eq r) => Tableau r -> RowIndex -> Row r -> Tableau r
setRow tbl i row = assert (isValidTableau tbl) $ assert (isValidTableau tbl') $ tbl'
where
tbl' = IM.insert i (normalizeRow tbl row) tbl
addRow :: (Num r, Eq r) => Tableau r -> RowIndex -> Row r -> Tableau r
addRow tbl i row = assert (i `IM.notMember` tbl) $ setRow tbl i row
setObjFun :: (Num r, Eq r) => Tableau r -> LA.Expr r -> Tableau r
setObjFun tbl e = addRow tbl objRowIndex row
where
row =
case LA.extract LA.unitVar e of
(c, e') -> (LA.coeffMap (negateV e'), c)
copyObjRow :: (Num r, Eq r) => Tableau r -> Tableau r -> Tableau r
copyObjRow from to =
case IM.lookup objRowIndex from of
Nothing -> IM.delete objRowIndex to
Just row -> addRow to objRowIndex row
currentValue :: Num r => Tableau r -> Var -> r
currentValue tbl v =
case IM.lookup v tbl of
Nothing -> 0
Just (_, val) -> val
currentObjValue :: Num r => Tableau r -> r
currentObjValue = snd . lookupRow objRowIndex
isValidTableau :: Tableau r -> Bool
isValidTableau tbl =
and [v `IM.notMember` m | (v, (m,_)) <- IM.toList tbl, v /= objRowIndex] &&
and [IS.null (IM.keysSet m `IS.intersection` vs) | (m,_) <- IM.elems tbl']
where
tbl' = IM.delete objRowIndex tbl
vs = IM.keysSet tbl'
isFeasible :: Real r => Tableau r -> Bool
isFeasible tbl =
and [val >= 0 | (v, (_,val)) <- IM.toList tbl, v /= objRowIndex]
isOptimal :: Real r => OptDir -> Tableau r -> Bool
isOptimal optdir tbl =
and [not (cmp cj) | cj <- IM.elems (fst (lookupRow objRowIndex tbl))]
where
cmp = case optdir of
OptMin -> (0<)
OptMax -> (0>)
isImproving :: Real r => OptDir -> Tableau r -> Tableau r -> Bool
isImproving OptMin from to = currentObjValue to <= currentObjValue from
isImproving OptMax from to = currentObjValue to >= currentObjValue from
simplex :: (Real r, Fractional r) => OptDir -> Tableau r -> (Bool, Tableau r)
simplex optdir = go
where
go tbl = assert (isFeasible tbl) $
case primalPivot optdir tbl of
PivotFinished -> assert (isOptimal optdir tbl) (True, tbl)
PivotUnbounded -> (False, tbl)
PivotSuccess tbl' -> assert (isImproving optdir tbl tbl') $ go tbl'
primalPivot :: (Real r, Fractional r) => OptDir -> Tableau r -> PivotResult r
primalPivot optdir tbl
| null cs = PivotFinished
| null rs = PivotUnbounded
| otherwise = PivotSuccess (pivot r s tbl)
where
cmp = case optdir of
OptMin -> (0<)
OptMax -> (0>)
cs = [(j,cj) | (j,cj) <- IM.toList (fst (lookupRow objRowIndex tbl)), cmp cj]
s = fst $ head cs
rs = [ (i, y_i0 / y_is)
| (i, (row_i, y_i0)) <- IM.toList tbl, i /= objRowIndex
, let y_is = IM.findWithDefault 0 s row_i, y_is > 0
]
r = fst $ minimumBy (comparing snd) rs
dualSimplex :: (Real r, Fractional r) => OptDir -> Tableau r -> (Bool, Tableau r)
dualSimplex optdir = go
where
go tbl = assert (isOptimal optdir tbl) $
case dualPivot optdir tbl of
PivotFinished -> assert (isFeasible tbl) $ (True, tbl)
PivotUnbounded -> (False, tbl)
PivotSuccess tbl' -> assert (isImproving optdir tbl' tbl) $ go tbl'
dualPivot :: (Real r, Fractional r) => OptDir -> Tableau r -> PivotResult r
dualPivot optdir tbl
| null rs = PivotFinished
| null cs = PivotUnbounded
| otherwise = PivotSuccess (pivot r s tbl)
where
rs = [(i, row_i) | (i, (row_i, y_i0)) <- IM.toList tbl, i /= objRowIndex, 0 > y_i0]
(r, row_r) = head rs
cs = [ (j, if optdir==OptMin then y_0j / y_rj else y_0j / y_rj)
| (j, y_rj) <- IM.toList row_r
, y_rj < 0
, let y_0j = IM.findWithDefault 0 j obj
]
s = fst $ minimumBy (comparing snd) cs
(obj,_) = lookupRow objRowIndex tbl
phaseI :: (Real r, Fractional r) => Tableau r -> VarSet -> (Bool, Tableau r)
phaseI tbl avs
| currentObjValue tbl1' /= 0 = (False, tbl1')
| otherwise = (True, copyObjRow tbl $ removeArtificialVariables avs $ tbl1')
where
optdir = OptMax
tbl1 = setObjFun tbl $ negateV $ sumV [LA.var v | v <- IS.toList avs]
tbl1' = go tbl1
go tbl2
| currentObjValue tbl2 == 0 = tbl2
| otherwise =
case primalPivot optdir tbl2 of
PivotSuccess tbl2' -> assert (isImproving optdir tbl2 tbl2') $ go tbl2'
PivotFinished -> assert (isOptimal optdir tbl2) tbl2
PivotUnbounded -> error "phaseI: should not happen"
removeArtificialVariables :: (Real r, Fractional r) => VarSet -> Tableau r -> Tableau r
removeArtificialVariables avs tbl0 = tbl2
where
tbl1 = foldl' f (IM.delete objRowIndex tbl0) (IS.toList avs)
tbl2 = IM.map (\(row,val) -> (IM.filterWithKey (\j _ -> j `IS.notMember` avs) row, val)) tbl1
f tbl i =
case IM.lookup i tbl of
Nothing -> tbl
Just row ->
case [j | (j,c) <- IM.toList (fst row), c /= 0, j `IS.notMember` avs] of
[] -> IM.delete i tbl
j:_ -> pivot i j tbl
data PDResult = PDUnsat | PDOptimal | PDUnbounded
primalDualSimplex :: (Real r, Fractional r) => OptDir -> Tableau r -> (Bool, Tableau r)
primalDualSimplex optdir = go
where
go tbl =
case pdPivot optdir tbl of
Left PDOptimal -> assert (isFeasible tbl) $ assert (isOptimal optdir tbl) $ (True, tbl)
Left PDUnsat -> assert (not (isFeasible tbl)) $ (False, tbl)
Left PDUnbounded -> assert (not (isOptimal optdir tbl)) $ (False, tbl)
Right tbl' -> go tbl'
pdPivot :: (Real r, Fractional r) => OptDir -> Tableau r -> Either PDResult (Tableau r)
pdPivot optdir tbl
| null ps && null qs = Left PDOptimal
| otherwise =
case ret of
Left p ->
let rs = [ (i, (bi t) / y_ip)
| (i, (row_i, bi)) <- IM.toList tbl, i /= objRowIndex
, let y_ip = IM.findWithDefault 0 p row_i, y_ip > 0
]
q = fst $ minimumBy (comparing snd) rs
in if null rs
then Left PDUnsat
else Right (pivot q p tbl)
Right q ->
let (row_q, _bq) = (tbl IM.! q)
cs = [ (j, (cj't) / (y_qj))
| (j, y_qj) <- IM.toList row_q
, y_qj < 0
, let cj = IM.findWithDefault 0 j obj
, let cj' = if optdir==OptMax then cj else cj
]
p = fst $ maximumBy (comparing snd) cs
(obj,_) = lookupRow objRowIndex tbl
in if null cs
then Left PDUnbounded
else Right (pivot q p tbl)
where
qs = [ (Right i, bi) | (i, (row_i, bi)) <- IM.toList tbl, i /= objRowIndex, 0 > bi ]
ps = [ (Left j, cj')
| (j,cj) <- IM.toList (fst (lookupRow objRowIndex tbl))
, let cj' = if optdir==OptMax then cj else cj
, 0 > cj' ]
(ret, t) = minimumBy (comparing snd) (qs ++ ps)
toCSV :: (Num r, Eq r, Show r) => (r -> String) -> Tableau r -> String
toCSV showCell tbl = unlines . map (concat . intersperse ",") $ header : body
where
header :: [String]
header = "" : map colName cols ++ [""]
body :: [[String]]
body = [showRow i (lookupRow i tbl) | i <- rows]
rows :: [RowIndex]
rows = IM.keys (IM.delete objRowIndex tbl) ++ [objRowIndex]
cols :: [ColIndex]
cols = [0..colMax]
where
colMax = maximum (1 : [c | (row, _) <- IM.elems tbl, c <- IM.keys row])
rowName :: RowIndex -> String
rowName i = if i==objRowIndex then "obj" else "x" ++ show i
colName :: ColIndex -> String
colName j = "x" ++ show j
showRow i (row, row_val) = rowName i : [showCell (IM.findWithDefault 0 j row') | j <- cols] ++ [showCell row_val]
where row' = IM.insert i 1 row