From f5932899043ac722ef1aedc6053e6811864ec8c5 Mon Sep 17 00:00:00 2001 From: Junaid Rasheed Date: Thu, 9 Jul 2026 14:46:18 +0100 Subject: [PATCH] docs: update README and changelog metadata - Refresh README examples and signatures for VarDomainMap, multiple objectives, and SimplexResult - Clarify transformed feasible systems and lower-level solver assumptions - Update stale changelog project name and release links, including the restored v0.2.0.0 tag --- ChangeLog.md | 6 +- README.md | 217 +++++++++++++++++++++++++++++++++------------------ 2 files changed, 143 insertions(+), 80 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index 8e07df1..d47e0a4 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,4 +1,4 @@ -# Changelog for simplex-haskell +# Changelog for simplex-method ## Unreleased changes @@ -31,7 +31,7 @@ - Explicit import lists on all modules - Bump Stackage LTS to 22.44 -## [v0.2.0.0](https://github.com/rasheedja/LPPaver/tree/v0.2.0.0) +## [v0.2.0.0](https://github.com/rasheedja/simplex-method/tree/v0.2.0.0) - Setup CI - Use fourmolu formatter @@ -44,6 +44,6 @@ - Bump Stackage LTS - Rename Linear.Simplex.Simplex -> Linear.Simplex.TwoPhase.Simplex -## [v0.1.0.0](https://github.com/rasheedja/LPPaver/tree/v0.1.0.0) +## [v0.1.0.0](https://github.com/rasheedja/simplex-method/tree/v0.1.0.0) - Initial release diff --git a/README.md b/README.md index faef359..3c4f97e 100644 --- a/README.md +++ b/README.md @@ -1,139 +1,202 @@ # simplex-method -`simplex-method` is a Haskell library that implements the two-phase [simplex method](https://en.wikipedia.org/wiki/Simplex_algorithm) in exact rational arithmetic. +`simplex-method` is a Haskell library that implements the two-phase +[simplex method](https://en.wikipedia.org/wiki/Simplex_algorithm) in exact +rational arithmetic. ## Quick Overview -The `Linear.Simplex.Solver.TwoPhase` module contain both phases of the two-phase simplex method. +The `Linear.Simplex.Solver.TwoPhase` module contains phase-one feasibility +search, phase-two optimization, and a convenience `twoPhaseSimplex` entrypoint +that runs both phases. -### Phase One - -Phase one is implemented by `findFeasibleSolution`: +Variables are identified by positive `Int` values, and all numeric values use +`Rational`: ```haskell -findFeasibleSolution :: (MonadIO m, MonadLogger m) => [PolyConstraint] -> m (Maybe FeasibleSystem) +type Var = Int +type SimplexNum = Rational +type VarLitMapSum = Map Var SimplexNum ``` -`findFeasibleSolution` takes a list of `PolyConstraint`s. -The `PolyConstraint` type, as well as other custom types required by this library, are defined in the `Linear.Simplex.Types` module. -`PolyConstraint` is defined as: +`VarLitMapSum` represents a linear expression. For example, +`Map.fromList [(1, 2), (2, -3)]` represents `2x1 - 3x2`. + +## Constraints And Objectives + +Constraints use `LEQ`, `GEQ`, and `EQ`: ```haskell data PolyConstraint = LEQ {lhs :: VarLitMapSum, rhs :: SimplexNum} | GEQ {lhs :: VarLitMapSum, rhs :: SimplexNum} | EQ {lhs :: VarLitMapSum, rhs :: SimplexNum} - deriving (Show, Read, Eq, Generic) ``` -`SimplexNum` is an alias for `Rational`, and `VarLitMapSum` is an alias for `VarLitMap`, which is an alias for `Map Var SimplexNum`. -`Var` is an alias of `Int`. - -A `VarLitMapSum` is read as `Integer` variables mapped to their `Rational` coefficients, with an implicit `+` between each entry. -For example: `Map.fromList [(1, 2), (2, (-3)), (1, 3)]` is equivalent to `(2x1 + (-3x2) + 3x1)`. - -And a `PolyConstraint` is an inequality/equality where the LHS is a `VarLitMapSum` and the RHS is a `Rational`. -For example: `LEQ (Map.fromList [(1, 2), (2, (-3)), (1, 3)] 60)` is equivalent to `(2x1 + (-3x2) + 3x1) <= 60`. - -Passing a `[PolyConstraint]` to `findFeasibleSolution` will return a `FeasibleSystem` if a feasible solution exists: +Objectives use `Max` or `Min`: ```haskell -data FeasibleSystem = FeasibleSystem - { dict :: Dict - , slackVars :: [Var] - , artificialVars :: [Var] - , objectiveVar :: Var - } - deriving (Show, Read, Eq, Generic) +data ObjectiveFunction + = Max {objective :: VarLitMapSum} + | Min {objective :: VarLitMapSum} ``` +## Variable Domains + +`twoPhaseSimplex` accepts a `VarDomainMap` so variables can be non-negative, +unbounded, lower-bounded, upper-bounded, or bounded on both sides: + ```haskell -type Dict = M.Map Var DictValue +newtype VarDomainMap = VarDomainMap {unVarDomainMap :: Map Var VarDomain} -data DictValue = DictValue - { varMapSum :: VarLitMapSum - , constant :: SimplexNum - } - deriving (Show, Read, Eq, Generic) +nonNegative :: VarDomain +unbounded :: VarDomain +lowerBoundOnly :: Rational -> VarDomain +upperBoundOnly :: Rational -> VarDomain +boundedRange :: Rational -> Rational -> VarDomain ``` -`Dict` can be thought of as a set of equations, where the key represents a basic variable on the LHS of the equation -that is equal to the RHS represented as a `DictValue` value. +Variables missing from the `VarDomainMap` are treated as `unbounded`. To keep +the traditional simplex assumption that every variable is non-negative, provide +`nonNegative` for every variable in the problem. -### Phase Two +## Solving -`optimizeFeasibleSystem` performs phase two of the simplex method, and has the type: +The main entrypoint is: ```haskell +twoPhaseSimplex :: + (MonadIO m, MonadLogger m) => + VarDomainMap -> + [ObjectiveFunction] -> + [PolyConstraint] -> + m SimplexResult +``` + +`twoPhaseSimplex` can optimize multiple objectives over the same constraint set. +Pass an empty objective list to run phase one only. -optimizeFeasibleSystem :: (MonadIO m, MonadLogger m) => ObjectiveFunction -> FeasibleSystem -> m (Maybe Result) +Results are returned as: -data ObjectiveFunction = Max {objective :: VarLitMapSum} | Min {objective :: VarLitMapSum} +```haskell +data SimplexResult = SimplexResult + { feasibleSystem :: Maybe FeasibleSystem + , objectiveResults :: [ObjectiveResult] + } -data Result = Result - { objectiveVar :: Var - , varValMap :: VarLitMap +data ObjectiveResult = ObjectiveResult + { objectiveFunction :: ObjectiveFunction + , outcome :: OptimisationOutcome } - deriving (Show, Read, Eq, Generic) + +data OptimisationOutcome + = Optimal {varValMap :: VarLitMap} + | Unbounded ``` -We give `optimizeFeasibleSystem` an `ObjectiveFunction` along with a `FeasibleSystem`. +For an `Optimal` result, `varValMap` contains values for original decision +variables. Variables with value zero may be absent from the map. -### Two-Phase Simplex +`twoPhaseSimplex` applies domain transformations before solving. Its +`feasibleSystem` field is therefore the feasible system for the transformed +non-negative problem, while each `Optimal.varValMap` is postprocessed back into +the original variable space. -`twoPhaseSimplex` performs both phases of the simplex method. -It has the type: +`computeObjective` can be used to evaluate an objective against an optimal +variable map: ```haskell -twoPhaseSimplex :: (MonadIO m, MonadLogger m) => ObjectiveFunction -> [PolyConstraint] -> m (Maybe Result) +computeObjective :: ObjectiveFunction -> Map Var Rational -> Rational ``` -### Extracting Results +## Phase One And Phase Two -The result of the objective function is present in the returned `Result` type of both `twoPhaseSimplex` and `optimizeFeasibleSystem`, but this can be difficult to grok in systems with many variables, so the following function will extract the value of the objective function for you. +The lower-level phase functions do not apply `VarDomainMap` transformations. +Use them when the system is already in the non-negative variable space expected +by simplex, or call `twoPhaseSimplex` when solving a problem with variable +domain metadata. + +For lower-level usage, phase one is exposed as: ```haskell -dictionaryFormToTableau :: Dict -> Tableau +findFeasibleSolution :: + (MonadIO m, MonadLogger m) => + [PolyConstraint] -> + m (Maybe FeasibleSystem) ``` -There are similar functions for `DictionaryForm` as well as other custom types in the module `Linear.Simplex.Util`. +Phase two can optimize a feasible system: + +```haskell +optimizeFeasibleSystem :: + (MonadIO m, MonadLogger m) => + ObjectiveFunction -> + FeasibleSystem -> + m OptimisationOutcome +``` ## Example ```haskell -exampleFunction :: (ObjectiveFunction, [PolyConstraint]) -exampleFunction = - ( - Max {objective = Map.fromList [(1, 3), (2, 5)]}, -- 3x1 + 5x2 - [ - LEQ {lhs = Map.fromList [(1, 3), (2, 1)], rhs = 15}, -- 3x1 + x2 <= 15 - LEQ {lhs = Map.fromList [(1, 1), (2, 1)], rhs = 7}, -- x1 + x2 <= 7 - LEQ {lhs = Map.fromList [(2, 1)], rhs = 4}, -- x2 <= 4 - LEQ {lhs = Map.fromList [(1, -1), (2, 2)], rhs = 6} -- -x1 + 2x2 <= 6 - ] +import Control.Monad.Logger (LogLevel (LevelInfo), filterLogger, runStdoutLoggingT) +import qualified Data.Map as Map +import Linear.Simplex.Solver.TwoPhase (collectAllVars, computeObjective, twoPhaseSimplex) +import Linear.Simplex.Types + ( ObjectiveFunction (Max) + , ObjectiveResult (ObjectiveResult) + , OptimisationOutcome (Optimal) + , PolyConstraint (LEQ) + , SimplexResult (SimplexResult) + , VarDomainMap (VarDomainMap) + , nonNegative ) -twoPhaseSimplex (fst exampleFunction) (snd exampleFunction) +example :: IO () +example = do + let objective = Max (Map.fromList [(1, 3), (2, 5)]) + constraints = + [ LEQ (Map.fromList [(1, 3), (2, 1)]) 15 + , LEQ (Map.fromList [(1, 1), (2, 1)]) 7 + , LEQ (Map.fromList [(2, 1)]) 4 + , LEQ (Map.fromList [(1, -1), (2, 2)]) 6 + ] + allVars = collectAllVars [objective] constraints + domainMap = VarDomainMap $ Map.fromSet (const nonNegative) allVars + + SimplexResult feasibleSystem objectiveResults <- + runStdoutLoggingT $ + filterLogger (\_ logLevel -> logLevel > LevelInfo) $ + twoPhaseSimplex domainMap [objective] constraints + + case (feasibleSystem, objectiveResults) of + (Just _, [ObjectiveResult _ (Optimal varMap)]) -> do + print varMap + print $ computeObjective objective varMap + (Just _, [_]) -> + putStrLn "Objective is unbounded" + _ -> + putStrLn "System is infeasible" ``` -The result of the call above is: +Ignoring `Rational` formatting details, this prints an optimal assignment +equivalent to: ```haskell -Just - (Result - { objectiveVar = 7 -- Integer representing objective function - , varValMap = Map.fromList - [ (7, 29) -- Value for variable 7, so max(3x1 + 5x2) = 29. - , (1, 3) -- Value for variable 1, so x1 = 3 - , (2, 4) -- Value for variable 2, so x2 = 4 - ] - } - ) +Map.fromList [(1, 3), (2, 4)] +29 ``` -There are many more examples in test/TestFunctions.hs. -You may use `prettyShowVarConstMap`, `prettyShowPolyConstraint`, and `prettyShowObjectiveFunction` to convert these tests into a more human-readable format. +## Pretty Printing + +`Linear.Simplex.Prettify` provides helpers for human-readable output: + +```haskell +prettyShowVarLitMapSum :: VarLitMapSum -> String +prettyShowPolyConstraint :: PolyConstraint -> String +prettyShowObjectiveFunction :: ObjectiveFunction -> String +``` ## Issues -Please share any bugs you find [here](https://github.com/rasheedja/simplex-haskell/issues). +Please share any bugs you find at +[github.com/rasheedja/simplex-method/issues](https://github.com/rasheedja/simplex-method/issues).