align-affine-0.1.0.0: src/Data/Align/Affine.hs
{-# LANGUAGE RecordWildCards #-}
-- | Pairwise & multi-sequence alignment with an affine gap penalty model. Forked from "Data.Align". This algorithm uses a gap opening penalty and a gap extension penalty to score alignments.
module Data.Align.Affine
(
-- * Global alignment
align
, AlignConfig
, alignConfig
, Step
, Trace, traceScore, trace
, debugAlign, debugStrAlign
-- * Multi-sequence alignment
, centerStar
, MultiStep, center, others, stepOfAll
, MultiTrace, centerIndex, otherIndices, allIndices, multiTrace
, debugMultiAlign
) where
import Control.Monad.Trans.State.Strict (evalState, gets, modify)
import Data.Function (on)
import Data.Maybe (fromMaybe)
import qualified Data.List as L
import qualified Data.Map.Strict as M
import Data.Ord (comparing)
import qualified Data.Vector.Generic as G
-- | Either an unmatched item or a match.
type Step a = Either (Either a a) (a, a)
stepLeft :: a -> Either (Either a b1) b2
stepLeft = Left . Left
stepRight :: b1 -> Either (Either a b1) b2
stepRight = Left . Right
stepBoth :: a1 -> b -> Either a2 (a1, b)
stepBoth a b = Right (a,b)
-- | The result of the alignment.
data Trace a s = Trace
{ traceScore :: s
, trace :: [Step a]
}
instance (Show a, Show s) => Show (Trace a s) where
show (Trace s t) = "Trace(score = " ++ show s ++ ", steps = " ++ show t ++ ")"
tappend :: Num s => Trace a s -> (s, Step a) -> Trace a s
Trace a b `tappend` (y,z) = Trace (a+y) (z:b)
data AffineTrace a s = AffineTrace {
at_max :: Trace a s
, at_left_gap :: Trace a s
, at_right_gap :: Trace a s
}
data AlignConfig a s = AlignConfig
{ acPairScore :: a -> a -> s
, acGapOpen :: s
, acGapExtension :: s
}
-- | This algorithm uses an affine gap penalty model. See section 12.6 "Convex gap weights" in Gusfield 1997: <https://doi.org/10.1017/CBO9780511574931>.
alignConfig :: (a -> a -> s) -- ^ Scoring function.
-> s -- ^ Gap opening score.
-> s -- ^ Gap extension score.
-> AlignConfig a s
alignConfig = AlignConfig
-- | Aligns two sequences using the Needleman-Wunsch algorithm. See Needleman & Wunsch 1970: <https://doi.org/10.1016/0022-2836(70)90057-4>.
align :: (G.Vector v a, Num s, Ord s)
=> AlignConfig a s
-> v a -- ^ Left sequence.
-> v a -- ^ Right sequence.
-> Trace a s
align AlignConfig{..} as bs =
let p = (lastIndex as, lastIndex bs)
in revTrace . at_max $ evalState (go p) M.empty
where
revTrace (Trace s t) = Trace s (reverse t)
lastIndex v = G.length v - 1
--
go p = do
res <- gets $ M.lookup p
case res of
Just r -> return r
Nothing -> do
newRes <- pgo p
modify (M.insert p newRes)
return newRes
--
pgo (i,j)
| i == (-1) || j == (-1) = return $
if i == j then AffineTrace (Trace 0 []) (Trace 0 []) (Trace 0 [])
else if i == (-1)
then skipInit j stepRight bs
else skipInit i stepLeft as
| otherwise = do
let a = as G.! i
b = bs G.! j
diag <- go (i-1,j-1)
let diag_max = (at_max diag) `tappend` (acPairScore a b, stepBoth a b)
a_gaps <- go (i-1, j)
let a_gap1 = (at_max a_gaps) `tappend` (acGapOpen + acGapExtension, stepLeft a)
let a_gap2 = (at_left_gap) a_gaps `tappend` (acGapExtension, stepLeft a)
let a_gap_max = L.maximumBy (comparing traceScore) [a_gap1, a_gap2]
b_gaps <- go ( i,j-1)
let b_gap1 = (at_max b_gaps) `tappend` (acGapOpen + acGapExtension, stepRight b)
let b_gap2 = (at_right_gap b_gaps) `tappend` (acGapExtension, stepRight b)
let b_gap_max = L.maximumBy (comparing traceScore) [b_gap1, b_gap2]
let maxi = L.maximumBy (comparing traceScore) [diag_max, a_gap_max, b_gap_max]
return $ AffineTrace maxi a_gap_max b_gap_max
--
skipInit idx stepFun xs =
let score = acGapOpen + acGapExtension * fromIntegral (idx+1)
tr = reverse [stepFun (xs G.! xi) | xi <- [0..idx]]
in AffineTrace (Trace score tr) (Trace score tr) (Trace score tr)
-- | Utility for displaying a Char-based alignment.
--
-- == Example
--
-- > >>> :{
-- > let tr = align (alignConfig (\a b -> if a == b then 0.5 else (-0.5))
-- > (-3) (-0.25))
-- > (Data.Vector.fromList "circumambulate")
-- > (Data.Vector.fromList "perambulatory")
-- > in do
-- > print $ traceScore tr
-- > putStrLn . debugAlign . trace $ tr
-- > :}
--
-- == Output
--
-- > -4.75
-- > circumambulate--
-- > per---ambulatory
debugAlign :: [Step Char] -> String
debugAlign = go [] []
where
go as bs [] = reverse as ++ "\n" ++ reverse bs
go as bs (t:ts) = case t of
Left (Left c) -> go (c:as) ('-':bs) ts
Left (Right c) -> go ('-':as) (c:bs) ts
Right (c, d) -> go (c:as) (d:bs) ts
-- | Utility for displaying a String-based alignment.
--
-- == Example
--
-- >>> :{
-- let tr = align (alignConfig (\a b -> if a == b then 0.5 else (-0.5))
-- (-3) (-0.25))
-- (Data.Vector.fromList ["kra","ya","ṇā","ddha","ra","ṇā","tyā","cñā","yāḥ"])
-- (Data.Vector.fromList ["bha","ra","ṇā","da","pa","ha","ra","ṇā","tyā","cña","yā"])
-- in do
-- print $ traceScore tr
-- putStrLn . debugStrAlign . trace $ tr
-- :}
--
-- == Output
--
-- > -4.0
-- > |kra|ya|ṇā|ddha|--|--|ra|ṇā|tyā|cñā|yāḥ|
-- > |bha|ra|ṇā|da |pa|ha|ra|ṇā|tyā|cñā|yā |
debugStrAlign :: [Step String] -> String
debugStrAlign = go id id
where
go as bs [] = as "|\n" ++ bs "|"
go as bs (t:ts) = case t of
Left (Left c) -> go (as . ("|"++) . (c ++))
(bs . ("|"++) . (take (length c) (repeat '-') ++)) ts
Left (Right c) -> go (as . ("|"++) . (take (length c) (repeat '-') ++))
(bs . ("|"++) . (c ++)) ts
Right (c,d) -> go (as . ("|"++) . (c ++) . (filldc ++))
(bs . ("|"++) . (d ++) . (fillcd ++)) ts
where
fill n = take n $ repeat ' '
filldc = fill (length d - length c)
fillcd = fill (length c - length d)
-- | A step in a multi-sequence alignment.
data MultiStep a = MultiStep
{ center :: Maybe a -- ^ 'Nothing' means gap insertion.
, others :: [Maybe a] -- ^ Parallel to 'otherIndices'.
}
-- | The result of a multi-sequence alignment.
data MultiTrace i a s = MultiTrace
{ centerIndex :: i
, otherIndices :: [i]
, multiTrace :: [MultiStep a]
}
-- | The center step followed by other steps.
stepOfAll :: MultiStep a -> [Maybe a]
stepOfAll MultiStep{..} = center:others
-- | The center index followed by other indices.
allIndices :: MultiTrace i a s -> [i]
allIndices MultiTrace{..} = centerIndex:otherIndices
-- | Align multiple sequences using the Center Star method.
--
-- See, for example, section 14.6.2, "A bounded-error approximation method for SP alignment" in Gusfield 1997.
--
-- Assumes the list of sequences to have length > 1, and the indices to be unique.
centerStar :: (G.Vector v a, Num s, Ord s, Ord i)
=> AlignConfig a s
-> [(i, v a)] -- TODO use internal indices rather to make uniqueness sure
-> MultiTrace i a s
centerStar conf vs =
let (firstPair:rest) = centerPairs
initialTrace = MultiTrace
{ centerIndex = fst . fst $ firstPair
, otherIndices = [snd . fst $ firstPair]
, multiTrace = initialSteps . trace . snd $ firstPair
}
in foldl mergePair initialTrace rest
where
initialSteps = go []
where
go acc [] = reverse acc
go acc (s:xs) = go (conv s []:acc) xs
--
conv s rest = case s of
Right (c, d) -> MultiStep (Just c) (Just d:rest)
Left (Left c) -> MultiStep (Just c) (Nothing:rest)
Left (Right d) -> MultiStep Nothing (Just d:rest)
--
mergePair MultiTrace{..} ((_,j), tr) = MultiTrace
{ centerIndex = centerIndex
, otherIndices = j:otherIndices
, multiTrace = mergeSteps multiTrace (trace tr)
}
where
mergeSteps mss' = go [] mss'
where
noOthers = map (const Nothing) . others . head $ mss'
--
go acc [] [] = reverse acc
go acc (MultiStep{..}:mss) [] =
go (MultiStep center (Nothing:others):acc) mss []
go acc [] (s:ss) = go (conv s noOthers:acc) [] ss
go acc (m@MultiStep{..}:mss) (s:ss) = case (center, s) of
(Nothing, Left (Right d)) ->
go (MultiStep center (Just d:others):acc) mss ss
(Nothing, _) ->
go (MultiStep center (Nothing:others):acc) mss (s:ss)
(Just _, Right (_, d)) ->
go (MultiStep center (Just d:others):acc) mss ss
(Just _, Left (Left _)) ->
go (MultiStep center (Nothing:others):acc) mss ss
(Just _, Left (Right d)) ->
go (MultiStep Nothing (Just d:noOthers):acc) (m:mss) ss
--
centerPairs
= snd -- drop cache
. L.maximumBy (comparing fst)
. map (\g -> (starSum g, g)) -- cache scores
. L.groupBy ((==) `on` (fst . fst))
. L.sortBy (comparing fst)
$ pairAligns
where
pairAligns = do
((i,v):rest) <- L.tails vs
(j,w) <- rest
let tr = align conf v w
[((i,j), tr), ((j,i), flipLR tr)]
where
flipLR tr = tr { trace = map go . trace $ tr }
where
go (Left (Left a)) = Left (Right a)
go (Left (Right a)) = Left (Left a)
go (Right (c,d)) = Right (d,c)
--
starSum = sum . map (traceScore . snd)
-- | Renders a char-based multi-alignment result to a string.
debugMultiAlign :: [MultiStep Char] -> String
debugMultiAlign =
unlines . map (map charOrDash) . L.transpose . map stepOfAll
where
charOrDash = fromMaybe '-'