moonlight-linalg-0.1.0.0: src-dense/Moonlight/LinAlg/Pure/Dense/Block.hs
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE StandaloneKindSignatures #-}
module Moonlight.LinAlg.Pure.Dense.Block
( BlockMatrixFailure (..),
invertRationalBlock,
invertGF2Block,
invertUnimodularIntegerBlock,
)
where
import Control.Monad (foldM)
import Data.Kind (Type)
import Data.List (transpose)
import Data.Ratio (denominator, numerator)
import Moonlight.LinAlg.Internal.Discrete (GF2 (..))
-- | Failures for finite dense block inversion. These are obstruction values,
-- not runtime accidents: Schur contraction is only valid when the pivot block is
-- square and invertible in the requested coefficient domain.
type BlockMatrixFailure :: Type
data BlockMatrixFailure
= BlockMatrixNotSquare !Int ![Int]
| BlockMatrixSingular !Int
| BlockMatrixNonUnimodular ![[Rational]]
| BlockMatrixInverseLawFailed !Int
deriving stock (Eq, Show)
type FieldBlockOps :: Type -> Type
data FieldBlockOps coefficient = FieldBlockOps
{ fboZero :: !coefficient,
fboOne :: !coefficient,
fboAdd :: coefficient -> coefficient -> coefficient,
fboNegate :: coefficient -> coefficient,
fboMultiply :: coefficient -> coefficient -> coefficient,
fboInverse :: coefficient -> Maybe coefficient
}
invertRationalBlock :: [[Rational]] -> Either BlockMatrixFailure [[Rational]]
invertRationalBlock = invertFieldBlock rationalBlockOps
{-# INLINEABLE invertRationalBlock #-}
invertGF2Block :: [[GF2]] -> Either BlockMatrixFailure [[GF2]]
invertGF2Block = invertFieldBlock gf2BlockOps
{-# INLINEABLE invertGF2Block #-}
invertUnimodularIntegerBlock :: [[Integer]] -> Either BlockMatrixFailure [[Integer]]
invertUnimodularIntegerBlock matrix = do
dimension <- squareDimension matrix
rationalInverse <- invertRationalBlock (fmap (fmap toRational) matrix)
integerInverse <-
maybe
(Left (BlockMatrixNonUnimodular rationalInverse))
Right
(traverse (traverse rationalToIntegerExact) rationalInverse)
if matrixProductInteger matrix integerInverse == identityMatrix dimension
&& matrixProductInteger integerInverse matrix == identityMatrix dimension
then Right integerInverse
else Left (BlockMatrixInverseLawFailed dimension)
{-# INLINEABLE invertUnimodularIntegerBlock #-}
invertFieldBlock :: Eq coefficient => FieldBlockOps coefficient -> [[coefficient]] -> Either BlockMatrixFailure [[coefficient]]
invertFieldBlock ops matrix = do
dimension <- squareDimension matrix
let augmented = zipWith (<>) matrix (identityMatrixWith ops dimension)
reduced <- foldM (rrefPivotStep ops dimension) augmented [0 .. dimension - 1]
let leftBlock = fmap (take dimension) reduced
rightBlock = fmap (drop dimension) reduced
identity = identityMatrixWith ops dimension
if leftBlock == identity
&& matrixProductWith ops matrix rightBlock == identity
&& matrixProductWith ops rightBlock matrix == identity
then Right rightBlock
else Left (BlockMatrixInverseLawFailed dimension)
{-# INLINEABLE invertFieldBlock #-}
rationalBlockOps :: FieldBlockOps Rational
rationalBlockOps =
FieldBlockOps
{ fboZero = 0,
fboOne = 1,
fboAdd = (+),
fboNegate = negate,
fboMultiply = (*),
fboInverse = \value -> if value == 0 then Nothing else Just (recip value)
}
gf2BlockOps :: FieldBlockOps GF2
gf2BlockOps =
FieldBlockOps
{ fboZero = GF2Zero,
fboOne = GF2One,
fboAdd = (+),
fboNegate = id,
fboMultiply = (*),
fboInverse = \value -> case value of
GF2Zero -> Nothing
GF2One -> Just GF2One
}
squareDimension :: [[coefficient]] -> Either BlockMatrixFailure Int
squareDimension matrix =
let rowCount = length matrix
widths = fmap length matrix
in if all (== rowCount) widths
then Right rowCount
else Left (BlockMatrixNotSquare rowCount widths)
{-# INLINEABLE squareDimension #-}
rrefPivotStep :: Eq coefficient => FieldBlockOps coefficient -> Int -> [[coefficient]] -> Int -> Either BlockMatrixFailure [[coefficient]]
rrefPivotStep ops dimension rows pivotIndex = do
pivotRowIndex <-
requireBlockValue
(BlockMatrixSingular dimension)
(findPivotRow ops pivotIndex rows)
swappedRows <- swapRows pivotIndex pivotRowIndex rows
pivotRow <-
requireBlockValue
(BlockMatrixSingular dimension)
(rowAt pivotIndex swappedRows)
pivotValue <-
requireBlockValue
(BlockMatrixSingular dimension)
(entryAt pivotIndex pivotRow)
pivotInverse <-
requireBlockValue
(BlockMatrixSingular dimension)
(fboInverse ops pivotValue)
let normalizedPivot = fmap (fboMultiply ops pivotInverse) pivotRow
normalizedRows = replaceRow pivotIndex normalizedPivot swappedRows
pure (eliminatePivotColumn ops pivotIndex normalizedPivot normalizedRows)
{-# INLINEABLE rrefPivotStep #-}
findPivotRow :: Eq coefficient => FieldBlockOps coefficient -> Int -> [[coefficient]] -> Maybe Int
findPivotRow ops pivotIndex =
fmap fst
. findFirst
( \(rowIndex, rowValues) ->
rowIndex >= pivotIndex
&& maybe False (/= fboZero ops) (entryAt pivotIndex rowValues)
)
. zip [0 ..]
{-# INLINEABLE findPivotRow #-}
eliminatePivotColumn :: Eq coefficient => FieldBlockOps coefficient -> Int -> [coefficient] -> [[coefficient]] -> [[coefficient]]
eliminatePivotColumn ops pivotIndex normalizedPivot =
fmap
( \(rowIndex, rowValues) ->
if rowIndex == pivotIndex
then normalizedPivot
else
case entryAt pivotIndex rowValues of
Nothing -> rowValues
Just factor
| factor == fboZero ops -> rowValues
| otherwise -> subtractMultiple ops factor normalizedPivot rowValues
)
. zip [0 ..]
{-# INLINEABLE eliminatePivotColumn #-}
subtractMultiple :: FieldBlockOps coefficient -> coefficient -> [coefficient] -> [coefficient] -> [coefficient]
subtractMultiple ops factor pivotRow targetRow =
zipWith
(\targetEntry pivotEntry -> fboAdd ops targetEntry (fboNegate ops (fboMultiply ops factor pivotEntry)))
targetRow
pivotRow
{-# INLINEABLE subtractMultiple #-}
swapRows :: Int -> Int -> [[coefficient]] -> Either BlockMatrixFailure [[coefficient]]
swapRows leftIndex rightIndex rows = do
leftRow <- requireBlockValue (BlockMatrixSingular (length rows)) (rowAt leftIndex rows)
rightRow <- requireBlockValue (BlockMatrixSingular (length rows)) (rowAt rightIndex rows)
pure
( fmap
( \(rowIndex, rowValues) ->
if rowIndex == leftIndex
then rightRow
else
if rowIndex == rightIndex
then leftRow
else rowValues
)
(zip [0 ..] rows)
)
{-# INLINEABLE swapRows #-}
replaceRow :: Int -> [coefficient] -> [[coefficient]] -> [[coefficient]]
replaceRow targetIndex replacement =
fmap
(\(rowIndex, rowValues) -> if rowIndex == targetIndex then replacement else rowValues)
. zip [0 ..]
{-# INLINEABLE replaceRow #-}
requireBlockValue :: BlockMatrixFailure -> Maybe value -> Either BlockMatrixFailure value
requireBlockValue failureValue =
maybe (Left failureValue) Right
{-# INLINEABLE requireBlockValue #-}
rowAt :: Int -> [row] -> Maybe row
rowAt indexValue rows
| indexValue < 0 = Nothing
| otherwise =
case drop indexValue rows of
rowValue : _ -> Just rowValue
[] -> Nothing
{-# INLINE rowAt #-}
entryAt :: Int -> [entry] -> Maybe entry
entryAt indexValue entries
| indexValue < 0 = Nothing
| otherwise =
case drop indexValue entries of
entryValue : _ -> Just entryValue
[] -> Nothing
{-# INLINE entryAt #-}
identityMatrix :: Num coefficient => Int -> [[coefficient]]
identityMatrix dimension =
[ [ if rowIndex == columnIndex then 1 else 0
| columnIndex <- [0 .. dimension - 1]
]
| rowIndex <- [0 .. dimension - 1]
]
{-# INLINEABLE identityMatrix #-}
identityMatrixWith :: FieldBlockOps coefficient -> Int -> [[coefficient]]
identityMatrixWith ops dimension =
[ [ if rowIndex == columnIndex then fboOne ops else fboZero ops
| columnIndex <- [0 .. dimension - 1]
]
| rowIndex <- [0 .. dimension - 1]
]
{-# INLINEABLE identityMatrixWith #-}
matrixProductInteger :: [[Integer]] -> [[Integer]] -> [[Integer]]
matrixProductInteger = matrixProductWith integerBlockOps
{-# INLINEABLE matrixProductInteger #-}
integerBlockOps :: FieldBlockOps Integer
integerBlockOps =
FieldBlockOps
{ fboZero = 0,
fboOne = 1,
fboAdd = (+),
fboNegate = negate,
fboMultiply = (*),
fboInverse = \value -> case value of
1 -> Just 1
-1 -> Just (-1)
_ -> Nothing
}
matrixProductWith :: FieldBlockOps coefficient -> [[coefficient]] -> [[coefficient]] -> [[coefficient]]
matrixProductWith ops left right =
let rightColumns = transpose right
in fmap
( \leftRow ->
fmap
(dotWith ops leftRow)
rightColumns
)
left
{-# INLINEABLE matrixProductWith #-}
dotWith :: FieldBlockOps coefficient -> [coefficient] -> [coefficient] -> coefficient
dotWith ops left right =
foldl'
(fboAdd ops)
(fboZero ops)
(zipWith (fboMultiply ops) left right)
{-# INLINEABLE dotWith #-}
rationalToIntegerExact :: Rational -> Maybe Integer
rationalToIntegerExact value =
if denominator value == 1
then Just (numerator value)
else Nothing
{-# INLINEABLE rationalToIntegerExact #-}
findFirst :: (a -> Bool) -> [a] -> Maybe a
findFirst predicate =
foldr
(\value rest -> if predicate value then Just value else rest)
Nothing
{-# INLINE findFirst #-}