packages feed

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

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

{- |
Module      :  TLynx.Shuffle.Shuffle
Description :  Shuffle a phylogeny
Copyright   :  (c) Dominik Schrempf 2020
License     :  GPL-3.0-or-later

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

Creation date: Thu Sep 19 15:01:52 2019.

The coalescent times are unaffected. The topology and the leaf order is
shuffled. Branch support values are ignored and lost.

-}

module TLynx.Shuffle.Shuffle
  ( shuffleCmd
  )
where

import           Control.Comonad                ( extend )
import           Control.Monad                  ( forM
                                                , replicateM
                                                , when
                                                )
import           Control.Monad.IO.Class         ( liftIO )
import           Control.Monad.Logger           ( logDebug
                                                , logInfo
                                                )
import           Control.Monad.Trans.Reader     ( ask )
import           Control.Monad.Primitive        ( PrimMonad
                                                , PrimState
                                                )
import           Data.Array                     ( elems )
import           Data.Array.ST                  ( newListArray
                                                , readArray
                                                , runSTArray
                                                , writeArray
                                                )
import qualified Data.ByteString.Lazy.Char8    as L
import           Data.Maybe                     ( isNothing )
import           Data.Tree                      ( Tree
                                                , flatten
                                                , rootLabel
                                                )
import           System.IO                      ( hClose )
import           System.Random.MWC              ( Gen
                                                , initialize
                                                , uniformR
                                                )

import           TLynx.Shuffle.Options

import           ELynx.Data.Tree
import           ELynx.Export.Tree.Newick       ( toNewick )
import           ELynx.Import.Tree.Newick       ( oneNewick )
import           ELynx.Simulate.PointProcess    ( PointProcess(PointProcess)
                                                , toReconstructedTree
                                                )
import           ELynx.Tools

-- | Shuffle a tree. Get all coalescent times, shuffle them. Get all leaves,
-- shuffle them. Connect the shuffled leaves with the shuffled coalescent times.
-- The shuffled tree has a new topology while keeping the same set of coalescent
-- times and leaves.
shuffleCmd :: ELynx ShuffleArguments ()
shuffleCmd = do
  l <- local <$> ask
  h <- outHandle "results" ".tree"
  let nwF = nwFormat l
  t <- liftIO $ parseFileWith (oneNewick nwF) (inFile l)
  $(logInfo) "Input tree:"
  $(logInfo) $ fromBs $ toNewick t

  -- Check if all branches have a given length. However, the length of the stem is not important.
  let r  = rootLabel t
      r' = r { brLen = Just 0 }
      t' = t { rootLabel = r' }
  when
    (isNothing $ traverse brLen t')
    (do
      $(logDebug) $ tShow t'
      error "Not all branches have a given length."
    )

  -- Check if tree is ultrametric enough.
  let dh = sum $ map (height t -) (distancesOriginLeaves t)
  $(logDebug) $ "Distance in branch length to being ultrametric: " <> tShow dh
  when (dh > 2e-4) (error "Tree is not ultrametric.")
  when (dh > eps && dh < 2e-4)
    $ $(logInfo)
        "Tree is nearly ultrametric, ignore branch length differences smaller than 2e-4."
  when (dh < eps) $ $(logInfo) "Tree is ultrametric."

  let cs = filter (> 0) $ flatten $ extend rootHeight t
      ls = map getName $ leaves t
  $(logDebug) $ "Number of coalescent times: " <> tShow (length cs)
  $(logDebug) $ "Number of leaves: " <> tShow (length ls)
  $(logDebug) "The coalescent times are: "
  $(logDebug) $ tShow cs

  gen <- case argsSeed l of
    Random  -> error "Seed not available; please contact maintainer."
    Fixed s -> liftIO $ initialize s

  ts <- liftIO $ shuffle (nReplicates l) (height t) cs ls gen
  liftIO $ L.hPutStr h $ L.unlines $ map toNewick ts

  liftIO $ hClose h

shuffle
  :: PrimMonad m
  => Int            -- How many?
  -> Double         -- Stem length.
  -> [Double]       -- Coalescent times.
  -> [L.ByteString] -- Leave names.
  -> Gen (PrimState m)
  -> m [Tree (PhyloLabel L.ByteString)]
shuffle n o cs ls gen = do
  css <- grabble cs n (length cs) gen
  lss <- grabble ls n (length ls) gen
  return
    [ toReconstructedTree "" (PointProcess names times o)
    | (times, names) <- zip css lss
    ]

-- | From https://wiki.haskell.org/Random_shuffle.
--
-- @grabble xs m n@ is /O(m*n')/, where @n' = min n (length xs)@. Choose @n@
-- elements from @xs@, without replacement, and that @m@ times.
grabble :: PrimMonad m => [a] -> Int -> Int -> Gen (PrimState m) -> m [[a]]
grabble xs m n gen = do
  swapss <- replicateM m $ forM [0 .. min (maxIx - 1) n] $ \i -> do
    j <- uniformR (i, maxIx) gen
    return (i, j)
  return $ map (take n . swapElems xs) swapss
  where maxIx = length xs - 1

-- grabbleOnce :: MonadRandom m => [a] -> Int -> m [a]
-- grabbleOnce xs n = head `liftM` grabble xs 1 n

swapElems :: [a] -> [(Int, Int)] -> [a]
swapElems xs swaps = elems $ runSTArray
  (do
    arr <- newListArray (0, maxIx) xs
    mapM_ (swap arr) swaps
    return arr
  )
 where
  maxIx = length xs - 1
  swap arr (i, j) = do
    vi <- readArray arr i
    vj <- readArray arr j
    writeArray arr i vj
    writeArray arr j vi