packages feed

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

module Moonlight.Homology.Pure.Topology.Persistence
  ( mkFilteredFiniteChainComplex,
    mod2PersistentPairs,
    mod2PersistenceTopologyWitness,
    mod2PersistentBoundaryColumn,
    persistentPairs,
    persistenceTopologyWitness,
    persistentBoundaryColumn,
    persistenceEssentialBirths,
    reducePersistentColumn,
    reduceBoundaryColumn,
    materializeFinitePersistencePair,
    materializeEssentialPersistencePair,
    orderedFilteredCells,
    validateBirthCoverage,
    validateBirthExactness,
    validateFiltrationMonotonicity,
  )
where

import Data.Function ((&))
import Data.IntMap.Strict qualified as IntMap
import Data.Kind (Type)
import qualified Data.List as List
import qualified Data.Map.Strict as Map
import Data.Maybe (mapMaybe)
import qualified Data.Set as Set
import Moonlight.Homology.Boundary.Finite
  ( FiniteChainComplex,
    incidenceMatrixAt,
  )
import Moonlight.Homology.Boundary.LinAlg
  ( BoundaryEntry,
    BoundaryIncidence,
    boundaryCoefficient,
    boundaryEntries,
    sourceIndex,
    targetIndex,
  )
import Moonlight.Homology.Pure.Chain
  ( HomologicalDegree (..),
    PersistencePair (..),
    TopologyWitness (..),
    decrementDegree,
    emptyTopologyWitness,
  )
import Moonlight.Homology.Pure.Failure (HomologyFailure (..))
import Moonlight.Homology.Pure.Topology.Core

type OrderedFilteredCell :: Type
data OrderedFilteredCell = OrderedFilteredCell
  { orderedCellIdentity :: BasisCellRef,
    orderedCellBirth :: FiltrationValue
  }
  deriving stock (Eq, Show)

type PersistenceState :: Type
data PersistenceState = PersistenceState
  { persistenceLowColumns :: !(Map.Map Int (Set.Set Int)),
    persistencePairsByIndex :: ![(Int, Int)],
    persistenceCreators :: !(Set.Set Int)
  }

emptyPersistenceState :: PersistenceState
emptyPersistenceState =
  PersistenceState
    { persistenceLowColumns = Map.empty,
      persistencePairsByIndex = [],
      persistenceCreators = Set.empty
    }

mkFilteredFiniteChainComplex ::
  Integral r =>
  FiniteChainComplex r ->
  [(BasisCellRef, FiltrationValue)] ->
  Either HomologyFailure (FilteredFiniteChainComplex r)
mkFilteredFiniteChainComplex finite births = do
  let birthMap = Map.fromList births
  validateBirthUniqueness births birthMap
  validateBirthCoverage finite birthMap
  validateBirthExactness finite birthMap
  validateFiltrationMonotonicity finite birthMap
  pure
    FilteredFiniteChainComplex
      { filteredBaseComplex = finite,
        filteredCellBirths = birthMap
      }

mod2PersistentPairs ::
  Integral r =>
  FilteredFiniteChainComplex r ->
  Either HomologyFailure [PersistencePair FiltrationValue]
mod2PersistentPairs filtered = do
  let orderedCells = orderedFilteredCells filtered
      orderedCellByIndex =
        orderedCells
          & zip [0 :: Int ..]
          & Map.fromList
      globalIndexByCell =
        orderedCells
          & zip [0 :: Int ..]
          & fmap (\(globalIndexValue, orderedCell) -> (orderedCellIdentity orderedCell, globalIndexValue))
          & Map.fromList
      groupedBoundaryEntries = boundaryEntriesByDegree (filteredBaseComplex filtered)
      boundaryColumns = fmap (mod2PersistentBoundaryColumnFromIndex groupedBoundaryEntries globalIndexByCell) orderedCells
      stateAfterReduction = foldl' reducePersistentColumn emptyPersistenceState (zip [0 :: Int ..] boundaryColumns)
      finitePairs =
        persistencePairsByIndex stateAfterReduction
          & reverse
          & mapMaybe (uncurry (materializeFinitePersistencePair orderedCellByIndex))
      essentialPairs =
        persistenceEssentialBirths stateAfterReduction
          & Set.toAscList
          & mapMaybe (materializeEssentialPersistencePair orderedCellByIndex)
  pure (finitePairs <> essentialPairs)

persistentPairs ::
  Integral r =>
  FilteredFiniteChainComplex r ->
  Either HomologyFailure [PersistencePair FiltrationValue]
{-# DEPRECATED persistentPairs "Use mod2PersistentPairs — this computes mod-2 persistence despite its Integral constraint" #-}
persistentPairs = mod2PersistentPairs

mod2PersistenceTopologyWitness ::
  Integral r =>
  FilteredFiniteChainComplex r ->
  Either HomologyFailure (TopologyWitness scaffold spectral FiltrationValue coefficient basis)
mod2PersistenceTopologyWitness filtered = do
  pairs <- mod2PersistentPairs filtered
  pure
    emptyTopologyWitness
      { topologyPersistencePairs = pairs
      }

persistenceTopologyWitness ::
  Integral r =>
  FilteredFiniteChainComplex r ->
  Either HomologyFailure (TopologyWitness scaffold spectral FiltrationValue coefficient basis)
{-# DEPRECATED persistenceTopologyWitness "Use mod2PersistenceTopologyWitness — this computes mod-2 persistence despite its Integral constraint" #-}
persistenceTopologyWitness = mod2PersistenceTopologyWitness

persistenceEssentialBirths :: PersistenceState -> Set.Set Int
persistenceEssentialBirths stateValue =
  let pairedBirths = persistencePairsByIndex stateValue & fmap fst & Set.fromList
   in persistenceCreators stateValue `Set.difference` pairedBirths

reducePersistentColumn :: PersistenceState -> (Int, Set.Set Int) -> PersistenceState
reducePersistentColumn stateValue (columnIndexValue, initialColumn) =
  let reducedColumn = reduceBoundaryColumn (persistenceLowColumns stateValue) initialColumn
   in case lowIndex reducedColumn of
        Nothing ->
          stateValue
            { persistenceCreators = Set.insert columnIndexValue (persistenceCreators stateValue)
            }
        Just lowValue ->
          stateValue
            { persistenceLowColumns = Map.insert lowValue reducedColumn (persistenceLowColumns stateValue),
              persistencePairsByIndex = (lowValue, columnIndexValue) : persistencePairsByIndex stateValue
            }

reduceBoundaryColumn :: Map.Map Int (Set.Set Int) -> Set.Set Int -> Set.Set Int
reduceBoundaryColumn lowColumns columnValue =
  case lowIndex columnValue >>= (`Map.lookup` lowColumns) of
    Nothing -> columnValue
    Just pivotColumn -> reduceBoundaryColumn lowColumns (symmetricDifference columnValue pivotColumn)

materializeFinitePersistencePair ::
  Map.Map Int OrderedFilteredCell ->
  Int ->
  Int ->
  Maybe (PersistencePair FiltrationValue)
materializeFinitePersistencePair orderedCellByIndex birthIndexValue deathIndexValue =
  case (Map.lookup birthIndexValue orderedCellByIndex, Map.lookup deathIndexValue orderedCellByIndex) of
    (Just birthCell, Just deathCell) ->
      Just
        PersistencePair
          { persistenceDegree = cellDegree (orderedCellIdentity birthCell),
            persistenceBirth = orderedCellBirth birthCell,
            persistenceDeath = Just (orderedCellBirth deathCell)
          }
    _ -> Nothing

materializeEssentialPersistencePair ::
  Map.Map Int OrderedFilteredCell ->
  Int ->
  Maybe (PersistencePair FiltrationValue)
materializeEssentialPersistencePair orderedCellByIndex birthIndexValue =
  Map.lookup birthIndexValue orderedCellByIndex
    & fmap
      ( \birthCell ->
          PersistencePair
            { persistenceDegree = cellDegree (orderedCellIdentity birthCell),
              persistenceBirth = orderedCellBirth birthCell,
              persistenceDeath = Nothing
            }
      )

orderedFilteredCells :: FilteredFiniteChainComplex r -> [OrderedFilteredCell]
orderedFilteredCells filtered =
  allBasisCellRefs (filteredBaseComplex filtered)
    & mapMaybe
      ( \cellRefValue ->
          fmap (OrderedFilteredCell cellRefValue)
            (Map.lookup cellRefValue (filteredCellBirths filtered))
      )
    & List.sortOn
      ( \orderedCell ->
          let degreeValue = cellDegree (orderedCellIdentity orderedCell)
           in ( orderedCellBirth orderedCell,
                unHomologicalDegree degreeValue,
                cellIndex (orderedCellIdentity orderedCell)
              )
      )

mod2PersistentBoundaryColumn ::
  Integral r =>
  FilteredFiniteChainComplex r ->
  Map.Map BasisCellRef Int ->
  OrderedFilteredCell ->
  Set.Set Int
mod2PersistentBoundaryColumn filtered globalIndexByCell orderedCell =
  let cellRefValue = orderedCellIdentity orderedCell
      degreeValue = cellDegree cellRefValue
      incidence = incidenceMatrixAt (filteredBaseComplex filtered) degreeValue
   in mod2PersistentBoundaryColumnFromEntries (entriesBySource incidence) globalIndexByCell orderedCell

mod2PersistentBoundaryColumnFromIndex ::
  Integral r =>
  Map.Map HomologicalDegree (IntMap.IntMap [BoundaryEntry r]) ->
  Map.Map BasisCellRef Int ->
  OrderedFilteredCell ->
  Set.Set Int
mod2PersistentBoundaryColumnFromIndex groupedBoundaryEntries globalIndexByCell orderedCell =
  let cellRefValue = orderedCellIdentity orderedCell
      degreeEntries = Map.findWithDefault IntMap.empty (cellDegree cellRefValue) groupedBoundaryEntries
   in mod2PersistentBoundaryColumnFromEntries degreeEntries globalIndexByCell orderedCell

mod2PersistentBoundaryColumnFromEntries ::
  Integral r =>
  IntMap.IntMap [BoundaryEntry r] ->
  Map.Map BasisCellRef Int ->
  OrderedFilteredCell ->
  Set.Set Int
mod2PersistentBoundaryColumnFromEntries groupedEntries globalIndexByCell orderedCell =
  let cellRefValue = orderedCellIdentity orderedCell
      degreeValue = cellDegree cellRefValue
   in IntMap.findWithDefault [] (cellIndex cellRefValue) groupedEntries
        & filter (\entry -> odd (abs (boundaryCoefficient entry)))
        & fmap
          ( \entry ->
              BasisCellRef
                { cellDegree = decrementDegree degreeValue,
                  cellIndex = targetIndex entry
                }
          )
        & mapMaybeWithLookup globalIndexByCell
        & Set.fromList

boundaryEntriesByDegree ::
  FiniteChainComplex r ->
  Map.Map HomologicalDegree (IntMap.IntMap [BoundaryEntry r])
boundaryEntriesByDegree finite =
  dimensionsOf finite
    & fmap (\degreeValue -> (degreeValue, entriesBySource (incidenceMatrixAt finite degreeValue)))
    & Map.fromList

entriesBySource :: BoundaryIncidence r -> IntMap.IntMap [BoundaryEntry r]
entriesBySource incidence =
  boundaryEntries incidence
    & foldr
      ( \entryValue ->
          IntMap.insertWith (<>) (sourceIndex entryValue) [entryValue]
      )
      IntMap.empty

persistentBoundaryColumn ::
  Integral r =>
  FilteredFiniteChainComplex r ->
  Map.Map BasisCellRef Int ->
  OrderedFilteredCell ->
  Set.Set Int
{-# DEPRECATED persistentBoundaryColumn "Use mod2PersistentBoundaryColumn — this computes mod-2 boundary despite its Integral constraint" #-}
persistentBoundaryColumn = mod2PersistentBoundaryColumn

validateBirthUniqueness ::
  [(BasisCellRef, FiltrationValue)] ->
  Map.Map BasisCellRef FiltrationValue ->
  Either HomologyFailure ()
validateBirthUniqueness births birthMap =
  if length births == Map.size birthMap
    then Right ()
    else Left (InvalidTopologyInput "duplicate birth assignments for the same cell")

validateBirthCoverage ::
  FiniteChainComplex r ->
  Map.Map BasisCellRef FiltrationValue ->
  Either HomologyFailure ()
validateBirthCoverage finite birthMap =
  allBasisCellRefs finite
    & List.find (\cellRefValue -> Map.notMember cellRefValue birthMap)
    & maybe (Right ()) missingCellFailure
  where
    missingCellFailure :: BasisCellRef -> Either HomologyFailure ()
    missingCellFailure cellRefValue =
      Left
        ( InvalidTopologyInput
            ( "missing filtration value for cell "
                <> show cellRefValue
            )
        )

validateBirthExactness ::
  FiniteChainComplex r ->
  Map.Map BasisCellRef FiltrationValue ->
  Either HomologyFailure ()
validateBirthExactness finite birthMap =
  let basisSet = Set.fromList (allBasisCellRefs finite)
      extraKeys = Map.keysSet birthMap `Set.difference` basisSet
   in if Set.null extraKeys
        then Right ()
        else
          Left
            ( InvalidTopologyInput
                ( "birth map contains cells absent from the chain complex: "
                    <> show (Set.toList extraKeys)
                )
            )

validateFiltrationMonotonicity ::
  Integral r =>
  FiniteChainComplex r ->
  Map.Map BasisCellRef FiltrationValue ->
  Either HomologyFailure ()
validateFiltrationMonotonicity finite birthMap =
  filtrationViolations
    & List.find (const True)
    & maybe (Right ()) (Left . InvalidTopologyInput)
  where
    filtrationViolations =
      dimensionsOf finite
        >>= ( \degreeValue@(HomologicalDegree degreeIndex) ->
                if degreeIndex <= 0
                  then []
                  else
                    let incidence = incidenceMatrixAt finite degreeValue
                     in boundaryEntries incidence
                          & filter (\entry -> boundaryCoefficient entry /= 0)
                          & mapMaybe
                            ( \entry ->
                                let sourceCell =
                                      BasisCellRef
                                        { cellDegree = degreeValue,
                                          cellIndex = sourceIndex entry
                                        }
                                    targetCell =
                                      BasisCellRef
                                        { cellDegree = decrementDegree degreeValue,
                                          cellIndex = targetIndex entry
                                        }
                                 in case (Map.lookup sourceCell birthMap, Map.lookup targetCell birthMap) of
                                      (Just sourceBirth, Just targetBirth) ->
                                        if targetBirth <= sourceBirth
                                          then Nothing
                                          else
                                            Just
                                              ( "filtration violates face monotonicity for "
                                                  <> show sourceCell
                                                  <> " -> "
                                                  <> show targetCell
                                              )
                                      _ -> Nothing
                            )
           )