misc/haskell/Eq.hs
2018-08-05 18:53:07 +02:00

106 lines
2.7 KiB
Haskell
Executable File

#!/usr/bin/env nix-script
#!>haskell
#! haskell | attoparsec hmatrix
{- Balance chemical reactions using linear algebra. -}
{-# LANGUAGE OverloadedStrings #-}
import Control.Applicative (many, (<|>))
import Data.Function (on)
import Data.Maybe (fromMaybe)
import Data.List (groupBy, sort, nub)
import Data.Text (pack)
import Data.Attoparsec.Text
import Numeric.LinearAlgebra (Field, ident, inv, rank)
import Numeric.LinearAlgebra.Data
type Atom = String
type Element = (Atom, Z)
type Compound = [Element]
type Reaction = ([Compound], [Compound])
-- Parsers --
element :: Parser Element
element = do
atom <- ((++) <$> upper <*> lower) <|> upper
quantity <- option 1 decimal
return (atom, quantity)
where upper = charRange "A-Z"
lower = charRange "a-z"
compound :: Parser Compound
compound = reduce <$> many element
hand :: Parser [Compound]
hand = compound `sepBy` spaced "+"
reaction :: Parser Reaction
reaction = (,) <$> (hand <* spaced "->") <*> hand
-- parser helpers --
-- | Parse while ignoring enclosing whitespace
spaced :: Parser a -> Parser ()
spaced p = skipSpace >> p >> skipSpace
-- | Create a parser from a characters range
charRange :: String -> Parser String
charRange = fmap pure . satisfy . inClass
-- stochiometrycal matrix --
-- | Reduce a 'Compound' to his empirical formula
reduce :: Compound -> Compound
reduce = map count . groupBy ((==) `on` fst) . sort
where count ((s,n):xs) = (,) s (n + sum (map snd xs))
-- | Find a vector basis for the reaction
basisFor :: Reaction -> [Atom]
basisFor = nub . map fst . concat . fst
-- | Calculate the coordinats vector
coords :: [Atom] -> Compound -> Vector Z
coords basis v = fromList (map (flip count v) basis)
where count e = fromMaybe 0 . lookup e
-- | Calculate stochoimetrical matrix
stoch :: Reaction -> Matrix Z
stoch r@(x, y) = fromColumns (a ++ b)
where col = coords (basisFor r)
a = map col x
b = map (negate . col) y
-- linear algebra --
-- | Find a base of Ker A
kernel :: Matrix Z -> Vector Z
kernel a = cmap (round . min) t
where a' = extend (fromZ a) :: Matrix R
t = toColumns (inv a') !! (cols a'-1)
min = (/ minimum (toList t))
-- | Extend a Matrix to a square
extend :: Field k => Matrix k -> Matrix k
extend a
| m == n = a
| otherwise = a === fromRows padd
where (m,n) = size a
id = ident n
padd = map (toColumns id !!) [rank a-1..m-1]
main :: IO ()
main = do
input <- pack <$> prompt
case parseOnly reaction input of
Right react -> print (kernel $ stoch react)
Left err -> putStrLn ("Failed to parse: " ++ err)
main
where prompt = putStr "eq> " >> getLine