packages feed

biohazard-0.6.1: tools/bam-resample.hs

{-# LANGUAGE BangPatterns #-}
-- Resample m out of n `virtual' BAM records.
--
-- Strategy for fair down sampling:  we first count the number of
-- records, then scan again to sample.  Input must be grouped by QNAME
-- (sorted by QNAME is fine).
--
-- Usage: resample [NUM] [FILE...]

import Bio.Bam.Header
import Bio.Bam.Reader
import Bio.Bam.Rec
import Bio.Bam.Writer
import Bio.Iteratee
import Data.Version ( showVersion )
import Paths_biohazard ( version )
import System.Environment
import System.Exit ( exitFailure )
import System.Random
import System.IO ( hPutStr )

main :: IO ()
main = do
    args <- getArgs
    case args of
        []             -> complain
        [_num]         -> complain
        num_ : files   -> case reads num_ of
            [(num,"")] -> main' num files
            _          -> complain

complain :: IO ()
complain = do pn <- getProgName
              hPutStr stderr $ pn ++ ", version " ++ showVersion version
                            ++ "\nUsage: " ++ pn ++ " <num> [file...]\n"
              exitFailure

main' :: Int -> [String] -> IO ()
main' num files = do
    hPutStr stderr "counting... "
    total <- enumInputs files >=> run $
             joinI $ decodeAnyBam $ \_hdr ->
             joinI $ groupOn (b_qname . unpackBam) $
             foldStream (\a _ -> 1+a) 0
    hPutStr stderr $ shows total " records.\n"

    add_pg <- addPG (Just version)
    enumInputs files >=> run $
             joinI $ decodeAnyBam $
             joinI . groupOn (b_qname . unpackBam) .
             joinI . resample num total .
             protectTerm . pipeBamOutput . add_pg


resample :: MonadIO m => Int -> Int -> Enumeratee [[BamRaw]] [BamRaw] m a
resample m0 n0 | m0 > n0 = error "upsampling requested"
resample m0 n0 = eneeCheckIfDone (go m0 n0)
  where
    go  !m !n k = tryHead >>= maybe (return (liftI k)) (go' m n k)
    go' !m !n k a = do r <- liftIO $ randomRIO (0,n-1)
                       if r < m
                         then eneeCheckIfDone (go (m-1) (n-1)) . k $ Chunk a
                         else go m (n-1) k

groupOn :: (Monad m, Eq b) => (a -> b) -> Enumeratee [a] [[a]] m c
groupOn f = eneeCheckIfDone (\k -> tryHead >>= maybe (return $ liftI k) (\a -> go k [a] (f a)))
  where
    go  k acc fa = tryHead >>= maybe (return . k $ Chunk [reverse acc]) (go' k acc fa)
    go' k acc fa b | fa == f b = go k (b:acc) fa
                   | otherwise = eneeCheckIfDone (\k' -> go k' [b] (f b)) . k $ Chunk [reverse acc]