packages feed

moonlight-homology-0.1.0.0: src-matrix/Moonlight/Homology/Boundary/Finite.hs

module Moonlight.Homology.Boundary.Finite
  ( FiniteChainComplex,
    mkFiniteChainComplex,
    mkFiniteChainComplexChecked,
    maxHomologicalDegree,
    incidenceMatrixAt,
    degreeCardinality,
    basisCellNodeId,
    finiteChainBasisRefsAtDegree,
    basisIndexCellMapAtDegree,
    inverseBasisRefMap,
    validateFiniteChainComplexShape,
    restrictComplex,
  )
where

import Data.Foldable (traverse_)
import Data.Function ((&))
import Data.Kind (Type)
import Data.Maybe (mapMaybe)
import qualified Data.Map.Strict as Map
import Data.Set (Set)
import qualified Data.Set as Set
import Moonlight.Core (Semiring)
import Moonlight.Homology.Boundary.LinAlg
  ( BoundaryIncidence,
    boundaryCoefficient,
    boundaryEntries,
    composeBoundaryIncidence,
    emptyBoundaryIncidence,
    emptyBoundaryIncidenceOf,
    materializeIncidenceBoundary,
    sourceCardinality,
    sourceIndex,
    targetCardinality,
    targetIndex,
  )
import Moonlight.Homology.Pure.Degree (HomologicalDegree (..), decrementDegree)
import Moonlight.Homology.Pure.Failure (HomologyFailure (..))
import Moonlight.Homology.Pure.Carrier (BasisCellRef (..))

type FiniteChainComplex :: Type -> Type
data FiniteChainComplex r = FiniteChainComplex
  { maxDimension :: Int,
    incidenceMatrix :: Int -> BoundaryIncidence r
  }

mkFiniteChainComplex :: HomologicalDegree -> (HomologicalDegree -> BoundaryIncidence r) -> FiniteChainComplex r
mkFiniteChainComplex (HomologicalDegree maxDimensionValue) incidenceLookup =
  FiniteChainComplex
    { maxDimension = maxDimensionValue,
      incidenceMatrix = incidenceLookup . HomologicalDegree
    }

mkFiniteChainComplexChecked ::
  (Eq r, Num r, Semiring r) =>
  HomologicalDegree ->
  (HomologicalDegree -> BoundaryIncidence r) ->
  Either HomologyFailure (FiniteChainComplex r)
mkFiniteChainComplexChecked maxDegree incidenceLookup = do
  let finite = mkFiniteChainComplex maxDegree incidenceLookup
  validateFiniteChainComplexShape finite
  traverse_ (adjacentNilpotenceAt finite) [0 .. maxDimension finite - 1]
  pure finite

adjacentNilpotenceAt ::
  (Eq r, Num r, Semiring r) =>
  FiniteChainComplex r ->
  Int ->
  Either HomologyFailure ()
adjacentNilpotenceAt finite degreeIndex =
  composeBoundaryIncidence
    (incidenceMatrixAt finite (HomologicalDegree degreeIndex))
    (incidenceMatrixAt finite (HomologicalDegree (degreeIndex + 1)))
    & either
      (Left . InvalidBoundaryIncidence . show)
      ( \composed ->
          if null (boundaryEntries composed)
            then Right ()
            else Left (ChainComplexNilpotenceViolation degreeIndex)
      )

maxHomologicalDegree :: FiniteChainComplex r -> HomologicalDegree
maxHomologicalDegree =
  HomologicalDegree . maxDimension

incidenceMatrixAt :: FiniteChainComplex r -> HomologicalDegree -> BoundaryIncidence r
incidenceMatrixAt finite =
  incidenceMatrix finite . unHomologicalDegree

degreeCardinality :: FiniteChainComplex r -> HomologicalDegree -> Int
degreeCardinality finite degreeValue@(HomologicalDegree degreeIndex) =
  case maxHomologicalDegree finite of
    HomologicalDegree maxDegreeValue
      | degreeIndex < 0 -> 0
      | degreeIndex > maxDegreeValue -> 0
      | otherwise -> sourceCardinality (incidenceMatrixAt finite degreeValue)

basisCellNodeId :: FiniteChainComplex r -> BasisCellRef -> Int
basisCellNodeId finite basisCellRef =
  case cellDegree basisCellRef of
    HomologicalDegree degreeValue ->
      sum
        (fmap (degreeCardinality finite . HomologicalDegree) [0 .. degreeValue - 1])
        + cellIndex basisCellRef

finiteChainBasisRefsAtDegree :: FiniteChainComplex r -> HomologicalDegree -> [BasisCellRef]
finiteChainBasisRefsAtDegree finite degreeValue =
  fmap
    (\cellIndexValue -> BasisCellRef {cellDegree = degreeValue, cellIndex = cellIndexValue})
    [0 .. degreeCardinality finite degreeValue - 1]

basisIndexCellMapAtDegree ::
  HomologicalDegree ->
  Map.Map cell BasisCellRef ->
  Map.Map Int cell
basisIndexCellMapAtDegree degreeValue =
  Map.foldrWithKey
    ( \cellValue basisCellRef ->
        if cellDegree basisCellRef == degreeValue
          then Map.insert (cellIndex basisCellRef) cellValue
          else id
    )
    Map.empty

inverseBasisRefMap :: Map.Map cell BasisCellRef -> Map.Map BasisCellRef cell
inverseBasisRefMap =
  Map.fromList . fmap (\(cellValue, basisRef) -> (basisRef, cellValue)) . Map.toList

validateFiniteChainComplexShape :: FiniteChainComplex r -> Either HomologyFailure ()
validateFiniteChainComplexShape finite =
  dimensionsOf finite
    & mapMaybe (shapeViolation finite)
    & safeHead
    & maybe (Right ()) Left

dimensionsOf :: FiniteChainComplex r -> [HomologicalDegree]
dimensionsOf finite =
  fmap HomologicalDegree [0 .. maxDimension finite]

shapeViolation :: FiniteChainComplex r -> HomologicalDegree -> Maybe HomologyFailure
shapeViolation finite degreeValue@(HomologicalDegree degreeIndex) =
  let incidence = incidenceMatrixAt finite degreeValue
      expectedTarget =
        if degreeIndex <= 0
          then 0
          else sourceCardinality (incidenceMatrixAt finite (decrementDegree degreeValue))
   in if targetCardinality incidence == expectedTarget
        then Nothing
        else Just (ChainComplexShapeMismatch degreeIndex expectedTarget (targetCardinality incidence))

-- | Restrict a complex to the given active cells. Every retained degree is
-- materialized exactly once up front (the previous incarnation re-ran a full
-- entry-list scan per source cell on every incidence access), and any
-- materialization failure propagates as a 'Left' instead of being collapsed
-- into a silently empty boundary matrix.
restrictComplex :: Set BasisCellRef -> FiniteChainComplex Int -> Either HomologyFailure (FiniteChainComplex Int)
restrictComplex activeCells finiteComplex = do
  let retainedByDegree = groupByDegree (Set.toAscList activeCells)
      maxDimensionValue = maximum (0 : Map.keys retainedByDegree)
  restrictedBoundaries <-
    traverse
      (restrictedBoundaryAt activeCells retainedByDegree finiteComplex)
      [0 .. maxDimensionValue]
  let boundaryByDegree = Map.fromList (zip [0 .. maxDimensionValue] restrictedBoundaries)
  mkFiniteChainComplexChecked
    (HomologicalDegree maxDimensionValue)
    ( \(HomologicalDegree degreeValue) ->
        Map.findWithDefault emptyBoundaryIncidence degreeValue boundaryByDegree
    )

restrictedBoundaryAt ::
  Set BasisCellRef ->
  Map.Map Int [BasisCellRef] ->
  FiniteChainComplex Int ->
  Int ->
  Either HomologyFailure (BoundaryIncidence Int)
restrictedBoundaryAt activeCells retainedByDegree finiteComplex dimensionValue
  | dimensionValue <= 0 =
      Right
        ( emptyBoundaryIncidenceOf
            (fromIntegral (length (Map.findWithDefault [] 0 retainedByDegree)))
            0
        )
  | otherwise =
      let incidence = incidenceMatrixAt finiteComplex (HomologicalDegree dimensionValue)
          entriesBySource =
            foldl'
              ( \accumulator entryValue ->
                  Map.insertWith (<>) (sourceIndex entryValue) [entryValue] accumulator
              )
              Map.empty
              (boundaryEntries incidence)
          restrictedEntriesOf sourceCell =
            Map.findWithDefault [] (cellIndex sourceCell) entriesBySource
              & mapMaybe
                ( \entryValue ->
                    let targetCell =
                          BasisCellRef
                            { cellDegree = HomologicalDegree (dimensionValue - 1),
                              cellIndex = targetIndex entryValue
                            }
                     in if Set.member targetCell activeCells
                          then Just (boundaryCoefficient entryValue, targetCell)
                          else Nothing
                )
       in materializeIncidenceBoundary
            restrictedEntriesOf
            (Map.findWithDefault [] dimensionValue retainedByDegree)
            (Map.findWithDefault [] (dimensionValue - 1) retainedByDegree)

groupByDegree :: [BasisCellRef] -> Map.Map Int [BasisCellRef]
groupByDegree cellRefs =
  -- Strict left fold with O(1) prepends, then one reversal per bucket:
  -- preserves the ascending order 'Set.toAscList' delivers without either a
  -- foldr thunk spine or quadratic list appends.
  Map.map
    reverse
    ( foldl'
        (\accumulator cellRef -> Map.insertWith (<>) (unHomologicalDegree (cellDegree cellRef)) [cellRef] accumulator)
        Map.empty
        cellRefs
    )

safeHead :: [a] -> Maybe a
safeHead values =
  case values of
    firstValue : _ -> Just firstValue
    [] -> Nothing