moonlight-linalg-0.1.0.0: src-dense/Moonlight/LinAlg/Pure/Dense/Exterior.hs
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE StandaloneKindSignatures #-}
module Moonlight.LinAlg.Pure.Dense.Exterior
( ExteriorBasis (..),
ExteriorPowerFailure (..),
choose,
exteriorBasis,
exteriorBasisCardinality,
exteriorPowerMatrix,
exteriorPowerMatrixWithShape,
)
where
import Data.Kind (Type)
import Data.Vector qualified as Box
-- | Ordered basis of Λ^p(k^n), represented by increasing source-coordinate
-- subsets. The order is lexicographic and therefore stable across coefficient
-- rings.
type ExteriorBasis :: Type
data ExteriorBasis = ExteriorBasis
{ ebDegree :: !Int,
ebRank :: !Int,
ebBasisVectors :: ![[Int]]
}
deriving stock (Eq, Ord, Show)
type ExteriorPowerFailure :: Type
data ExteriorPowerFailure
= ExteriorNegativeDegree !Int
| ExteriorNegativeSourceRank !Int
| ExteriorNegativeTargetRank !Int
| ExteriorMatrixShapeMismatch
!Int
!Int
!Int
![Int]
deriving stock (Eq, Ord, Show)
choose :: Int -> Int -> Integer
choose n k
| n < 0 || k < 0 || k > n = 0
| otherwise = choosePositive n (min k (n - k))
where
choosePositive :: Int -> Int -> Integer
choosePositive _ 0 = 1
choosePositive nValue kValue =
product [fromIntegral (nValue - kValue + 1) .. fromIntegral nValue]
`div` product [1 .. fromIntegral kValue]
{-# INLINEABLE choose #-}
exteriorBasis :: Int -> Int -> Either ExteriorPowerFailure ExteriorBasis
exteriorBasis degree rankValue
| degree < 0 = Left (ExteriorNegativeDegree degree)
| rankValue < 0 = Left (ExteriorNegativeSourceRank rankValue)
| otherwise =
Right
ExteriorBasis
{ ebDegree = degree,
ebRank = rankValue,
ebBasisVectors = combinationsOf degree [0 .. rankValue - 1]
}
{-# INLINEABLE exteriorBasis #-}
exteriorBasisCardinality :: ExteriorBasis -> Int
exteriorBasisCardinality =
length . ebBasisVectors
{-# INLINE exteriorBasisCardinality #-}
-- | Infer source/target ranks from a rectangular dense matrix and compute the
-- induced Λ^p matrix. Use 'exteriorPowerMatrixWithShape' when a zero-row matrix
-- must still remember its source rank.
exteriorPowerMatrix :: Num coefficient => Int -> [[coefficient]] -> Either ExteriorPowerFailure [[coefficient]]
exteriorPowerMatrix degree matrix =
case inferredShape matrix of
Left failure -> Left failure
Right (targetRank, sourceRank) ->
exteriorPowerMatrixWithShape degree targetRank sourceRank matrix
{-# INLINEABLE exteriorPowerMatrix #-}
-- | Compute the induced matrix Λ^p(f) for an explicitly shaped matrix
-- f : k^sourceRank -> k^targetRank. Rows are target coordinates; columns are
-- source coordinates. Entry (I,J) is the determinant of the I×J minor.
exteriorPowerMatrixWithShape ::
Num coefficient =>
Int ->
Int ->
Int ->
[[coefficient]] ->
Either ExteriorPowerFailure [[coefficient]]
exteriorPowerMatrixWithShape degree targetRank sourceRank matrix
| degree < 0 = Left (ExteriorNegativeDegree degree)
| sourceRank < 0 = Left (ExteriorNegativeSourceRank sourceRank)
| targetRank < 0 = Left (ExteriorNegativeTargetRank targetRank)
| actualShapeRows matrix /= targetRank || any (/= sourceRank) (actualShapeColumns matrix) =
Left
( ExteriorMatrixShapeMismatch
targetRank
sourceRank
(actualShapeRows matrix)
(actualShapeColumns matrix)
)
| otherwise = do
targetBasis <- exteriorBasisWithRole targetRank
sourceBasis <- exteriorBasisWithRole sourceRank
let entries = denseEntries matrix
shapeFailure =
ExteriorMatrixShapeMismatch
targetRank
sourceRank
(actualShapeRows matrix)
(actualShapeColumns matrix)
traverse
(\targetVector -> traverse (minorDeterminant shapeFailure sourceRank entries targetVector) (ebBasisVectors sourceBasis))
(ebBasisVectors targetBasis)
where
exteriorBasisWithRole rankValue =
case exteriorBasis degree rankValue of
Left (ExteriorNegativeSourceRank badRank) -> Left (ExteriorNegativeTargetRank badRank)
Left failure -> Left failure
Right basis -> Right basis
{-# INLINEABLE exteriorPowerMatrixWithShape #-}
inferredShape :: [[coefficient]] -> Either ExteriorPowerFailure (Int, Int)
inferredShape matrix =
case actualShapeColumns matrix of
[] -> Right (0, 0)
firstWidth : widths ->
if all (== firstWidth) widths
then Right (length matrix, firstWidth)
else Left (ExteriorMatrixShapeMismatch (length matrix) firstWidth (length matrix) (firstWidth : widths))
actualShapeRows :: [[coefficient]] -> Int
actualShapeRows =
length
{-# INLINE actualShapeRows #-}
actualShapeColumns :: [[coefficient]] -> [Int]
actualShapeColumns =
fmap length
{-# INLINE actualShapeColumns #-}
combinationsOf :: Int -> [a] -> [[a]]
combinationsOf degree values
| degree < 0 = []
| otherwise =
case (degree, values) of
(0, _) -> [[]]
(_, []) -> []
(remaining, value : rest) ->
fmap (value :) (combinationsOf (remaining - 1) rest)
<> combinationsOf remaining rest
{-# INLINEABLE combinationsOf #-}
denseEntries :: [[coefficient]] -> Box.Vector coefficient
denseEntries matrix =
Box.fromList (concat matrix)
{-# INLINE denseEntries #-}
entryAt ::
ExteriorPowerFailure ->
Int ->
Box.Vector coefficient ->
Int ->
Int ->
Either ExteriorPowerFailure coefficient
entryAt failure sourceRank entries targetIndex sourceIndex =
maybe (Left failure) Right (entries Box.!? (targetIndex * sourceRank + sourceIndex))
{-# INLINE entryAt #-}
minorDeterminant ::
Num coefficient =>
ExteriorPowerFailure ->
Int ->
Box.Vector coefficient ->
[Int] ->
[Int] ->
Either ExteriorPowerFailure coefficient
minorDeterminant failure sourceRank entries targetVector sourceVector =
case (targetVector, sourceVector) of
([], []) -> Right 1
([row0], [column0]) ->
entryAt failure sourceRank entries row0 column0
([row0, row1], [column0, column1]) -> do
a <- entryAt failure sourceRank entries row0 column0
b <- entryAt failure sourceRank entries row0 column1
c <- entryAt failure sourceRank entries row1 column0
d <- entryAt failure sourceRank entries row1 column1
Right ((a * d) - (b * c))
([row0, row1, row2], [column0, column1, column2]) -> do
a <- entryAt failure sourceRank entries row0 column0
b <- entryAt failure sourceRank entries row0 column1
c <- entryAt failure sourceRank entries row0 column2
d <- entryAt failure sourceRank entries row1 column0
e <- entryAt failure sourceRank entries row1 column1
f <- entryAt failure sourceRank entries row1 column2
g <- entryAt failure sourceRank entries row2 column0
h <- entryAt failure sourceRank entries row2 column1
i <- entryAt failure sourceRank entries row2 column2
Right ((a * e * i) + (b * f * g) + (c * d * h) - (c * e * g) - (b * d * i) - (a * f * h))
_ ->
fmap determinant
( traverse
(\targetIndex -> traverse (entryAt failure sourceRank entries targetIndex) sourceVector)
targetVector
)
{-# INLINE minorDeterminant #-}
determinant :: Num coefficient => [[coefficient]] -> coefficient
determinant matrix =
case matrix of
[] -> 1
[singleRow] ->
case singleRow of
[value] -> value
_ -> 0
firstRow : remainingRows ->
sum
( fmap
(\(columnIndex, value) -> signFor columnIndex * value * determinant (removeColumn columnIndex remainingRows))
(zip [0 ..] firstRow)
)
{-# INLINEABLE determinant #-}
removeColumn :: Int -> [[coefficient]] -> [[coefficient]]
removeColumn columnIndex =
fmap (fmap snd . filter ((/= columnIndex) . fst) . zip [0 :: Int ..])
{-# INLINE removeColumn #-}
signFor :: Num coefficient => Int -> coefficient
signFor columnIndex =
if even columnIndex then 1 else (-1)
{-# INLINE signFor #-}