packages feed

too-many-cells-3.0.1.0: src/TooManyCells/Matrix/Preprocess.hs

{- TooManyCells.MakeTree.Preprocess
Gregory W. Schwartz

Collects functions pertaining to preprocessing the data.
-}

{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TupleSections #-}

module TooManyCells.Matrix.Preprocess
    ( scaleRMat
    , scaleDenseMat
    , scaleSparseMat
    , totalScaleSparseMat
    , logCPMSparseMat
    , uqScaleSparseMat
    , medScaleSparseMat
    , minMaxNormSparseMat
    , transposeSparseMat
    , quantileScaleSparseMat
    , centerScaleSparseCell
    , tfidfScaleSparseMat
    , filterRMat
    , filterDenseMat
    , filterNumSparseMat
    , filterWhitelistSparseMat
    , getCellWhitelist
    , featureSelectionRandomForest
    , removeCorrelated
    , pcaRMat
    , pcaDenseSc
    , pcaSparseSc
    , lsaSparseSc
    , svdSparseSc
    , shiftPositiveSc
    , pcaDenseMat
    , pcaSparseMat
    , lsaSparseMat
    , svdSparseMat
    , shiftPositiveMat
    , emptyMatErr
    , labelRows
    , labelCols
    , fastBinJoinRows
    , fastBinJoinCols
    , transformChrRegions
    ) where

-- Remote
import BirchBeer.Types (LabelMap (..), Id (..), Label (..), Feature (..))
import Data.Bool (bool)
import Data.List (sort, foldl', transpose)
import Data.Monoid ((<>))
import Data.Maybe (fromMaybe, isJust)
import H.Prelude (io)
import Language.R as R
import Language.R.QQ (r)
import Math.Clustering.Spectral.Sparse (B1 (..), B2 (..), b1ToB2)
import Statistics.Quantile (quantile, s)
import Statistics.Sample (mean, stdDev)
import TextShow (showt)
import qualified Control.Foldl as Fold
import qualified Control.Lens as L
import qualified Data.HashMap.Strict as HMap
import qualified Data.HashSet as HSet
import qualified Data.IntMap as IMap
import qualified Data.IntSet as ISet
import qualified Data.IntervalMap.Interval as IntervalMap
import qualified Data.IntervalMap.Strict as IntervalMap
import qualified Data.Map.Strict as Map
import qualified Data.Set as Set
import qualified Data.Sparse.Common as S
import qualified Data.Text as T
import qualified Data.Text.IO as T
import qualified Data.Vector as V
import qualified Data.Vector.Algorithms.Radix as V
import qualified Data.Vector.Storable as VS
import qualified Math.Clustering.Spectral.Sparse as S
import qualified Numeric.LinearAlgebra as H
import qualified Numeric.LinearAlgebra.Devel as H
import qualified Numeric.LinearAlgebra.HMatrix as H
import qualified Numeric.LinearAlgebra.SVD.SVDLIBC as SVD

-- Local
import TooManyCells.File.Types
import TooManyCells.Matrix.Types
import TooManyCells.Matrix.Utility

-- | Empty matrix error.
emptyMatErr :: String -> String
emptyMatErr checkType = "Matrix is empty in " <> checkType <> ". Check --filter-thresholds, --normalization, or the input matrix for over filtering or incorrect input format."

-- | Check if valid features or cells are empty, error if so.
emptyMatCheckErr :: String -> [a] -> [a]
emptyMatCheckErr checkType xs = bool xs (error $ emptyMatErr checkType) . null $ xs

-- | Scale a matrix.
scaleRMat :: RMatObsRow s -> R s (RMatObsRow s)
scaleRMat (RMatObsRow mat) = do
    fmap
        RMatObsRow
        [r| mat = scale(t(mat_hs));
            t(mat[,colSums(!is.na(mat)) > 0])
        |]

-- | Scale a matrix based on the library size for each cell and median feature
-- size.
scaleDenseMat :: MatObsRow -> MatObsRow
scaleDenseMat (MatObsRow mat) = MatObsRow
                              . hToSparseMat
                              . H.fromColumns
                              . fmap scaleDenseMol
                              . H.toColumns
                              . H.fromRows
                              . fmap scaleDenseCell
                              . H.toRows
                              . sparseToHMat
                              $ mat

-- | Scale a matrix based on the library size for each cell and median feature
-- size.
scaleSparseMat :: MatObsRow -> MatObsRow
scaleSparseMat (MatObsRow mat) = MatObsRow
                               . S.sparsifySM
                               . S.fromColsL
                               . fmap scaleSparseMol
                               . S.toRowsL -- A bit confusing, but this returns the original columns due to the earlier fromColsL . toRowsL.
                               . S.fromColsL
                               . fmap scaleSparseCell
                               . S.toRowsL
                               $ mat

-- | Scale a matrix based on the library size.
totalScaleSparseMat :: MatObsRow -> MatObsRow
totalScaleSparseMat = MatObsRow
                    . S.sparsifySM
                    . S.fromRowsL
                    . fmap scaleSparseCell
                    . S.toRowsL
                    . unMatObsRow

-- | Scale a matrix based on the upper quartile.
uqScaleSparseMat :: MatObsRow -> MatObsRow
uqScaleSparseMat (MatObsRow mat) = MatObsRow
                                 . S.sparsifySM
                                 . S.fromRowsL
                                 . fmap uqScaleSparseCell
                                 . S.toRowsL
                                 $ mat

-- | Scale a matrix based on log(CPM + 1).
logCPMSparseMat :: Double -> MatObsRow -> MatObsRow
logCPMSparseMat b (MatObsRow mat) = MatObsRow
                                  . S.sparsifySM
                                  . S.fromRowsL
                                  . fmap (logCPMSparseCell b)
                                  . S.toRowsL
                                  $ mat

-- | Scale a matrix based on the median.
medScaleSparseMat :: MatObsRow -> MatObsRow
medScaleSparseMat (MatObsRow mat) = MatObsRow
                                 . S.sparsifySM
                                 . S.fromRowsL
                                 . fmap medScaleSparseCell
                                 . S.toRowsL
                                 $ mat

-- | Scale a matrix based quantile normalization. Will ignore 0s if missing from
-- input.
quantileScaleSparseMat :: MatObsRow -> MatObsRow
quantileScaleSparseMat (MatObsRow mat) =
  MatObsRow
    . S.sparsifySM
    . fmap (flip (IMap.findWithDefault 0) totalRankMap)
    $ rankMat
  where
    totalRankMap = IMap.fromList
                 . zip [1,2..]
                 . fmap (Fold.fold avg)
                 . transpose
                 . fmap (sort . fmap snd . S.toListSV)
                 . S.toRowsL
                 $ mat
    avg = (/) <$> Fold.sum <*> Fold.genericLength
    rankMat :: S.SpMatrix Int
    rankMat = S.fromRowsL . fmap rankTransform . S.toRowsL $ mat
    rankTransform :: S.SpVector Double -> S.SpVector Int
    rankTransform xs = fmap (flip (Map.findWithDefault 0) rankMap) xs
      where
        rankMap :: Map.Map Double Int
        rankMap = Map.fromList
                . flip zip [1,2..]
                . Set.toList
                . Set.fromList
                . sort
                . fmap snd
                . S.toListSV
                $ xs

-- | TF-IDF normalization.
tfidfScaleSparseMat :: MatObsRow -> MatObsRow
tfidfScaleSparseMat = MatObsRow . unB2 . b1ToB2 . B1 . unMatObsRow

-- | Scale matrix rows based on min-max normalization.
minMaxNormSparseMat :: MatObsRow -> MatObsRow
minMaxNormSparseMat (MatObsRow mat) =
  MatObsRow
    . S.sparsifySM
    . S.fromRowsL
    . fmap (\x -> fromMaybe x $ minMaxNormSV x)
    . S.toRowsL
    $ mat

-- | Transpose matrix (helper to apply normalizations to columns instead, for instance).
transposeSparseMat :: MatObsRow -> MatObsRow
transposeSparseMat (MatObsRow mat) = MatObsRow . S.transposeSM $ mat

-- | Scale a cell by the library size.
scaleDenseCell :: H.Vector H.R -> H.Vector H.R
scaleDenseCell xs = H.cmap (/ total) xs
  where
    total = H.sumElements xs

-- | Scale a cell by the library size.
scaleSparseCell :: S.SpVector Double -> S.SpVector Double
scaleSparseCell xs = fmap (/ total) xs
  where
    total = foldl' (+) 0 xs

-- | log(CPM + 1) normalization for cell.
logCPMSparseCell :: Double -> S.SpVector Double -> S.SpVector Double
logCPMSparseCell b xs = fmap (logBase b . (+ 1) . cpm) xs
  where
    cpm x = x / tpm
    tpm = foldl' (+) 0 xs / 1000000

-- | Upper quartile scale cells.
uqScaleSparseCell :: S.SpVector Double -> S.SpVector Double
uqScaleSparseCell xs = fmap (/ uq) xs
  where
    uq = quantile s 3 4
       . VS.fromList
       . filter (/= 0)
       . fmap snd
       . S.toListSV
       $ xs

-- | Median scale cells.
medScaleSparseCell :: S.SpVector Double -> S.SpVector Double
medScaleSparseCell xs = fmap (/ med) xs
  where
    med = quantile s 2 4
        . VS.fromList
        . filter (/= 0)
        . fmap snd
        . S.toListSV
        $ xs

-- | Center and scale a cell.
centerScaleSparseCell :: S.SpVector Double -> S.SpVector Double
centerScaleSparseCell xs = fmap standard xs
  where
    standard x
      | sigma == 0 = 0
      | otherwise  = (x - mu) / sigma
    mu = mean v
    sigma = stdDev v
    v = S.toVector xs

-- | Center a sparse vector.
centerSparseVector :: S.SpVector Double -> S.SpVector Double
centerSparseVector xs = fmap center xs
  where
    center x = x - mu
    mu = mean $ S.toVector xs

-- | Median scale molecules across cells.
scaleDenseMol :: H.Vector H.R -> H.Vector H.R
scaleDenseMol xs = H.cmap (/ med) xs
  where
    med = quantile s 2 4 . VS.filter (/= 0) $ xs

-- | Median scale molecules across cells.
scaleSparseMol :: S.SpVector Double -> S.SpVector Double
scaleSparseMol xs = fmap (/ med) xs
  where
    med = quantile s 2 4
        . VS.fromList
        . filter (/= 0)
        . fmap snd
        . S.toListSV
        $ xs

-- | Min-max normalization of sparse vector.
minMaxNormSV :: S.SpVector Double -> Maybe (S.SpVector Double)
minMaxNormSV xs = do
  (mi, ma) <-
    L.sequenceOf L.both $ Fold.fold ((,) <$> Fold.minimum <*> Fold.maximum) xs
  pure $ fmap (\x -> (x - mi) / (ma - mi)) xs

-- | Filter a matrix to remove low count cells and features.
filterDenseMat :: FilterThresholds -> SingleCells -> SingleCells
filterDenseMat (FilterThresholds (rowThresh, colThresh)) sc =
    SingleCells { _matrix   = m
                , _rowNames = r
                , _colNames = c
                }
  where
    m = MatObsRow . hToSparseMat $ filteredMat
    rowFilter = (>= rowThresh) . H.sumElements
    colFilter = (>= colThresh) . H.sumElements
    mat            = sparseToHMat . unMatObsRow . _matrix $ sc
    validRows = ISet.fromList
              . emptyMatCheckErr "cells"
              . fmap fst
              . filter (rowFilter . snd)
              . zip [0..]
              . H.toRows
              $ mat
    validCols = ISet.fromList
              . emptyMatCheckErr "features"
              . fmap fst
              . filter (colFilter . snd)
              . zip [0..]
              . H.toColumns
              $ mat
    filteredMat = mat
             H.?? ( H.Pos (H.idxs (ISet.toAscList validRows))
                  , H.Pos (H.idxs (ISet.toAscList validCols))
                  )
    r = V.ifilter (\i _ -> ISet.member i validRows)
      . _rowNames
      $ sc
    c = V.ifilter (\i _ -> ISet.member i validCols)
      . _colNames
      $ sc

-- | Filter a matrix to remove low count cells and features.
filterNumSparseMat :: FilterThresholds -> SingleCells -> SingleCells
filterNumSparseMat (FilterThresholds (rowThresh, colThresh)) sc =
    SingleCells { _matrix   = m
                , _rowNames = r
                , _colNames = c
                }
  where
    m = MatObsRow colFilteredMat
    rowFilter = (>= rowThresh) . foldl' (+) 0
    colFilter = (>= colThresh) . foldl' (+) 0
    mat            = unMatObsRow . _matrix $ sc
    validRows = ISet.fromList
              . emptyMatCheckErr "cells"
              . fmap fst
              . filter (rowFilter . snd)
              . zip [0..]
              . S.toRowsL
              $ mat
    validCols = ISet.fromList
              . emptyMatCheckErr "features"
              . fmap fst
              . filter (colFilter . snd)
              . zip [0..]
              . S.toRowsL
              . S.transposeSM -- toRowsL is much faster.
              $ mat
    rowFilteredMat = S.fromRowsL -- fromRowsL fixed.
                   . fmap snd
                   . filter (flip ISet.member validRows . fst)
                   . zip [0..]
                   . S.toRowsL
                   $ mat
    colFilteredMat = S.fromColsL
                   . fmap snd
                   . filter (flip ISet.member validCols . fst)
                   . zip [0..]
                   . S.toRowsL -- Rows of transpose are faster.
                   . S.transposeSM
                   $ rowFilteredMat
    r = V.ifilter (\i _ -> ISet.member i validRows)
      . _rowNames
      $ sc
    c = V.ifilter (\i _ -> ISet.member i validCols)
      . _colNames
      $ sc

-- | Filter a matrix to keep whitelist cells.
filterWhitelistSparseMat :: CellWhitelist
                         -> SingleCells
                         -> SingleCells
filterWhitelistSparseMat (CellWhitelist wl) sc =
    sc { _matrix   = m
       , _rowNames = r
       }
  where
    m = MatObsRow rowFilteredMat
    mat            = unMatObsRow . _matrix $ sc
    validIdx       = V.modify V.sort
                   . V.map fst
                   . V.filter (\(_, (Cell !c)) -> HSet.member c wl)
                   . V.imap (\i v -> (i, v))
                   . _rowNames
                   $ sc
    rowFilteredMat = S.fromRowsL -- fromRowsL fixed.
                   . fmap (S.extractRow mat)
                   . emptyMatCheckErr "cells"
                   . V.toList
                   $ validIdx
    r = V.map (\x -> fromMaybe (error $ "\nWhitelist row index out of bounds (do the whitelist barcodes match the data?): " <> show x <> " out of " <> (show . length . _rowNames $ sc))
                   . (V.!?) (_rowNames sc)
                   $ x
              )
      $ validIdx

-- | Get a cell white list from a file.
getCellWhitelist :: CellWhitelistFile -> IO CellWhitelist
getCellWhitelist (CellWhitelistFile file) = do
    contents <- T.readFile file

    let whiteList = CellWhitelist
                  . HSet.fromList
                  . filter (not . T.null)
                  . T.lines
                  $ contents

    return whiteList

-- | Filter a matrix to remove low count cells. R version.
filterRMat :: RMatObsRow s -> R s (RMatObsRow s)
filterRMat (RMatObsRow mat) =
    fmap RMatObsRow [r| mat = mat_hs[,colSums(mat_hs) >= 250] |]

-- | Perform feature selection on a matrix.
featureSelectionRandomForest :: RMatObsRow s -> R s (RMatObsRow s)
featureSelectionRandomForest (RMatObsRow mat) = do
    [r| suppressPackageStartupMessages(library(randomForest)) |]

    importance   <- [r| randomForest(mat_hs, na.action = na.omit)$importance |]
    importantMat <- [r| mat_hs[,importance_hs > sort(importance_hs, decreasing = TRUE)[10]] |]

    return . RMatObsRow $ importantMat

-- | Remove highly correlated (> 0.6) variables in a matrix.
removeCorrelated :: RMatObsRow s -> R s (RMatObsRow s)
removeCorrelated (RMatObsRow mat) = do
    [r| suppressPackageStartupMessages(library(caret)) |]

    cor   <- [r| cor(mat_hs) |]
    importantMat <- [r| mat_hs[,-findCorrelation(cor_hs, cutoff = 0.6, exact = FALSE)] |]

    return . RMatObsRow $ importantMat

-- | Conduct PCA on a matrix, using components > 5% of variance.
pcaRMat :: RMatObsRow s -> R s (RMatObsRow s)
pcaRMat (RMatObsRow mat) = do
    fmap
        RMatObsRow
        [r| mat = prcomp(t(mat_hs), tol = 0.95)$x
        |]

-- | Conduct PCA on a SingleCells, taking the first principal components.
pcaDenseSc :: DropDimensionFlag -> PCADim -> SingleCells -> SingleCells
pcaDenseSc df p@(PCADim n) sc =
  L.set colNames (V.fromList . fmap (\x -> Feature $ "PCA " <> showt x) $ [begin..end])
    . L.over matrix (pcaDenseMat df p)
    $ sc
  where
    begin = bool 1 (n + 1) . unDropDimensionFlag $ df
    end   = bool n ((S.ncols . unMatObsRow . L.view matrix $ sc) - n + 1)
          . unDropDimensionFlag
          $ df

-- | Conduct PCA on a SingleCells, taking the first principal components.
pcaSparseSc :: DropDimensionFlag -> PCADim -> SingleCells -> SingleCells
pcaSparseSc df p@(PCADim n) sc =
  L.set colNames (V.fromList . fmap (\x -> Feature $ "PCA " <> showt x) $ [begin..end])
    . L.over matrix (pcaSparseMat df p)
    $ sc
  where
    begin = bool 1 (n + 1) . unDropDimensionFlag $ df
    end   = bool n ((S.ncols . unMatObsRow . L.view matrix $ sc) - n + 1)
          . unDropDimensionFlag
          $ df

-- | Conduct LSA on a SingleCells, taking the first singular values.
lsaSparseSc :: DropDimensionFlag -> LSADim -> SingleCells -> SingleCells
lsaSparseSc df l@(LSADim n) sc =
  L.set colNames (V.fromList . fmap (\x -> Feature $ "LSA " <> showt x) $ [begin..end])
    . L.over matrix (lsaSparseMat df l)
    $ sc
  where
    begin = bool 1 (n + 1) . unDropDimensionFlag $ df
    end   = bool n ((S.ncols . unMatObsRow . L.view matrix $ sc) - n + 1)
          . unDropDimensionFlag
          $ df

-- | Conduct SVD on a SingleCells, taking the first singular values.
svdSparseSc :: DropDimensionFlag -> SVDDim -> SingleCells -> SingleCells
svdSparseSc df s@(SVDDim n) sc =
  L.set colNames (V.fromList . fmap (\x -> Feature $ "SVD " <> showt x) $ [begin..end])
    . L.over matrix (svdSparseMat df s)
    $ sc
  where
    begin = bool 1 (n + 1) . unDropDimensionFlag $ df
    end   = bool n ((S.ncols . unMatObsRow . L.view matrix $ sc) - n + 1)
          . unDropDimensionFlag
          $ df

-- | Obtain the right singular vectors from N to E on of a sparse
-- matrix.
sparseRightSVD :: Int -> Int -> S.SpMatrix Double -> [S.SpVector Double]
sparseRightSVD n e =
  fmap (S.sparsifySV . S.fromListDenseSV e . drop (n - 1) . H.toList)
    . H.toColumns
    . (\(_, _, !z) -> z)
    . SVD.sparseSvd (e + (n - 1))
    . H.mkCSR
    . fmap (\(!i, !j, !x) -> ((i, j), x))
    . S.toListSM

-- | Conduct PCA on a matrix, retaining the specified number of dimensions.
pcaDenseMat :: DropDimensionFlag -> PCADim -> MatObsRow -> MatObsRow
pcaDenseMat df (PCADim pcaDim) (MatObsRow matObs) =
  MatObsRow
    . hToSparseMat
    . H.takeColumns pcaDim
    . H.mul mat
    . (\(u, _, _) -> u)
    . H.svd
    . H.unSym
    . snd
    . H.meanCov
    $ mat
  where
    mat = sparseToHMat matObs
    begin = bool 1 (pcaDim + 1) . unDropDimensionFlag $ df
    end   = bool pcaDim (S.ncols matObs - pcaDim)
          . unDropDimensionFlag
          $ df

-- | Conduct SVD on a matrix, retaining the specified number of dimensions.
pcaSparseMat :: DropDimensionFlag -> PCADim -> MatObsRow -> MatObsRow
pcaSparseMat df (PCADim pcaDim) (MatObsRow mat) =
  MatObsRow
    . (S.#~#) scaled
    . S.fromRowsL
    . sparseRightSVD begin end
    $ scaled
  where
    scaled = S.fromColsL
           . fmap centerScaleSparseCell
           . S.toRowsL -- Scale features for PCA
           . S.transpose
           $ mat
    begin = bool 1 (pcaDim + 1) . unDropDimensionFlag $ df
    end   = bool pcaDim (S.ncols mat - pcaDim) . unDropDimensionFlag $ df

-- | Conduct LSA on a matrix, retaining the specified number of dimensions.
lsaSparseMat :: DropDimensionFlag -> LSADim -> MatObsRow -> MatObsRow
lsaSparseMat df (LSADim lsaDim) (MatObsRow mat) =
  MatObsRow
    . S.fromRowsL
    . S.secondLeft begin end
    . unB2
    . b1ToB2
    . B1
    $ mat
  where
    begin = bool 1 (lsaDim + 1) . unDropDimensionFlag $ df
    end   = bool lsaDim (S.ncols mat - lsaDim) . unDropDimensionFlag $ df

-- | Conduct SVD on a matrix, retaining the specified number of dimensions.
svdSparseMat :: DropDimensionFlag -> SVDDim -> MatObsRow -> MatObsRow
svdSparseMat df (SVDDim svdDim) (MatObsRow mat) =
  MatObsRow
    . S.fromRowsL
    . S.secondLeft begin end
    . S.fromRowsL
    . fmap centerScaleSparseCell
    . S.toRowsL
    $ mat
  where
    begin = bool 1 (svdDim + 1) . unDropDimensionFlag $ df
    end   = bool svdDim (S.ncols mat - svdDim) . unDropDimensionFlag $ df


-- | Shift features to positive values.
shiftPositiveMat :: MatObsRow -> MatObsRow
shiftPositiveMat = MatObsRow
              . S.fromColsL
              . fmap (\ xs -> bool xs (shift . S.toDenseListSV $ xs)
                            . any (< 0)
                            . S.toDenseListSV
                            $ xs
                     )
              . S.toRowsL -- toRowsL much faster than toColsL
              . S.transpose
              . unMatObsRow
  where
    shift xs = S.sparsifySV . S.vr . fmap (+ (abs $ minimum xs)) $ xs

-- | Shift features to positive values for SingleCells.
shiftPositiveSc :: SingleCells -> SingleCells
shiftPositiveSc = L.over matrix shiftPositiveMat

-- | Optionally give the filepath as a label to the rows.
labelRows :: Maybe CustomLabel -> SingleCells -> LabelMat
labelRows Nothing sc = LabelMat (sc, Nothing)
labelRows (Just (CustomLabel l)) sc =
  LabelMat (L.set rowNames newRowNames sc, Just labelMap)
  where
    labelMap = LabelMap
             . Map.fromList
             . flip zip (repeat (Label l))
             . fmap (Id . unCell)
             . V.toList
             $ newRowNames
    newRowNames = fmap labelRow . L.view rowNames $ sc
    labelRow (Cell x) = Cell $ x <> "-" <> l

-- | Optionally give the filepath as a label to the columns when transposing
-- matrix.
labelCols :: Maybe CustomLabel -> SingleCells -> LabelMat
labelCols Nothing sc = LabelMat (sc, Nothing)
labelCols (Just (CustomLabel l)) sc =
  LabelMat (L.set colNames newColNames sc, Just labelMap)
  where
    labelMap = LabelMap
             . Map.fromList
             . flip zip (repeat (Label l))
             . fmap (Id . unFeature)
             . V.toList
             $ newColNames
    newColNames = fmap labelCol . L.view colNames $ sc
    labelCol (Feature x) = Feature $ x <> "-" <> l

-- | Add or remove a single character to speed up joining if region data is
-- binned across rows before transposition.
fastBinJoinRows :: Maybe BinWidth -> Bool -> SingleCells -> SingleCells
fastBinJoinRows binWidth joining =
  bool id (L.over rowNames (fmap (Cell . j . unCell))) $ isJust binWidth
    where
      j = if joining then T.cons 'B' else T.drop 1

-- | Add or remove a single character to speed up joining if region data is
-- binned across columns.
fastBinJoinCols :: Maybe BinWidth -> Bool -> SingleCells -> SingleCells
fastBinJoinCols binWidth joining =
  bool id (L.over colNames (fmap (Feature . j . unFeature)))
    $ isJust binWidth
    where
      j = if joining then T.cons 'B' else T.drop 1

-- | Transform a list of chromosome region features to a list of custom features.
transformChrRegions :: CustomRegions -> SingleCells -> SingleCells
transformChrRegions (CustomRegions customRegions) sc =
  L.set matrix mat . L.set colNames features $ sc
  where
    features = V.fromList . fmap (Feature . showt) $ customRegions
    knownFeatureMap = foldl' (HMap.unionWith IntervalMap.union) HMap.empty
                    . fmap (\ (!i, (!c, !interval))
                           -> HMap.singleton c
                            $ IntervalMap.singleton interval i
                           )
                    . zip ([0..] :: [Int])
                    . fmap unChrRegion
                    $ customRegions
    findErrKnown :: T.Text -> [Int]
    findErrKnown x = fromMaybe []
                   . (\ (!c, !i) -> HMap.lookup c knownFeatureMap
                                >>= Just
                                  . IntervalMap.elems
                                  . flip IntervalMap.intersecting i
                     )
                   . either error unChrRegion
                   . parseChrRegion
                   $ x
    mat = MatObsRow
        . foldl' addToMat init
        . concatMap (\ (!i, !j, !v)
                    -> fmap (i, , v)
                     . findErrKnown
                     . unFeature
                     . fromMaybe (error $ "transformChrRegions/mat: No indices found: " <> show j)
                     $ (V.!?) (L.view colNames sc) j
                    )
        . S.toListSM
        $ oldMat
    init = S.zeroSM (S.nrows oldMat) $ V.length features
    addToMat !m x = m S.^+^ S.fromListSM (S.dim init) [x]
    oldMat = unMatObsRow . L.view matrix $ sc