Skip to content

Commit a068ab2

Browse files
committed
feat (wip): VarDomain supports optional upper and lower bounds
1 parent a5e7e2e commit a068ab2

4 files changed

Lines changed: 318 additions & 113 deletions

File tree

ChangeLog.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,23 @@
22

33
## Unreleased changes
44

5+
- **BREAKING CHANGE**: Restructured `VarDomain` type to support upper bounds
6+
- Replaced `NonNegative`, `LowerBound SimplexNum`, and `Unbounded` constructors with
7+
a single `Bounded { lowerBound :: Maybe SimplexNum, upperBound :: Maybe SimplexNum }` record
8+
- Added smart constructors for convenience: `unbounded`, `nonNegative`, `lowerBoundOnly`,
9+
`upperBoundOnly`, and `boundedRange`
10+
- `Bounded Nothing Nothing` is equivalent to `Unbounded`
11+
- `Bounded (Just 0) Nothing` is equivalent to `NonNegative`
12+
- Upper bounds are now supported and automatically added as LEQ constraints
13+
- Added `AddUpperBound` constructor to `VarTransform` for upper bound constraint generation
14+
- Updated `getTransform` to return a list of transforms (can now generate both lower and upper bound transforms)
515
- Use Hspec for tests
616
- Add nix flake
17+
- twoPhaseSimplex now takes a VarDomainMap (as the first param)
18+
- You can specify each Var's domain using smart constructors: `nonNegative`, `unbounded`,
19+
`lowerBoundOnly`, `upperBoundOnly`, or `boundedRange`
20+
- If a VarDomain for a Var is undefined, it's assumed to be `unbounded`
21+
- If you want to keep the same behaviour as before (all vars non-negative), use `nonNegative` for all Vars
722

823
## [v0.2.0.0](https://github.com/rasheedja/LPPaver/tree/v0.2.0.0)
924

src/Linear/Simplex/Solver/TwoPhase.hs

Lines changed: 45 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ import qualified Data.Set as Set
4949
import GHC.Real (Ratio)
5050
import Linear.Simplex.Types
5151
import Linear.Simplex.Util
52+
import qualified Control.Applicative as LPPaver
5253

5354
-- | Find a feasible solution for the given system of 'PolyConstraint's by performing the first phase of the two-phase simplex method
5455
-- All variables in the 'PolyConstraint' must be positive.
@@ -402,7 +403,9 @@ optimizeFeasibleSystem objFunction fsys@(FeasibleSystem {dict = phase1Dict, ..})
402403
-- Variables not in the VarDomainMap are assumed to be Unbounded (no lower bound).
403404
-- This function applies necessary transformations before solving and unapplies them after.
404405
-- The returned Result contains variable values and objective value in the original space.
405-
-- TODO: use this as twoPhaseSimplex, add instructions in CHANGELOG for old users
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
406409
twoPhaseSimplex :: (MonadIO m, MonadLogger m) => VarDomainMap -> ObjectiveFunction -> [PolyConstraint] -> m (Maybe Result)
407410
twoPhaseSimplex domainMap objFunction constraints = do
408411
logMsg LevelInfo $
@@ -489,26 +492,40 @@ collectAllVars objFunction constraints =
489492
-- Returns updated (transforms, nextFreshVar).
490493
generateTransform :: M.Map Var VarDomain -> Var -> ([VarTransform], Var) -> ([VarTransform], Var)
491494
generateTransform domainMap var (transforms, nextFreshVar) =
492-
let domain = M.findWithDefault Unbounded var domainMap
493-
in case getTransform nextFreshVar var domain of
494-
Nothing -> (transforms, nextFreshVar)
495-
Just t@(AddLowerBound {}) -> (t : transforms, nextFreshVar)
496-
Just t@(Shift {}) -> (t : transforms, nextFreshVar + 1)
497-
Just t@(Split {}) -> (t : transforms, nextFreshVar + 2)
498-
499-
-- | Determine what transform (if any) is needed for a variable given its domain.
500-
getTransform :: Var -> Var -> VarDomain -> Maybe VarTransform
501-
getTransform nextFreshVar var domain =
502-
case domain of
503-
NonNegative -> Nothing
504-
505-
LowerBound l
506-
| l == 0 -> Nothing
507-
| l > 0 -> Just $ AddLowerBound var l
508-
| otherwise -> Just $ Shift var nextFreshVar l -- l < 0, need to shift
509-
510-
Unbounded ->
511-
Just $ Split var nextFreshVar (nextFreshVar + 1)
495+
let domain = M.findWithDefault unbounded var domainMap
496+
(newTransforms, varOffset) = getTransform nextFreshVar var domain
497+
in (newTransforms ++ transforms, nextFreshVar + varOffset)
498+
499+
-- | Determine what transforms are needed for a variable given its domain.
500+
-- Returns a list of transforms and the number of fresh variables consumed.
501+
getTransform :: Var -> Var -> VarDomain -> ([VarTransform], Var)
502+
getTransform nextFreshVar var (Bounded mLower mUpper) =
503+
let -- Handle lower bound
504+
(lowerTransforms, varOffset) = case mLower of
505+
Nothing -> ([], 0) -- No lower bound: will need Split
506+
Just l
507+
| l == 0 -> ([], 0) -- NonNegative: no transform needed
508+
| l > 0 -> ([AddLowerBound var l], 0) -- Positive lower bound: add constraint
509+
| otherwise -> ([Shift var nextFreshVar l], 1) -- Negative lower bound: shift
510+
511+
-- Handle upper bound (if present)
512+
upperTransforms = case mUpper of
513+
Nothing -> []
514+
Just u -> [AddUpperBound var u]
515+
516+
-- If no lower bound (Nothing), we need Split transformation
517+
-- Split replaces the variable, so upper bound would apply to the original var
518+
-- which gets expressed as posVar - negVar
519+
(finalTransforms, finalOffset) = case mLower of
520+
Nothing ->
521+
-- Unbounded: split the variable
522+
-- Note: upperTransforms will still be added and will apply to the original variable
523+
-- expression (posVar - negVar) via the constraint system
524+
(Split var nextFreshVar (nextFreshVar + 1) : upperTransforms, 2)
525+
Just _ ->
526+
(lowerTransforms ++ upperTransforms, varOffset)
527+
528+
in (finalTransforms, finalOffset)
512529

513530
-- | Apply all transforms to the objective function and constraints.
514531
applyTransforms :: [VarTransform] -> ObjectiveFunction -> [PolyConstraint] -> (ObjectiveFunction, [PolyConstraint])
@@ -523,6 +540,10 @@ applyTransform transform (objFunction, constraints) =
523540
AddLowerBound v bound ->
524541
(objFunction, GEQ (M.singleton v 1) bound : constraints)
525542

543+
-- AddUpperBound: Add a LEQ constraint for the variable
544+
AddUpperBound v bound ->
545+
(objFunction, LEQ (M.singleton v 1) bound : constraints)
546+
526547
-- Shift: originalVar = shiftedVar + shiftBy (where shiftBy < 0)
527548
-- Substitute: wherever we see originalVar, replace with shiftedVar
528549
-- and adjust the RHS by -coeff * shiftBy
@@ -624,6 +645,9 @@ unapplyTransform transform result@(Result {varValMap = valMap, ..}) =
624645
-- AddLowerBound: No variable substitution was done, nothing to unapply
625646
AddLowerBound {} -> result
626647

648+
-- AddUpperBound: No variable substitution was done, nothing to unapply
649+
AddUpperBound {} -> result
650+
627651
-- Shift: originalVar = shiftedVar + shiftBy
628652
-- So originalVar's value = shiftedVar's value + shiftBy
629653
Shift origVar shiftedVar shiftBy ->

src/Linear/Simplex/Types.hs

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -122,16 +122,45 @@ data PivotObjective = PivotObjective
122122
}
123123
deriving (Show, Read, Eq, Generic)
124124

125-
-- | Domain specification for a variable's lower bound.
126-
-- Note: This only concerns lower bounds. Upper bounds are handled via constraints.
127-
-- Variables not in the VarDomainMap are assumed to be Unbounded.
128-
data VarDomain
129-
= NonNegative -- ^ var >= 0 (standard simplex assumption, no transformation needed)
130-
| LowerBound SimplexNum -- ^ var >= L for some L (if L < 0: shift, if L > 0: add constraint)
131-
| Unbounded -- ^ No lower bound (split into difference of two non-negative vars)
132-
-- TODO: Upperbound can still be useful, can negate it to get a loewr bound, can add it to the constraints
125+
-- | Domain specification for a variable's bounds.
126+
-- Variables not in the VarDomainMap are assumed to be Unbounded (both bounds Nothing).
127+
--
128+
-- Bounds semantics:
129+
-- * @lowerBound = Just L@ means var >= L
130+
-- * @lowerBound = Nothing@ means no lower bound (var can be arbitrarily negative)
131+
-- * @upperBound = Just U@ means var <= U
132+
-- * @upperBound = Nothing@ means no upper bound (var can be arbitrarily positive)
133+
--
134+
-- Note: @Bounded Nothing Nothing@ is equivalent to unbounded. Use the smart constructors
135+
-- ('unbounded', 'nonNegative', etc.) for clarity.
136+
data VarDomain = Bounded
137+
{ lowerBound :: Maybe SimplexNum -- ^ Lower bound (Nothing = -∞)
138+
, upperBound :: Maybe SimplexNum -- ^ Upper bound (Nothing = +∞)
139+
}
133140
deriving stock (Show, Read, Eq, Generic)
134141

142+
-- | Smart constructor for an unbounded variable (no lower or upper bound).
143+
-- The variable can take any real value.
144+
unbounded :: VarDomain
145+
unbounded = Bounded Nothing Nothing
146+
147+
-- | Smart constructor for a non-negative variable (var >= 0).
148+
-- This is the standard simplex assumption.
149+
nonNegative :: VarDomain
150+
nonNegative = Bounded (Just 0) Nothing
151+
152+
-- | Smart constructor for a variable with only a lower bound (var >= L).
153+
lowerBoundOnly :: SimplexNum -> VarDomain
154+
lowerBoundOnly l = Bounded (Just l) Nothing
155+
156+
-- | Smart constructor for a variable with only an upper bound (var <= U).
157+
upperBoundOnly :: SimplexNum -> VarDomain
158+
upperBoundOnly u = Bounded Nothing (Just u)
159+
160+
-- | Smart constructor for a variable with both lower and upper bounds (L <= var <= U).
161+
boundedRange :: SimplexNum -> SimplexNum -> VarDomain
162+
boundedRange l u = Bounded (Just l) (Just u)
163+
135164
-- | Map from variables to their domain specifications.
136165
-- Variables not in this map are assumed to be Unbounded.
137166
newtype VarDomainMap = VarDomainMap { unVarDomainMap :: M.Map Var VarDomain }
@@ -143,6 +172,10 @@ data VarTransform
143172
{ var :: !Var
144173
, bound :: !SimplexNum
145174
} -- ^ var >= bound where bound > 0. Adds GEQ constraint to system.
175+
| AddUpperBound
176+
{ var :: !Var
177+
, bound :: !SimplexNum
178+
} -- ^ var <= bound. Adds LEQ constraint to system.
146179
| Shift
147180
{ originalVar :: !Var
148181
, shiftedVar :: !Var

0 commit comments

Comments
 (0)