packages feed

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

module Moonlight.Homology.Pure.Topology.Persistence
  ( mkFilteredFiniteChainComplex,
    mod2PersistentPairs,
    CriticalBettiTable,
    criticalBettiDegreeCount,
    criticalBettiRankCount,
    criticalBettiTableValues,
    criticalBettiVectors,
    mod2PersistentPairsWithCriticalBettiTable,
    persistentBettiAt,
    persistentBettiAtMany,
    persistentBettiAtCriticalValues,
    mod2PersistenceTopologyWitness,
    mod2PersistentBoundaryColumn,
    persistentPairs,
    persistenceTopologyWitness,
    persistentBoundaryColumn,
    persistenceEssentialBirths,
    materializeFinitePersistencePair,
    materializeEssentialPersistencePair,
    orderedFilteredCells,
    validateBirthCoverage,
    validateBirthExactness,
    validateFiltrationMonotonicity,
  )
where

import Control.Monad (foldM)
import Control.Monad.ST (ST, runST)
import Data.Foldable (traverse_)
import Data.Function ((&))
import Data.IntMap.Strict qualified as IntMap
import Data.IntSet qualified as IntSet
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 Data.Vector qualified as Vector
import Data.Vector.Mutable qualified as BoxedMutableVector
import Data.Vector.Unboxed qualified as UnboxedVector
import Data.Vector.Unboxed.Mutable qualified as MutableVector
import Moonlight.Homology.Boundary.Finite
  ( FiniteChainComplex,
    incidenceMatrixAt,
  )
import Moonlight.Homology.Boundary.LinAlg
  ( BoundaryEntry,
    BoundaryIncidence,
    boundaryCoefficient,
    boundaryEntriesBySource,
    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 -> Type
data OrderedFilteredCell filtration = OrderedFilteredCell
  { orderedCellIdentity :: BasisCellRef,
    orderedCellBirth :: filtration,
    orderedCellBirthRank :: Int
  }
  deriving stock (Eq, Show)

-- | Dense Betti profiles over the finite complex's critical-rank and degree
-- cover. Values are stored rank-major in one unboxed vector; the constructor
-- is withheld so the two dimensions cannot disagree with its storage.
data CriticalBettiTable = CriticalBettiTable
  { criticalBettiDegreeCount :: !Int,
    criticalBettiRankCount :: !Int,
    criticalBettiTableValues :: !(UnboxedVector.Vector Int)
  }
  deriving stock (Eq, Show)

-- | Materialize the familiar rank-indexed Betti vectors from the dense table.
criticalBettiVectors :: CriticalBettiTable -> [[Int]]
criticalBettiVectors table =
  fmap profileAt [0 .. criticalBettiRankCount table - 1]
 where
  degreeCount = criticalBettiDegreeCount table
  values = criticalBettiTableValues table
  profileAt rankValue =
    values
      & UnboxedVector.drop (rankValue * degreeCount)
      & UnboxedVector.take degreeCount
      & UnboxedVector.toList

type PersistenceState :: Type
data PersistenceState = PersistenceState
  { persistencePairsByIndex :: ![(Int, Int)],
    persistencePairedBirths :: !IntSet.IntSet,
    persistenceCreators :: !IntSet.IntSet
  }

type BettiSweepPoint :: Type
data BettiSweepPoint = BettiSweepPoint
  { bettiSweepDelta :: !(Map.Map HomologicalDegree Int),
    bettiSweepRequested :: !Bool
  }

emptyPersistenceState :: PersistenceState
emptyPersistenceState =
  PersistenceState
    { persistencePairsByIndex = [],
      persistencePairedBirths = IntSet.empty,
      persistenceCreators = IntSet.empty
    }

mkFilteredFiniteChainComplex ::
  (Integral r, Ord filtration) =>
  FiniteChainComplex r ->
  [(BasisCellRef, filtration)] ->
  Either HomologyFailure (FilteredFiniteChainComplex filtration r)
mkFilteredFiniteChainComplex finite births = do
  let birthMap = Map.fromList births
  validateBirthUniqueness births birthMap
  denseBirthsByDegree <- denseBirthSection finite birthMap
  let expectedCellCount = Vector.sum (fmap Vector.length denseBirthsByDegree)
  if Map.size birthMap == expectedCellCount
    then Right ()
    else validateBirthExactness finite birthMap
  let birthSections =
        Map.foldlWithKey'
          (\sections cellRefValue birthValue -> Map.insertWith (<>) birthValue [cellRefValue] sections)
          Map.empty
          birthMap
          & Map.toAscList
      rankedBirthSections = zip [0 :: Int ..] birthSections
      criticalValues = fmap (fst . snd) rankedBirthSections
      birthValueRanks =
        rankedBirthSections
          & fmap (\(rankValue, (birthValue, _)) -> (birthValue, rankValue))
          & Map.fromDistinctAscList
      cellBirthRanks =
        rankedBirthSections
          >>= ( \(rankValue, (_, cellsAtBirth)) ->
                  fmap (\cellRefValue -> (cellRefValue, rankValue)) cellsAtBirth
              )
          & Map.fromList
      rankedCells =
        rankedBirthSections
          >>= ( \(rankValue, (birthValue, cellsAtBirth)) ->
                  reverse cellsAtBirth
                    & fmap (\cellRefValue -> (cellRefValue, birthValue, rankValue))
              )
  validateDenseFiltrationMonotonicity finite denseBirthsByDegree
  pure
    FilteredFiniteChainComplex
      { filteredBaseComplex = finite,
        filteredCellBirths = birthMap,
        filteredCriticalValues = criticalValues,
        filteredBirthValueRanks = birthValueRanks,
        filteredCellBirthRanks = cellBirthRanks,
        filteredRankedCells = rankedCells
      }

mod2PersistentPairs ::
  Integral r =>
  FilteredFiniteChainComplex filtration r ->
  Either HomologyFailure [PersistencePair filtration]
mod2PersistentPairs filtered =
  let (orderedCellByIndex, stateAfterReduction) = reduceMod2Persistence filtered
   in Right (materializePersistencePairs orderedCellByIndex stateAfterReduction)

-- | Reduce once, then derive both the exact barcode and all critical-rank
-- Betti profiles from the same indexed witness. The fused observation avoids
-- throwing away proven cell ranks only to recover them through exact-value
-- map lookups in 'persistentBettiAtCriticalValues'.
mod2PersistentPairsWithCriticalBettiTable ::
  Integral r =>
  FilteredFiniteChainComplex filtration r ->
  Either
    HomologyFailure
    ( [PersistencePair filtration]
    , CriticalBettiTable
    )
mod2PersistentPairsWithCriticalBettiTable filtered =
  let (orderedCellByIndex, stateAfterReduction) = reduceMod2Persistence filtered
      pairs = materializePersistencePairs orderedCellByIndex stateAfterReduction
      criticalBettiTable =
        criticalBettiTableFromRankedEvents
          (length (dimensionsOf (filteredBaseComplex filtered)))
          (length (filteredCriticalValues filtered))
          (persistenceStateRankedEvents orderedCellByIndex stateAfterReduction)
   in Right (pairs, criticalBettiTable)

reduceMod2Persistence ::
  Integral r =>
  FilteredFiniteChainComplex filtration r ->
  (Vector.Vector (OrderedFilteredCell filtration), PersistenceState)
reduceMod2Persistence filtered =
  let orderedCells = orderedFilteredCells filtered
      orderedCellByIndex =
        orderedCells
          & Vector.fromList
      globalIndicesByDegree =
        denseGlobalIndicesByDegree
          (filteredBaseComplex filtered)
          (zip [0 :: Int ..] orderedCells)
      groupedBoundaryEntries = boundaryEntriesByDegree (filteredBaseComplex filtered)
      degreeCount = length (dimensionsOf (filteredBaseComplex filtered))
      indexedCellsByDegree =
        zip [0 :: Int ..] orderedCells
          & mapMaybe (indexedCellDegreeUpdate degreeCount)
          & Vector.accum
            (flip (:))
            (Vector.replicate degreeCount [])
          & fmap reverse
      higherDegreeColumns =
        indexedCellsByDegree
          & Vector.drop 2
          & fmap
            ( fmap
                ( \(columnIndexValue, orderedCell) ->
                    ( columnIndexValue
                    , mod2PersistentBoundaryColumnFromIndex
                        groupedBoundaryEntries
                        globalIndicesByDegree
                        orderedCell
                    )
                )
            )
          & Vector.toList
          & reverse
      degreeOneColumns =
        maybe [] id (indexedCellsByDegree Vector.!? 1)
          & fmap
            ( \(columnIndexValue, orderedCell) ->
                ( columnIndexValue
                , mod2PersistentBoundaryTargetsFromIndex
                    groupedBoundaryEntries
                    globalIndicesByDegree
                    orderedCell
                )
            )
      degreeZeroIndices =
        maybe [] id (indexedCellsByDegree Vector.!? 0)
          & fmap fst
      stateAfterReduction =
        reducePersistentColumnsByDegree
          (length orderedCells)
          higherDegreeColumns
          degreeOneColumns
          degreeZeroIndices
   in (orderedCellByIndex, stateAfterReduction)

indexedCellDegreeUpdate ::
  Int ->
  (Int, OrderedFilteredCell filtration) ->
  Maybe (Int, (Int, OrderedFilteredCell filtration))
indexedCellDegreeUpdate degreeCount indexedCell@(_, orderedCell) =
  let degreeIndex =
        unHomologicalDegree (cellDegree (orderedCellIdentity orderedCell))
   in if degreeIndex < 0 || degreeIndex >= degreeCount
        then Nothing
        else Just (degreeIndex, indexedCell)

materializePersistencePairs ::
  Vector.Vector (OrderedFilteredCell filtration) ->
  PersistenceState ->
  [PersistencePair filtration]
materializePersistencePairs orderedCellByIndex stateAfterReduction =
  let finitePairs =
        persistencePairsByIndex stateAfterReduction
          & List.sortOn snd
          & mapMaybe (uncurry (materializeFinitePersistencePair orderedCellByIndex))
      essentialPairs =
        persistenceEssentialBirths stateAfterReduction
          & IntSet.toAscList
          & mapMaybe (materializeEssentialPersistencePair orderedCellByIndex)
   in finitePairs <> essentialPairs

persistenceStateRankedEvents ::
  Vector.Vector (OrderedFilteredCell filtration) ->
  PersistenceState ->
  [(Int, (Int, Int))]
persistenceStateRankedEvents orderedCellByIndex stateAfterReduction =
  let finiteEvents =
        persistencePairsByIndex stateAfterReduction
          >>= ( \(birthIndexValue, deathIndexValue) ->
                  case
                      ( orderedCellByIndex Vector.!? birthIndexValue
                      , orderedCellByIndex Vector.!? deathIndexValue
                      )
                    of
                      (Just birthCell, Just deathCell) ->
                        rankedIntervalEvents
                          (cellDegree (orderedCellIdentity birthCell))
                          (orderedCellBirthRank birthCell)
                          (Just (orderedCellBirthRank deathCell))
                      _ -> []
              )
      essentialEvents =
        persistenceEssentialBirths stateAfterReduction
          & IntSet.toAscList
          >>= ( \birthIndexValue ->
                  maybe
                    []
                    ( \birthCell ->
                        rankedIntervalEvents
                          (cellDegree (orderedCellIdentity birthCell))
                          (orderedCellBirthRank birthCell)
                          Nothing
                    )
                    (orderedCellByIndex Vector.!? birthIndexValue)
              )
   in finiteEvents <> essentialEvents

-- | Read the Betti numbers of one closed filtration sublevel from an already
-- reduced barcode. The returned map is sparse: absent degrees have Betti
-- number zero. Finite persistence intervals are half-open, so a class whose
-- death equals the requested threshold is no longer present after every cell
-- at that threshold has entered the complex.
persistentBettiAt ::
  Ord filtration =>
  filtration ->
  [PersistencePair filtration] ->
  Map.Map HomologicalDegree Int
persistentBettiAt threshold =
  Map.fromListWith (+)
    . fmap (\pairValue -> (persistenceDegree pairValue, 1))
    . filter (persistencePairAliveAt threshold)

-- | Read several closed filtration sublevels in one ordered sweep over the
-- barcode. Results retain the supplied threshold order and multiplicity.
-- Unlike mapping 'persistentBettiAt', this does not rescan every persistence
-- interval for every requested threshold.
--
-- Finite intervals use the same half-open semantics as 'persistentBettiAt'. A
-- birth and death at the same threshold cancel before that threshold is
-- observed. Malformed finite intervals whose death does not follow their birth
-- contribute nowhere, matching the pointwise query.
persistentBettiAtMany ::
  Ord filtration =>
  [filtration] ->
  [PersistencePair filtration] ->
  [Map.Map HomologicalDegree Int]
persistentBettiAtMany thresholds pairs =
  let requestedPoints =
        thresholds
          & fmap (\threshold -> (threshold, requestedBettiSweepPoint))
          & Map.fromListWith mergeBettiSweepPoints
      eventPoints =
        foldl'
          insertPersistencePairSweepPoints
          Map.empty
          pairs
      sweepPoints = Map.unionWith mergeBettiSweepPoints eventPoints requestedPoints
      observedByThreshold =
        sweepPoints
          & Map.mapAccumWithKey applyBettiSweepPoint Map.empty
          & snd
          & Map.mapMaybe id
   in fmap
        (\threshold -> Map.findWithDefault Map.empty threshold observedByThreshold)
        thresholds

-- | Read every exact critical sublevel of one admitted filtered complex. The
-- complex's cached exact-value quotient transports pair endpoints to dense
-- ranks, performs the sweep over 'Int', then returns profiles aligned with
-- 'filteredCriticalValues'. Exact births remain authoritative; ranks never
-- escape this derived observation.
persistentBettiAtCriticalValues ::
  Ord filtration =>
  FilteredFiniteChainComplex filtration r ->
  [PersistencePair filtration] ->
  Either HomologyFailure [Map.Map HomologicalDegree Int]
persistentBettiAtCriticalValues filtered pairs = do
  rankedPairs <- traverse (rankPersistencePair filtered) pairs
  pure
    ( criticalProfilesFromRankedEvents
        (length (filteredCriticalValues filtered))
        (rankedPairs >>= rankedPersistencePairEvents)
    )

criticalProfilesFromRankedEvents ::
  Int ->
  [(Int, (Int, Int))] ->
  [Map.Map HomologicalDegree Int]
criticalProfilesFromRankedEvents criticalRankCount rankedEvents =
  let eventDeltas =
        Vector.accum
          insertRankedBettiEvent
          (Vector.replicate criticalRankCount IntMap.empty)
          rankedEvents
      (_, criticalProfiles) =
        List.mapAccumL
          applyRankedBettiEvents
          IntMap.empty
          (Vector.toList eventDeltas)
   in criticalProfiles

criticalBettiTableFromRankedEvents ::
  Int ->
  Int ->
  [(Int, (Int, Int))] ->
  CriticalBettiTable
criticalBettiTableFromRankedEvents degreeCount criticalRankCount rankedEvents =
  let eventDeltas =
        Vector.accum
          insertRankedBettiEvent
          (Vector.replicate criticalRankCount IntMap.empty)
          rankedEvents
      degreeIndices = [0 .. degreeCount - 1]
      values =
        UnboxedVector.create $ do
          mutableValues <-
            MutableVector.new (max 0 (degreeCount * criticalRankCount))
          _ <-
            Vector.ifoldM'
              ( \currentBetti rankValue rankEvents -> do
                  let nextBetti = nextRankedBetti currentBetti rankEvents
                  traverse_
                    ( \degreeValue ->
                        MutableVector.write
                          mutableValues
                          (rankValue * degreeCount + degreeValue)
                          (IntMap.findWithDefault 0 degreeValue nextBetti)
                    )
                    degreeIndices
                  pure nextBetti
              )
              IntMap.empty
              eventDeltas
          pure mutableValues
   in CriticalBettiTable
        { criticalBettiDegreeCount = degreeCount,
          criticalBettiRankCount = criticalRankCount,
          criticalBettiTableValues = values
        }

rankPersistencePair ::
  Ord filtration =>
  FilteredFiniteChainComplex filtration r ->
  PersistencePair filtration ->
  Either HomologyFailure (PersistencePair Int)
rankPersistencePair filtered pairValue = do
  birthRank <-
    maybe
      (Left (InvalidTopologyInput "persistence-pair birth is absent from the filtered complex"))
      Right
      (Map.lookup (persistenceBirth pairValue) (filteredBirthValueRanks filtered))
  deathRank <-
    traverse
      ( \deathValue ->
          maybe
            (Left (InvalidTopologyInput "persistence-pair death is absent from the filtered complex"))
            Right
            (Map.lookup deathValue (filteredBirthValueRanks filtered))
      )
      (persistenceDeath pairValue)
  pure
    PersistencePair
      { persistenceDegree = persistenceDegree pairValue,
        persistenceBirth = birthRank,
        persistenceDeath = deathRank
      }

rankedPersistencePairEvents ::
  PersistencePair Int ->
  [(Int, (Int, Int))]
rankedPersistencePairEvents pairValue =
  rankedIntervalEvents
    (persistenceDegree pairValue)
    (persistenceBirth pairValue)
    (persistenceDeath pairValue)

rankedIntervalEvents ::
  HomologicalDegree ->
  Int ->
  Maybe Int ->
  [(Int, (Int, Int))]
rankedIntervalEvents degreeValue birthRank deathRank =
  case deathRank of
    Nothing -> [rankedBettiEventAt 1 birthRank degreeValue]
    Just finiteDeathRank ->
      if birthRank < finiteDeathRank
        then
          [ rankedBettiEventAt 1 birthRank degreeValue
          , rankedBettiEventAt (-1) finiteDeathRank degreeValue
          ]
        else []

rankedBettiEventAt ::
  Int ->
  Int ->
  HomologicalDegree ->
  (Int, (Int, Int))
rankedBettiEventAt deltaValue criticalRank degreeValue =
  (criticalRank, (unHomologicalDegree degreeValue, deltaValue))

insertRankedBettiEvent ::
  IntMap.IntMap Int ->
  (Int, Int) ->
  IntMap.IntMap Int
insertRankedBettiEvent eventDeltas (degreeValue, deltaValue) =
  IntMap.insertWith
    (+)
    degreeValue
    deltaValue
    eventDeltas

applyRankedBettiEvents ::
  IntMap.IntMap Int ->
  IntMap.IntMap Int ->
  (IntMap.IntMap Int, Map.Map HomologicalDegree Int)
applyRankedBettiEvents currentBetti eventDeltas =
  let nextBetti = nextRankedBetti currentBetti eventDeltas
      publicBetti =
        nextBetti
          & IntMap.foldrWithKey
            ( \degreeValue countValue remainingDegrees ->
                (HomologicalDegree degreeValue, countValue) : remainingDegrees
            )
            []
          & Map.fromDistinctAscList
   in (nextBetti, publicBetti)

nextRankedBetti ::
  IntMap.IntMap Int ->
  IntMap.IntMap Int ->
  IntMap.IntMap Int
nextRankedBetti currentBetti eventDeltas =
  IntMap.unionWith (+) currentBetti eventDeltas
    & IntMap.filter (/= 0)

requestedBettiSweepPoint :: BettiSweepPoint
requestedBettiSweepPoint =
  BettiSweepPoint
    { bettiSweepDelta = Map.empty,
      bettiSweepRequested = True
    }

insertPersistencePairSweepPoints ::
  Ord filtration =>
  Map.Map filtration BettiSweepPoint ->
  PersistencePair filtration ->
  Map.Map filtration BettiSweepPoint
insertPersistencePairSweepPoints eventPoints pairValue =
  case persistenceDeath pairValue of
    Nothing -> insertPersistencePairDelta 1 (persistenceBirth pairValue) pairValue eventPoints
    Just deathValue ->
      if persistenceBirth pairValue < deathValue
        then
          eventPoints
            & insertPersistencePairDelta 1 (persistenceBirth pairValue) pairValue
            & insertPersistencePairDelta (-1) deathValue pairValue
        else eventPoints

insertPersistencePairDelta ::
  Ord filtration =>
  Int ->
  filtration ->
  PersistencePair filtration ->
  Map.Map filtration BettiSweepPoint ->
  Map.Map filtration BettiSweepPoint
insertPersistencePairDelta deltaValue threshold pairValue =
  Map.insertWith
    mergeBettiSweepPoints
    threshold
    (persistencePairDelta deltaValue pairValue)

persistencePairDelta :: Int -> PersistencePair filtration -> BettiSweepPoint
persistencePairDelta deltaValue pairValue =
  BettiSweepPoint
    { bettiSweepDelta = Map.singleton (persistenceDegree pairValue) deltaValue,
      bettiSweepRequested = False
    }

mergeBettiSweepPoints :: BettiSweepPoint -> BettiSweepPoint -> BettiSweepPoint
mergeBettiSweepPoints leftPoint rightPoint =
  BettiSweepPoint
    { bettiSweepDelta = Map.unionWith (+) (bettiSweepDelta leftPoint) (bettiSweepDelta rightPoint),
      bettiSweepRequested = bettiSweepRequested leftPoint || bettiSweepRequested rightPoint
    }

applyBettiSweepPoint ::
  Map.Map HomologicalDegree Int ->
  filtration ->
  BettiSweepPoint ->
  (Map.Map HomologicalDegree Int, Maybe (Map.Map HomologicalDegree Int))
applyBettiSweepPoint currentBetti _ pointValue =
  let nextBetti =
        Map.unionWith (+) currentBetti (bettiSweepDelta pointValue)
          & Map.filter (/= 0)
      observation =
        if bettiSweepRequested pointValue
          then Just nextBetti
          else Nothing
   in (nextBetti, observation)

persistencePairAliveAt ::
  Ord filtration =>
  filtration ->
  PersistencePair filtration ->
  Bool
persistencePairAliveAt threshold pairValue =
  persistenceBirth pairValue <= threshold
    && maybe True (threshold <) (persistenceDeath pairValue)

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

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

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

persistenceEssentialBirths :: PersistenceState -> IntSet.IntSet
persistenceEssentialBirths stateValue =
  persistenceCreators stateValue
    `IntSet.difference` persistencePairedBirths stateValue

-- | Reduce one homological degree after all higher-degree pairings are known.
-- A cell already paired as the low simplex of a higher-dimensional column is
-- a proven creator, so the standard persistence clearing theorem lets us
-- replace its column by zero rather than rediscovering that fact by reduction.
reducePersistentDegree ::
  Int ->
  PersistenceState ->
  [(Int, IntSet.IntSet)] ->
  PersistenceState
reducePersistentDegree totalCellCount stateValue indexedColumns =
  let clearedColumnIndices = persistencePairedBirths stateValue
   in runST $ do
        pivotColumns <- BoxedMutableVector.replicate totalCellCount Nothing
        foldM
          (reduceOrClearPersistentColumn pivotColumns clearedColumnIndices)
          stateValue
          indexedColumns

reduceOrClearPersistentColumn ::
  BoxedMutableVector.MVector s (Maybe IntSet.IntSet) ->
  IntSet.IntSet ->
  PersistenceState ->
  (Int, IntSet.IntSet) ->
  ST s PersistenceState
reduceOrClearPersistentColumn pivotColumns clearedColumnIndices stateValue indexedColumn@(columnIndexValue, _) =
  if IntSet.member columnIndexValue clearedColumnIndices
    then
      pure
        stateValue
          { persistenceCreators = IntSet.insert columnIndexValue (persistenceCreators stateValue)
          }
    else reducePersistentColumn pivotColumns stateValue indexedColumn

-- | Reduce one sparse column against the dense pivot section for its
-- homological degree. Global row identifiers are assigned from @[0 .. n)@
-- before boundary materialization, so the fresh arena is a total local view;
-- it cannot escape this degree's sealed 'ST' descent.
reducePersistentColumn ::
  BoxedMutableVector.MVector s (Maybe IntSet.IntSet) ->
  PersistenceState ->
  (Int, IntSet.IntSet) ->
  ST s PersistenceState
reducePersistentColumn pivotColumns stateValue (columnIndexValue, initialColumn) = do
  reducedColumn <- reduceBoundaryColumn pivotColumns initialColumn
  case IntSet.lookupMax reducedColumn of
    Nothing ->
      pure
        stateValue
          { persistenceCreators = IntSet.insert columnIndexValue (persistenceCreators stateValue)
          }
    Just lowValue -> do
      BoxedMutableVector.write pivotColumns lowValue (Just reducedColumn)
      pure
        stateValue
          { persistencePairsByIndex = (lowValue, columnIndexValue) : persistencePairsByIndex stateValue,
            persistencePairedBirths = IntSet.insert lowValue (persistencePairedBirths stateValue)
          }

-- | Descend through the degree cover. Higher-dimensional columns use the
-- general sparse reducer; the graph boundary uses its equivalent elder-rule
-- union-find section when every mod-two one-cell boundary has cardinality zero
-- or two. Any non-graph fiber is a typed local incompatibility with that
-- specialization and glues back to the general reducer instead.
reducePersistentColumnsByDegree ::
  Int ->
  [[(Int, IntSet.IntSet)]] ->
  [(Int, [Int])] ->
  [Int] ->
  PersistenceState
reducePersistentColumnsByDegree totalCellCount higherDegreeColumns degreeOneColumns degreeZeroIndices =
  let stateAfterHigherDegrees =
        foldl'
          (reducePersistentDegree totalCellCount)
          emptyPersistenceState
          higherDegreeColumns
      clearedDegreeOneIndices =
        persistencePairedBirths stateAfterHigherDegrees
      graphReduction =
        reduceGraphBoundaryDegree
          totalCellCount
          clearedDegreeOneIndices
          degreeZeroIndices
          degreeOneColumns
   in case graphReduction of
        Just (graphPairs, graphCreators) ->
          stateAfterHigherDegrees
            { persistencePairsByIndex = graphPairs <> persistencePairsByIndex stateAfterHigherDegrees,
              persistencePairedBirths =
                IntSet.union
                  (IntSet.fromList (fmap fst graphPairs))
                  (persistencePairedBirths stateAfterHigherDegrees),
              persistenceCreators = IntSet.union graphCreators (persistenceCreators stateAfterHigherDegrees)
            }
        Nothing ->
          let sparseDegreeOneColumns =
                fmap
                  (\(columnIndexValue, targets) -> (columnIndexValue, IntSet.fromList targets))
                  degreeOneColumns
              degreeZeroColumns =
                fmap (\columnIndexValue -> (columnIndexValue, IntSet.empty)) degreeZeroIndices
           in foldl'
                (reducePersistentDegree totalCellCount)
                stateAfterHigherDegrees
                [sparseDegreeOneColumns, degreeZeroColumns]

reduceGraphBoundaryDegree ::
  Int ->
  IntSet.IntSet ->
  [Int] ->
  [(Int, [Int])] ->
  Maybe ([(Int, Int)], IntSet.IntSet)
reduceGraphBoundaryDegree totalCellCount clearedEdgeIndices vertexIndices edgeColumns =
  runST $ do
    parents <- MutableVector.generate totalCellCount id
    foldM
      (reduceGraphBoundaryColumn parents clearedEdgeIndices)
      (Just ([], IntSet.fromList vertexIndices))
      edgeColumns

reduceGraphBoundaryColumn ::
  MutableVector.MVector s Int ->
  IntSet.IntSet ->
  Maybe ([(Int, Int)], IntSet.IntSet) ->
  (Int, [Int]) ->
  ST s (Maybe ([(Int, Int)], IntSet.IntSet))
reduceGraphBoundaryColumn _ _ Nothing _ = pure Nothing
reduceGraphBoundaryColumn parents clearedEdgeIndices (Just (pairsByIndex, creators)) (edgeIndex, boundaryTargets) =
  if IntSet.member edgeIndex clearedEdgeIndices
    then pure (Just (pairsByIndex, IntSet.insert edgeIndex creators))
    else
      case boundaryTargets of
        [] -> pure (Just (pairsByIndex, IntSet.insert edgeIndex creators))
        [firstVertex, secondVertex] -> do
          firstRoot <- findGraphComponentRoot parents firstVertex
          secondRoot <- findGraphComponentRoot parents secondVertex
          case (firstRoot, secondRoot) of
            (Just firstRootIndex, Just secondRootIndex) ->
              if firstRootIndex == secondRootIndex
                then pure (Just (pairsByIndex, IntSet.insert edgeIndex creators))
                else do
                  let olderRoot = min firstRootIndex secondRootIndex
                      youngerRoot = max firstRootIndex secondRootIndex
                  MutableVector.write parents youngerRoot olderRoot
                  pure (Just ((youngerRoot, edgeIndex) : pairsByIndex, creators))
            _ -> pure Nothing
        _ -> pure Nothing

findGraphComponentRoot ::
  MutableVector.MVector s Int ->
  Int ->
  ST s (Maybe Int)
findGraphComponentRoot parents indexValue =
  if indexValue < 0 || indexValue >= MutableVector.length parents
    then pure Nothing
    else do
      parentIndex <- MutableVector.read parents indexValue
      if parentIndex == indexValue
        then pure (Just indexValue)
        else do
          rootIndex <- findGraphComponentRoot parents parentIndex
          traverse
            (\rootValue -> MutableVector.write parents indexValue rootValue >> pure rootValue)
            rootIndex

reduceBoundaryColumn ::
  BoxedMutableVector.MVector s (Maybe IntSet.IntSet) ->
  IntSet.IntSet ->
  ST s IntSet.IntSet
reduceBoundaryColumn pivotColumns columnValue =
  case IntSet.lookupMax columnValue of
    Nothing -> pure columnValue
    Just lowValue -> do
      pivotColumn <- BoxedMutableVector.read pivotColumns lowValue
      case pivotColumn of
        Nothing -> pure columnValue
        Just existingPivot ->
          reduceBoundaryColumn
            pivotColumns
            (IntSet.symmetricDifference columnValue existingPivot)

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

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

orderedFilteredCells ::
  FilteredFiniteChainComplex filtration r ->
  [OrderedFilteredCell filtration]
orderedFilteredCells filtered =
  filteredRankedCells filtered
    & fmap
      ( \(cellRefValue, birthValue, birthRank) ->
          OrderedFilteredCell
            { orderedCellIdentity = cellRefValue,
              orderedCellBirth = birthValue,
              orderedCellBirthRank = birthRank
            }
      )

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

mod2PersistentBoundaryColumnFromIndex ::
  Integral r =>
  Vector.Vector (Vector.Vector [BoundaryEntry r]) ->
  Vector.Vector (Vector.Vector (Maybe Int)) ->
  OrderedFilteredCell filtration ->
  IntSet.IntSet
mod2PersistentBoundaryColumnFromIndex groupedBoundaryEntries globalIndicesByDegree =
  IntSet.fromList
    . mod2PersistentBoundaryTargetsFromIndex groupedBoundaryEntries globalIndicesByDegree

mod2PersistentBoundaryTargetsFromIndex ::
  Integral r =>
  Vector.Vector (Vector.Vector [BoundaryEntry r]) ->
  Vector.Vector (Vector.Vector (Maybe Int)) ->
  OrderedFilteredCell filtration ->
  [Int]
mod2PersistentBoundaryTargetsFromIndex groupedBoundaryEntries globalIndicesByDegree orderedCell =
  let cellRefValue = orderedCellIdentity orderedCell
      degreeValue = unHomologicalDegree (cellDegree cellRefValue)
      degreeEntries =
        maybe Vector.empty id (groupedBoundaryEntries Vector.!? degreeValue)
      targetIndices =
        maybe
          Vector.empty
          id
          (globalIndicesByDegree Vector.!? (degreeValue - 1))
   in maybe [] id (degreeEntries Vector.!? cellIndex cellRefValue)
        & filter (\entry -> odd (abs (boundaryCoefficient entry)))
        & mapMaybe (\entry -> targetIndices Vector.!? (targetIndex entry) >>= id)

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

boundaryEntriesByDegree ::
  FiniteChainComplex r ->
  Vector.Vector (Vector.Vector [BoundaryEntry r])
boundaryEntriesByDegree finite =
  dimensionsOf finite
    & fmap
      ( \degreeValue ->
          boundaryEntriesBySource (incidenceMatrixAt finite degreeValue)
      )
    & Vector.fromList

denseGlobalIndicesByDegree ::
  FiniteChainComplex r ->
  [(Int, OrderedFilteredCell filtration)] ->
  Vector.Vector (Vector.Vector (Maybe Int))
denseGlobalIndicesByDegree finite indexedCells =
  let degreeCount = length (dimensionsOf finite)
      updatesByDegree =
        Vector.accum
          (flip (:))
          (Vector.replicate degreeCount [])
          (mapMaybe (globalIndexUpdate degreeCount) indexedCells)
   in Vector.imap
        ( \degreeIndex updates ->
            Vector.accum
              (\_ globalIndexValue -> Just globalIndexValue)
              ( Vector.replicate
                  (cellCountAtDegree finite (HomologicalDegree degreeIndex))
                  Nothing
              )
              updates
        )
        updatesByDegree
  where
    globalIndexUpdate ::
      Int ->
      (Int, OrderedFilteredCell filtration) ->
      Maybe (Int, (Int, Int))
    globalIndexUpdate degreeCountValue (globalIndexValue, orderedCell) =
      let cellRefValue = orderedCellIdentity orderedCell
          degreeIndex = unHomologicalDegree (cellDegree cellRefValue)
       in if degreeIndex < 0 || degreeIndex >= degreeCountValue
            then Nothing
            else Just (degreeIndex, (cellIndex cellRefValue, globalIndexValue))

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

validateBirthUniqueness ::
  [(BasisCellRef, filtration)] ->
  Map.Map BasisCellRef filtration ->
  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 filtration ->
  Either HomologyFailure ()
validateBirthCoverage finite birthMap =
  () <$ denseBirthSection finite birthMap

validateBirthExactness ::
  FiniteChainComplex r ->
  Map.Map BasisCellRef filtration ->
  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, Ord filtration) =>
  FiniteChainComplex r ->
  Map.Map BasisCellRef filtration ->
  Either HomologyFailure ()
validateFiltrationMonotonicity finite birthMap = do
  denseBirthsByDegree <- denseBirthSection finite birthMap
  validateDenseFiltrationMonotonicity finite denseBirthsByDegree

denseBirthSection ::
  FiniteChainComplex r ->
  Map.Map BasisCellRef filtration ->
  Either HomologyFailure (Vector.Vector (Vector.Vector filtration))
denseBirthSection finite birthMap =
  dimensionsOf finite
    & traverse
      ( \degreeValue ->
          Vector.generateM
            (cellCountAtDegree finite degreeValue)
            ( \indexValue ->
                let cellRefValue = BasisCellRef degreeValue indexValue
                 in maybe
                      ( Left
                          ( InvalidTopologyInput
                              ("missing filtration value for cell " <> show cellRefValue)
                          )
                      )
                      Right
                      (Map.lookup cellRefValue birthMap)
            )
      )
    & fmap Vector.fromList

validateDenseFiltrationMonotonicity ::
  (Integral r, Ord filtration) =>
  FiniteChainComplex r ->
  Vector.Vector (Vector.Vector filtration) ->
  Either HomologyFailure ()
validateDenseFiltrationMonotonicity finite denseBirthsByDegree =
  dimensionsOf finite
    & mapMaybe
      ( \degreeValue ->
          firstFiltrationViolation
            denseBirthsByDegree
            degreeValue
            (incidenceMatrixAt finite degreeValue)
      )
    & List.find (const True)
    & maybe (Right ()) (Left . InvalidTopologyInput)

firstFiltrationViolation ::
  (Integral r, Ord filtration) =>
  Vector.Vector (Vector.Vector filtration) ->
  HomologicalDegree ->
  BoundaryIncidence r ->
  Maybe String
firstFiltrationViolation denseBirthsByDegree degreeValue@(HomologicalDegree degreeIndex) incidence
  | degreeIndex <= 0 = Nothing
  | otherwise =
      Vector.foldr
        (firstColumnFiltrationViolation denseBirthsByDegree degreeValue)
        Nothing
        (boundaryEntriesBySource incidence)

firstColumnFiltrationViolation ::
  (Integral r, Ord filtration) =>
  Vector.Vector (Vector.Vector filtration) ->
  HomologicalDegree ->
  [BoundaryEntry r] ->
  Maybe String ->
  Maybe String
firstColumnFiltrationViolation denseBirthsByDegree degreeValue entries remainingViolation =
  case List.find (const True) (mapMaybe (filtrationViolationAtEntry denseBirthsByDegree degreeValue) entries) of
    Just violation -> Just violation
    Nothing -> remainingViolation

filtrationViolationAtEntry ::
  (Integral r, Ord filtration) =>
  Vector.Vector (Vector.Vector filtration) ->
  HomologicalDegree ->
  BoundaryEntry r ->
  Maybe String
filtrationViolationAtEntry denseBirthsByDegree degreeValue entry
  | boundaryCoefficient entry == 0 = Nothing
  | otherwise =
      let sourceCell =
            BasisCellRef
              { cellDegree = degreeValue,
                cellIndex = sourceIndex entry
              }
          targetCell =
            BasisCellRef
              { cellDegree = decrementDegree degreeValue,
                cellIndex = targetIndex entry
              }
       in case (denseBirthAt denseBirthsByDegree sourceCell, denseBirthAt denseBirthsByDegree targetCell) of
            (Just sourceBirth, Just targetBirth)
              | targetBirth > sourceBirth ->
                  Just
                    ( "filtration violates face monotonicity for "
                        <> show sourceCell
                        <> " -> "
                        <> show targetCell
                    )
            _ -> Nothing

denseBirthAt ::
  Vector.Vector (Vector.Vector filtration) ->
  BasisCellRef ->
  Maybe filtration
denseBirthAt denseBirthsByDegree cellRefValue =
  case cellDegree cellRefValue of
    HomologicalDegree degreeIndex ->
      denseBirthsByDegree Vector.!? degreeIndex
        >>= (\birthsAtDegree -> birthsAtDegree Vector.!? cellIndex cellRefValue)