packages feed

exact-kantorovich-0.1.0.0: src/Math/Optimization/Kantorovich.hs

{-|
Module      : Math.Optimization.Kantorovich
Description : Exact Kantorovich distance.
Copyright   : (c) Stéphane Laurent, 2024
License     : BSD-3-Clause
Maintainer  : laurent_step@outlook.fr

Computes the exact Kantorovich distance between two finite probability measures. 
This assumes that the probability masses are rational numbers and that the 
distance function takes rational values only.
-}
module Math.Optimization.Kantorovich
  ( 
    RandomVariable
  , KantorovichValue
  , KantorovichSolution
  , KantorovichResult
  , kantorovich
  , prettyKantorovichSolution
  ) where
import           Prelude                hiding   ( EQ )
import           Control.Monad.Logger            (
                                                   runStdoutLoggingT
                                                 , filterLogger
                                                 )
import           Data.List.Extra                 (
                                                   nubSort
                                                 )
import           Data.Map.Strict                 ( 
                                                   fromList
                                                 , mapKeys
                                                 , singleton
                                                 , Map
                                                 )
import qualified Data.Map.Strict                 as DM
import           Data.Matrix                     (
                                                   fromLists
                                                 , prettyMatrix
                                                 )
import           Data.Maybe                      (
                                                   isJust
                                                 , fromJust
                                                 )
import           Data.Tuple.Extra                (
                                                   (***)
                                                 )
import           Linear.Simplex.Solver.TwoPhase  (
                                                   twoPhaseSimplex
                                                 )
import           Linear.Simplex.Types            (
                                                   Result ( .. )
                                                 , PolyConstraint ( .. )
                                                 , ObjectiveFunction ( .. )
                                                 )
import           Linear.Simplex.Util             (
                                                   simplifySystem
                                                 )

-- | A random variable is defined as a map from a set to the set of rational
-- numbers. It maps an element to its probability mass.
type RandomVariable a = Map a Rational 

-- | Type for the value of the Kantorovich distance.
type KantorovichValue = Rational

-- | Type for the solution of the underlying linear programming problem used 
-- to compute the Kantorovich distance. The solution is a joining of the two
-- random variables. It is then a random variable on the Cartesian product of 
-- the sets on which the two random variables are distributed. 
type KantorovichSolution a b = RandomVariable (a, b)

-- | Type for the result of the `kantorovich` function.
type KantorovichResult a b = (KantorovichValue, KantorovichSolution a b) 

stack :: Int -> (Int, Int) -> Int
stack ncol (i, j) = (i - 1) * ncol + j

unstack :: Int -> Int -> (Int, Int)
unstack ncol k = quotRem (k-1) ncol

-- | Prints the random variable representing the Kantorovich solution in the style
-- of a matrix.
prettyKantorovichSolution :: 
  (Ord a, Ord b)
  => Maybe (KantorovichResult a b) -- ^ an output of the `kantorovich` function
  -> String
prettyKantorovichSolution maybeKantorovichResult = 
  if isJust maybeKantorovichResult then prettyMatrix m else ""
  where
    kantorovichSolution = snd $ fromJust maybeKantorovichResult
    pairs = DM.keys kantorovichSolution
    (rows, cols) = (nubSort *** nubSort) (unzip pairs)
    m = fromLists 
      [ 
        [ 
          kantorovichSolution DM.! (i, j) | j <- cols
        ] 
        | i <- rows 
      ]

-- | Kantorovich distance between two probability measures (random variables).
kantorovich :: 
  (Ord a, Ord b)
  => RandomVariable a     -- ^ first random variable
  -> RandomVariable b     -- ^ second random variable
  -> ((a, b) -> Rational) -- ^ distance function taking /positive/ rational values
  -> Bool                 -- ^ whether to print the details of the simplex algorithm to stdout
  -> IO (Maybe (KantorovichResult a b))
kantorovich rvA rvB dist info = do 
  maybeResult <- runStdoutLoggingT $ filterLogger (\_ _ -> info) $ 
                  twoPhaseSimplex objFunc polyConstraints
  return $ getObjectiveValueAndSolution maybeResult
  where
    ncol = DM.size rvB
    as = DM.keys rvA
    mu = DM.elems rvA
    bs = DM.keys rvB
    nu = DM.elems rvB
    objFunc = kantorovichObjectiveFunction as bs dist
    polyConstraints = kantorovichConstraints mu nu
    getObjectiveValueAndSolution maybeResult = 
      case maybeResult of
        Just (Result var varLitMap) -> 
          Just (
                 varLitMap DM.! var
               , mapKeys (\k -> (((!!) as) *** ((!!) bs)) (unstack ncol k)) 
                          (DM.delete var varLitMap) 
               )
        Nothing -> Nothing

kantorovichObjectiveFunction :: 
  [a] -> [b] -> ((a, b) -> Rational) -> ObjectiveFunction
kantorovichObjectiveFunction as bs dist = Min 
  { 
    objective = fromList 
      [ (stack n (i, j), dist (as!!(i-1), bs!!(j-1))) | i <- rows, j <- cols ]
  }
  where
    n = length bs
    rows = [ 1 .. length as ]
    cols = [ 1 .. n ]

kantorovichConstraints :: [Rational] -> [Rational] -> [PolyConstraint]
kantorovichConstraints mu nu = 
  simplifySystem $ 
    positivityConstraints ++ rowMarginsConstraints ++ colMarginsConstraints
  where
    m = length mu
    n = length nu
    rows = [ 1 .. m ]
    cols = [ 1 .. n ]
    positivityConstraints = -- not useless because simplex-method drops some zeros 
      [ 
        GEQ { 
              lhs = singleton (stack n (i, j)) 1, rhs = 0 
            } 
        | i <- rows, j <- cols 
      ]
    rowMarginsConstraints = 
      [ 
        EQ { 
             lhs = fromList [ (stack n (i, j), 1) | j <- cols ]
           , rhs = mu !! (i-1) 
           } 
        | i <- rows 
      ]
    colMarginsConstraints = 
      [ 
        EQ { 
             lhs = fromList [ (stack n (i, j), 1) | i <- rows ]
           , rhs = nu !! (j-1) 
           } 
        | j <- cols 
      ]