• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

msakai / haskell-MIP / 344

04 Jan 2026 03:39PM UTC coverage: 76.98% (+0.3%) from 76.66%
344

push

github

web-flow
Merge cc7d6067e into 9f1c6930b

1565 of 2033 relevant lines covered (76.98%)

0.77 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

89.89
/MIP/src/Numeric/Optimization/MIP/MPSFile.hs
1
{-# OPTIONS_GHC -Wall -fno-warn-unused-do-bind #-}
2
{-# OPTIONS_HADDOCK show-extensions #-}
3
{-# LANGUAGE ConstraintKinds #-}
4
{-# LANGUAGE FlexibleContexts #-}
5
{-# LANGUAGE OverloadedStrings #-}
6
{-# LANGUAGE ScopedTypeVariables #-}
7
{-# LANGUAGE TypeFamilies #-}
8
{-# LANGUAGE TypeOperators #-}
9
-----------------------------------------------------------------------------
10
-- |
11
-- Module      :  Numeric.Optimization.MIP.MPSFile
12
-- Copyright   :  (c) Masahiro Sakai 2012-2014
13
-- License     :  BSD-style
14
--
15
-- Maintainer  :  masahiro.sakai@gmail.com
16
-- Stability   :  provisional
17
-- Portability :  non-portable
18
--
19
-- A @.mps@ format parser library.
20
--
21
-- References:
22
--
23
-- * <http://pic.dhe.ibm.com/infocenter/cosinfoc/v12r4/topic/ilog.odms.cplex.help/CPLEX/File_formats_reference/topics/MPS_synopsis.html>
24
--
25
-- * <http://pic.dhe.ibm.com/infocenter/cosinfoc/v12r4/topic/ilog.odms.cplex.help/CPLEX/File_formats_reference/topics/MPS_ext_synopsis.html>
26
--
27
-- * <http://www.gurobi.com/documentation/5.0/reference-manual/node744>
28
--
29
-- * <http://en.wikipedia.org/wiki/MPS_(format)>
30
--
31
-----------------------------------------------------------------------------
32
module Numeric.Optimization.MIP.MPSFile
33
  ( parseString
34
  , parseFile
35
  , ParseError
36
  , parser
37
  , render
38
  ) where
39

40
import Control.Exception (throwIO)
41
import Control.Monad
42
import Control.Monad.Writer
43
import Control.Monad.ST
44
import Data.Default.Class
45
import qualified Data.IntMap.Strict as IntMap
46
import Data.Maybe
47
import Data.Set (Set)
48
import qualified Data.Set as Set
49
import Data.Map (Map)
50
import qualified Data.Map as Map
51
import Data.Scientific
52
import Data.Interned
53
import Data.Interned.Text
54
import Data.STRef
55
import Data.String
56
import qualified Data.Text as T
57
import qualified Data.Text.Lazy as TL
58
import Data.Text.Lazy.Builder (Builder)
59
import qualified Data.Text.Lazy.Builder as B
60
import System.IO
61
import Text.Megaparsec hiding  (ParseError)
62
import Text.Megaparsec.Char hiding (string', eol, newline)
63
import qualified Text.Megaparsec.Char as P
64
import qualified Text.Megaparsec.Char.Lexer as Lexer
65

66
import Data.OptDir
67
import qualified Numeric.Optimization.MIP.Base as MIP
68
import Numeric.Optimization.MIP.FileUtils (ParseError, readTextFile)
69

70
type Column = MIP.Var
71
type Row = InternedText
72

73
data BoundType
74
  = LO  -- lower bound
75
  | UP  -- upper bound
76
  | FX  -- variable is fixed at the specified value
77
  | FR  -- free variable (no lower or upper bound)
78
  | MI  -- infinite lower bound
79
  | PL  -- infinite upper bound
80
  | BV  -- variable is binary (equal 0 or 1)
81
  | LI  -- lower bound for integer variable
82
  | UI  -- upper bound for integer variable
83
  | SC  -- upper bound for semi-continuous variable
84
  | SI  -- upper bound for semi-integer variable
85
  deriving (Eq, Ord, Show, Read, Enum, Bounded)
×
86

87
-- ---------------------------------------------------------------------------
88

89
type C e s m = (MonadParsec e s m, Token s ~ Char, IsString (Tokens s))
90

91
-- | Parse a string containing MPS file data.
92
--
93
-- The source name is only used in error messages and may be the empty string.
94
parseString :: (Stream s, Token s ~ Char, IsString (Tokens s)) => MIP.FileOptions -> String -> s -> Either (ParseError s) (MIP.Problem Scientific)
95
parseString _ = parse (parser <* eof)
1✔
96

97
-- | Parse a file containing MPS file data.
98
parseFile :: MIP.FileOptions -> FilePath -> IO (MIP.Problem Scientific)
99
parseFile opt fname = do
1✔
100
  s <- readTextFile opt fname
1✔
101
  case parse (parser <* eof) fname s of
×
102
    Left e -> throwIO (e :: ParseError TL.Text)
×
103
    Right a -> return a
1✔
104

105
-- ---------------------------------------------------------------------------
106

107

108
anyChar :: C e s m => m Char
109
anyChar = anySingle
1✔
110

111
space' :: C e s m => m Char
112
space' = oneOf [' ', '\t']
1✔
113

114
spaces' :: C e s m => m ()
115
spaces' = skipMany space'
1✔
116

117
spaces1' :: C e s m => m ()
118
spaces1' = skipSome space'
1✔
119

120
commentline :: C e s m => m ()
121
commentline = do
1✔
122
  _ <- char '*'
1✔
123
  _ <- manyTill anyChar P.eol
1✔
124
  return ()
×
125

126
eol' :: C e s m => m ()
127
eol' = do
1✔
128
  spaces'
1✔
129
  _ <- P.eol
1✔
130
  skipMany commentline
1✔
131
  return ()
×
132

133
tok :: C e s m => m a -> m a
134
tok p = do
1✔
135
  x <- p
1✔
136
  msum [eof, lookAhead (void P.eol), spaces1']
1✔
137
  return x
1✔
138

139
row :: C e s m => m Row
140
row = liftM intern ident
1✔
141

142
column :: C e s m => m Column
143
column = liftM MIP.Var $ ident
1✔
144

145
ident :: C e s m => m T.Text
146
ident = liftM fromString $ tok $ some $ noneOf [' ', '\t', '\r', '\n']
1✔
147

148
stringLn :: C e s m => String -> m ()
149
stringLn s = string (fromString s) >> eol'
1✔
150

151
number :: forall e s m. C e s m => m Scientific
152
number = tok $ Lexer.signed (return ()) Lexer.scientific
×
153

154
-- ---------------------------------------------------------------------------
155

156
-- | MPS file parser
157
parser :: (MonadParsec e s m, Token s ~ Char, IsString (Tokens s)) => m (MIP.Problem Scientific)
158
parser = do
1✔
159
  many commentline
1✔
160

161
  name <- nameSection
1✔
162

163
  -- http://pic.dhe.ibm.com/infocenter/cosinfoc/v12r4/topic/ilog.odms.cplex.help/CPLEX/File_formats_reference/topics/MPS_ext_objsen.html
164
  -- CPLEX extends the MPS standard by allowing two additional sections: OBJSEN and OBJNAME.
165
  -- If these options are used, they must appear in order and as the first and second sections after the NAME section.
166
  objsense <- optional $ objSenseSection
1✔
167
  objname  <- optional $ objNameSection
1✔
168

169
  rows <- rowsSection
1✔
170

171
  -- http://pic.dhe.ibm.com/infocenter/cosinfoc/v12r4/topic/ilog.odms.cplex.help/CPLEX/File_formats_reference/topics/MPS_ext_usercuts.html
172
  -- The order of sections must be ROWS USERCUTS.
173
  usercuts <- option [] userCutsSection
1✔
174

175
  -- http://pic.dhe.ibm.com/infocenter/cosinfoc/v12r4/topic/ilog.odms.cplex.help/CPLEX/File_formats_reference/topics/MPS_ext_lazycons.html
176
  -- The order of sections must be ROWS USERCUTS LAZYCONS.
177
  lazycons <- option [] lazyConsSection
1✔
178

179
  (cols, intvs1) <- colsSection
1✔
180
  rhss <- rhsSection
1✔
181
  rngs <- option Map.empty rangesSection
1✔
182
  bnds <- option [] boundsSection
1✔
183

184
  -- http://pic.dhe.ibm.com/infocenter/cosinfoc/v12r4/topic/ilog.odms.cplex.help/CPLEX/File_formats_reference/topics/MPS_ext_quadobj.html
185
  -- Following the BOUNDS section, a QMATRIX section may be specified.
186
  qobj <- msum [quadObjSection, qMatrixSection, return []]
1✔
187

188
  -- http://pic.dhe.ibm.com/infocenter/cosinfoc/v12r4/topic/ilog.odms.cplex.help/CPLEX/File_formats_reference/topics/MPS_ext_sos.html
189
  -- Note that in an MPS file, the SOS section must follow the BOUNDS section.
190
  sos <- option [] sosSection
1✔
191

192
  -- http://pic.dhe.ibm.com/infocenter/cosinfoc/v12r4/topic/ilog.odms.cplex.help/CPLEX/File_formats_reference/topics/MPS_ext_qcmatrix.html
193
  -- QCMATRIX sections appear after the optional SOS section.
194
  qterms <- liftM Map.fromList $ many qcMatrixSection
1✔
195

196
  -- http://pic.dhe.ibm.com/infocenter/cosinfoc/v12r4/topic/ilog.odms.cplex.help/CPLEX/File_formats_reference/topics/MPS_ext_indicators.html
197
  -- The INDICATORS section follows any quadratic constraint section and any quadratic objective section.
198
  inds <- option Map.empty indicatorsSection
1✔
199

200
  string "ENDATA"
1✔
201
  P.space
1✔
202

203
  let objrow =
1✔
204
        case objname of
1✔
205
          Nothing -> head [r | (Nothing, r) <- rows] -- XXX
1✔
206
          Just r  -> intern r
1✔
207
      objdir = fromMaybe OptMin objsense
1✔
208
      vs     = Map.keysSet cols `Set.union` Set.fromList [col | (_,col,_) <- bnds]
1✔
209
      intvs2 = Set.fromList [col | (t,col,_) <- bnds, t `elem` [BV,LI,UI]]
1✔
210
      scvs   = Set.fromList [col | (SC,col,_) <- bnds]
1✔
211
      sivs   = Set.fromList [col | (SI,col,_) <- bnds]
1✔
212

213
  let explicitBounds = Map.fromListWith f
1✔
214
        [ case typ of
1✔
215
            LO -> (col, (Just (MIP.Finite val), Nothing))
1✔
216
            UP -> (col, (Nothing, Just (MIP.Finite val)))
1✔
217
            FX -> (col, (Just (MIP.Finite val), Just (MIP.Finite val)))
1✔
218
            FR -> (col, (Just MIP.NegInf, Just MIP.PosInf))
1✔
219
            MI -> (col, (Just MIP.NegInf, Nothing))
1✔
220
            PL -> (col, (Nothing, Just MIP.PosInf))
1✔
221
            BV -> (col, (Just (MIP.Finite 0), Just (MIP.Finite 1)))
1✔
222
            LI -> (col, (Just (MIP.Finite val), Nothing))
1✔
223
            UI -> (col, (Nothing, Just (MIP.Finite val)))
1✔
224
            SC -> (col, (Nothing, Just (MIP.Finite val)))
1✔
225
            SI -> (col, (Nothing, Just (MIP.Finite val)))
×
226
        | (typ,col,val) <- bnds ]
1✔
227
        where
228
          f (a1,b1) (a2,b2) = (g a1 a2, g b1 b2)
×
229
          g _ (Just x) = Just x
1✔
230
          g x Nothing  = x
1✔
231

232
  let bounds = Map.fromList
1✔
233
        [ case Map.lookup v explicitBounds of
1✔
234
            Nothing ->
235
              if v `Set.member` intvs1
1✔
236
              then
237
                -- http://eaton.math.rpi.edu/cplex90html/reffileformatscplex/reffileformatscplex9.html
238
                -- If no bounds are specified for the variables within markers, bounds of 0 (zero) and 1 (one) are assumed.
239
                (v, (MIP.Finite 0, MIP.Finite 1))
1✔
240
              else
241
                (v, (MIP.Finite 0, MIP.PosInf))
1✔
242
            Just (Nothing, Just (MIP.Finite ub)) | ub < 0 ->
1✔
243
              {-
244
                http://pic.dhe.ibm.com/infocenter/cosinfoc/v12r4/topic/ilog.odms.cplex.help/CPLEX/File_formats_reference/topics/MPS_records.html
245
                If no bounds are specified, CPLEX assumes a lower
246
                bound of 0 (zero) and an upper bound of +∞. If only a
247
                single bound is specified, the unspecified bound
248
                remains at 0 or +∞, whichever applies, with one
249
                exception. If an upper bound of less than 0 is
250
                specified and no other bound is specified, the lower
251
                bound is automatically set to -∞. CPLEX deviates
252
                slightly from a convention used by some MPS readers
253
                when it encounters an upper bound of 0 (zero). Rather
254
                than automatically set this variable’s lower bound to
255
                -∞, CPLEX accepts both a lower and upper bound of 0,
256
                effectively fixing that variable at 0. CPLEX resets
257
                the lower bound to -∞ only if the upper bound is less
258
                than 0. A warning message is issued when this
259
                exception is encountered.
260
              -}
261
              (v, (MIP.NegInf, MIP.Finite ub))
1✔
262
            {-
263
              lp_solve uses 1 as default lower bound for semi-continuous variable.
264
              <http://lpsolve.sourceforge.net/5.5/mps-format.htm>
265
              But Gurobi Optimizer uses 0 as default lower bound for semi-continuous variable.
266
              Here we adopt Gurobi's way.
267
            -}
268
{-
269
            Just (Nothing, ub) | v `Set.member` scvs ->
270
              (v, (MIP.Finite 1, fromMaybe MIP.PosInf ub))
271
-}
272
            Just (lb,ub) ->
273
              (v, (fromMaybe (MIP.Finite 0) lb, fromMaybe MIP.PosInf ub))
1✔
274
        | v <- Set.toList vs ]
1✔
275

276
  let rowCoeffs :: Map Row (Map Column Scientific)
277
      rowCoeffs = Map.fromListWith Map.union [(r, Map.singleton col coeff) | (col,m) <- Map.toList cols, (r,coeff) <- Map.toList m]
1✔
278

279
  let f :: Bool -> (Maybe MIP.RelOp, Row) -> [MIP.Constraint Scientific]
280
      f _isLazy (Nothing, _row) = []
1✔
281
      f isLazy (Just op, r) = do
1✔
282
        let lhs = [MIP.Term c [col] | (col,c) <- Map.toList (Map.findWithDefault Map.empty r rowCoeffs)]
1✔
283
                  ++ Map.findWithDefault [] r qterms
1✔
284
        let rhs = Map.findWithDefault 0 r rhss
1✔
285
            (lb,ub) =
286
              case Map.lookup r rngs of
1✔
287
                Nothing  ->
288
                  case op of
1✔
289
                    MIP.Ge  -> (MIP.Finite rhs, MIP.PosInf)
1✔
290
                    MIP.Le  -> (MIP.NegInf, MIP.Finite rhs)
1✔
291
                    MIP.Eql -> (MIP.Finite rhs, MIP.Finite rhs)
1✔
292
                Just rng ->
293
                  case op of
1✔
294
                    MIP.Ge  -> (MIP.Finite rhs, MIP.Finite (rhs + abs rng))
1✔
295
                    MIP.Le  -> (MIP.Finite (rhs - abs rng), MIP.Finite rhs)
1✔
296
                    MIP.Eql ->
297
                      if rng < 0
1✔
298
                      then (MIP.Finite (rhs + rng), MIP.Finite rhs)
1✔
299
                      else (MIP.Finite rhs, MIP.Finite (rhs + rng))
1✔
300
        return $
1✔
301
          MIP.Constraint
1✔
302
          { MIP.constrLabel     = Just $ unintern r
1✔
303
          , MIP.constrIndicator = Map.lookup r inds
1✔
304
          , MIP.constrIsLazy    = isLazy
1✔
305
          , MIP.constrExpr      = MIP.Expr lhs
1✔
306
          , MIP.constrLB        = lb
1✔
307
          , MIP.constrUB        = ub
1✔
308
          }
309

310
  let objOffset =
1✔
311
        case Map.lookup objrow rhss of
1✔
312
          Nothing -> 0
1✔
313
          Just c -> MIP.constExpr (-c)
1✔
314

315
  let mip =
1✔
316
        MIP.Problem
1✔
317
        { MIP.name                  = name
1✔
318
        , MIP.objectiveFunction     = def
1✔
319
            { MIP.objDir = objdir
1✔
320
            , MIP.objLabel = Just (unintern objrow)
1✔
321
            , MIP.objExpr =
322
                MIP.Expr [MIP.Term c [col] | (col,m) <- Map.toList cols, c <- maybeToList (Map.lookup objrow m)] +
1✔
323
                MIP.Expr qobj +
1✔
324
                objOffset
1✔
325
            }
326
        , MIP.constraints           = concatMap (f False) rows ++ concatMap (f True) lazycons
1✔
327
        , MIP.sosConstraints        = sos
1✔
328
        , MIP.userCuts              = concatMap (f False) usercuts
×
329
        , MIP.varDomains            = Map.fromAscList
1✔
330
            [ (v, (t, bs))
1✔
331
            | v <- Set.toAscList vs
1✔
332
            , let t | v `Set.member` sivs = MIP.SemiIntegerVariable
1✔
333
                    | v `Set.member` intvs1 && v `Set.member` scvs = MIP.SemiIntegerVariable
1✔
334
                    | v `Set.member` intvs1 || v `Set.member` intvs2 = MIP.IntegerVariable
1✔
335
                    | v `Set.member` scvs = MIP.SemiContinuousVariable
1✔
336
                    | otherwise = MIP.ContinuousVariable
×
337
            , let bs = Map.findWithDefault MIP.defaultBounds v bounds
×
338
            ]
339
        }
340

341
  return mip
1✔
342

343
nameSection :: C e s m => m (Maybe T.Text)
344
nameSection = do
1✔
345
  string "NAME"
1✔
346
  n <- optional $ try $ do
1✔
347
    spaces1'
1✔
348
    ident
1✔
349
  eol'
1✔
350
  return n
1✔
351

352
objSenseSection :: C e s m => m OptDir
353
objSenseSection = do
1✔
354
  try $ stringLn "OBJSENSE"
1✔
355
  spaces1'
1✔
356
  (try (stringLn "MAX") >> return OptMax)
1✔
357
    <|> (stringLn "MIN" >> return OptMin)
×
358

359
objNameSection :: C e s m => m T.Text
360
objNameSection = do
1✔
361
  try $ stringLn "OBJNAME"
1✔
362
  spaces1'
1✔
363
  name <- ident
1✔
364
  eol'
1✔
365
  return name
1✔
366

367
rowsSection :: C e s m => m [(Maybe MIP.RelOp, Row)]
368
rowsSection = do
1✔
369
  try $ stringLn "ROWS"
1✔
370
  rowsBody
1✔
371

372
userCutsSection :: C e s m => m [(Maybe MIP.RelOp, Row)]
373
userCutsSection = do
1✔
374
  try $ stringLn "USERCUTS"
1✔
375
  rowsBody
1✔
376

377
lazyConsSection :: C e s m => m [(Maybe MIP.RelOp, Row)]
378
lazyConsSection = do
1✔
379
  try $ stringLn "LAZYCONS"
1✔
380
  rowsBody
1✔
381

382
rowsBody :: C e s m => m [(Maybe MIP.RelOp, Row)]
383
rowsBody = many $ do
1✔
384
  spaces1'
1✔
385
  op <- msum
1✔
386
        [ char 'N' >> return Nothing
1✔
387
        , char 'G' >> return (Just MIP.Ge)
1✔
388
        , char 'L' >> return (Just MIP.Le)
1✔
389
        , char 'E' >> return (Just MIP.Eql)
1✔
390
        ]
391
  spaces1'
1✔
392
  name <- row
1✔
393
  eol'
1✔
394
  return (op, name)
1✔
395

396
colsSection :: forall e s m. C e s m => m (Map Column (Map Row Scientific), Set Column)
397
colsSection = do
1✔
398
  try $ stringLn "COLUMNS"
1✔
399
  body False Map.empty Set.empty
1✔
400
  where
401
    body :: Bool -> Map Column (Map Row Scientific) -> Set Column -> m (Map Column (Map Row Scientific), Set Column)
402
    body isInt rs ivs = msum
1✔
403
      [ do _ <- spaces1'
1✔
404
           x <- ident
1✔
405
           msum
1✔
406
             [ do isInt' <- try intMarker
1✔
407
                  body isInt' rs ivs
1✔
408
             , do (k,v) <- entry x
1✔
409
                  let rs'  = Map.insertWith Map.union k v rs
1✔
410
                      ivs' = if isInt then Set.insert k ivs else ivs
1✔
411
                  seq rs' $ seq ivs' $ body isInt rs' ivs'
1✔
412
             ]
413
      , return (rs, ivs)
1✔
414
      ]
415

416
    intMarker :: m Bool
417
    intMarker = do
1✔
418
      string "'MARKER'"
1✔
419
      spaces1'
1✔
420
      b <-  (try (string "'INTORG'") >> return True)
1✔
421
        <|> (string "'INTEND'" >> return False)
×
422
      eol'
1✔
423
      return b
1✔
424

425
    entry :: T.Text -> m (Column, Map Row Scientific)
426
    entry x = do
1✔
427
      let col = MIP.Var x
1✔
428
      rv1 <- rowAndVal
1✔
429
      opt <- optional rowAndVal
1✔
430
      eol'
1✔
431
      case opt of
1✔
432
        Nothing -> return (col, rv1)
1✔
433
        Just rv2 ->  return (col, Map.union rv1 rv2)
1✔
434

435
rowAndVal :: C e s m => m (Map Row Scientific)
436
rowAndVal = do
1✔
437
  r <- row
1✔
438
  val <- number
1✔
439
  return $ Map.singleton r val
1✔
440

441
rhsSection :: C e s m => m (Map Row Scientific)
442
rhsSection = do
1✔
443
  try $ stringLn "RHS"
1✔
444
  liftM Map.unions $ many entry
1✔
445
  where
446
    entry = do
1✔
447
      spaces1'
1✔
448
      _name <- ident
1✔
449
      rv1 <- rowAndVal
1✔
450
      opt <- optional rowAndVal
1✔
451
      eol'
1✔
452
      case opt of
1✔
453
        Nothing  -> return rv1
1✔
454
        Just rv2 -> return $ Map.union rv1 rv2
1✔
455

456
rangesSection :: C e s m => m (Map Row Scientific)
457
rangesSection = do
1✔
458
  try $ stringLn "RANGES"
1✔
459
  liftM Map.unions $ many entry
1✔
460
  where
461
    entry = do
1✔
462
      spaces1'
1✔
463
      _name <- ident
1✔
464
      rv1 <- rowAndVal
1✔
465
      opt <- optional rowAndVal
1✔
466
      eol'
1✔
467
      case opt of
1✔
468
        Nothing  -> return rv1
1✔
469
        Just rv2 -> return $ Map.union rv1 rv2
×
470

471
boundsSection :: C e s m => m [(BoundType, Column, Scientific)]
472
boundsSection = do
1✔
473
  try $ stringLn "BOUNDS"
1✔
474
  many entry
1✔
475
  where
476
    entry = do
1✔
477
      spaces1'
1✔
478
      typ   <- boundType
1✔
479
      _name <- ident
1✔
480
      col   <- column
1✔
481
      val   <- if typ `elem` [FR, BV, MI, PL]
1✔
482
               then return 0
×
483
               else number
1✔
484
      eol'
1✔
485
      return (typ, col, val)
1✔
486

487
boundType :: C e s m => m BoundType
488
boundType = tok $ do
1✔
489
  msum [try (string (fromString (show k))) >> return k | k <- [minBound..maxBound]]
1✔
490

491
sosSection :: forall e s m. C e s m => m [MIP.SOSConstraint Scientific]
492
sosSection = do
1✔
493
  try $ stringLn "SOS"
1✔
494
  many entry
1✔
495
  where
496
    entry = do
1✔
497
      spaces1'
1✔
498
      typ <-  (try (string "S1") >> return MIP.SOS1)
1✔
499
          <|> (string "S2" >> return MIP.SOS2)
1✔
500
      spaces1'
1✔
501
      name <- ident
1✔
502
      eol'
1✔
503
      xs <- many (try identAndVal)
1✔
504
      return $ MIP.SOSConstraint{ MIP.sosLabel = Just name, MIP.sosType = typ, MIP.sosBody = xs }
1✔
505

506
    identAndVal :: m (Column, Scientific)
507
    identAndVal = do
1✔
508
      spaces1'
1✔
509
      col <- column
1✔
510
      val <- number
1✔
511
      eol'
1✔
512
      return (col, val)
1✔
513

514
quadObjSection :: C e s m => m [MIP.Term Scientific]
515
quadObjSection = do
1✔
516
  try $ stringLn "QUADOBJ"
1✔
517
  many entry
1✔
518
  where
519
    entry = do
1✔
520
      spaces1'
1✔
521
      col1 <- column
1✔
522
      col2 <- column
1✔
523
      val  <- number
1✔
524
      eol'
1✔
525
      return $ MIP.Term (if col1 /= col2 then val else val / 2) [col1, col2]
1✔
526

527
qMatrixSection :: C e s m => m [MIP.Term Scientific]
528
qMatrixSection = do
1✔
529
  try $ stringLn "QMATRIX"
1✔
530
  many entry
1✔
531
  where
532
    entry = do
1✔
533
      spaces1'
1✔
534
      col1 <- column
1✔
535
      col2 <- column
1✔
536
      val  <- number
1✔
537
      eol'
1✔
538
      return $ MIP.Term (val / 2) [col1, col2]
1✔
539

540
qcMatrixSection :: C e s m => m (Row, [MIP.Term Scientific])
541
qcMatrixSection = do
1✔
542
  try $ string "QCMATRIX"
1✔
543
  spaces1'
1✔
544
  r <- row
1✔
545
  eol'
1✔
546
  xs <- many entry
1✔
547
  return (r, xs)
1✔
548
  where
549
    entry = do
1✔
550
      spaces1'
1✔
551
      col1 <- column
1✔
552
      col2 <- column
1✔
553
      val  <- number
1✔
554
      eol'
1✔
555
      return $ MIP.Term val [col1, col2]
1✔
556

557
indicatorsSection :: C e s m => m (Map Row (Column, Scientific))
558
indicatorsSection = do
1✔
559
  try $ stringLn "INDICATORS"
1✔
560
  liftM Map.fromList $ many entry
1✔
561
  where
562
    entry = do
1✔
563
      spaces1'
1✔
564
      string "IF"
1✔
565
      spaces1'
1✔
566
      r <- row
1✔
567
      var <- column
1✔
568
      val <- number
1✔
569
      eol'
1✔
570
      return (r, (var, val))
1✔
571

572
-- ---------------------------------------------------------------------------
573

574
type M a = Writer Builder a
575

576
execM :: M a -> TL.Text
577
execM m = B.toLazyText $ execWriter m
1✔
578

579
writeText :: T.Text -> M ()
580
writeText s = tell $ B.fromText s
1✔
581

582
writeChar :: Char -> M ()
583
writeChar c = tell $ B.singleton c
1✔
584

585
-- ---------------------------------------------------------------------------
586

587
-- | Render a problem into a 'TL.Text' containing MPS file data.
588
render :: MIP.FileOptions -> MIP.Problem Scientific -> Either String TL.Text
589
render _ mip | not (checkAtMostQuadratic mip) = Left "Expression must be atmost quadratic"
×
590
render opt mip = Right $ execM $ render' opt $ normalizeConstTerm $ nameRows mip
1✔
591

592
render' :: MIP.FileOptions -> MIP.Problem Scientific -> M ()
593
render' opt mip = do
1✔
594
  let newline =
1✔
595
        case fromMaybe LF (MIP.optNewline opt) of
1✔
596
          LF -> "\n"
1✔
597
          CRLF -> "\r\n"
1✔
598

599
      writeSectionHeader :: T.Text -> M ()
600
      writeSectionHeader = writeSectionHeader' newline
1✔
601

602
      writeFields :: [T.Text] -> M ()
603
      writeFields = writeFields' newline
1✔
604

605
  -- NAME section
606
  -- The name starts in column 15 in fixed formats.
607
  let probName = fromMaybe "" (MIP.name mip)
1✔
608
  writeSectionHeader $ "NAME" <> T.replicate 10 " " <> probName
1✔
609

610
  let MIP.ObjectiveFunction
611
       { MIP.objLabel = Just objName
612
       , MIP.objDir = dir
613
       , MIP.objExpr = obj
614
       } = MIP.objectiveFunction mip
1✔
615

616
  -- OBJSENSE section
617
  when (MIP.optMPSWriteObjSense opt == MIP.WriteAlways ||
1✔
618
        MIP.optMPSWriteObjSense opt == MIP.WriteIfNotDefault && dir /= OptMin) $ do
1✔
619
    writeSectionHeader "OBJSENSE"
1✔
620
    case dir of
1✔
621
      OptMin -> writeFields ["MIN"]
×
622
      OptMax -> writeFields ["MAX"]
1✔
623

624
  -- OBJNAME section
625
  -- Note: GLPK-4.48 does not support this section.
626
  when (MIP.optMPSWriteObjName opt) $ do
1✔
627
    writeSectionHeader "OBJNAME"
1✔
628
    writeFields [objName]
1✔
629

630
  let splitRange c =
1✔
631
        case (MIP.constrLB c, MIP.constrUB c) of
1✔
632
          (MIP.Finite x, MIP.PosInf) -> ((MIP.Ge, x), Nothing)
1✔
633
          (MIP.NegInf, MIP.Finite x) -> ((MIP.Le, x), Nothing)
1✔
634
          (MIP.Finite x1, MIP.Finite x2)
635
            | x1 == x2 -> ((MIP.Eql, x1), Nothing)
1✔
636
            | x1 < x2  -> ((MIP.Eql, x1), Just (x2 - x1))
×
637
          _ -> error "invalid constraint bound"
×
638

639
  let renderRows cs = do
1✔
640
        forM_ cs $ \c -> do
1✔
641
          let ((op,_), _) = splitRange c
1✔
642
          let s = case op of
1✔
643
                    MIP.Le  -> "L"
1✔
644
                    MIP.Ge  -> "G"
1✔
645
                    MIP.Eql -> "E"
1✔
646
          writeFields [s, fromJust $ MIP.constrLabel c]
1✔
647

648
  -- ROWS section
649
  writeSectionHeader "ROWS"
1✔
650
  writeFields ["N", objName]
1✔
651
  renderRows [c | c <- MIP.constraints mip, not (MIP.constrIsLazy c)]
1✔
652

653
  -- USERCUTS section
654
  unless (null (MIP.userCuts mip)) $ do
1✔
655
    writeSectionHeader "USERCUTS"
1✔
656
    renderRows (MIP.userCuts mip)
1✔
657

658
  -- LAZYCONS section
659
  let lcs = [c | c <- MIP.constraints mip, MIP.constrIsLazy c]
1✔
660
  unless (null lcs) $ do
1✔
661
    writeSectionHeader "LAZYCONS"
1✔
662
    renderRows lcs
1✔
663

664
  -- COLUMNS section
665
  writeSectionHeader "COLUMNS"
1✔
666
  let cols :: Map Column [(T.Text, Scientific)]
667
      cols = runST $ do
1✔
668
        -- Use internedTextId and IntMap to avoid 'compare' on MIP.Var (i.e. Text).
669
        refs <- fmap IntMap.fromList $ forM (Map.toList (MIP.varDomains mip)) $ \(MIP.Var' v, _) -> do
1✔
670
          ref <- newSTRef []
1✔
671
          pure (internedTextId v, ref)
1✔
672

673
        let g (Just l, xs) = do
1✔
674
              let xs' = IntMap.fromListWith (+) [(internedTextId v, d) | MIP.Term d [MIP.Var' v] <- MIP.terms xs]
×
675
              forM_ (IntMap.toList xs') $ \(i, d) -> modifySTRef (refs IntMap.! i) ((l,d) :)
1✔
676
            g (Nothing, _) = error "should not happen"
×
677
        g (Just objName, obj)
1✔
678
        mapM_ g [(MIP.constrLabel c, MIP.constrExpr c) | c <- MIP.constraints mip]
1✔
679
        mapM_ g [(MIP.constrLabel c, MIP.constrExpr c) | c <- MIP.userCuts mip]
1✔
680

681
        Map.traverseWithKey (\(MIP.Var' v) _ -> reverse <$> readSTRef (refs IntMap.! internedTextId v)) (MIP.varDomains mip)
1✔
682

683
      printColumn col xs =
1✔
684
        forM_ xs $ \(r, d) -> do
1✔
685
          writeFields ["", MIP.varName col, r, showValue d]
1✔
686
      ivs = Map.filter (\(vt, _) -> vt == MIP.IntegerVariable || vt == MIP.SemiIntegerVariable) (MIP.varDomains mip)
1✔
687

688
  forM_ (Map.toList (cols Map.\\ ivs)) $ \(col, xs) -> printColumn col xs
1✔
689
  unless (Map.null ivs) $ do
1✔
690
    writeFields ["", "MARK0000", "'MARKER'", "", "'INTORG'"]
1✔
691
    forM_ (Map.toList (Map.intersection cols ivs)) $ \(col, xs) -> printColumn col xs
1✔
692
    writeFields ["", "MARK0001", "'MARKER'", "", "'INTEND'"]
1✔
693

694
  -- RHS section
695
  let rs = [(fromJust $ MIP.constrLabel c, rhs) | c <- MIP.constraints mip ++ MIP.userCuts mip, let ((_,rhs),_) = splitRange c, rhs /= 0]
1✔
696
  writeSectionHeader "RHS"
1✔
697
  case sum [d | MIP.Term d [] <- MIP.terms obj] of
1✔
698
    0 -> pure ()
×
699
    offset -> writeFields ["", "rhs", objName, showValue (- offset)]
1✔
700
  forM_ rs $ \(name, val) -> do
1✔
701
    writeFields ["", "rhs", name, showValue val]
1✔
702

703
  -- RANGES section
704
  let rngs = [(fromJust $ MIP.constrLabel c, fromJust rng) | c <- MIP.constraints mip ++ MIP.userCuts mip, let ((_,_), rng) = splitRange c, isJust rng]
1✔
705
  unless (null rngs) $ do
1✔
706
    writeSectionHeader "RANGES"
1✔
707
    forM_ rngs $ \(name, val) -> do
1✔
708
      writeFields ["", "rhs", name, showValue val]
1✔
709

710
  -- BOUNDS section
711
  writeSectionHeader "BOUNDS"
1✔
712
  forM_ (Map.toList (MIP.varDomains mip)) $ \(col, (vt, (lb,ub))) -> do
1✔
713
    case (lb,ub)  of
1✔
714
      (MIP.NegInf, MIP.PosInf) -> do
1✔
715
        -- free variable (no lower or upper bound)
716
        writeFields ["FR", "bound", MIP.varName col]
1✔
717

718
      (MIP.Finite 0, MIP.Finite 1) | vt == MIP.IntegerVariable -> do
1✔
719
        -- variable is binary (equal 0 or 1)
720
        writeFields ["BV", "bound", MIP.varName col]
1✔
721

722
      (MIP.Finite a, MIP.Finite b) | a == b -> do
1✔
723
        -- variable is fixed at the specified value
724
        writeFields ["FX", "bound", MIP.varName col, showValue a]
1✔
725

726
      _ -> do
1✔
727
        case lb of
1✔
728
          MIP.PosInf -> error "should not happen"
×
729
          MIP.NegInf -> do
1✔
730
            -- Minus infinity
731
            writeFields ["MI", "bound", MIP.varName col]
1✔
732
          MIP.Finite 0 | vt == MIP.ContinuousVariable -> return ()
×
733
          MIP.Finite a -> do
1✔
734
            let t = case vt of
1✔
735
                      MIP.IntegerVariable -> "LI" -- lower bound for integer variable
1✔
736
                      _ -> "LO" -- Lower bound
1✔
737
            writeFields [t, "bound", MIP.varName col, showValue a]
1✔
738

739
        case ub of
1✔
740
          MIP.NegInf -> error "should not happen"
×
741
          MIP.PosInf | vt == MIP.ContinuousVariable -> return ()
×
742
          MIP.PosInf -> do
1✔
743
            when (vt == MIP.SemiContinuousVariable || vt == MIP.SemiIntegerVariable) $
1✔
744
              error "cannot express +inf upper bound of semi-continuous or semi-integer variable"
×
745
            writeFields ["PL", "bound", MIP.varName col] -- Plus infinity
1✔
746
          MIP.Finite a -> do
1✔
747
            let t = case vt of
1✔
748
                      MIP.SemiContinuousVariable -> "SC" -- Upper bound for semi-continuous variable
1✔
749
                      MIP.SemiIntegerVariable ->
750
                        -- Gurobi uses "SC" while lpsolve uses "SI" for upper bound of semi-integer variable
751
                        "SC"
1✔
752
                      MIP.IntegerVariable -> "UI" -- Upper bound for integer variable
1✔
753
                      _ -> "UP" -- Upper bound
1✔
754
            writeFields [t, "bound", MIP.varName col, showValue a]
1✔
755

756
  -- QMATRIX section
757
  -- Gurobiは対称行列になっていないと "qmatrix isn't symmetric" というエラーを発生させる
758
  do let qm = Map.map (2*) $ quadMatrix obj
1✔
759
     unless (Map.null qm) $ do
1✔
760
       writeSectionHeader "QMATRIX"
1✔
761
       forM_ (Map.toList qm) $ \((v1,v2), val) -> do
1✔
762
         writeFields ["", MIP.varName v1, MIP.varName v2, showValue val]
1✔
763

764
  -- SOS section
765
  unless (null (MIP.sosConstraints mip)) $ do
1✔
766
    writeSectionHeader "SOS"
1✔
767
    forM_ (MIP.sosConstraints mip) $ \sos -> do
1✔
768
      let t = case MIP.sosType sos of
1✔
769
                MIP.SOS1 -> "S1"
1✔
770
                MIP.SOS2 -> "S2"
1✔
771
      writeFields $ t : maybeToList (MIP.sosLabel sos)
1✔
772
      forM_ (MIP.sosBody sos) $ \(var,val) -> do
1✔
773
        writeFields ["", MIP.varName var, showValue val]
1✔
774

775
  -- QCMATRIX section
776
  let xs = [ (fromJust $ MIP.constrLabel c, qm)
1✔
777
           | c <- MIP.constraints mip ++ MIP.userCuts mip
1✔
778
           , let lhs = MIP.constrExpr c
1✔
779
           , let qm = quadMatrix lhs
1✔
780
           , not (Map.null qm) ]
1✔
781
  unless (null xs) $ do
1✔
782
    forM_ xs $ \(r, qm) -> do
1✔
783
      -- The name starts in column 12 in fixed formats.
784
      writeSectionHeader $ "QCMATRIX" <> T.replicate 3 " " <> r
1✔
785
      forM_ (Map.toList qm) $ \((v1,v2), val) -> do
1✔
786
        writeFields ["", MIP.varName v1, MIP.varName v2, showValue val]
1✔
787

788
  -- INDICATORS section
789
  -- Note: Gurobi-5.6.3 does not support this section.
790
  let ics = [c | c <- MIP.constraints mip, isJust $ MIP.constrIndicator c]
1✔
791
  unless (null ics) $ do
1✔
792
    writeSectionHeader "INDICATORS"
1✔
793
    forM_ ics $ \c -> do
1✔
794
      let Just (var,val) = MIP.constrIndicator c
1✔
795
      writeFields ["IF", fromJust (MIP.constrLabel c), MIP.varName var, showValue val]
1✔
796

797
  -- ENDATA section
798
  writeSectionHeader "ENDATA"
1✔
799

800
writeSectionHeader' :: T.Text -> T.Text -> M ()
801
writeSectionHeader' newline s = writeText s >> writeText newline
1✔
802

803
-- Fields start in column 2, 5, 15, 25, 40 and 50
804
writeFields' :: T.Text -> [T.Text] -> M ()
805
writeFields' newline xs0 = f1 xs0 >> writeText newline
1✔
806
  where
807
    -- columns 1-4
808
    f1 [] = return ()
×
809
    f1 [x] = writeChar ' ' >> writeText x
1✔
810
    f1 (x:xs) = do
1✔
811
      writeChar ' '
1✔
812
      writeText x
1✔
813
      let len = T.length x
1✔
814
      when (len < 2) $ writeText $ T.replicate (2 - len) " "
1✔
815
      writeChar ' '
1✔
816
      f2 xs
1✔
817

818
    -- columns 5-14
819
    f2 [] = return ()
×
820
    f2 [x] = writeText x
1✔
821
    f2 (x:xs) = do
1✔
822
      writeText x
1✔
823
      let len = T.length x
1✔
824
      when (len < 9) $ writeText $ T.replicate (9 - len) " "
1✔
825
      writeChar ' '
1✔
826
      f3 xs
1✔
827

828
    -- columns 15-24
829
    f3 [] = return ()
×
830
    f3 [x] = writeText x
1✔
831
    f3 (x:xs) = do
1✔
832
      writeText x
1✔
833
      let len = T.length x
1✔
834
      when (len < 9) $ writeText $ T.replicate (9 - len) " "
1✔
835
      writeChar ' '
1✔
836
      f4 xs
1✔
837

838
    -- columns 25-39
839
    f4 [] = return ()
×
840
    f4 [x] = writeText x
1✔
841
    f4 (x:xs) = do
1✔
842
      writeText x
1✔
843
      let len = T.length x
1✔
844
      when (len < 14) $ writeText $ T.replicate (14 - len) " "
1✔
845
      writeChar ' '
1✔
846
      f5 xs
1✔
847

848
    -- columns 40-49
849
    f5 [] = return ()
×
850
    f5 [x] = writeText x
1✔
851
    f5 (x:xs) = do
×
852
      writeText x
×
853
      let len = T.length x
×
854
      when (len < 19) $ writeText $ T.replicate (19 - len) " "
×
855
      writeChar ' '
×
856
      f6 xs
×
857

858
    -- columns 50-
859
    f6 [] = return ()
×
860
    f6 [x] = writeText x
×
861
    f6 _ = error "MPSFile: >6 fields (this should not happen)"
×
862

863
showValue :: Scientific -> T.Text
864
showValue = fromString . show
1✔
865

866
nameRows :: MIP.Problem r -> MIP.Problem r
867
nameRows mip
1✔
868
  = mip
1✔
869
  { MIP.objectiveFunction = (MIP.objectiveFunction mip){ MIP.objLabel = Just objName' }
1✔
870
  , MIP.constraints = f (MIP.constraints mip) [T.pack $ "row" ++ show n | n <- [(1::Int)..]]
1✔
871
  , MIP.userCuts = f (MIP.userCuts mip) [T.pack $ "usercut" ++ show n | n <- [(1::Int)..]]
×
872
  , MIP.sosConstraints = g (MIP.sosConstraints mip) [T.pack $ "sos" ++ show n | n <- [(1::Int)..]]
×
873
  }
874
  where
875
    objName = MIP.objLabel $ MIP.objectiveFunction mip
1✔
876
    used = Set.fromList $ catMaybes $ objName : [MIP.constrLabel c | c <- MIP.constraints mip ++ MIP.userCuts mip] ++ [MIP.sosLabel c | c <- MIP.sosConstraints mip]
×
877
    objName' = fromMaybe (head [name | n <- [(1::Int)..], let name = T.pack ("obj" ++ show n), name `Set.notMember` used]) objName
×
878

879
    f [] _ = []
1✔
880
    f (c:cs) (name:names)
881
      | isJust (MIP.constrLabel c) = c : f cs (name:names)
×
882
      | name `Set.notMember` used = c{ MIP.constrLabel = Just name } : f cs names
×
883
      | otherwise = f (c:cs) names
×
884
    f _ [] = error "should not happen"
×
885

886
    g [] _ = []
1✔
887
    g (c:cs) (name:names)
888
      | isJust (MIP.sosLabel c) = c : g cs (name:names)
×
889
      | name `Set.notMember` used = c{ MIP.sosLabel = Just name } : g cs names
×
890
      | otherwise = g (c:cs) names
×
891
    g _ [] = error "should not happen"
×
892

893
quadMatrix :: Fractional r => MIP.Expr r -> Map (MIP.Var, MIP.Var) r
894
quadMatrix e = Map.fromList $ do
1✔
895
  let m = Map.fromListWith (+) [(if v1<=v2 then (v1,v2) else (v2,v1), c) | MIP.Term c [v1,v2] <- MIP.terms e]
1✔
896
  ((v1,v2),c) <- Map.toList m
1✔
897
  if v1==v2 then
1✔
898
    [((v1,v2), c)]
1✔
899
  else
900
    [((v1,v2), c/2), ((v2,v1), c/2)]
1✔
901

902
checkAtMostQuadratic :: forall r. MIP.Problem r -> Bool
903
checkAtMostQuadratic mip =  all (all f . MIP.terms) es
1✔
904
  where
905
    es = MIP.objExpr (MIP.objectiveFunction mip) :
1✔
906
         [lhs | c <- MIP.constraints mip ++ MIP.userCuts mip, let lhs = MIP.constrExpr c]
1✔
907
    f :: MIP.Term r -> Bool
908
    f (MIP.Term _ []) = True
1✔
909
    f (MIP.Term _ [_]) = True
1✔
910
    f (MIP.Term _ [_,_]) = True
1✔
911
    f _ = False
×
912

913
normalizeConstTerm :: (Num r, Ord r) => MIP.Problem r -> MIP.Problem r
914
normalizeConstTerm mip =
1✔
915
  mip
1✔
916
  { MIP.constraints = map f (MIP.constraints mip)
1✔
917
  , MIP.userCuts = map f (MIP.userCuts mip)
1✔
918
  }
919
  where
920
    f constr =
1✔
921
      constr
1✔
922
      { MIP.constrLB = MIP.constrLB constr - MIP.Finite offset
1✔
923
      , MIP.constrUB = MIP.constrUB constr - MIP.Finite offset
1✔
924
      , MIP.constrExpr = MIP.Expr [t | t@(MIP.Term _ (_:_)) <- MIP.terms (MIP.constrExpr constr)]
1✔
925
      }
926
      where
927
        offset = sum [c | MIP.Term c [] <- MIP.terms (MIP.constrExpr constr)]
×
928

929
-- ---------------------------------------------------------------------------
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc