packages feed

moonlight-homology-0.1.0.0: src-topology/Moonlight/Homology/Pure/Topology/Integral.hs

module Moonlight.Homology.Pure.Topology.Integral
  ( SmithDecomposition (..),
    IntegralHomologyDegreeWitness (..),
    integralHomologyGroupsOf,
    exactRepresentativeClassesOf,
    integralHomologyWitnessesOf,
    integralHomologyWitnessAt,
    smithDecompositionOf,
    representativeClassesFromSmith,
    vectorToIntegralRepresentative,
  )
where

import Data.Bifunctor (first)
import Data.Function ((&))
import qualified Data.IntMap.Strict as IntMap
import Data.Kind (Type)
import qualified Data.List as List
import Moonlight.Homology.Boundary.Finite
  ( FiniteChainComplex,
    incidenceMatrixAt,
  )
import Moonlight.Homology.Boundary.LinAlg
  ( BoundaryIncidence,
    boundaryCoefficient,
    boundaryEntries,
    sourceCardinality,
    sourceIndex,
    targetCardinality,
    targetIndex,
  )
import Moonlight.Homology.Pure.Chain
  ( ExactRepresentativeClass (..),
    HomologicalDegree (..),
    RepresentativeChain (..),
    RepresentativeCycle,
  )
import Moonlight.Homology.Pure.Failure (HomologyFailure (..), HomologyLaw (..))
import Moonlight.Homology.Pure.Group (HomologyGroup (..))
import Moonlight.Homology.Pure.Matrix.Validated
  ( validatedColumnAt,
    validatedDiagonal,
    validatedMatrixFromRows,
  )
import Moonlight.Homology.Pure.Topology.Core
import Moonlight.Homology.Pure.Topology.Graph
  ( GraphOneComplex (..),
    graphOneComplexFromComplex,
  )
import Moonlight.LinAlg.Pure.Dense.Dynamic (mkDynMatrix, withDynMatrix)
import Moonlight.LinAlg.Pure.Dense.Types (toListMatrix)
import Moonlight.LinAlg.Pure.Domain.Smith (SmithNormalForm (..), smithNormalForm)

type SmithDecomposition :: Type
data SmithDecomposition = SmithDecomposition
  { smithLeftRows :: [[Integer]],
    smithDiagonalRows :: [[Integer]],
    smithRightRows :: [[Integer]],
    smithLeftInverseRows :: [[Integer]],
    smithRightInverseRows :: [[Integer]],
    smithDiagonalValues :: [Integer],
    smithRankValue :: Int
  }
  deriving stock (Eq, Show)

type IntegralBoundarySummary :: Type
data IntegralBoundarySummary = IntegralBoundarySummary
  { integralBoundaryDegree :: HomologicalDegree,
    integralBoundarySourceRank :: Int,
    integralBoundaryTargetRank :: Int,
    integralBoundaryRows :: [[Integer]],
    integralBoundarySmith :: SmithDecomposition
  }
  deriving stock (Eq, Show)

type KernelCoordinateHomology :: Type
data KernelCoordinateHomology = KernelCoordinateHomology
  { kernelCoordinateGroup :: HomologyGroup Integer,
    kernelCoordinateImageSmith :: SmithDecomposition
  }

type IntegralHomologyDegreeWitness :: Type -> Type
data IntegralHomologyDegreeWitness basis = IntegralHomologyDegreeWitness
  { integralWitnessDegree :: HomologicalDegree,
    integralWitnessGroup :: HomologyGroup Integer,
    integralWitnessClasses :: [ExactRepresentativeClass basis]
  }

integralHomologyGroupsOf :: Integral r => FiniteChainComplex r -> Either HomologyFailure [HomologyGroup Integer]
integralHomologyGroupsOf finite =
  case graphIntegralHomologyGroupsOf finite of
    Just graphGroups -> graphGroups
    Nothing -> do
      preparedBoundaries <- prepareIntegralBoundarySummaries finite
      traverse (integralHomologyGroupAtPrepared preparedBoundaries) (dimensionsOf finite)

graphIntegralHomologyGroupsOf ::
  Integral r =>
  FiniteChainComplex r ->
  Maybe (Either HomologyFailure [HomologyGroup Integer])
graphIntegralHomologyGroupsOf finite =
  graphIntegralHomologyGroups (HomologicalDegree 1 `elem` dimensionsOf finite)
    <$> graphOneComplexFromComplex finite

graphIntegralHomologyGroups :: Bool -> GraphOneComplex -> Either HomologyFailure [HomologyGroup Integer]
graphIntegralHomologyGroups includesDegreeOne graph =
  let vertexCount = graphOneVertexCount graph
      edgeCount = graphOneEdgeCount graph
      componentCount = length (graphOneComponents graph)
      boundaryRank = vertexCount - componentCount
      h0 =
        HomologyGroup
          { freeRank = componentCount,
            torsionInvariants = []
          }
      h1 =
        HomologyGroup
          { freeRank = edgeCount - boundaryRank,
            torsionInvariants = []
          }
   in pure
        ( if includesDegreeOne
            then [h0, h1]
            else [h0]
        )

prepareIntegralBoundarySummaries ::
  Integral r =>
  FiniteChainComplex r ->
  Either HomologyFailure (IntMap.IntMap IntegralBoundarySummary)
prepareIntegralBoundarySummaries finite =
  traverse
    ( \degreeValue@(HomologicalDegree degreeIndex) -> do
        boundarySummary <- integralBoundarySummaryAt finite degreeValue
        pure (degreeIndex, boundarySummary)
    )
    (dimensionsOf finite <> [nextDegreeAfterFinite finite])
    & fmap IntMap.fromList

nextDegreeAfterFinite :: FiniteChainComplex r -> HomologicalDegree
nextDegreeAfterFinite finite =
  case dimensionsOf finite of
    [] -> HomologicalDegree 0
    degreeValues ->
      HomologicalDegree
        ( 1
            + maximum
              (fmap (\(HomologicalDegree degreeIndex) -> degreeIndex) degreeValues)
        )

integralBoundarySummaryAt ::
  Integral r =>
  FiniteChainComplex r ->
  HomologicalDegree ->
  Either HomologyFailure IntegralBoundarySummary
integralBoundarySummaryAt finite degreeValue =
  let boundaryIncidenceValue = incidenceMatrixAt finite degreeValue
      rowCount = targetCardinality boundaryIncidenceValue
      columnCount = sourceCardinality boundaryIncidenceValue
      boundaryRows = integralBoundaryMatrixRows boundaryIncidenceValue
   in do
        smithValue <- smithDecompositionOf rowCount columnCount boundaryRows
        pure
          IntegralBoundarySummary
            { integralBoundaryDegree = degreeValue,
              integralBoundarySourceRank = columnCount,
              integralBoundaryTargetRank = rowCount,
              integralBoundaryRows = boundaryRows,
              integralBoundarySmith = smithValue
            }

integralHomologyGroupAtPrepared ::
  IntMap.IntMap IntegralBoundarySummary ->
  HomologicalDegree ->
  Either HomologyFailure (HomologyGroup Integer)
integralHomologyGroupAtPrepared preparedBoundaries degreeValue@(HomologicalDegree degreeIndex) = do
  boundarySummary <- requirePreparedBoundary preparedBoundaries degreeValue
  incomingSummary <- requirePreparedBoundary preparedBoundaries (HomologicalDegree (degreeIndex + 1))
  let chainRank = integralBoundarySourceRank boundarySummary
      boundaryRank = smithRankValue (integralBoundarySmith boundarySummary)
      incomingRank = smithRankValue (integralBoundarySmith incomingSummary)
      kernelRank = chainRank - boundaryRank
  if boundaryRank == 0
    then cokernelGroup chainRank incomingSummary
    else
      if incomingRank == 0
        then pure HomologyGroup {freeRank = kernelRank, torsionInvariants = []}
        else
          kernelCoordinateGroup
            <$> kernelCoordinateHomologyFromSummaries boundarySummary incomingSummary

requirePreparedBoundary ::
  IntMap.IntMap IntegralBoundarySummary ->
  HomologicalDegree ->
  Either HomologyFailure IntegralBoundarySummary
requirePreparedBoundary preparedBoundaries degreeValue@(HomologicalDegree degreeIndex) =
  case IntMap.lookup degreeIndex preparedBoundaries of
    Just boundarySummary -> Right boundarySummary
    Nothing ->
      Left
        ( InvalidTopologyInput
            ( "integral homology missing prepared boundary summary at degree "
                <> show degreeValue
            )
        )

cokernelGroup ::
  Int ->
  IntegralBoundarySummary ->
  Either HomologyFailure (HomologyGroup Integer)
cokernelGroup ambientRank incomingSummary =
  if incomingBoundaryCompatibleWithAmbient ambientRank incomingSummary
    then
      let incomingSmith = integralBoundarySmith incomingSummary
       in pure
            HomologyGroup
              { freeRank = ambientRank - smithRankValue incomingSmith,
                torsionInvariants =
                  smithDiagonalValues incomingSmith
                    & take (smithRankValue incomingSmith)
                    & torsionInvariantsFromDiagonal
              }
    else incompatibleIncomingBoundary ambientRank incomingSummary

kernelCoordinateHomologyFromSummaries ::
  IntegralBoundarySummary ->
  IntegralBoundarySummary ->
  Either HomologyFailure KernelCoordinateHomology
kernelCoordinateHomologyFromSummaries boundarySummary incomingSummary =
  let chainRank = integralBoundarySourceRank boundarySummary
   in if incomingBoundaryCompatibleWithAmbient chainRank incomingSummary
        then do
          let boundarySmith = integralBoundarySmith boundarySummary
              boundaryRank = smithRankValue boundarySmith
              kernelRank = chainRank - boundaryRank
          imageCoordinatesFull <-
            integerMatrixProduct
              chainRank
              (smithRightInverseRows boundarySmith)
              chainRank
              (ambientIncomingRows chainRank incomingSummary)
          let leadingImageCoordinates = takeRows boundaryRank imageCoordinatesFull
              kernelImageCoordinates = dropRows boundaryRank imageCoordinatesFull
          if allZeroMatrix leadingImageCoordinates
            then do
              imageSmith <-
                smithDecompositionOf
                  kernelRank
                  (integralBoundarySourceRank incomingSummary)
                  kernelImageCoordinates
              pure
                KernelCoordinateHomology
                  { kernelCoordinateGroup =
                      HomologyGroup
                        { freeRank = kernelRank - smithRankValue imageSmith,
                          torsionInvariants =
                            smithDiagonalValues imageSmith
                              & take (smithRankValue imageSmith)
                              & torsionInvariantsFromDiagonal
                        },
                    kernelCoordinateImageSmith = imageSmith
                  }
            else Left (LawViolation ChainNilpotenceLaw)
        else incompatibleIncomingBoundary chainRank incomingSummary

incomingBoundaryCompatibleWithAmbient :: Int -> IntegralBoundarySummary -> Bool
incomingBoundaryCompatibleWithAmbient ambientRank incomingSummary =
  integralBoundarySourceRank incomingSummary == 0
    || integralBoundaryTargetRank incomingSummary == ambientRank

ambientIncomingRows :: Int -> IntegralBoundarySummary -> [[Integer]]
ambientIncomingRows ambientRank incomingSummary =
  if integralBoundaryTargetRank incomingSummary == ambientRank
    then integralBoundaryRows incomingSummary
    else replicate ambientRank []

incompatibleIncomingBoundary ::
  Int ->
  IntegralBoundarySummary ->
  Either HomologyFailure a
incompatibleIncomingBoundary ambientRank incomingSummary =
  Left
    ( InvalidTopologyInput
        ( "incoming boundary at degree "
            <> show (integralBoundaryDegree incomingSummary)
            <> " targets "
            <> show (integralBoundaryTargetRank incomingSummary)
            <> " cells, expected "
            <> show ambientRank
        )
    )

torsionInvariantsFromDiagonal :: [Integer] -> [Integer]
torsionInvariantsFromDiagonal =
  filter (> 1) . fmap abs

exactRepresentativeClassesOf ::
  Integral r =>
  FiniteChainComplex r ->
  Either HomologyFailure [ExactRepresentativeClass Int]
exactRepresentativeClassesOf finite =
  integralHomologyWitnessesOf finite
    & fmap (concatMap integralWitnessClasses)

integralHomologyWitnessesOf ::
  Integral r =>
  FiniteChainComplex r ->
  Either HomologyFailure [IntegralHomologyDegreeWitness Int]
integralHomologyWitnessesOf finite = do
  preparedBoundaries <- prepareIntegralBoundarySummaries finite
  traverse (integralHomologyWitnessAtPrepared preparedBoundaries) (dimensionsOf finite)

integralHomologyWitnessAt ::
  Integral r =>
  FiniteChainComplex r ->
  HomologicalDegree ->
  Either HomologyFailure (IntegralHomologyDegreeWitness Int)
integralHomologyWitnessAt finite degreeValue = do
  preparedBoundaries <- prepareIntegralBoundarySummaries finite
  integralHomologyWitnessAtPrepared preparedBoundaries degreeValue

integralHomologyWitnessAtPrepared ::
  IntMap.IntMap IntegralBoundarySummary ->
  HomologicalDegree ->
  Either HomologyFailure (IntegralHomologyDegreeWitness Int)
integralHomologyWitnessAtPrepared preparedBoundaries degreeValue@(HomologicalDegree degreeIndex) = do
  boundarySummary <- requirePreparedBoundary preparedBoundaries degreeValue
  incomingSummary <- requirePreparedBoundary preparedBoundaries (HomologicalDegree (degreeIndex + 1))
  integralHomologyWitnessFromSummaries degreeValue boundarySummary incomingSummary

integralHomologyWitnessFromSummaries ::
  HomologicalDegree ->
  IntegralBoundarySummary ->
  IntegralBoundarySummary ->
  Either HomologyFailure (IntegralHomologyDegreeWitness Int)
integralHomologyWitnessFromSummaries degreeValue boundarySummary incomingSummary = do
  kernelCoordinateHomology <-
    kernelCoordinateHomologyFromSummaries boundarySummary incomingSummary
  let chainRank = integralBoundarySourceRank boundarySummary
      boundarySmith = integralBoundarySmith boundarySummary
      boundaryRank = smithRankValue boundarySmith
      kernelRank = chainRank - boundaryRank
      kernelBasis = dropColumns boundaryRank (smithRightRows boundarySmith)
      imageSmith = kernelCoordinateImageSmith kernelCoordinateHomology
      homologyGroupValue = kernelCoordinateGroup kernelCoordinateHomology
  classBasis <-
    integerMatrixProduct
      kernelRank
      kernelBasis
      kernelRank
      (smithLeftInverseRows imageSmith)
  classes <-
    representativeClassesFromSmith
      degreeValue
      homologyGroupValue
      (smithDiagonalValues imageSmith)
      (smithRankValue imageSmith)
      classBasis
  pure
    IntegralHomologyDegreeWitness
      { integralWitnessDegree = degreeValue,
        integralWitnessGroup = homologyGroupValue,
        integralWitnessClasses = classes
      }

smithDecompositionOf ::
  Int ->
  Int ->
  [[Integer]] ->
  Either HomologyFailure SmithDecomposition
smithDecompositionOf rowCount columnCount matrixRows =
  if rowCount < 0 || columnCount < 0
    then Left (InvalidTopologyInput "matrix dimensions must be non-negative")
    else
      if not (rowsRespectWidth columnCount matrixRows) || length matrixRows /= rowCount
        then Left (InvalidTopologyInput "matrix rows do not match their declared shape")
        else
          if rowCount == 0 || columnCount == 0
            then
              Right
                SmithDecomposition
                  { smithLeftRows = identityMatrix rowCount,
                    smithDiagonalRows = zeroMatrix rowCount columnCount,
                    smithRightRows = identityMatrix columnCount,
                    smithLeftInverseRows = identityMatrix rowCount,
                    smithRightInverseRows = identityMatrix columnCount,
                    smithDiagonalValues = [],
                    smithRankValue = 0
                  }
            else do
              dynamicMatrix <-
                first (BackendFailure . show) (mkDynMatrix rowCount columnCount (concat matrixRows))
              (leftEntries, diagonalEntries, rightEntries, leftInverseEntries, rightInverseEntries) <-
                first (BackendFailure . show) $
                  withDynMatrix dynamicMatrix
                    ( \matrixValue ->
                        smithNormalForm matrixValue
                          >>= \smithValue ->
                            Right
                              ( toListMatrix (smithLeft smithValue),
                                toListMatrix (smithDiagonal smithValue),
                                toListMatrix (smithRight smithValue),
                                toListMatrix (smithLeftInverse smithValue),
                                toListMatrix (smithRightInverse smithValue)
                              )
                    )
                    >>= id
              let leftRows = chunkColumns rowCount leftEntries
                  diagonalRows = chunkColumns columnCount diagonalEntries
                  rightRows = chunkColumns columnCount rightEntries
                  leftInverseRows = chunkColumns rowCount leftInverseEntries
                  rightInverseRows = chunkColumns columnCount rightInverseEntries
              validatedDiagonalMatrix <- validatedMatrixFromRows diagonalRows
              let diagonalValues = validatedDiagonal validatedDiagonalMatrix
                  rankValue = length (filter (/= 0) diagonalValues)
              pure
                SmithDecomposition
                  { smithLeftRows = leftRows,
                    smithDiagonalRows = diagonalRows,
                    smithRightRows = rightRows,
                    smithLeftInverseRows = leftInverseRows,
                    smithRightInverseRows = rightInverseRows,
                    smithDiagonalValues = diagonalValues,
                    smithRankValue = rankValue
                  }

integralBoundaryMatrixRows :: Integral r => BoundaryIncidence r -> [[Integer]]
integralBoundaryMatrixRows incidence =
  let rowCount = targetCardinality incidence
      columnCount = sourceCardinality incidence
      rowBuckets =
        boundaryEntries incidence
          & List.foldl'
            ( \buckets entry ->
                IntMap.insertWith
                  (IntMap.unionWith (+))
                  (targetIndex entry)
                  (IntMap.singleton (sourceIndex entry) (fromIntegral (boundaryCoefficient entry)))
                  buckets
            )
            IntMap.empty
   in enumerateFromZero rowCount
        & fmap
          ( \rowIndexValue ->
              let rowValue = IntMap.findWithDefault IntMap.empty rowIndexValue rowBuckets
               in
              enumerateFromZero columnCount
                & fmap
                  ( \columnIndexValue ->
                      IntMap.findWithDefault 0 columnIndexValue rowValue
                  )
          )

integerMatrixProduct ::
  Int ->
  [[Integer]] ->
  Int ->
  [[Integer]] ->
  Either HomologyFailure [[Integer]]
integerMatrixProduct leftColumnCount leftRows rightRowCount rightRows =
  if leftColumnCount /= rightRowCount
    then Left (InvalidTopologyInput "integer matrix multiplication encountered mismatched inner dimensions")
    else
      if
        not (rowsRespectWidth leftColumnCount leftRows)
          || length rightRows /= rightRowCount
          || not (rowsRespectWidth (matrixColumnCount rightRows) rightRows)
        then Left (InvalidTopologyInput "integer matrix multiplication received malformed rows")
        else
          let rightColumns = integerSparseColumns (matrixColumnCount rightRows) rightRows
              leftSparseRows = fmap integerSparseRow leftRows
           in Right
                ( leftSparseRows
                    & fmap
                      ( \leftRow ->
                          rightColumns
                            & fmap (integerSparseDot leftRow)
                      )
                )

type IntegerSparseRow :: Type
type IntegerSparseRow = IntMap.IntMap Integer

integerSparseColumns :: Int -> [[Integer]] -> [IntegerSparseRow]
integerSparseColumns columnCount rows =
  let columnBuckets =
        rows
          & zip [0 :: Int ..]
          & List.foldl'
            ( \buckets (rowIndex, rowValue) ->
                rowValue
                  & zip [0 :: Int ..]
                  & List.foldl'
                    ( \innerBuckets (columnIndex, coefficient) ->
                        if coefficient == 0
                          then innerBuckets
                          else
                            IntMap.insertWith
                              IntMap.union
                              columnIndex
                              (IntMap.singleton rowIndex coefficient)
                              innerBuckets
                    )
                    buckets
            )
            IntMap.empty
   in enumerateFromZero columnCount
        & fmap (\columnIndex -> IntMap.findWithDefault IntMap.empty columnIndex columnBuckets)

integerSparseRow :: [Integer] -> IntegerSparseRow
integerSparseRow values =
  values
    & zip [0 :: Int ..]
    & List.foldl'
      ( \rowValue (columnIndex, coefficient) ->
          if coefficient == 0
            then rowValue
            else IntMap.insert columnIndex coefficient rowValue
      )
      IntMap.empty

integerSparseDot :: IntegerSparseRow -> IntegerSparseRow -> Integer
integerSparseDot leftRow rightColumn =
  IntMap.foldlWithKey'
    ( \accumulator columnIndex coefficient ->
        accumulator + coefficient * IntMap.findWithDefault 0 columnIndex rightColumn
    )
    0
    leftRow

representativeClassesFromSmith ::
  HomologicalDegree ->
  HomologyGroup Integer ->
  [Integer] ->
  Int ->
  [[Integer]] ->
  Either HomologyFailure [ExactRepresentativeClass Int]
representativeClassesFromSmith degreeValue homologyGroupValue diagonalValues imageRank classBasis =
  case validatedMatrixFromRows classBasis of
    Left failureValue -> Left failureValue
    Right validatedClassBasis -> do
      torsionValues <- torsionClasses validatedClassBasis diagonalValues imageRank
      freeValues <- freeClasses validatedClassBasis (freeRank homologyGroupValue)
      pure (torsionValues <> freeValues)
  where
    torsionClasses validatedClassBasis invariants rankValue =
      concat
        <$> traverse
          ( \(indexValue, invariantValue) ->
              if abs invariantValue > 1
                then do
                  representativeVector <- validatedColumnAt indexValue validatedClassBasis
                  pure
                    [ ExactRepresentativeClass
                        { exactClassDegree = degreeValue,
                          exactClassOrder = Just (abs invariantValue),
                          exactClassRepresentative =
                            vectorToIntegralRepresentative degreeValue representativeVector
                        }
                    ]
                else pure []
          )
          (zip [0 :: Int ..] (take rankValue invariants))
    freeClasses validatedClassBasis freeCount =
      traverse
        ( \offsetValue ->
            let classIndex = imageRank + offsetValue
             in do
                  representativeVector <- validatedColumnAt classIndex validatedClassBasis
                  pure
                    ExactRepresentativeClass
                      { exactClassDegree = degreeValue,
                        exactClassOrder = Nothing,
                        exactClassRepresentative =
                          vectorToIntegralRepresentative degreeValue representativeVector
                      }
        )
        (enumerateFromZero freeCount)

vectorToIntegralRepresentative :: HomologicalDegree -> [Integer] -> RepresentativeCycle Integer Int
vectorToIntegralRepresentative degreeValue vectorValue =
  RepresentativeChain
    { representativeDegree = degreeValue,
      representativeTerms =
        vectorValue
          & zip [0 :: Int ..]
          & filter (\(_, coefficientValue) -> coefficientValue /= 0)
          & fmap (\(basisIndexValue, coefficientValue) -> (coefficientValue, basisIndexValue))
    }