packages feed

tlynx-0.2.1: src/TLynx/Distance/Distance.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell   #-}

{- |
Description :  Compute distances between trees
Copyright   :  (c) Dominik Schrempf 2020
License     :  GPL-3.0-or-later

Maintainer  :  dominik.schrempf@gmail.com
Stability   :  unstable
Portability :  portable

Creation date: Wed May 29 18:09:39 2019.

- Symmetric (Robinson-Foulds) distance.
- Incompatible splits distance.

-}

module TLynx.Distance.Distance
  ( distance
  )
where

import           Control.Monad                  ( unless
                                                , when
                                                )
import           Control.Monad.IO.Class         ( liftIO )
import           Control.Monad.Logger           ( logDebug
                                                , logInfo
                                                )
import           Control.Monad.Trans.Class      ( lift )
import           Control.Monad.Trans.Reader     ( ask )
import qualified Data.ByteString.Lazy.Char8    as L
import           Data.List                      ( sort )
import           Data.Maybe                     ( isNothing )
import qualified Data.Text                     as T
import qualified Data.Text.IO                  as T
import qualified Data.Text.Lazy                as LT
import qualified Data.Text.Lazy.Encoding       as LT
import           Data.Tree                      ( Tree )
import qualified Data.Vector.Unboxed           as V
import           Statistics.Sample              ( mean
                                                , variance
                                                )
import           System.IO                      ( hClose
                                                , hPutStrLn
                                                )
import           Text.Printf                    ( PrintfArg
                                                , printf
                                                )

import           TLynx.Distance.Options

import           ELynx.Data.Tree
import           ELynx.Export.Tree.Newick
import           ELynx.Import.Tree.Newick
import           ELynx.Tools                    ( alignLeft
                                                , alignRight
                                                , tShow
                                                , outHandle
                                                , parseFileWith
                                                , ELynx
                                                , Arguments(..)
                                                )

median :: Ord a => [a] -> a
median xs = sort xs !! l2 where l2 = length xs `div` 2

pf :: String
pf = "%.3f"

header :: Int -> Int -> DistanceMeasure -> L.ByteString
header n m d =
  alignLeft (n + 2) "Tree 1" <> alignLeft (n + 2) "Tree 2" <> alignRight
    (m + 2)
    (L.pack $ show d)

showTriplet
  :: (PrintfArg a) => Int -> Int -> [String] -> (Int, Int, a) -> L.ByteString
showTriplet n m args (i, j, d) = i' <> j' <> d'
 where
  i' = alignLeft (n + 2) $ L.pack (args !! i)
  j' = alignLeft (n + 2) $ L.pack (args !! j)
  d' = alignRight (m + 2) $ L.pack (printf pf d)

-- | Compute distance functions between phylogenetic trees.
distance :: ELynx DistanceArguments ()
distance = do
  l <- local <$> ask
  let nwFormat = argsNewickFormat l
  -- Determine output handle (stdout or file).
  outH <- outHandle "results" ".out"
  -- Master tree (in case it is given).
  let mname = argsMasterTreeFile l
  mtree <- case mname of
    Nothing -> return Nothing
    Just f  -> do
      $(logInfo) $ T.pack $ "Read master tree from file: " <> f <> "."
      t <- liftIO $ parseFileWith (oneNewick nwFormat) f
      $(logInfo) "Compute distances between all trees and master tree."
      return $ Just t
  let tfps = argsInFiles l
  (trees, names) <- case tfps of
    []   -> error "No tree input files given."
    [tf] -> do
      $(logInfo) "Read trees from single file."
      ts <- liftIO $ parseFileWith (manyNewick nwFormat) tf
      $(logInfo) $ tShow (length ts) <> " trees found in file."
      $(logInfo) "Trees are indexed with integers."
      return (ts, map show [0 .. length ts - 1])
    _ -> do
      $(logInfo) "Read trees from files."
      ts <- liftIO $ mapM (parseFileWith (oneNewick nwFormat)) tfps
      $(logInfo) "Trees are named according to their file names."
      return (ts, tfps)

  when (null trees) (error "Not enough trees found in files.")
  when (isNothing mtree && length trees == 1)
       (error "Not enough trees found in files.")
  -- when (isNothing mtree) $ $(logInfo)
  --   "Compute pairwise distances between trees from different files."
  $(logDebug) "The trees are:"
  $(logDebug) $ LT.toStrict $ LT.decodeUtf8 $ L.unlines $ map toNewick trees

  -- Set the distance measure.
  let dist = argsDistance l
  case argsDistance l of
    Symmetric -> $(logInfo) "Use symmetric (Robinson-Foulds) distance."
    IncompatibleSplit val -> do
      $(logInfo) "Use incompatible split distance."
      $(logInfo)
        $  T.pack
        $  "Collapse nodes with support less than "
        ++ show val
        ++ "."
    BranchScore -> $(logInfo) "Use branch score distance."
  let distanceMeasure'
        :: Tree (PhyloLabel L.ByteString)
        -> Tree (PhyloLabel L.ByteString)
        -> Double
      distanceMeasure' = case dist of
        Symmetric -> \t1 t2 -> fromIntegral $ symmetric t1 t2
        IncompatibleSplit _ ->
          \t1 t2 -> fromIntegral $ incompatibleSplits t1 t2
        BranchScore -> branchScore
  -- Possibly intersect trees before distance calculation.
  when (argsIntersect l)
    $ $(logInfo) "Intersect trees before calculation of distances."
  let distanceMeasure = if argsIntersect l
        then
          (\t1 t2 ->
            let [t1', t2'] = intersectWith getName extendBranchLength [t1, t2]
            in  distanceMeasure' t1' t2'
          )
        else distanceMeasure'

  -- Possibly normalize trees.
  when (argsNormalize l)
    $ $(logInfo) "Normalize trees before calculation of distances."
  let normalizeF = if argsNormalize l then normalizeBranchLength else id

  -- Possibly collapse unsupported nodes.
  let collapseF = case dist of
        -- For the incompatible split distance we have to collapse branches with
        -- support lower than the given value. Before doing so, we normalize the
        -- branch support values.
        IncompatibleSplit val -> collapse val . normalizeBranchSupport
        _                     -> id

  -- The trees can be prepared now.
  let trees' = map (collapseF . normalizeF) trees

  $(logDebug) "The prepared trees are:"
  $(logDebug) $ LT.toStrict $ LT.decodeUtf8 $ L.unlines $ map toNewick trees'
  let dsTriplets = case mtree of
        Nothing -> pairwise distanceMeasure trees'
        Just t ->
          [ (0, i, distanceMeasure t t') | (i, t') <- zip [1 ..] trees' ]
      ds    = map (\(_, _, x) -> x) dsTriplets
      dsVec = V.fromList ds

  liftIO
    $  hPutStrLn outH
    $  "Summary statistics of "
    ++ show dist
    ++ " Distance:"
  liftIO $ T.hPutStrLn outH $ T.justifyLeft 10 ' ' "Mean: " <> T.pack
    (printf pf (mean dsVec))
  liftIO $ T.hPutStrLn outH $ T.justifyLeft 10 ' ' "Median: " <> T.pack
    (printf pf (median ds))
  liftIO $ T.hPutStrLn outH $ T.justifyLeft 10 ' ' "Variance: " <> T.pack
    (printf pf (variance dsVec))
  -- L.putStrLn $ L.unlines $ map toNewick ts
  -- L.putStrLn $ L.unlines $ map toNewick tsN
  -- L.putStrLn $ L.unlines $ map toNewick tsC

  lift $ unless
    (argsSummaryStatistics l)
    (do
      let n = maximum $ 6 : map length names
          m = length $ show dist
      lift $ hPutStrLn outH ""
      lift $ L.hPutStrLn outH $ header n m dist
      case mname of
        Nothing -> lift $ L.hPutStr outH $ L.unlines
          (map (showTriplet n m names) dsTriplets)
        Just mn -> lift $ L.hPutStr outH $ L.unlines
          (map (showTriplet n m (mn : names)) dsTriplets)
    )

  liftIO $ hClose outH