@@ -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
3535import 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
280279optimizeFeasibleSystem 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.
443466computeObjective :: 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'.
661692simplexPivot :: (MonadIO m , MonadLogger m ) => PivotObjective -> Dict -> m (Maybe Dict )
0 commit comments