packages feed

integreat-0.2.2.1: src/Alignment/Cosine.hs

{- Alignment.Cosine
Gregory W. Schwartz

Collections the functions pertaining to getting integration by cosine
similarity.
-}

{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PackageImports #-}

module Alignment.Cosine
    ( cosineIntegrate
    , cosineSim
    , cosineSimIMap
    , cosinePerm
    , cosineBoot
    ) where

-- Standard
import Control.Monad
import Data.List
import Data.Maybe
import Data.Monoid
import Data.Tuple
import qualified Data.IntMap.Strict as IMap

-- Cabal
import Control.Concurrent.Async
import Control.Lens
import Data.Random
import Numeric.LinearAlgebra hiding ((<>))
import Statistics.Resampling
import Statistics.Resampling.Bootstrap
import "statistics" Statistics.Types
import System.Random.MWC (createSystemRandom)
import qualified Control.Foldl as Fold
import qualified Data.Text as T
import qualified Data.Vector as VB
import qualified Data.Vector.Generic as VG
import qualified Data.Vector.Unboxed as VU

-- Local
import Types
import Utility

-- | Get the cosine similarity of two levels.
cosineIntegrate :: Permutations
                -> Size
                -> VertexSimMap
                -> LevelName
                -> LevelName
                -> EdgeSimMatrix
                -> EdgeSimMatrix
                -> IO NodeCorrScores
cosineIntegrate nPerm size vMap l1 l2 e1 e2 = do
    let newE1 :: IMap.IntMap (IMap.IntMap Double)
        newE1     = unEdgeSimMatrix
                  . cosineUpdateSimMat e1
                  . unVertexSimValues
                  $ vertexSim
        newE2 :: IMap.IntMap (IMap.IntMap Double)
        newE2     = unEdgeSimMatrix
                  . cosineUpdateSimMat e2
                  . unVertexSimValues
                  $ vertexSim
        vertexSim = getVertexSim l1 l2 vMap

    fmap ( NodeCorrScores
         . VB.fromList
         . fmap snd
         . IMap.toAscList
         . fillIntMap size
         )
        . mapConcurrently (uncurry (cosineBoot nPerm size))
        . fmap (over both IntMapContainer)
        . IMap.intersectionWith (,) newE1
        $ newE2

-- | Update the similarity matrices where the diagonal contains the similarity
-- between vertices of different levels.
cosineUpdateSimMat :: EdgeSimMatrix -> [((Int, Int), Double)] -> EdgeSimMatrix
cosineUpdateSimMat (EdgeSimMatrix edgeSimMat) =
    EdgeSimMatrix
        . foldl' (\acc ((!x, !y), !z) -> IMap.alter (f y z) x acc) edgeSimMat
        . (\xs -> xs ++ fmap (over _1 swap) xs)
  where
    f y z Nothing  = Just . IMap.singleton y $ z
    f y z (Just v) = Just . IMap.insert y z $ v

-- | Cosine similarity.
cosineSim :: Vector Double -> Vector Double -> Double
cosineSim x y = dot x y / (norm_2 x * norm_2 y)

-- | Cosine similarity of two IntMaps.
cosineSimIMap :: IMap.IntMap Double -> IMap.IntMap Double -> Double
cosineSimIMap x y = (imapSum $ IMap.intersectionWith (*) x y)
                  / (imapNorm x * imapNorm y)
  where
    imapNorm = sqrt . imapSum . IMap.map (^ 2)
    imapSum  = IMap.foldl' (+) 0

-- | Cosine similarity of a foldable of tuples.
cosineSimFold :: (Foldable f) => f (Double, Double) -> Double
cosineSimFold = Fold.fold cosine
  where
    cosine = (/) <$> num <*> den
    num = Fold.Fold (\acc (x, y) -> acc + (x * y)) 0 id
    den = (*) <$> norm fst <*> norm snd
    norm f = Fold.Fold (\acc x -> acc + ((f x) ^ 2)) 0 sqrt

-- | Cosine similarity of an unboxed vector.
cosineSimVec :: VU.Vector (Double, Double) -> Double
cosineSimVec xs = num / den
  where
    num  = VU.sum . VU.map (\(!x, !y) -> x * y) $ xs
    den  = (norm . VU.map fst $ xs) * (norm . VU.map snd $ xs)
    norm = sqrt . VU.sum . VU.map (^ 2)

-- | Get the cosine similarity and the p value of that similarity through the
-- permutation test by shuffling non-zero edges of the second vertex.
cosinePerm :: Permutations
           -> IMap.IntMap Double
           -> IMap.IntMap Double
           -> IO (Double, Maybe Statistic)
cosinePerm (Permutations nPerm) xs ys = do
    let obs     = cosineSimIMap xs ys
        expTest = (>= abs obs) . abs

    let successes :: Int -> Int -> IO Int
        successes !acc 0  = return acc
        successes !acc !n = do
            res <-  shuffleCosine xs $ ys
            if expTest res
                then successes (acc + 1) (n - 1)
                else successes acc (n - 1)

    exp <- successes 0 nPerm

    let pVal = PValue $ (fromIntegral exp) / (fromIntegral nPerm)

    return (obs, Just pVal)

-- | Get the cosine similarity and the p value of that similarity through the
-- permutation test, using the edge distribution from the second network.
cosinePermFromDist
    :: Permutations
    -> EdgeValues
    -> IMap.IntMap Double
    -> IMap.IntMap Double
    -> IO (Double, Maybe Statistic)
cosinePermFromDist (Permutations nPerm) edgeVals xs ys = do
    let obs     = cosineSimIMap xs ys
        expTest = (>= abs obs) . abs
        successes :: Int -> Int -> IO Int
        successes !acc 0  = return acc
        successes !acc !n = do
            res <- randomCosine edgeVals xs $ ys
            if expTest res
                then successes (acc + 1) (n - 1)
                else successes acc (n - 1)

    exp <- successes 0 nPerm

    let pVal = PValue $ (fromIntegral exp) / (fromIntegral nPerm)

    return (obs, Just pVal)

-- | Get the cosine similarity and the bootstrap using the edge distribution
-- from the second network. These are special IntMaps where the key is the index
-- of a sparse vector.
cosineBoot
    :: Permutations
    -> Size
    -> VectorType
    -> VectorType
    -> IO (Double, Maybe Statistic)
cosineBoot (Permutations nPerm) (Size size) xs ys = do
    let obs            = case (xs, ys) of
                            (IntMapContainer !xs, IntMapContainer !ys) -> cosineSimIMap xs ys
                            (VectorContainer !xs, VectorContainer !ys) -> cosineSim xs ys
        originalSample = VU.fromList . fmap fromIntegral $ [0..size - 1]
        toVec (IntMapContainer xs) = (imapToVec (Size size) 0 $ xs) :: VU.Vector Double
        toVec (VectorContainer xs) = VU.convert xs
        xsYSVec        = VU.zip (toVec xs) . toVec $ ys
        bootstrapFunc :: Sample -> Double
        bootstrapFunc = (\x -> if isNaN x then 0 else x) -- Convert NaN values to 0. Whether this should be done or not is a matter of opinion.
                      . cosineSimVec
                      . VU.map (\x -> xsYSVec VU.! truncate x)

    g <- createSystemRandom

    randomSamples <- resample g [Function bootstrapFunc] nPerm originalSample

    let bootstrap = head
                    . bootstrapBCA
                        0.95
                        originalSample
                        [Function bootstrapFunc]
                    $ randomSamples

    return (obs, Just . Bootstrap $ bootstrap)

-- | Get the cosine similarity and the subsampling using the edge distribution
-- from the second network (out of service right now).
cosineSubsampleBoot
    :: Permutations
    -> Size
    -> IMap.IntMap Double
    -> IMap.IntMap Double
    -> IO (Double, Maybe Statistic)
cosineSubsampleBoot (Permutations nPerm) (Size size) xs ys = do
    let obs               = cosineSimIMap xs ys
        originalSample    = VU.fromList . fmap fromIntegral $ [0..size - 1]
        bootstrapFunc :: Sample -> Double
        bootstrapFunc idx =
            cosineSimIMap (subsetIMap idxList xs) . subsetIMap idxList $ ys
          where
            idxList = fmap truncate . VU.toList $ idx

    g <- createSystemRandom

    randomSamples <- resample g [Function bootstrapFunc] nPerm originalSample

    let allSame = Estimate { estPoint           = obs
                           , estLowerBound      = obs
                           , estUpperBound      = obs
                           , estConfidenceLevel = 0.95
                           }

    if all (== head randomSamples) randomSamples
        then return (obs, Just . Bootstrap $ allSame)
        else do
            let bootstrap =
                  head
                    . bootstrapBCA 0.95 originalSample [Function bootstrapFunc]
                    $ randomSamples

            return (obs, Just . Bootstrap $ bootstrap)

-- | Get the cosine similarity and the subsampling using the edge distribution
-- from the second network (out of service right now).
cosineOldBoot
    :: Permutations
    -> Size
    -> IMap.IntMap Double
    -> IMap.IntMap Double
    -> IO (Double, Maybe Statistic)
cosineOldBoot (Permutations nPerm) (Size size) xs ys = do
    let obs               = cosineSimIMap xs ys
        originalSample    = VU.fromList . fmap fromIntegral $ [0..size - 1]
        bootstrapFunc :: Sample -> Double
        bootstrapFunc idx =
            cosineSimIMap (subsetIMap idxList xs) . subsetIMap idxList $ ys
          where
            idxList = fmap truncate . VU.toList $ idx

    g <- createSystemRandom

    randomSamples <- resample g [Function bootstrapFunc] nPerm originalSample

    let allSame = Estimate { estPoint           = obs
                           , estLowerBound      = obs
                           , estUpperBound      = obs
                           , estConfidenceLevel = 0.95
                           }

    if all (== head randomSamples) randomSamples
        then return (obs, Just . Bootstrap $ allSame)
        else do
            let bootstrap =
                  head
                    . bootstrapBCA 0.95 originalSample [Function bootstrapFunc]
                    $ randomSamples

            return (obs, Just . Bootstrap $ bootstrap)

-- | Random sample cosine.
randomCosine :: EdgeValues
             -> IMap.IntMap Double
             -> IMap.IntMap Double
             -> IO Double
randomCosine (EdgeValues edgeVals) xs ys = do
    shuffledYS <- getSampleMap edgeVals ys

    return . cosineSimIMap xs $ shuffledYS

-- | Sample map values from the complete set of values.
getSampleMap :: (VG.Vector v a) => v a -> IMap.IntMap a -> IO (IMap.IntMap a)
getSampleMap vals m = do
    let keys = IMap.keys m
        len = VG.length vals - 1

    newIdxs <- sample . replicateM (IMap.size m) . uniform 0 $ len

    return . IMap.fromList . zip keys . fmap (vals VG.!) $ newIdxs

-- | Random shuffle cosine.
shuffleCosine :: IMap.IntMap Double
              -> IMap.IntMap Double
              -> IO Double
shuffleCosine xs ys = do
    shuffledYS <- shuffleMap ys

    return . cosineSimIMap xs $ shuffledYS

-- | Shuffle map keys.
shuffleMap :: IMap.IntMap a -> IO (IMap.IntMap a)
shuffleMap m = do
    let (keys, elems) = unzip . IMap.toList $ m

    newKeys <- sample . shuffle $ keys

    return . IMap.fromList . zip newKeys $ elems