Skip to content

Commit b205f1f

Browse files
committed
feat: support a list of objective functions
+ useful if you want to optimise multiple vars with a single set of constraints + can also send 0 objective functions if you just want to run phase 1
1 parent 2dbbbc4 commit b205f1f

File tree

4 files changed

+601
-369
lines changed

4 files changed

+601
-369
lines changed

src/Linear/Simplex/Solver/TwoPhase.hs

Lines changed: 106 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ module Linear.Simplex.Solver.TwoPhase
2828
, applyShiftToConstraint
2929
, applySplitToObjective
3030
, applySplitToConstraint
31-
, unapplyTransforms
32-
, unapplyTransform
31+
, unapplyTransformsToVarMap
32+
, unapplyTransformToVarMap
3333
) where
3434

3535
import Prelude hiding (EQ)
@@ -274,33 +274,39 @@ findFeasibleSolution unsimplifiedSystem = do
274274
-- | Optimize a feasible system by performing the second phase of the two-phase simplex method.
275275
-- We first pass an 'ObjectiveFunction'.
276276
-- Then, the feasible system in 'DictionaryForm' as well as a list of slack variables, a list artificial variables, and the objective variable.
277-
-- Returns a pair with the first item being the 'Integer' variable equal to the 'ObjectiveFunction'
278-
-- and the second item being a map of the values of all 'Integer' variables appearing in the system, including the 'ObjectiveFunction'.
279-
optimizeFeasibleSystem :: (MonadIO m, MonadLogger m) => ObjectiveFunction -> FeasibleSystem -> m (Maybe Result)
277+
-- Returns 'Optimal' with variable values if an optimal solution is found, or 'Unbounded' if the objective is unbounded.
278+
optimizeFeasibleSystem :: (MonadIO m, MonadLogger m) => ObjectiveFunction -> FeasibleSystem -> m OptimisationOutcome
280279
optimizeFeasibleSystem objFunction fsys@(FeasibleSystem {dict = phase1Dict, ..}) = do
281280
logMsg LevelInfo $
282281
"optimizeFeasibleSystem: Optimizing feasible system " <> showT fsys <> " with objective " <> showT objFunction
283-
if null artificialVars
282+
mResult <- if null artificialVars
284283
then do
285284
logMsg LevelInfo $
286285
"optimizeFeasibleSystem: No artificial vars, system is feasible. Pivoting system (in dict form) "
287286
<> showT phase1Dict
288287
<> " with objective "
289288
<> showT normalObjective
290-
fmap (displayResults . dictionaryFormToTableau) <$> simplexPivot normalObjective phase1Dict
289+
simplexPivot normalObjective phase1Dict
291290
else do
292291
logMsg LevelInfo $
293292
"optimizeFeasibleSystem: Artificial vars present. Pivoting system (in dict form) "
294293
<> showT phase1Dict
295294
<> " with objective "
296295
<> showT adjustedObjective
297-
fmap (displayResults . dictionaryFormToTableau) <$> simplexPivot adjustedObjective phase1Dict
296+
simplexPivot adjustedObjective phase1Dict
297+
case mResult of
298+
Nothing -> do
299+
logMsg LevelInfo "optimizeFeasibleSystem: Objective is unbounded (ratio test failed)"
300+
pure Unbounded
301+
Just resultDict -> do
302+
let result = displayResults (dictionaryFormToTableau resultDict)
303+
logMsg LevelInfo $ "optimizeFeasibleSystem: Found optimal solution: " <> showT result
304+
pure result
298305
where
299-
-- \| displayResults takes a 'Tableau' and returns a 'Result'. The 'Tableau'
306+
-- \| displayResults takes a 'Tableau' and returns an 'OptimisationOutcome'. The 'Tableau'
300307
-- represents the final tableau of a linear program after the simplex
301-
-- algorithm has been applied. The 'Result' contains the value of the
302-
-- objective variable and a map of the values of all variables appearing
303-
-- in the system, including the objective variable.
308+
-- algorithm has been applied. The 'OptimisationOutcome' contains the values of all
309+
-- variables appearing in the system.
304310
--
305311
-- The function first filters out the rows of the tableau that correspond
306312
-- to the slack and artificial variables. It then extracts the values of
@@ -310,12 +316,9 @@ optimizeFeasibleSystem objFunction fsys@(FeasibleSystem {dict = phase1Dict, ..})
310316
-- is a minimization problem, the map contains the values of the variables
311317
-- as they appear in the final tableau, except for the objective variable,
312318
-- which is negated.
313-
displayResults :: Tableau -> Result
319+
displayResults :: Tableau -> OptimisationOutcome
314320
displayResults tableau =
315-
Result
316-
{ objectiveVar = objectiveVar
317-
, varValMap = extractVarVals
318-
}
321+
Optimal extractVarVals
319322
where
320323
extractVarVals =
321324
let tableauWithOriginalVars =
@@ -402,42 +405,62 @@ optimizeFeasibleSystem objFunction fsys@(FeasibleSystem {dict = phase1Dict, ..})
402405
-- | Perform the two phase simplex method with variable domain information.
403406
-- Variables not in the VarDomainMap are assumed to be Unbounded (no lower bound).
404407
-- This function applies necessary transformations before solving and unapplies them after.
405-
-- The returned Result contains variable values and objective value in the original space.
406-
-- TODO: we need to be able to support multiple objective functions for the LPPaver.
407-
-- one way to do this is to have a list of objective functions and optimize them one by one.
408-
-- think about cases where the opitmal result is infinity
409-
twoPhaseSimplex :: (MonadIO m, MonadLogger m) => VarDomainMap -> ObjectiveFunction -> [PolyConstraint] -> m (Maybe Result)
410-
twoPhaseSimplex domainMap objFunction constraints = do
408+
-- The returned SimplexResult contains:
409+
-- * The feasible system (Nothing if infeasible)
410+
-- * Results for each objective function (empty if infeasible)
411+
twoPhaseSimplex :: (MonadIO m, MonadLogger m) => VarDomainMap -> [ObjectiveFunction] -> [PolyConstraint] -> m SimplexResult
412+
twoPhaseSimplex domainMap objFunctions constraints = do
411413
logMsg LevelInfo $
412414
"twoPhaseSimplex: Solving system with domain map " <> showT domainMap
413-
let (transformedObj, transformedConstraints, transforms) = preprocess objFunction domainMap constraints
415+
-- Collect original variables before any transformations
416+
let originalVars = collectAllVars objFunctions constraints
417+
let (transformedObjs, transformedConstraints, transforms) = preprocess objFunctions domainMap constraints
414418
logMsg LevelInfo $
415419
"twoPhaseSimplex: Applied transforms " <> showT transforms
416-
<> "; Transformed objective: " <> showT transformedObj
420+
<> "; Transformed objectives: " <> showT transformedObjs
417421
<> "; Transformed constraints: " <> showT transformedConstraints
418422
mFeasibleSystem <- findFeasibleSolution transformedConstraints
419-
let phase1FailureLog = logMsg LevelInfo "twoPhaseSimplex: No feasible solution found in phase 1"
420-
let runPhase2 feasibleSystem = do
421-
logMsg LevelInfo $
422-
"twoPhaseSimplex: Feasible system found for transformed system; Feasible system: "
423-
<> showT feasibleSystem
424-
mOptimizedSystem <- optimizeFeasibleSystem transformedObj feasibleSystem
425-
let mFinalResult = postprocess objFunction transforms <$> mOptimizedSystem
426-
logMsg LevelInfo $ maybe "twoPhaseSimplex: No optimized solution found in phase 2" (("twoPhaseSimplex: Postprocessed result: " <>) . showT) mFinalResult
427-
pure mFinalResult
428-
maybe (phase1FailureLog >> pure Nothing) runPhase2 mFeasibleSystem
429-
430-
-- | Postprocess the result by unapplying variable transformations and computing
431-
-- the objective value in the original space.
432-
postprocess :: ObjectiveFunction -> [VarTransform] -> Result -> Result
433-
postprocess objFunction transforms result =
434-
let -- First unapply transforms to get variable values in original space
435-
unappliedResult = unapplyTransforms transforms result
436-
-- Then compute the objective value using the original objective function
437-
objVal = computeObjective objFunction unappliedResult.varValMap
438-
-- Update the objective value in the result
439-
finalVarValMap = M.insert unappliedResult.objectiveVar objVal unappliedResult.varValMap
440-
in unappliedResult { varValMap = finalVarValMap }
423+
case mFeasibleSystem of
424+
Nothing -> do
425+
logMsg LevelInfo "twoPhaseSimplex: No feasible solution found in phase 1"
426+
pure $ SimplexResult Nothing []
427+
Just feasibleSystem -> do
428+
logMsg LevelInfo $
429+
"twoPhaseSimplex: Feasible system found for transformed system; Feasible system: "
430+
<> showT feasibleSystem
431+
objResults <- optimizeAllObjectives originalVars transforms feasibleSystem (zip objFunctions transformedObjs)
432+
logMsg LevelInfo $ "twoPhaseSimplex: All objective results: " <> showT objResults
433+
pure $ SimplexResult (Just feasibleSystem) objResults
434+
435+
-- | Optimize all objective functions over the given feasible system.
436+
-- Returns a list of ObjectiveResult, one for each objective function.
437+
-- The originalVars set is used to filter the result to only include original decision variables.
438+
optimizeAllObjectives :: (MonadIO m, MonadLogger m)
439+
=> Set.Set Var -- ^ Original decision variables
440+
-> [VarTransform]
441+
-> FeasibleSystem
442+
-> [(ObjectiveFunction, ObjectiveFunction)] -- ^ (original, transformed) objective pairs
443+
-> m [ObjectiveResult]
444+
optimizeAllObjectives originalVars transforms feasibleSystem objPairs =
445+
mapM optimizeOne objPairs
446+
where
447+
optimizeOne (origObj, transformedObj) = do
448+
outcome <- optimizeFeasibleSystem transformedObj feasibleSystem
449+
let postprocessedOutcome = postprocess originalVars transforms outcome
450+
pure $ ObjectiveResult origObj postprocessedOutcome
451+
452+
-- | Postprocess the optimisation outcome by unapplying variable transformations
453+
-- and filtering to only include the original decision variables.
454+
-- For Optimal outcomes, unapplies transforms to get variable values in original space.
455+
-- For Unbounded outcomes, passes through unchanged.
456+
postprocess :: Set.Set Var -> [VarTransform] -> OptimisationOutcome -> OptimisationOutcome
457+
postprocess _originalVars _transforms Unbounded = Unbounded
458+
postprocess originalVars transforms (Optimal varVals) =
459+
let -- Unapply transforms to get variable values in original space
460+
unappliedVarVals = unapplyTransformsToVarMap transforms varVals
461+
-- Filter to only include original decision variables
462+
filteredVarVals = M.filterWithKey (\k _ -> Set.member k originalVars) unappliedVarVals
463+
in Optimal filteredVarVals
441464

442465
-- | Compute the value of an objective function given variable values.
443466
computeObjective :: ObjectiveFunction -> M.Map Var SimplexNum -> SimplexNum
@@ -448,32 +471,42 @@ computeObjective objFunction varVals =
448471
in sum $ map (\(var, coeff) -> coeff * M.findWithDefault 0 var varVals) (M.toList coeffs)
449472

450473
-- | Preprocess the system by applying variable transformations based on domain information.
451-
-- Returns the transformed objective, constraints, and the list of transforms applied.
452-
preprocess :: ObjectiveFunction
474+
-- Returns the transformed objectives, constraints, and the list of transforms applied.
475+
preprocess :: [ObjectiveFunction]
453476
-> VarDomainMap
454477
-> [PolyConstraint]
455-
-> (ObjectiveFunction, [PolyConstraint], [VarTransform])
456-
preprocess objFunction (VarDomainMap domainMap) constraints =
457-
let -- Collect all variables in the system
458-
allVars = collectAllVars objFunction constraints
478+
-> ([ObjectiveFunction], [PolyConstraint], [VarTransform])
479+
preprocess objFunctions (VarDomainMap domainMap) constraints =
480+
let -- Collect all variables in the system (from all objectives and constraints)
481+
allVars = collectAllVars objFunctions constraints
459482
-- Find the maximum variable to generate fresh variables
460483
maxVar = if Set.null allVars then 0 else Set.findMax allVars
461484
-- Generate transforms for each variable based on its domain
462485
-- Variables not in domainMap are treated as Unbounded
463486
(transforms, _) = foldr (generateTransform domainMap) ([], maxVar) (Set.toList allVars)
464487
-- Apply transforms to get the transformed system
465-
(transformedObj, transformedConstraints) = applyTransforms transforms objFunction constraints
466-
in (transformedObj, transformedConstraints, transforms)
467-
468-
-- | Collect all variables appearing in the objective function and constraints
469-
collectAllVars :: ObjectiveFunction -> [PolyConstraint] -> Set Var
470-
collectAllVars objFunction constraints =
471-
let objVars = case objFunction of
472-
Max m -> M.keysSet m
473-
Min m -> M.keysSet m
488+
transformedObjs = map (\obj -> fst $ applyTransforms transforms obj constraints) objFunctions
489+
(_, transformedConstraints) = case objFunctions of
490+
[] -> (Max M.empty, applyTransformsToConstraints transforms constraints)
491+
(obj:_) -> applyTransforms transforms obj constraints
492+
in (transformedObjs, transformedConstraints, transforms)
493+
494+
-- | Apply transforms to constraints only (used when there are no objectives)
495+
applyTransformsToConstraints :: [VarTransform] -> [PolyConstraint] -> [PolyConstraint]
496+
applyTransformsToConstraints transforms constraints =
497+
snd $ applyTransforms transforms (Max M.empty) constraints
498+
499+
-- | Collect all variables appearing in the objective functions and constraints
500+
collectAllVars :: [ObjectiveFunction] -> [PolyConstraint] -> Set Var
501+
collectAllVars objFunctions constraints =
502+
let objVars = Set.unions $ map getObjVars objFunctions
474503
constraintVars = Set.unions $ map getConstraintVars constraints
475504
in Set.union objVars constraintVars
476505
where
506+
getObjVars :: ObjectiveFunction -> Set Var
507+
getObjVars (Max m) = M.keysSet m
508+
getObjVars (Min m) = M.keysSet m
509+
477510
getConstraintVars :: PolyConstraint -> Set Var
478511
getConstraintVars (LEQ m _) = M.keysSet m
479512
getConstraintVars (GEQ m _) = M.keysSet m
@@ -624,38 +657,36 @@ applySplitToConstraint origVar posVar negVar constraint =
624657
Nothing -> m
625658
Just coeff -> M.insert pVar coeff (M.insert nVar (-coeff) (M.delete oldVar m))
626659

627-
-- | Unapply transforms to convert the result back to original variables.
628-
unapplyTransforms :: [VarTransform] -> Result -> Result
629-
unapplyTransforms transforms result =
660+
-- | Unapply transforms to convert a variable value map back to original variables.
661+
unapplyTransformsToVarMap :: [VarTransform] -> VarLitMap -> VarLitMap
662+
unapplyTransformsToVarMap transforms valMap =
630663
-- Apply transforms in reverse order (since we applied them with foldr)
631-
foldl (flip unapplyTransform) result transforms
664+
foldl (flip unapplyTransformToVarMap) valMap transforms
632665

633-
-- | Unapply a single transform to convert result back to original variable.
634-
unapplyTransform :: VarTransform -> Result -> Result
635-
unapplyTransform transform result@(Result {varValMap = valMap, ..}) =
666+
-- | Unapply a single transform to convert a variable value map back to original variables.
667+
unapplyTransformToVarMap :: VarTransform -> VarLitMap -> VarLitMap
668+
unapplyTransformToVarMap transform valMap =
636669
case transform of
637670
-- AddLowerBound: No variable substitution was done, nothing to unapply
638-
AddLowerBound {} -> result
671+
AddLowerBound {} -> valMap
639672

640673
-- AddUpperBound: No variable substitution was done, nothing to unapply
641-
AddUpperBound {} -> result
674+
AddUpperBound {} -> valMap
642675

643676
-- Shift: originalVar = shiftedVar + shiftBy
644677
-- So originalVar's value = shiftedVar's value + shiftBy
645678
Shift origVar shiftedVar shiftBy ->
646679
let shiftedVal = M.findWithDefault 0 shiftedVar valMap
647680
origVal = shiftedVal + shiftBy
648-
newMap = M.insert origVar origVal (M.delete shiftedVar valMap)
649-
in result { varValMap = newMap }
681+
in M.insert origVar origVal (M.delete shiftedVar valMap)
650682

651683
-- Split: originalVar = posVar - negVar
652684
-- So originalVar's value = posVar's value - negVar's value
653685
Split origVar posVar negVar ->
654686
let posVal = M.findWithDefault 0 posVar valMap
655687
negVal = M.findWithDefault 0 negVar valMap
656688
origVal = posVal - negVal
657-
newMap = M.insert origVar origVal (M.delete posVar (M.delete negVar valMap))
658-
in result { varValMap = newMap }
689+
in M.insert origVar origVal (M.delete posVar (M.delete negVar valMap))
659690

660691
-- | Perform the simplex pivot algorithm on a system with basic vars, assume that the first row is the 'ObjectiveFunction'.
661692
simplexPivot :: (MonadIO m, MonadLogger m) => PivotObjective -> Dict -> m (Maybe Dict)

src/Linear/Simplex/Types.hs

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -39,21 +39,26 @@ data FeasibleSystem = FeasibleSystem
3939
}
4040
deriving (Show, Read, Eq, Generic)
4141

42-
data Result = Result
43-
{ objectiveVar :: Var
44-
, varValMap :: VarLitMap
45-
-- TODO:
46-
-- Maybe VarLitMap
47-
-- , feasible :: Bool
48-
-- , optimisable :: Bool
42+
-- | The outcome of optimizing a single objective function.
43+
data OptimisationOutcome
44+
= Optimal { varValMap :: VarLitMap } -- ^ An optimal solution was found
45+
| Unbounded -- ^ The objective is unbounded
46+
deriving (Show, Read, Eq, Generic)
47+
48+
-- | Result for a single objective function optimization.
49+
data ObjectiveResult = ObjectiveResult
50+
{ objectiveFunction :: ObjectiveFunction -- ^ The objective that was optimized
51+
, outcome :: OptimisationOutcome -- ^ The optimization outcome
4952
}
5053
deriving (Show, Read, Eq, Generic)
5154

52-
data SimplexMeta = SimplexMeta
53-
{ objective :: ObjectiveFunction
54-
, feasibleSystem :: Maybe FeasibleSystem
55-
, optimisedResult :: Maybe Result
55+
-- | Complete result of the two-phase simplex method.
56+
-- Contains feasibility information and results for all requested objectives.
57+
data SimplexResult = SimplexResult
58+
{ feasibleSystem :: Maybe FeasibleSystem -- ^ The feasible system (Nothing if infeasible)
59+
, objectiveResults :: [ObjectiveResult] -- ^ Results for each objective (empty if infeasible)
5660
}
61+
deriving (Show, Read, Eq, Generic)
5762

5863
type VarLitMap = M.Map Var SimplexNum
5964

src/Linear/Simplex/Util.hs

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -106,14 +106,18 @@ tableauInDictionaryForm =
106106
}
107107
)
108108

109-
-- | If this function is given 'Nothing', return 'Nothing'.
110-
-- Otherwise, we 'lookup' the 'Integer' given in the first item of the pair in the map given in the second item of the pair.
111-
-- This is typically used to extract the value of the 'ObjectiveFunction' after calling 'Linear.Simplex.Solver.TwoPhase.twoPhaseSimplex'.
112-
extractObjectiveValue :: Maybe Result -> Maybe SimplexNum
113-
extractObjectiveValue = fmap $ \result ->
114-
case Map.lookup result.objectiveVar result.varValMap of
115-
Nothing -> error "Objective not found in results when extracting objective value"
116-
Just r -> r
109+
-- | Extract the objective value from an ObjectiveResult.
110+
-- Returns Nothing if the result is Unbounded.
111+
-- Returns Just the objective value if Optimal.
112+
extractObjectiveValue :: ObjectiveFunction -> ObjectiveResult -> Maybe SimplexNum
113+
extractObjectiveValue objFunction (ObjectiveResult _ outcome) =
114+
case outcome of
115+
Unbounded -> Nothing
116+
Optimal varVals ->
117+
let coeffs = case objFunction of
118+
Max m -> m
119+
Min m -> m
120+
in Just $ sum $ map (\(var, coeff) -> coeff * Map.findWithDefault 0 var varVals) (Map.toList coeffs)
117121

118122
-- | Combines two 'VarLitMapSums together by summing values with matching keys
119123
combineVarLitMapSums :: VarLitMapSum -> VarLitMapSum -> VarLitMapSum

0 commit comments

Comments
 (0)