packages feed

toysolver-0.9.0: src/ToySolver/Converter/PB.hs

{-# OPTIONS_GHC -Wall #-}
{-# OPTIONS_HADDOCK show-extensions #-}
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeFamilies #-}
-----------------------------------------------------------------------------
-- |
-- Module      :  ToySolver.Converter.PB
-- Copyright   :  (c) Masahiro Sakai 2011-2014,2016-2018
-- License     :  BSD-style
--
-- Maintainer  :  masahiro.sakai@gmail.com
-- Stability   :  experimental
-- Portability :  non-portable
--
-----------------------------------------------------------------------------
module ToySolver.Converter.PB
  ( module ToySolver.Converter.Base
  , module ToySolver.Converter.Tseitin

  -- * Normalization of PB/WBO problems
  , normalizePB
  , normalizeWBO

  -- * Modify objective function
  , ObjType (..)
  , setObj

  -- * Linealization of PB/WBO problems
  , linearizePB
  , linearizeWBO
  , PBLinearizeInfo

  -- * Quadratization of PB problems
  , quadratizePB
  , quadratizePB'
  , PBQuadratizeInfo

  -- * Converting inequality constraints into equality constraints
  , inequalitiesToEqualitiesPB
  , PBInequalitiesToEqualitiesInfo (..)

  -- * Converting constraints into penalty terms in objective function
  , unconstrainPB
  , PBUnconstrainInfo (..)

  -- * PB↔WBO conversion
  , pb2wbo
  , PB2WBOInfo (..)
  , wbo2pb
  , WBO2PBInfo (..)
  , addWBO

  -- * SAT↔PB conversion
  , sat2pb
  , SAT2PBInfo
  , pb2sat
  , pb2satWith
  , PB2SATInfo

  -- * MaxSAT↔WBO conversion
  , maxsat2wbo
  , MaxSAT2WBOInfo
  , wbo2maxsat
  , wbo2maxsatWith
  , WBO2MaxSATInfo

  -- * PB→QUBO conversion
  , pb2qubo'
  , PB2QUBOInfo'

  -- * General data types
  , PBIdentityInfo (..)
  , PBTseitinInfo (..)

  -- * misc
  , pb2lsp
  , wbo2lsp
  , pb2smp
  ) where

import Control.Monad
import Control.Monad.Primitive
import Control.Monad.ST
import qualified Data.Aeson as J
import Data.Aeson ((.=), (.:))
import Data.Array.IArray
import Data.Bits hiding (And (..))
import Data.ByteString.Builder
import Data.Default.Class
import qualified Data.Foldable as F
import Data.IntMap.Strict (IntMap)
import qualified Data.IntMap.Strict as IntMap
import Data.IntSet (IntSet)
import qualified Data.IntSet as IntSet
import Data.List
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as Map
import Data.Maybe
import Data.Primitive.MutVar
import qualified Data.Sequence as Seq
import Data.Set (Set)
import qualified Data.Set as Set
import qualified Data.PseudoBoolean as PBFile

import ToySolver.Converter.Base
import qualified ToySolver.Converter.PB.Internal.Product as Product
import ToySolver.Converter.Tseitin
import qualified ToySolver.FileFormat.CNF as CNF
import ToySolver.Internal.JSON
import qualified ToySolver.SAT.Types as SAT
import qualified ToySolver.SAT.Encoder.Tseitin as Tseitin
import ToySolver.SAT.Encoder.Tseitin (Formula (..))
import qualified ToySolver.SAT.Encoder.PB as PB
import qualified ToySolver.SAT.Encoder.PBNLC as PBNLC
import ToySolver.SAT.Internal.JSON
import ToySolver.SAT.Store.CNF
import ToySolver.SAT.Store.PB

-- -----------------------------------------------------------------------------

-- XXX: we do not normalize objective function, because normalization might
-- introduce constant terms, but OPB file format does not allow constant terms.
--
-- Options:
-- (1) not normalize objective function (current implementation),
-- (2) normalize and simply delete constant terms (in pseudo-boolean package?),
-- (3) normalize and introduce dummy variable to make constant terms
--     into non-constant terms (in pseudo-boolean package?).
normalizePB :: PBFile.Formula -> PBFile.Formula
normalizePB formula =
  formula
  { PBFile.pbConstraints =
      map normalizePBConstraint (PBFile.pbConstraints formula)
  }

normalizeWBO :: PBFile.SoftFormula -> PBFile.SoftFormula
normalizeWBO formula =
  formula
  { PBFile.wboConstraints =
      map (\(w,constr) -> (w, normalizePBConstraint constr)) (PBFile.wboConstraints formula)
  }

normalizePBConstraint :: PBFile.Constraint -> PBFile.Constraint
normalizePBConstraint (lhs,op,rhs) =
  case mapAccumL h 0 lhs of
    (offset, lhs') -> (lhs', op, rhs - offset)
  where
    h s (w,[x]) | x < 0 = (s+w, (-w,[-x]))
    h s t = (s,t)

-- -----------------------------------------------------------------------------

data ObjType = ObjNone | ObjMaxOne | ObjMaxZero
  deriving (Eq, Ord, Enum, Bounded, Show)

setObj :: ObjType -> PBFile.Formula -> PBFile.Formula
setObj objType formula = formula{ PBFile.pbObjectiveFunction = Just obj2 }
  where
    obj2 = genObj objType formula

genObj :: ObjType -> PBFile.Formula -> PBFile.Sum
genObj objType formula =
  case objType of
    ObjNone    -> []
    ObjMaxOne  -> [(1,[-v]) | v <- [1 .. PBFile.pbNumVars formula]] -- minimize false literals
    ObjMaxZero -> [(1,[ v]) | v <- [1 .. PBFile.pbNumVars formula]] -- minimize true literals

-- -----------------------------------------------------------------------------

data PBIdentityInfo = PBIdentityInfo
  deriving (Show, Eq)

instance Transformer PBIdentityInfo where
  type Source PBIdentityInfo = SAT.Model
  type Target PBIdentityInfo = SAT.Model

instance ForwardTransformer PBIdentityInfo where
  transformForward _ = id

instance BackwardTransformer PBIdentityInfo where
  transformBackward _ = id

instance ObjValueTransformer PBIdentityInfo where
  type SourceObjValue PBIdentityInfo = Integer
  type TargetObjValue PBIdentityInfo = Integer

instance ObjValueForwardTransformer PBIdentityInfo where
  transformObjValueForward _ = id

instance ObjValueBackwardTransformer PBIdentityInfo where
  transformObjValueBackward _ = id

instance J.ToJSON PBIdentityInfo where
  toJSON PBIdentityInfo =
    J.object
    [ "type" .= ("PBIdentityInfo" :: J.Value)
    ]

instance J.FromJSON PBIdentityInfo where
  parseJSON = withTypedObject "PBIdentityInfo" $ \_ -> pure PBIdentityInfo


newtype PBTseitinInfo = PBTseitinInfo TseitinInfo
  deriving (Eq, Show)

instance Transformer PBTseitinInfo where
  type Source PBTseitinInfo = SAT.Model
  type Target PBTseitinInfo = SAT.Model

instance ForwardTransformer PBTseitinInfo where
  transformForward (PBTseitinInfo info) = transformForward info

instance BackwardTransformer PBTseitinInfo where
  transformBackward (PBTseitinInfo info) = transformBackward info

instance ObjValueTransformer PBTseitinInfo where
  type SourceObjValue PBTseitinInfo = Integer
  type TargetObjValue PBTseitinInfo = Integer

instance ObjValueForwardTransformer PBTseitinInfo where
  transformObjValueForward _ = id

instance ObjValueBackwardTransformer PBTseitinInfo where
  transformObjValueBackward _ = id

instance J.ToJSON PBTseitinInfo where
  toJSON (PBTseitinInfo info) =
    J.object
    [ "type" .= ("PBTseitinInfo" :: J.Value)
    , "base" .= info
    ]

instance J.FromJSON PBTseitinInfo where
  parseJSON = withTypedObject "PBTseitinInfo" $ \obj ->
    PBTseitinInfo <$> obj .: "base"

-- -----------------------------------------------------------------------------

type PBLinearizeInfo = PBTseitinInfo

linearizePB :: PBFile.Formula -> Bool -> (PBFile.Formula, PBLinearizeInfo)
linearizePB formula usePB = runST $ do
  db <- newPBStore
  SAT.newVars_ db (PBFile.pbNumVars formula)
  tseitin <-  Tseitin.newEncoderWithPBLin db
  Tseitin.setUsePB tseitin usePB
  pbnlc <- PBNLC.newEncoder db tseitin
  cs' <- forM (PBFile.pbConstraints formula) $ \(lhs,op,rhs) -> do
    let p = case op of
              PBFile.Ge -> Tseitin.polarityPos
              PBFile.Eq -> Tseitin.polarityBoth
    lhs' <- PBNLC.linearizePBSumWithPolarity pbnlc p lhs
    return ([(c,[l]) | (c,l) <- lhs'],op,rhs)
  obj' <-
    case PBFile.pbObjectiveFunction formula of
      Nothing -> return Nothing
      Just obj -> do
        obj' <- PBNLC.linearizePBSumWithPolarity pbnlc Tseitin.polarityNeg obj
        return $ Just [(c, [l]) | (c,l) <- obj']
  formula' <- getPBFormula db
  defs <- Tseitin.getDefinitions tseitin
  return $
    ( formula'
      { PBFile.pbObjectiveFunction = obj'
      , PBFile.pbConstraints = cs' ++ PBFile.pbConstraints formula'
      , PBFile.pbNumConstraints = PBFile.pbNumConstraints formula + PBFile.pbNumConstraints formula'
      }
    , PBTseitinInfo $ TseitinInfo (PBFile.pbNumVars formula) (PBFile.pbNumVars formula') defs
    )

-- -----------------------------------------------------------------------------

linearizeWBO :: PBFile.SoftFormula -> Bool -> (PBFile.SoftFormula, PBLinearizeInfo)
linearizeWBO formula usePB = runST $ do
  db <- newPBStore
  SAT.newVars_ db (PBFile.wboNumVars formula)
  tseitin <-  Tseitin.newEncoderWithPBLin db
  Tseitin.setUsePB tseitin usePB
  pbnlc <- PBNLC.newEncoder db tseitin
  cs' <- forM (PBFile.wboConstraints formula) $ \(cost,(lhs,op,rhs)) -> do
    let p = case op of
              PBFile.Ge -> Tseitin.polarityPos
              PBFile.Eq -> Tseitin.polarityBoth
    lhs' <- PBNLC.linearizePBSumWithPolarity pbnlc p lhs
    return (cost,([(c,[l]) | (c,l) <- lhs'],op,rhs))
  formula' <- getPBFormula db
  defs <- Tseitin.getDefinitions tseitin
  return $
    ( PBFile.SoftFormula
      { PBFile.wboTopCost = PBFile.wboTopCost formula
      , PBFile.wboConstraints = cs' ++ [(Nothing, constr) | constr <- PBFile.pbConstraints formula']
      , PBFile.wboNumVars = PBFile.pbNumVars formula'
      , PBFile.wboNumConstraints = PBFile.wboNumConstraints formula + PBFile.pbNumConstraints formula'
      }
    , PBTseitinInfo $ TseitinInfo (PBFile.wboNumVars formula) (PBFile.pbNumVars formula') defs
    )

-- -----------------------------------------------------------------------------

type PBQuadratizeInfo = PBTseitinInfo

-- | Quandratize PBO/PBS problem without introducing additional constraints.
quadratizePB :: PBFile.Formula -> ((PBFile.Formula, Integer), PBQuadratizeInfo)
quadratizePB formula = quadratizePB' (formula, SAT.pbUpperBound obj)
  where
    obj = fromMaybe [] $ PBFile.pbObjectiveFunction formula

-- | Quandratize PBO/PBS problem without introducing additional constraints.
quadratizePB' :: (PBFile.Formula, Integer) -> ((PBFile.Formula, Integer), PBQuadratizeInfo)
quadratizePB' (formula, maxObj) =
  ( ( PBFile.Formula
      { PBFile.pbObjectiveFunction = Just $ conv obj ++ penalty
      , PBFile.pbConstraints = [(conv lhs, op, rhs) | (lhs,op,rhs) <- PBFile.pbConstraints formula]
      , PBFile.pbNumVars = nv2
      , PBFile.pbNumConstraints = PBFile.pbNumConstraints formula
      }
    , maxObj
    )
  , PBTseitinInfo $ TseitinInfo nv1 nv2 (IntMap.fromList [(v, And [atom l1, atom l2]) | (v, (l1,l2)) <- prodDefs])
  )
  where
    nv1 = PBFile.pbNumVars formula
    nv2 = PBFile.pbNumVars formula + Set.size termsToReplace

    degGe3Terms :: Set IntSet
    degGe3Terms = collectDegGe3Terms formula

    m :: Map IntSet (IntSet,IntSet)
    m = Product.decomposeToBinaryProducts degGe3Terms

    termsToReplace :: Set IntSet
    termsToReplace = go ts0 Set.empty
      where
        ts0 = concat [[t1,t2] | t <- Set.toList degGe3Terms, let (t1,t2) = m Map.! t]
        go [] !ret = ret
        go (t : ts) !ret
          | IntSet.size t < 2  = go ts ret
          | t `Set.member` ret = go ts ret
          | otherwise =
              case Map.lookup t m of
                Nothing -> error "quadratizePB.termsToReplace: should not happen"
                Just (t1,t2) -> go (t1 : t2 : ts) (Set.insert t ret)

    fromV :: IntMap IntSet
    toV :: Map IntSet Int
    (fromV, toV) = (IntMap.fromList l, Map.fromList [(s,v) | (v,s) <- l])
      where
        l = zip [PBFile.pbNumVars formula + 1 ..] (Set.toList termsToReplace)

    prodDefs :: [(SAT.Var, (SAT.Var, SAT.Var))]
    prodDefs = [(v, (f t1, f t2)) | (v,t) <- IntMap.toList fromV, let (t1,t2) = m Map.! t]
      where
        f t
          | IntSet.size t == 1 = head (IntSet.toList t)
          | otherwise =
               case Map.lookup t toV of
                 Nothing -> error "quadratizePB.prodDefs: should not happen"
                 Just v -> v

    obj :: PBFile.Sum
    obj = fromMaybe [] $ PBFile.pbObjectiveFunction formula

    minObj :: Integer
    minObj = SAT.pbLowerBound obj

    penalty :: PBFile.Sum
    penalty = [(w * w2, ts) | (w,ts) <- concat [p x y z | (z,(x,y)) <- prodDefs]]
      where
        -- The penalty function P(x,y,z) = xy − 2xz − 2yz + 3z is such that
        -- P(x,y,z)=0 when z⇔xy and P(x,y,z)>0 when z⇎xy.
        p x y z = [(1,[x,y]), (-2,[x,z]), (-2,[y,z]), (3,[z])]
        w2 = max (maxObj - minObj) 0 + 1

    conv :: PBFile.Sum -> PBFile.Sum
    conv s = [(w, f t) | (w,t) <- s]
      where
        f t =
          case Map.lookup t' toV of
            Just v  -> [v]
            Nothing
              | IntSet.size t' >= 3 -> map g [t1, t2]
              | otherwise -> t
          where
            t' = IntSet.fromList t
            (t1, t2) = m Map.! t'
        g t
          | IntSet.size t == 1 = head $ IntSet.toList t
          | otherwise = toV Map.! t

    atom :: SAT.Lit -> Formula
    atom l
      | l < 0 = Not (Atom (- l))
      | otherwise = Atom l


collectDegGe3Terms :: PBFile.Formula -> Set IntSet
collectDegGe3Terms formula = Set.fromList [t' | t <- terms, let t' = IntSet.fromList t, IntSet.size t' >= 3]
  where
    sums = maybeToList (PBFile.pbObjectiveFunction formula) ++
           [lhs | (lhs,_,_) <- PBFile.pbConstraints formula]
    terms = [t | s <- sums, (_,t) <- s]

-- -----------------------------------------------------------------------------

-- | Convert inequality constraints into equality constraints by introducing surpass variables.
inequalitiesToEqualitiesPB :: PBFile.Formula -> (PBFile.Formula, PBInequalitiesToEqualitiesInfo)
inequalitiesToEqualitiesPB formula = runST $ do
  db <- newPBStore
  SAT.newVars_ db (PBFile.pbNumVars formula)

  defs <- liftM catMaybes $ forM (PBFile.pbConstraints formula) $ \constr -> do
    case constr of
      (lhs, PBFile.Eq, rhs) -> do
        SAT.addPBNLExactly db lhs rhs
        return Nothing
      (lhs, PBFile.Ge, rhs) -> do
        case asClause (lhs,rhs) of
          Just clause -> do
            SAT.addPBNLExactly db [(1, [- l | l <- clause])] 0
            return Nothing
          Nothing -> do
            let maxSurpass = max (SAT.pbUpperBound lhs - rhs) 0
                maxSurpassNBits = head [i | i <- [0..], maxSurpass < bit i]
            vs <- SAT.newVars db maxSurpassNBits
            SAT.addPBNLExactly db (lhs ++ [(-c,[x]) | (c,x) <- zip (iterate (*2) 1) vs]) rhs
            if maxSurpassNBits > 0 then do
              return $ Just (lhs, rhs, vs)
            else
              return Nothing

  formula' <- getPBFormula db
  return
    ( formula'{ PBFile.pbObjectiveFunction = PBFile.pbObjectiveFunction formula }
    , PBInequalitiesToEqualitiesInfo (PBFile.pbNumVars formula) (PBFile.pbNumVars formula') defs
    )
  where
    asLinSum :: SAT.PBSum -> Maybe (SAT.PBLinSum, Integer)
    asLinSum s = do
      ret <- forM s $ \(c, ls) -> do
        case ls of
          [] -> return (Nothing, c)
          [l] -> return (Just (c,l), 0)
          _ -> mzero
      return (catMaybes (map fst ret), sum (map snd ret))

    asClause :: (SAT.PBSum, Integer) -> Maybe SAT.Clause
    asClause (lhs, rhs) = do
      (lhs', off) <- asLinSum lhs
      let rhs' = rhs - off
      case SAT.normalizePBLinAtLeast (lhs', rhs') of
        (lhs'', 1) | all (\(c,_) -> c == 1) lhs'' -> return (map snd lhs'')
        _ -> mzero

data PBInequalitiesToEqualitiesInfo
  = PBInequalitiesToEqualitiesInfo !Int !Int [(PBFile.Sum, Integer, [SAT.Var])]
  deriving (Eq, Show)

instance Transformer PBInequalitiesToEqualitiesInfo where
  type Source PBInequalitiesToEqualitiesInfo = SAT.Model
  type Target PBInequalitiesToEqualitiesInfo = SAT.Model

instance ForwardTransformer PBInequalitiesToEqualitiesInfo where
  transformForward (PBInequalitiesToEqualitiesInfo _nv1 nv2 defs) m =
    array (1, nv2) $ assocs m ++ [(v, testBit n i) | (lhs, rhs, vs) <- defs, let n = SAT.evalPBSum m lhs - rhs, (i,v) <- zip [0..] vs]

instance BackwardTransformer PBInequalitiesToEqualitiesInfo where
  transformBackward (PBInequalitiesToEqualitiesInfo nv1 _nv2 _defs) = SAT.restrictModel nv1

instance ObjValueTransformer PBInequalitiesToEqualitiesInfo where
  type SourceObjValue PBInequalitiesToEqualitiesInfo = Integer
  type TargetObjValue PBInequalitiesToEqualitiesInfo = Integer

instance ObjValueForwardTransformer PBInequalitiesToEqualitiesInfo where
  transformObjValueForward _ = id

instance ObjValueBackwardTransformer PBInequalitiesToEqualitiesInfo where
  transformObjValueBackward _ = id

instance J.ToJSON PBInequalitiesToEqualitiesInfo where
  toJSON (PBInequalitiesToEqualitiesInfo nv1 nv2 defs) =
    J.object
    [ "type" .= ("PBInequalitiesToEqualitiesInfo" :: J.Value)
    , "num_original_variables" .= nv1
    , "num_transformed_variables" .= nv2
    , "slack" .=
        [ J.object
          [ "lhs" .= jPBSum lhs
          , "rhs" .= rhs
          , "slack" .= [jVarName v :: J.Value | v <- vs]
          ]
        | (lhs, rhs, vs) <- defs
        ]
    ]

instance J.FromJSON PBInequalitiesToEqualitiesInfo where
  parseJSON = withTypedObject "PBInequalitiesToEqualitiesInfo" $ \obj -> do
    PBInequalitiesToEqualitiesInfo
      <$> obj .: "num_original_variables"
      <*> obj .: "num_transformed_variables"
      <*> (mapM f =<< obj .: "slack")
    where
      f = J.withObject "slack" $ \obj -> do
        lhs <- parsePBSum =<< obj .: "lhs"
        rhs <- obj .: "rhs"
        vs <- mapM g =<< obj .: "slack"
        return (lhs, rhs, vs)
      g ('x' : rest) = pure $! read rest
      g s = fail ("fail to parse variable: " ++ show s)

-- -----------------------------------------------------------------------------

unconstrainPB :: PBFile.Formula -> ((PBFile.Formula, Integer), PBUnconstrainInfo)
unconstrainPB formula = (unconstrainPB' formula', PBUnconstrainInfo info)
  where
    (formula', info) = inequalitiesToEqualitiesPB formula

newtype PBUnconstrainInfo = PBUnconstrainInfo PBInequalitiesToEqualitiesInfo
  deriving (Eq, Show)

instance Transformer PBUnconstrainInfo where
  -- type Source PBUnconstrainInfo = Source PBInequalitiesToEqualitiesInfo
  type Source PBUnconstrainInfo = SAT.Model
  -- type Target PBUnconstrainInfo = Target PBInequalitiesToEqualitiesInfo
  type Target PBUnconstrainInfo = SAT.Model

instance ForwardTransformer PBUnconstrainInfo where
  transformForward (PBUnconstrainInfo info) = transformForward info

instance BackwardTransformer PBUnconstrainInfo where
  transformBackward (PBUnconstrainInfo info) = transformBackward info

instance ObjValueTransformer PBUnconstrainInfo where
  -- type SourceObjValue PBUnconstrainInfo = SourceObjValue PBInequalitiesToEqualitiesInfo
  type SourceObjValue PBUnconstrainInfo = Integer
  -- type TargetObjValue PBUnconstrainInfo = TargetObjValue PBInequalitiesToEqualitiesInfo
  type TargetObjValue PBUnconstrainInfo = Integer

instance ObjValueForwardTransformer PBUnconstrainInfo where
  transformObjValueForward (PBUnconstrainInfo info) = transformObjValueForward info

instance ObjValueBackwardTransformer PBUnconstrainInfo where
  transformObjValueBackward (PBUnconstrainInfo info) = transformObjValueBackward info

instance J.ToJSON PBUnconstrainInfo where
  toJSON (PBUnconstrainInfo info) =
    J.object
    [ "type" .= ("PBUnconstrainInfo" :: J.Value)
    , "base" .= info
    ]

instance J.FromJSON PBUnconstrainInfo where
  parseJSON = withTypedObject "PBUnconstrainInfo" $ \obj ->
    PBUnconstrainInfo <$> obj .: "base"

unconstrainPB' :: PBFile.Formula -> (PBFile.Formula, Integer)
unconstrainPB' formula =
  ( formula
    { PBFile.pbObjectiveFunction = Just $ obj1 ++ obj2
    , PBFile.pbConstraints = []
    , PBFile.pbNumConstraints = 0
    }
  , obj1ub
  )
  where
    obj1 = fromMaybe [] (PBFile.pbObjectiveFunction formula)
    obj1ub = SAT.pbUpperBound obj1
    obj1lb = SAT.pbLowerBound obj1
    p = obj1ub - obj1lb + 1
    obj2 = [(p*c, IntSet.toList ls) | (ls, c) <- Map.toList obj2', c /= 0]
    obj2' = Map.unionsWith (+) [sq ((-rhs, []) : lhs) | (lhs, PBFile.Eq, rhs) <- PBFile.pbConstraints formula]
    sq ts = Map.fromListWith (+) $ do
              (c1,ls1) <- ts
              (c2,ls2) <- ts
              let ls3 = IntSet.fromList ls1 `IntSet.union` IntSet.fromList ls2
              guard $ not $ isFalse ls3
              return (ls3, c1*c2)
    isFalse ls = not $ IntSet.null $ ls `IntSet.intersection` IntSet.map negate ls

-- -----------------------------------------------------------------------------

pb2qubo' :: PBFile.Formula -> ((PBFile.Formula, Integer), PB2QUBOInfo')
pb2qubo' formula = ((formula2, th2), ComposedTransformer info1 info2)
  where
    ((formula1, th1), info1) = unconstrainPB formula
    ((formula2, th2), info2) = quadratizePB' (formula1, th1)

type PB2QUBOInfo' = ComposedTransformer PBUnconstrainInfo PBQuadratizeInfo

-- -----------------------------------------------------------------------------

pb2wbo :: PBFile.Formula -> (PBFile.SoftFormula, PB2WBOInfo)
pb2wbo formula
  = ( PBFile.SoftFormula
      { PBFile.wboTopCost = Nothing
      , PBFile.wboConstraints = cs1 ++ cs2
      , PBFile.wboNumVars = PBFile.pbNumVars formula
      , PBFile.wboNumConstraints = PBFile.pbNumConstraints formula + length cs2
      }
    , PB2WBOInfo offset
    )
  where
    cs1 = [(Nothing, c) | c <- PBFile.pbConstraints formula]
    (cs2, offset) =
      case PBFile.pbObjectiveFunction formula of
        Nothing -> ([], 0)
        Just e  ->
          ( [ if w >= 0
              then (Just w,       ([(-1,ls)], PBFile.Ge, 0))
              else (Just (abs w), ([(1,ls)],  PBFile.Ge, 1))
            | (w,ls) <- e
            ]
          , sum [if w >= 0 then 0 else - w | (w, _) <- e]
          )

newtype PB2WBOInfo = PB2WBOInfo Integer
  deriving (Eq, Show)

instance Transformer PB2WBOInfo where
  type Source PB2WBOInfo = SAT.Model
  type Target PB2WBOInfo = SAT.Model

instance ForwardTransformer PB2WBOInfo where
  transformForward _ = id

instance BackwardTransformer PB2WBOInfo where
  transformBackward _ = id

instance ObjValueTransformer PB2WBOInfo where
  type SourceObjValue PB2WBOInfo = Integer
  type TargetObjValue PB2WBOInfo = Integer

instance ObjValueForwardTransformer PB2WBOInfo where
  transformObjValueForward (PB2WBOInfo offset) obj = obj + offset

instance ObjValueBackwardTransformer PB2WBOInfo where
  transformObjValueBackward (PB2WBOInfo offset) obj = obj - offset

instance J.ToJSON PB2WBOInfo where
  toJSON (PB2WBOInfo offset) =
    J.object
    [ "type" .= J.String "PB2WBOInfo"
    , "objective_function_offset" .= offset
    ]

instance J.FromJSON PB2WBOInfo where
  parseJSON =
    withTypedObject "PB2WBOInfo" $ \obj -> do
      offset <- obj .: "objective_function_offset"
      pure (PB2WBOInfo offset)

wbo2pb :: PBFile.SoftFormula -> (PBFile.Formula, WBO2PBInfo)
wbo2pb wbo = runST $ do
  let nv = PBFile.wboNumVars wbo
  db <- newPBStore
  (obj, defs) <- addWBO db wbo
  formula <- getPBFormula db
  return
    ( formula{ PBFile.pbObjectiveFunction = Just obj }
    , WBO2PBInfo nv (PBFile.pbNumVars formula) defs
    )

data WBO2PBInfo = WBO2PBInfo !Int !Int (SAT.VarMap PBFile.Constraint)
  deriving (Show, Eq)

instance Transformer WBO2PBInfo where
  type Source WBO2PBInfo = SAT.Model
  type Target WBO2PBInfo = SAT.Model

instance ForwardTransformer WBO2PBInfo where
  transformForward (WBO2PBInfo _nv1 nv2 defs) m =
    array (1, nv2) $ assocs m ++ [(v, SAT.evalPBConstraint m constr) | (v, constr) <- IntMap.toList defs]

instance BackwardTransformer WBO2PBInfo where
  transformBackward (WBO2PBInfo nv1 _nv2 _defs) = SAT.restrictModel nv1

instance ObjValueTransformer WBO2PBInfo where
  type SourceObjValue WBO2PBInfo = Integer
  type TargetObjValue WBO2PBInfo = Integer

instance ObjValueForwardTransformer WBO2PBInfo where
  transformObjValueForward _ = id

instance ObjValueBackwardTransformer WBO2PBInfo where
  transformObjValueBackward _ = id

instance J.ToJSON WBO2PBInfo where
  toJSON (WBO2PBInfo nv1 nv2 defs) =
    J.object
    [ "type" .= J.String "WBO2PBInfo"
    , "num_original_variables" .= nv1
    , "num_transformed_variables" .= nv2
    , "definitions" .= J.object
        [ jVarName v .= jPBConstraint constr
        | (v, constr) <- IntMap.toList defs
        ]
    ]

instance J.FromJSON WBO2PBInfo where
  parseJSON = withTypedObject "WBO2PBInfo" $ \obj -> do
    defs <- obj .: "definitions"
    WBO2PBInfo
      <$> obj .: "num_original_variables"
      <*> obj .: "num_transformed_variables"
      <*> (IntMap.fromList <$> mapM f (Map.toList defs))
    where
      f (name, constr) = do
        v <- parseVarNameText name
        constr' <- parsePBConstraint constr
        return (v, constr')

addWBO :: (PrimMonad m, SAT.AddPBNL m enc) => enc -> PBFile.SoftFormula -> m (SAT.PBSum, (SAT.VarMap PBFile.Constraint))
addWBO db wbo = do
  SAT.newVars_ db $ PBFile.wboNumVars wbo

  objRef <- newMutVar []
  objOffsetRef <- newMutVar 0
  defsRef <- newMutVar []
  trueLitRef <- newMutVar SAT.litUndef

  forM_ (PBFile.wboConstraints wbo) $ \(cost, constr@(lhs,op,rhs)) -> do
    case cost of
      Nothing -> do
        case op of
          PBFile.Ge -> SAT.addPBNLAtLeast db lhs rhs
          PBFile.Eq -> SAT.addPBNLExactly db lhs rhs
        trueLit <- readMutVar trueLitRef
        when (trueLit == SAT.litUndef) $ do
          case detectTrueLit constr of
            Nothing -> return ()
            Just l -> writeMutVar trueLitRef l
      Just w -> do
        case op of
          PBFile.Ge -> do
            case lhs of
              [(c,ls)] | c > 0 && (rhs + c - 1) `div` c == 1 -> do
                -- c ∧L ≥ rhs ⇔ ∧L ≥ ⌈rhs / c⌉
                -- ∧L ≥ 1 ⇔ ∧L
                -- obj += w * (1 - ∧L)
                unless (null ls) $ do
                  modifyMutVar objRef (\obj -> (-w,ls) : obj)
                  modifyMutVar objOffsetRef (+ w)
              [(c,ls)] | c < 0 && (rhs + abs c - 1) `div` abs c + 1 == 1 -> do
                -- c*∧L ≥ rhs ⇔ -1*∧L ≥ ⌈rhs / abs c⌉ ⇔ (1 - ∧L) ≥ ⌈rhs / abs c⌉ + 1 ⇔ ¬∧L ≥ ⌈rhs / abs c⌉ + 1
                -- ¬∧L ≥ 1 ⇔ ¬∧L
                -- obj += w * ∧L
                if null ls then do
                  modifyMutVar objOffsetRef (+ w)
                else do
                  modifyMutVar objRef ((w,ls) :)
              _ | rhs > 0 && and [c >= rhs && length ls == 1 | (c,ls) <- lhs] -> do
                -- ∑L ≥ 1 ⇔ ∨L ⇔ ¬∧¬L
                -- obj += w * ∧¬L
                if null lhs then do
                  modifyMutVar objOffsetRef (+ w)
                else do
                  modifyMutVar objRef ((w, [-l | (_,[l]) <- lhs]) :)
              _ -> do
                sel <- SAT.newVar db
                SAT.addPBNLAtLeastSoft db sel lhs rhs
                modifyMutVar objRef ((w,[-sel]) :)
                modifyMutVar defsRef ((sel,constr) :)
          PBFile.Eq -> do
            sel <- SAT.newVar db
            SAT.addPBNLExactlySoft db sel lhs rhs
            modifyMutVar objRef ((w,[-sel]) :)
            modifyMutVar defsRef ((sel,constr) :)

  offset <- readMutVar objOffsetRef
  when (offset /= 0) $ do
    l <- readMutVar trueLitRef
    trueLit <-
      if l /= SAT.litUndef then
        return l
      else do
        v <- SAT.newVar db
        SAT.addClause db [v]
        modifyMutVar defsRef ((v, ([], PBFile.Ge, 0)) :)
        return v
    modifyMutVar objRef ((offset,[trueLit]) :)

  obj <- liftM reverse $ readMutVar objRef
  defs <- liftM IntMap.fromList $ readMutVar defsRef

  case PBFile.wboTopCost wbo of
    Nothing -> return ()
    Just t -> SAT.addPBNLAtMost db obj (t - 1)

  return (obj, defs)


detectTrueLit :: PBFile.Constraint -> Maybe SAT.Lit
detectTrueLit (lhs, op, rhs) =
  case op of
    PBFile.Ge -> f lhs rhs
    PBFile.Eq -> f lhs rhs `mplus` f [(- c, ls) | (c,ls) <- lhs] (- rhs)
  where
    f [(c, [l])] rhs
      | c > 0 && (rhs + c - 1) `div` c == 1 =
          -- c l ≥ rhs ↔ l ≥ ⌈rhs / c⌉
          return l
      | c < 0 && rhs `div` c == 0 =
          -- c l ≥ rhs ↔ l ≤ ⌊rhs / c⌋
          return (- l)
    f _ _ = Nothing

-- -----------------------------------------------------------------------------

type SAT2PBInfo = IdentityTransformer SAT.Model

sat2pb :: CNF.CNF -> (PBFile.Formula, SAT2PBInfo)
sat2pb cnf
  = ( PBFile.Formula
      { PBFile.pbObjectiveFunction = Nothing
      , PBFile.pbConstraints = map f (CNF.cnfClauses cnf)
      , PBFile.pbNumVars = CNF.cnfNumVars cnf
      , PBFile.pbNumConstraints = CNF.cnfNumClauses cnf
      }
    , IdentityTransformer
    )
  where
    f clause = ([(1,[l]) | l <- SAT.unpackClause clause], PBFile.Ge, 1)

type PB2SATInfo = TseitinInfo

-- | Convert a pseudo boolean formula φ to a equisatisfiable CNF formula ψ
-- together with two functions f and g such that:
--
-- * if M ⊨ φ then f(M) ⊨ ψ
--
-- * if M ⊨ ψ then g(M) ⊨ φ
--
pb2sat :: PBFile.Formula -> (CNF.CNF, PB2SATInfo)
pb2sat = pb2satWith def

pb2satWith :: PB.Strategy -> PBFile.Formula -> (CNF.CNF, PB2SATInfo)
pb2satWith strategy formula = runST $ do
  db <- newCNFStore
  let nv1 = PBFile.pbNumVars formula
  SAT.newVars_ db nv1
  tseitin <-  Tseitin.newEncoder db
  pb <- PB.newEncoderWithStrategy tseitin strategy
  pbnlc <- PBNLC.newEncoder pb tseitin
  forM_ (PBFile.pbConstraints formula) $ \(lhs,op,rhs) -> do
    case op of
      PBFile.Ge -> SAT.addPBNLAtLeast pbnlc lhs rhs
      PBFile.Eq -> SAT.addPBNLExactly pbnlc lhs rhs
  cnf <- getCNFFormula db
  defs <- Tseitin.getDefinitions tseitin
  return (cnf, TseitinInfo nv1 (CNF.cnfNumVars cnf) defs)

-- -----------------------------------------------------------------------------

type MaxSAT2WBOInfo = PBIdentityInfo

maxsat2wbo :: CNF.WCNF -> (PBFile.SoftFormula, MaxSAT2WBOInfo)
maxsat2wbo
  CNF.WCNF
  { CNF.wcnfTopCost = top
  , CNF.wcnfClauses = cs
  , CNF.wcnfNumVars = nv
  , CNF.wcnfNumClauses = nc
  } =
  ( PBFile.SoftFormula
    { PBFile.wboTopCost = Nothing
    , PBFile.wboConstraints = map f cs
    , PBFile.wboNumVars = nv
    , PBFile.wboNumConstraints = nc
    }
  , PBIdentityInfo
  )
  where
    f (w,c)
     | w>=top    = (Nothing, p) -- hard constraint
     | otherwise = (Just w, p)  -- soft constraint
     where
       p = ([(1,[l]) | l <- SAT.unpackClause c], PBFile.Ge, 1)

type WBO2MaxSATInfo = PBTseitinInfo

wbo2maxsat :: PBFile.SoftFormula -> (CNF.WCNF, WBO2MaxSATInfo)
wbo2maxsat = wbo2maxsatWith def

wbo2maxsatWith :: PB.Strategy -> PBFile.SoftFormula -> (CNF.WCNF, WBO2MaxSATInfo)
wbo2maxsatWith strategy formula = runST $ do
  db <- newCNFStore
  SAT.newVars_ db (PBFile.wboNumVars formula)
  tseitin <-  Tseitin.newEncoder db
  pb <- PB.newEncoderWithStrategy tseitin strategy
  pbnlc <- PBNLC.newEncoder pb tseitin

  softClauses <- liftM mconcat $ forM (PBFile.wboConstraints formula) $ \(cost, (lhs,op,rhs)) -> do
    case cost of
      Nothing ->
        case op of
          PBFile.Ge -> SAT.addPBNLAtLeast pbnlc lhs rhs >> return mempty
          PBFile.Eq -> SAT.addPBNLExactly pbnlc lhs rhs >> return mempty
      Just c -> do
        case op of
          PBFile.Ge -> do
            lhs2 <- PBNLC.linearizePBSumWithPolarity pbnlc Tseitin.polarityPos lhs
            let (lhs3,rhs3) = SAT.normalizePBLinAtLeast (lhs2,rhs)
            if rhs3==1 && and [c==1 | (c,_) <- lhs3] then
              return $ Seq.singleton (c, SAT.packClause [l | (_,l) <- lhs3])
            else do
              lit <- PB.encodePBLinAtLeast pb (lhs3,rhs3)
              return $ Seq.singleton (c, SAT.packClause [lit])
          PBFile.Eq -> do
            lhs2 <- PBNLC.linearizePBSumWithPolarity pbnlc Tseitin.polarityBoth lhs
            lit1 <- PB.encodePBLinAtLeast pb (lhs2, rhs)
            lit2 <- PB.encodePBLinAtLeast pb ([(-c, l) | (c,l) <- lhs2], negate rhs)
            lit <- Tseitin.encodeConjWithPolarity tseitin Tseitin.polarityPos [lit1,lit2]
            return $ Seq.singleton (c, SAT.packClause [lit])

  case PBFile.wboTopCost formula of
    Nothing -> return ()
    Just top -> SAT.addPBNLAtMost pbnlc [(c, [-l | l <- SAT.unpackClause clause]) | (c,clause) <- F.toList softClauses] (top - 1)

  let top = F.sum (fst <$> softClauses) + 1
  cnf <- getCNFFormula db
  let cs = softClauses <> Seq.fromList [(top, clause) | clause <- CNF.cnfClauses cnf]
  let wcnf = CNF.WCNF
             { CNF.wcnfNumVars = CNF.cnfNumVars cnf
             , CNF.wcnfNumClauses = Seq.length cs
             , CNF.wcnfTopCost = top
             , CNF.wcnfClauses = F.toList cs
             }
  defs <- Tseitin.getDefinitions tseitin
  return (wcnf, PBTseitinInfo (TseitinInfo (PBFile.wboNumVars formula) (CNF.cnfNumVars cnf) defs))

-- -----------------------------------------------------------------------------

pb2lsp :: PBFile.Formula -> Builder
pb2lsp formula =
  byteString "function model() {\n" <>
  decls <>
  constrs <>
  obj2 <>
  "}\n"
  where
    nv = PBFile.pbNumVars formula

    decls = byteString "  for [i in 1.." <> intDec nv <> byteString "] x[i] <- bool();\n"

    constrs = mconcat
      [ byteString "  constraint " <>
        showConstrLSP c <>
        ";\n"
      | c <- PBFile.pbConstraints formula
      ]

    obj2 =
      case PBFile.pbObjectiveFunction formula of
        Just obj' -> byteString "  minimize " <> showSumLSP obj' <> ";\n"
        Nothing -> mempty

wbo2lsp :: PBFile.SoftFormula -> Builder
wbo2lsp softFormula =
  byteString "function model() {\n" <>
  decls <>
  constrs <>
  costDef <>
  topConstr <>
  byteString "  minimize cost;\n}\n"
  where
    nv = PBFile.wboNumVars softFormula

    decls = byteString "  for [i in 1.." <> intDec nv <> byteString "] x[i] <- bool();\n"

    constrs = mconcat
      [ byteString "  constraint " <>
        showConstrLSP c <>
        ";\n"
      | (Nothing, c) <- PBFile.wboConstraints softFormula
      ]

    costDef = byteString "  cost <- sum(\n" <> s <> ");\n"
      where
        s = mconcat . intersperse (",\n") $ xs
        xs = ["    " <> integerDec w <> "*!(" <> showConstrLSP c <> ")"
             | (Just w, c) <- PBFile.wboConstraints softFormula]

    topConstr =
      case PBFile.wboTopCost softFormula of
        Nothing -> mempty
        Just t -> byteString "  constraint cost <= " <> integerDec t <> ";\n"

showConstrLSP :: PBFile.Constraint -> Builder
showConstrLSP (lhs, PBFile.Ge, 1) | and [c==1 | (c,_) <- lhs] =
  "or(" <> mconcat (intersperse "," (map (f . snd) lhs)) <> ")"
  where
    f [l] = showLitLSP l
    f ls  = "and(" <> mconcat (intersperse "," (map showLitLSP ls)) <> ")"
showConstrLSP (lhs, op, rhs) = showSumLSP lhs  <> op2 <> integerDec rhs
  where
    op2 = case op of
            PBFile.Ge -> " >= "
            PBFile.Eq -> " == "

sum' :: [Builder] -> Builder
sum' xs = "sum(" <> mconcat (intersperse ", " xs) <> ")"

showSumLSP :: PBFile.Sum -> Builder
showSumLSP = sum' . map showTermLSP

showTermLSP :: PBFile.WeightedTerm -> Builder
showTermLSP (n,ls) = mconcat $ intersperse "*" $ [integerDec n | n /= 1] ++ [showLitLSP l | l<-ls]

showLitLSP :: PBFile.Lit -> Builder
showLitLSP l =
  if l < 0
  then "!x[" <> intDec (abs l) <> "]"
  else "x[" <> intDec l <> "]"

-- -----------------------------------------------------------------------------

pb2smp :: Bool -> PBFile.Formula -> Builder
pb2smp isUnix formula =
  header <>
  decls <>
  char7 '\n' <>
  obj2 <>
  char7 '\n' <>
  constrs <>
  char7 '\n' <>
  actions <>
  footer
  where
    nv = PBFile.pbNumVars formula

    header =
      if isUnix
      then byteString "#include \"simple.h\"\nvoid ufun()\n{\n\n"
      else mempty

    footer =
      if isUnix
      then "\n}\n"
      else mempty

    actions = byteString $
      "solve();\n" <>
      "x[i].val.print();\n" <>
      "cost.val.print();\n"

    decls =
      byteString "Element i(set=\"1 .. " <> intDec nv <>
      byteString "\");\nIntegerVariable x(type=binary, index=i);\n"

    constrs = mconcat
      [ showSum lhs <>
        op2 <>
        integerDec rhs <>
        ";\n"
      | (lhs, op, rhs) <- PBFile.pbConstraints formula
      , let op2 = case op of
                    PBFile.Ge -> " >= "
                    PBFile.Eq -> " == "
      ]

    showSum :: PBFile.Sum -> Builder
    showSum [] = char7 '0'
    showSum xs = mconcat $ intersperse " + " $ map showTerm xs

    showTerm (n,ls) = mconcat $ intersperse (char7 '*') $ showCoeff n ++ [showLit l | l<-ls]

    showCoeff n
      | n == 1    = []
      | n < 0     = [char7 '(' <> integerDec n <> char7 ')']
      | otherwise = [integerDec n]

    showLit l =
      if l < 0
      then "(1-x[" <> intDec (abs l) <> "])"
      else "x[" <> intDec l <> "]"

    obj2 =
      case PBFile.pbObjectiveFunction formula of
        Just obj' ->
          byteString "Objective cost(type=minimize);\ncost = " <> showSum obj' <> ";\n"
        Nothing -> mempty

-- -----------------------------------------------------------------------------