simplex-method-0.2.0.0: src/Linear/Simplex/Util.hs
-- |
-- Module : Linear.Simplex.Util
-- Description : Helper functions
-- Copyright : (c) Junaid Rasheed, 2020-2023
-- License : BSD-3
-- Maintainer : jrasheed178@gmail.com
-- Stability : experimental
--
-- Helper functions for performing the two-phase simplex method.
module Linear.Simplex.Util where
import Control.Lens
import Control.Monad.IO.Class (MonadIO (..))
import Control.Monad.Logger (LogLevel (..), LogLine, MonadLogger, logDebug, logError, logInfo, logWarn)
import Data.Bifunctor
import Data.Generics.Labels ()
import Data.Generics.Product (field)
import Data.List
import qualified Data.Map as Map
import qualified Data.Map.Merge.Lazy as MapMerge
import Data.Maybe (fromMaybe)
import qualified Data.Text as T
import Data.Time (getCurrentTime)
import Data.Time.Format.ISO8601 (iso8601Show)
import Linear.Simplex.Types
import Prelude hiding (EQ)
-- | Is the given 'ObjectiveFunction' to be 'Max'imized?
isMax :: ObjectiveFunction -> Bool
isMax (Max _) = True
isMax (Min _) = False
-- | Simplifies a system of 'PolyConstraint's by first calling 'simplifyPolyConstraint',
-- then reducing 'LEQ' and 'GEQ' with same LHS and RHS (and other similar situations) into 'EQ',
-- and finally removing duplicate elements using 'nub'.
simplifySystem :: [PolyConstraint] -> [PolyConstraint]
simplifySystem = nub . reduceSystem
where
reduceSystem :: [PolyConstraint] -> [PolyConstraint]
reduceSystem [] = []
-- Reduce LEQ with matching GEQ and EQ into EQ
reduceSystem ((LEQ lhs rhs) : pcs) =
let matchingConstraints =
filter
( \case
GEQ lhs' rhs' -> lhs == lhs' && rhs == rhs'
EQ lhs' rhs' -> lhs == lhs' && rhs == rhs'
_ -> False
)
pcs
in if null matchingConstraints
then LEQ lhs rhs : reduceSystem pcs
else EQ lhs rhs : reduceSystem (pcs \\ matchingConstraints)
-- Reduce GEQ with matching LEQ and EQ into EQ
reduceSystem ((GEQ lhs rhs) : pcs) =
let matchingConstraints =
filter
( \case
LEQ lhs' rhs' -> lhs == lhs' && rhs == rhs'
EQ lhs' rhs' -> lhs == lhs' && rhs == rhs'
_ -> False
)
pcs
in if null matchingConstraints
then GEQ lhs rhs : reduceSystem pcs
else EQ lhs rhs : reduceSystem (pcs \\ matchingConstraints)
-- Reduce EQ with matching LEQ and GEQ into EQ
reduceSystem ((EQ lhs rhs) : pcs) =
let matchingConstraints =
filter
( \case
LEQ lhs' rhs' -> lhs == lhs' && rhs == rhs'
GEQ lhs' rhs' -> lhs == lhs' && rhs == rhs'
_ -> False
)
pcs
in if null matchingConstraints
then EQ lhs rhs : reduceSystem pcs
else EQ lhs rhs : reduceSystem (pcs \\ matchingConstraints)
-- | Converts a 'Dict' to a 'Tableau' using 'dictEntryToTableauEntry'.
-- FIXME: maybe remove this line. The basic variables will have a coefficient of 1 in the 'Tableau'.
dictionaryFormToTableau :: Dict -> Tableau
dictionaryFormToTableau =
Map.mapWithKey
( \basicVar (DictValue {..}) ->
TableauRow
{ lhs = Map.insert basicVar 1 $ negate <$> varMapSum
, rhs = constant
}
)
-- | Converts a 'Tableau' to a 'Dict'.
-- We do this by isolating the basic variable on the LHS, ending up with all non basic variables and a 'SimplexNum' constant on the RHS.
tableauInDictionaryForm :: Tableau -> Dict
tableauInDictionaryForm =
Map.mapWithKey
( \basicVar (TableauRow {..}) ->
let basicVarCoeff = fromMaybe 1 $ Map.lookup basicVar lhs
in DictValue
{ varMapSum =
Map.map
(\c -> negate c / basicVarCoeff)
$ Map.delete basicVar lhs
, constant = rhs / basicVarCoeff
}
)
-- | If this function is given 'Nothing', return 'Nothing'.
-- Otherwise, we 'lookup' the 'Integer' given in the first item of the pair in the map given in the second item of the pair.
-- This is typically used to extract the value of the 'ObjectiveFunction' after calling 'Linear.Simplex.Solver.TwoPhase.twoPhaseSimplex'.
extractObjectiveValue :: Maybe Result -> Maybe SimplexNum
extractObjectiveValue = fmap $ \result ->
case Map.lookup result.objectiveVar result.varValMap of
Nothing -> error "Objective not found in results when extracting objective value"
Just r -> r
-- | Combines two 'VarLitMapSums together by summing values with matching keys
combineVarLitMapSums :: VarLitMapSum -> VarLitMapSum -> VarLitMapSum
combineVarLitMapSums =
MapMerge.merge
(MapMerge.mapMaybeMissing keepVal)
(MapMerge.mapMaybeMissing keepVal)
(MapMerge.zipWithMaybeMatched sumVals)
where
keepVal = const pure
sumVals k v1 v2 = Just $ v1 + v2
foldDictValue :: [DictValue] -> DictValue
foldDictValue [] = error "Empty list of DictValues given to foldDictValue"
foldDictValue [x] = x
foldDictValue (DictValue {varMapSum = vm1, constant = c1} : DictValue {varMapSum = vm2, constant = c2} : dvs) =
let combinedDictValue =
DictValue
{ varMapSum = foldVarLitMap [vm1, vm2]
, constant = c1 + c2
}
in foldDictValue $ combinedDictValue : dvs
foldVarLitMap :: [VarLitMap] -> VarLitMap
foldVarLitMap [] = error "Empty list of VarLitMaps given to foldVarLitMap"
foldVarLitMap [x] = x
foldVarLitMap (vm1 : vm2 : vms) =
let combinedVars = nub $ Map.keys vm1 <> Map.keys vm2
combinedVarMap =
Map.fromList $
map
( \var ->
let mVm1VarVal = Map.lookup var vm1
mVm2VarVal = Map.lookup var vm2
in ( var
, case (mVm1VarVal, mVm2VarVal) of
(Just vm1VarVal, Just vm2VarVal) -> vm1VarVal + vm2VarVal
(Just vm1VarVal, Nothing) -> vm1VarVal
(Nothing, Just vm2VarVal) -> vm2VarVal
(Nothing, Nothing) -> error "Reached unreachable branch in foldDictValue"
)
)
combinedVars
in foldVarLitMap $ combinedVarMap : vms
insertPivotObjectiveToDict :: PivotObjective -> Dict -> Dict
insertPivotObjectiveToDict objective = Map.insert objective.variable (DictValue {varMapSum = objective.function, constant = objective.constant})
showT :: (Show a) => a -> T.Text
showT = T.pack . show
logMsg :: (MonadIO m, MonadLogger m) => LogLevel -> T.Text -> m ()
logMsg lvl msg = do
currTime <- T.pack . iso8601Show <$> liftIO getCurrentTime
let msgToLog = currTime <> ": " <> msg
case lvl of
LevelDebug -> $logDebug msgToLog
LevelInfo -> $logInfo msgToLog
LevelWarn -> $logWarn msgToLog
LevelError -> $logError msgToLog
LevelOther otherLvl -> error "logMsg: LevelOther is not implemented"
extractTableauValues :: Tableau -> Map.Map Var SimplexNum
extractTableauValues = Map.map (.rhs)
extractDictValues :: Dict -> Map.Map Var SimplexNum
extractDictValues = Map.map (.constant)