packages feed

ConClusion-0.2.2: src/ConClusion/Chemistry/Topology.hs

{- |
Module      : ConClusion.Chemistry.Topology
Description : Principal Component Analysis
Copyright   : Phillip Seeber, 2023
License     : AGPL-3
Maintainer  : phillip.seeber@googlemail.com
Stability   : experimental
Portability : POSIX, Windows

This module implements routines to work with simple molden style XYZ trajectories. This includes
parsers as well as functions to obtain structural features in internal coordinates.

For an introduction into PCA see <https://www.cs.cmu.edu/~elaw/papers/pca.pdf>.

Diherdrals require a special metric, see <https://onlinelibrary.wiley.com/doi/full/10.1002/prot.20310)>.
-}
module ConClusion.Chemistry.Topology (
  Molecule,
  Trajectory,
  xyz,
  trajectory,
  B (..),
  A (..),
  D (..),
  Feature (..),
  getFeatures,
)
where

import ConClusion.Array.Conversion
import ConClusion.Array.Util hiding (angle)
import qualified ConClusion.Array.Util as ArrayUtil
import Data.Attoparsec.Text hiding (D)
import Data.Foldable
import Data.Massiv.Array as Massiv hiding (B, D, forM)
import qualified Numeric.LinearAlgebra as LA
import RIO
import qualified RIO.Seq as Seq
import qualified RIO.Text as Text
import qualified RIO.Vector.Boxed as VectorB

-- | A Molecule in cartesian coordinates.
data Molecule = Molecule
  { energy :: !Double
  -- ^ The energy of the molecule.
  , atoms :: !(VectorB.Vector Text)
  -- ^ Chemical symbols or names of the atoms. \(N\) vector.
  , coordinates :: !(Massiv.Matrix S Double)
  -- ^ Cartesian coordinates. Atoms as rows, xyz as columns. \(N \times 3\) matrix.
  }

type Trajectory = Seq Molecule

-- | Parser for molecules in Molden XYZ format.
xyz :: Parser Molecule
xyz = do
  -- Starting line with number of atoms.
  nAtoms <- do
    _ <- skipMany . char $ ' '
    n <- decimal
    _ <- skipMany . char $ ' '
    endOfLine
    return n

  -- Comment line. Must contain the energy.
  energy <- do
    _ <- skipMany . char $ ' '
    e <- double
    _ <- skipMany . char $ ' '
    endOfLine
    return e

  -- Multiple lines of atoms.
  atoms <- count nAtoms $ do
    _ <- skipMany . char $ ' '
    name <- many1 $ letter <|> digit
    _ <- skipMany1 . char $ ' '
    x <- double
    _ <- skipMany1 . char $ ' '
    y <- double
    _ <- skipMany1 . char $ ' '
    z <- double
    _ <- skipMany . char $ ' '
    endOfLine
    let row = Massiv.expandOuter (Sz 1) const . Massiv.fromList @S Seq $ [x, y, z]
    return (Text.pack name, row)

  return
    Molecule
      { energy = energy
      , atoms = VectorB.fromListN nAtoms . fmap fst $ atoms
      , coordinates = Massiv.compute . Massiv.concat' (Dim 2) . fmap snd $ atoms
      }

-- | Parser for trajectories in XYZ format as produced by CREST.
{-# SCC trajectory #-}
trajectory :: Parser Trajectory
trajectory = do
  mols <- Seq.fromList <$> many1 xyz
  skipSpace
  endOfInput
  return mols

-- | Selection of a bond between two atoms.
data B = B Int Int

-- | Selection of an angle between three atoms.
data A = A Int Int Int

-- | Selection of a dihedral angle between four atoms. Rotation around the central two.
data D = D Int Int Int Int

-- | Selections
data Feature
  = Energy
  | Bond B
  | Angle A
  | Dihedral D

-- | Calculate a bond distance.
bond :: (MonadThrow m) => B -> Molecule -> m Double
bond (B a b) Molecule{coordinates}
  | a == b = throwM . IndexException $ "selected atoms are identical"
  | otherwise = do
      coordA <- compute @U <$> (coordinates !?> a)
      coordB <- compute @U <$> (coordinates !?> b)
      vecAB <- coordA .-. coordB
      return . sqrt . Massiv.sum . Massiv.map (^ (2 :: Int)) $ vecAB

-- | Calculates the sinus of an angle defined by three atoms.
angle :: (MonadThrow m) => A -> Molecule -> m Double
angle (A a b c) Molecule{coordinates}
  | a == b || b == c || a == c = throwM . IndexException $ "selected atoms are identical"
  | otherwise = do
      coordA <- compute @U <$> (coordinates !?> a)
      coordB <- compute @U <$> (coordinates !?> b)
      coordC <- compute @U <$> (coordinates !?> c)
      vecAB <- coordA .-. coordB
      vecCB <- coordC .-. coordB
      return $ ArrayUtil.angle vecAB vecCB

{- | Calculates the dihedral angle defined by four atoms. Respects rotation direction. Obtains the
result in radian.
-}
dihedral' :: (MonadThrow m) => D -> Molecule -> m Double
dihedral' (D a b c d) Molecule{coordinates}
  | a == b || a == c || a == d || b == c || b == d || c == d = throwM . IndexException $ "selected atoms are indentical"
  | otherwise = do
      coordA <- compute @U <$> (coordinates !?> a)
      coordB <- compute @U <$> (coordinates !?> b)
      coordC <- compute @U <$> (coordinates !?> c)
      coordD <- compute @U <$> (coordinates !?> d)
      vecAB <- coordA .-. coordB
      vecBC <- coordB .-. coordC
      vecCD <- coordC .-. coordD
      let planeABC = vecH2M $ LA.cross (vecM2H vecAB) (vecM2H vecBC) :: Massiv.Vector U Double
          planeBCD = vecH2M $ LA.cross (vecM2H vecBC) (vecM2H vecCD) :: Massiv.Vector U Double
          normVecRot = LA.cross (vecM2H vecCD) (vecM2H vecBC) :: LA.Vector Double
          rotDir =
            if vecH2M normVecRot !.! vecAB < 0
              then -1
              else 1
      return $ rotDir * ArrayUtil.angle planeABC planeBCD

{- | Calculates a metric value of the dihedral angle defined by four atoms. This must create 2
values in the feature matrix, instead of one.
See <https://onlinelibrary.wiley.com/doi/full/10.1002/prot.20310)>
-}
dihedral :: (MonadThrow m) => D -> Molecule -> m (Double, Double)
dihedral d mol = do
  dihedRad <- dihedral' d mol
  return (sin dihedRad, cos dihedRad)

-- | Get all selected features from a molecule.
getFeature ::
  ( Traversable t
  , MonadThrow m
  ) =>
  -- | Selection of multiple features.
  t Feature ->
  -- | A Molecule where to apply to.
  Molecule ->
  m (Massiv.Vector Massiv.DL Double)
getFeature sel mol = do
  features <- for sel $ \case
    Energy -> return . Left . energy $ mol
    Bond b -> Left <$> bond b mol
    Angle a -> Left <$> angle a mol
    Dihedral d -> Right <$> dihedral d mol

  let featureVec =
        foldl
          ( \acc val -> case val of
              Left s -> acc `Massiv.snoc` s
              Right (a, b) -> acc `Massiv.snoc` a `Massiv.snoc` b
          )
          mempty
          features

  return featureVec

{- | Obtains the feature matrix \(\mathbf{X}\) for a principal component analysis. Given \(m\)
features to analyse in \(n\) measurements, \(\mathbf{X}\) will be a \(m \times n\) matrix.
-}
{-# SCC getFeatures #-}
getFeatures ::
  ( MonadThrow m
  , Traversable f
  ) =>
  f Feature ->
  Trajectory ->
  m (Massiv.Matrix DL Double)
getFeatures sel trj = traverse toCols trj >>= concatM (Dim 1)
 where
  toCols v = expandInner @U @Ix2 (Sz 1) const . compute @U <$> getFeature sel v