packages feed

moonlight-homology-0.1.0.3: src-topology/Moonlight/Homology/Pure/Topology/Zigzag.hs

{-# LANGUAGE DeriveFoldable #-}
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE DeriveTraversable #-}
{-# LANGUAGE LambdaCase #-}

-- | Exact persistence for finite, non-monotone diagrams of chain complexes.
--
-- A 'FiniteChainMap' is admitted only after its component maps commute with
-- the two boundary operators.  A 'FiniteChainZigzag' then glues checked maps
-- along structurally equal endpoints.  Persistence is computed on rational
-- homology by propagating the right filtration of each type-A prefix; quotient
-- layers that fail descent close at that arrow, and the terminal layers are
-- the surviving intervals.
module Moonlight.Homology.Pure.Topology.Zigzag
  ( ChainMapEndpoint (..),
    ZigzagDirection (..),
    ZigzagFailure (..),
    FiniteChainMap,
    finiteChainMapSource,
    finiteChainMapTarget,
    finiteChainMapAt,
    mkFiniteChainMapChecked,
    ZigzagArrow (..),
    zigzagArrowDirection,
    FiniteChainZigzag,
    mkFiniteChainZigzag,
    finiteChainZigzagComplexes,
    finiteChainZigzagArrows,
    ZigzagInterval (..),
    rationalZigzagIntervals,
    zigzagBettiAt,
  )
where

import Control.Monad (foldM)
import Data.Bifunctor (first)
import Data.Foldable (traverse_)
import Data.Function ((&))
import Data.IntMap.Strict qualified as IntMap
import Data.Kind (Type)
import Data.List qualified as List
import Data.List.NonEmpty (NonEmpty (..))
import Data.Map.Strict (Map)
import Data.Map.Strict qualified as Map
import Data.Vector (Vector)
import Data.Vector qualified as Vector
import Moonlight.Core (Semiring)
import Moonlight.Homology.Boundary.Finite
  ( FiniteChainComplex,
    degreeCardinality,
    incidenceMatrixAt,
    maxHomologicalDegree,
    validateFiniteChainComplexShape,
  )
import Moonlight.Homology.Boundary.LinAlg
  ( BoundaryIncidence,
    BoundaryIncidenceShapeError,
    composeBoundaryIncidence,
    emptyBoundaryIncidenceOf,
    sourceCardinality,
    targetCardinality,
  )
import Moonlight.Homology.Pure.Chain
  ( HomologicalDegree (..),
    RepresentativeChain (..),
  )
import Moonlight.Homology.Pure.Failure (HomologyFailure)
import Moonlight.Homology.Pure.Matrix.SparseLinAlg
  ( SparseCoordinateBasis,
    SparseColumnEchelon (..),
    SparseRow,
    compactSparseRow,
    sparseBoundaryColumns,
    sparseCoordinateBasis,
    sparseCoordinatesInBasis,
    sparseColumnEchelon,
    sparseEchelonBasis,
    sparseExtendEchelonBasis,
    sparseLinearCombination,
  )
import Moonlight.Homology.Pure.Topology.SparseAlgebra
  ( sparseHomologyBasisAt,
  )
import Numeric.Natural (Natural)

type ChainMapEndpoint :: Type
data ChainMapEndpoint
  = ChainMapSource
  | ChainMapTarget
  deriving stock (Eq, Ord, Show)

type ZigzagDirection :: Type
data ZigzagDirection
  = ZigzagForward
  | ZigzagBackward
  deriving stock (Eq, Ord, Show)

-- | Every refusal names the exact descent obligation that failed.  In
-- particular, absence of a persistence class is a successful empty result,
-- never one of these failures.
type ZigzagFailure :: Type
data ZigzagFailure
  = ZigzagComplexInvalid !ChainMapEndpoint !HomologyFailure
  | ZigzagMapSourceCardinalityMismatch !HomologicalDegree !Int !Int
  | ZigzagMapTargetCardinalityMismatch !HomologicalDegree !Int !Int
  | ZigzagMapComponentInvalid !HomologicalDegree !BoundaryIncidenceShapeError
  | ZigzagMapCompositionInvalid !HomologicalDegree !BoundaryIncidenceShapeError
  | ZigzagChainMapLawViolation !HomologicalDegree
  | ZigzagEndpointMismatch !Int !ZigzagDirection
  | ZigzagHomologyCoordinatesMissing !Int !HomologicalDegree
  | ZigzagNegativeIntervalMultiplicity !HomologicalDegree !Int !Int !Int
  deriving stock (Eq, Show)

-- | A checked degree-preserving chain map.  Its source, target, and materialized
-- degree components are retained together so no caller can later pair the map
-- with different complexes.
type FiniteChainMap :: Type -> Type
data FiniteChainMap r = FiniteChainMap
  { storedChainMapSource :: !(FiniteChainComplex r),
    storedChainMapTarget :: !(FiniteChainComplex r),
    storedChainMapComponents :: !(Vector (BoundaryIncidence r))
  }

finiteChainMapSource :: FiniteChainMap r -> FiniteChainComplex r
finiteChainMapSource = storedChainMapSource

finiteChainMapTarget :: FiniteChainMap r -> FiniteChainComplex r
finiteChainMapTarget = storedChainMapTarget

finiteChainMapAt :: FiniteChainMap r -> HomologicalDegree -> BoundaryIncidence r
finiteChainMapAt chainMap (HomologicalDegree degreeIndex) =
  if degreeIndex < 0
    then emptyBoundaryIncidenceOf 0 0
    else
      case storedChainMapComponents chainMap Vector.!? degreeIndex of
        Just component -> component
        Nothing -> emptyBoundaryIncidenceOf 0 0

-- | Admit a finite chain map after materializing its relevant degrees and
-- proving @d_target . f = f . d_source@ at every positive degree.
mkFiniteChainMapChecked ::
  (Eq r, Num r, Semiring r) =>
  FiniteChainComplex r ->
  FiniteChainComplex r ->
  (HomologicalDegree -> BoundaryIncidence r) ->
  Either ZigzagFailure (FiniteChainMap r)
mkFiniteChainMapChecked sourceComplex targetComplex componentAt = do
  first (ZigzagComplexInvalid ChainMapSource) (validateFiniteChainComplexShape sourceComplex)
  first (ZigzagComplexInvalid ChainMapTarget) (validateFiniteChainComplexShape targetComplex)
  let maximumDegree = max (maximumDegreeOf sourceComplex) (maximumDegreeOf targetComplex)
      degreeValues = fmap HomologicalDegree [0 .. maximumDegree]
      componentList = fmap componentAt degreeValues
      components = Vector.fromList componentList
  traverse_
    (uncurry (validateComponentShape sourceComplex targetComplex))
    (zip degreeValues componentList)
  traverse_
    (\(degreeValue, precedingComponent, degreeComponent) ->
        validateChainMapLaw sourceComplex targetComplex degreeValue precedingComponent degreeComponent
    )
    (zip3 (drop 1 degreeValues) componentList (drop 1 componentList))
  pure
    FiniteChainMap
      { storedChainMapSource = sourceComplex,
        storedChainMapTarget = targetComplex,
        storedChainMapComponents = components
      }

validateComponentShape ::
  FiniteChainComplex r ->
  FiniteChainComplex r ->
  HomologicalDegree ->
  BoundaryIncidence r ->
  Either ZigzagFailure ()
validateComponentShape sourceComplex targetComplex degreeValue component
  | sourceCardinality component /= expectedSource =
      Left (ZigzagMapSourceCardinalityMismatch degreeValue expectedSource (sourceCardinality component))
  | targetCardinality component /= expectedTarget =
      Left (ZigzagMapTargetCardinalityMismatch degreeValue expectedTarget (targetCardinality component))
  | otherwise = Right ()
 where
  expectedSource = degreeCardinality sourceComplex degreeValue
  expectedTarget = degreeCardinality targetComplex degreeValue

validateChainMapLaw ::
  (Eq r, Num r, Semiring r) =>
  FiniteChainComplex r ->
  FiniteChainComplex r ->
  HomologicalDegree ->
  BoundaryIncidence r ->
  BoundaryIncidence r ->
  Either ZigzagFailure ()
validateChainMapLaw sourceComplex targetComplex degreeValue precedingComponent degreeComponent = do
  targetAfterMap <-
    first (ZigzagMapCompositionInvalid degreeValue)
      ( composeBoundaryIncidence
          (finiteBoundaryAt targetComplex degreeValue)
          degreeComponent
      )
  mapAfterSource <-
    first (ZigzagMapCompositionInvalid degreeValue)
      ( composeBoundaryIncidence
          precedingComponent
          (finiteBoundaryAt sourceComplex degreeValue)
      )
  if targetAfterMap == mapAfterSource
    then Right ()
    else Left (ZigzagChainMapLawViolation degreeValue)

-- | Orientation separated from its payload.  The same functor carries checked
-- chain maps during authoring and exact linear maps during reduction.
type ZigzagArrow :: Type -> Type
data ZigzagArrow map
  = ForwardArrow !map
  | BackwardArrow !map
  deriving stock (Eq, Ord, Show, Functor, Foldable, Traversable)

zigzagArrowDirection :: ZigzagArrow r -> ZigzagDirection
zigzagArrowDirection = \case
  ForwardArrow _ -> ZigzagForward
  BackwardArrow _ -> ZigzagBackward

type FiniteChainZigzag :: Type -> Type
data FiniteChainZigzag r = FiniteChainZigzag
  { storedZigzagFirstComplex :: !(FiniteChainComplex r),
    storedZigzagArrows :: !(Vector (ZigzagArrow (FiniteChainMap r)))
  }

-- | Glue a line of already checked maps.  A forward arrow is interpreted as
-- @current -> next@; a backward arrow is @current <- next@.
mkFiniteChainZigzag ::
  Eq r =>
  FiniteChainComplex r ->
  [ZigzagArrow (FiniteChainMap r)] ->
  Either ZigzagFailure (FiniteChainZigzag r)
mkFiniteChainZigzag firstComplex arrows = do
  _ <- foldM glueZigzagArrow firstComplex (zip [0 :: Int ..] arrows)
  pure
    FiniteChainZigzag
      { storedZigzagFirstComplex = firstComplex,
        storedZigzagArrows = Vector.fromList arrows
      }

glueZigzagArrow ::
  Eq r =>
  FiniteChainComplex r ->
  (Int, ZigzagArrow (FiniteChainMap r)) ->
  Either ZigzagFailure (FiniteChainComplex r)
glueZigzagArrow currentComplex (arrowIndex, arrow) =
  let (requiredCurrent, nextComplex) = finiteArrowEndpoints arrow
   in if finiteComplexesAgree currentComplex requiredCurrent
        then Right nextComplex
        else Left (ZigzagEndpointMismatch arrowIndex (zigzagArrowDirection arrow))

finiteArrowEndpoints :: ZigzagArrow (FiniteChainMap r) -> (FiniteChainComplex r, FiniteChainComplex r)
finiteArrowEndpoints = \case
  ForwardArrow chainMap -> (finiteChainMapSource chainMap, finiteChainMapTarget chainMap)
  BackwardArrow chainMap -> (finiteChainMapTarget chainMap, finiteChainMapSource chainMap)

finiteChainZigzagComplexes :: FiniteChainZigzag r -> NonEmpty (FiniteChainComplex r)
finiteChainZigzagComplexes zigzag =
  storedZigzagFirstComplex zigzag
    :| Vector.toList
      (fmap (snd . finiteArrowEndpoints) (storedZigzagArrows zigzag))

finiteChainZigzagArrows :: FiniteChainZigzag r -> [ZigzagArrow (FiniteChainMap r)]
finiteChainZigzagArrows = Vector.toList . storedZigzagArrows

zigzagComplexVector :: FiniteChainZigzag r -> Vector (FiniteChainComplex r)
zigzagComplexVector zigzag =
  Vector.cons
    (storedZigzagFirstComplex zigzag)
    (fmap (snd . finiteArrowEndpoints) (storedZigzagArrows zigzag))

-- | One indecomposable interval of a zigzag barcode.  Both endpoints are
-- inclusive.  'Traversable' transports the same interval from numeric diagram
-- indices to a caller's stage vocabulary without defining a parallel carrier.
type ZigzagInterval :: Type -> Type
data ZigzagInterval endpoint = ZigzagInterval
  { zigzagIntervalDegree :: !HomologicalDegree,
    zigzagIntervalFirst :: !endpoint,
    zigzagIntervalLast :: !endpoint,
    zigzagIntervalMultiplicity :: !Int
  }
  deriving stock (Eq, Ord, Show, Functor, Foldable, Traversable)

-- | Compute the unique interval decomposition of the rational homology
-- zigzag.  Chain homology and every induced adjacent map are prepared once per
-- degree; one right-filtration descent then closes every interval.
rationalZigzagIntervals ::
  Integral r =>
  FiniteChainZigzag r ->
  Either ZigzagFailure [ZigzagInterval Int]
rationalZigzagIntervals zigzag =
  let complexes = zigzagComplexVector zigzag
      arrows = storedZigzagArrows zigzag
      maximumDegree =
        complexes
          & Vector.foldl'
            (\currentMaximum complexValue -> max currentMaximum (maximumDegreeOf complexValue))
            0
   in concat
        <$> traverse
          ( intervalsAtDegree
              (storedZigzagFirstComplex zigzag)
              arrows
              . HomologicalDegree
          )
          [0 .. maximumDegree]

-- | Betti numbers reconstructed from a barcode at one diagram index.  The map
-- is sparse; absent degrees have dimension zero.
zigzagBettiAt :: Int -> [ZigzagInterval Int] -> Map HomologicalDegree Int
zigzagBettiAt diagramIndex =
  Map.fromListWith (+)
    . fmap
      (\interval -> (zigzagIntervalDegree interval, zigzagIntervalMultiplicity interval))
    . filter
      ( \interval ->
          zigzagIntervalFirst interval <= diagramIndex
            && diagramIndex <= zigzagIntervalLast interval
      )

type HomologyPresentation :: Type
data HomologyPresentation = HomologyPresentation
  { homologyBasisVectors :: !(Vector SparseRow),
    homologyCoordinateBasis :: !SparseCoordinateBasis
  }

homologyDimension :: HomologyPresentation -> Int
homologyDimension = Vector.length . homologyBasisVectors

type RationalLinearMap :: Type
data RationalLinearMap = RationalLinearMap
  { rationalMapTargetDimension :: !Int,
    rationalMapColumns :: !(Vector SparseRow)
  }

-- | One quotient layer of the right filtration on the current endpoint.  The
-- vectors form a basis for that layer modulo every preceding layer; the layer
-- order, not numeric birth order, is the zigzag orientation witness.
type RightFiltrationLayer :: Type
data RightFiltrationLayer = RightFiltrationLayer
  { rightLayerBirthIndex :: !Int,
    rightLayerBasis :: ![SparseRow]
  }

type IntervalMultiplicities :: Type
type IntervalMultiplicities = Map (Int, Int) Int

intervalsAtDegree ::
  Integral r =>
  FiniteChainComplex r ->
  Vector (ZigzagArrow (FiniteChainMap r)) ->
  HomologicalDegree ->
  Either ZigzagFailure [ZigzagInterval Int]
intervalsAtDegree firstComplex arrows degreeValue = do
  let initialPresentation = homologyPresentationAt firstComplex degreeValue
      initialFiltration =
        [ RightFiltrationLayer
            { rightLayerBirthIndex = 0,
              rightLayerBasis = standardSparseBasis (homologyDimension initialPresentation)
            }
        ]
  -- Prepare, induce, and descend one arrow at a time: only the adjacent
  -- presentations and current filtration remain live.
  (_, terminalFiltration, closedIntervals) <-
    Vector.ifoldM'
      ( \(leftPresentation, currentFiltration, intervals) arrowIndex arrow -> do
          let rightPresentation =
                homologyPresentationAt
                  (snd (finiteArrowEndpoints arrow))
                  degreeValue
          inducedArrow <-
            inducedArrowAt
              degreeValue
              arrowIndex
              (leftPresentation, rightPresentation, arrow)
          (nextFiltration, nextIntervals) <-
            advanceRightFiltration
              degreeValue
              (currentFiltration, intervals)
              (arrowIndex, inducedArrow)
          pure (rightPresentation, nextFiltration, nextIntervals)
      )
      (initialPresentation, initialFiltration, Map.empty)
      arrows
  terminalIntervals <-
    closeTerminalIntervals
      degreeValue
      (Vector.length arrows)
      terminalFiltration
      closedIntervals
  pure (intervalsFromMultiplicities degreeValue terminalIntervals)

homologyPresentationAt ::
  Integral r =>
  FiniteChainComplex r ->
  HomologicalDegree ->
  HomologyPresentation
homologyPresentationAt finite degreeValue@(HomologicalDegree degreeIndex)
  | degreeIndex < 0 || degreeIndex > maximumDegreeOf finite =
      HomologyPresentation
        { homologyBasisVectors = Vector.empty,
          homologyCoordinateBasis = sparseCoordinateBasis []
        }
  | otherwise =
      let basisRows =
            sparseHomologyBasisAt finite degreeValue
              & fmap representativeSparseRow
          basisVectors = Vector.fromList basisRows
          boundaryGenerators =
            finiteBoundaryAt finite (HomologicalDegree (degreeIndex + 1))
              & sparseBoundaryColumns
       in HomologyPresentation
            { homologyBasisVectors = basisVectors,
              homologyCoordinateBasis =
                sparseCoordinateBasis
                  (basisRows <> Vector.toList boundaryGenerators)
            }

inducedArrowAt ::
  Integral r =>
  HomologicalDegree ->
  Int ->
  (HomologyPresentation, HomologyPresentation, ZigzagArrow (FiniteChainMap r)) ->
  Either ZigzagFailure (ZigzagArrow RationalLinearMap)
inducedArrowAt degreeValue arrowIndex (leftPresentation, rightPresentation, arrow) =
  case arrow of
    ForwardArrow chainMap ->
      ForwardArrow
        <$> inducedHomologyMap arrowIndex degreeValue chainMap leftPresentation rightPresentation
    BackwardArrow chainMap ->
      BackwardArrow
        <$> inducedHomologyMap arrowIndex degreeValue chainMap rightPresentation leftPresentation

inducedHomologyMap ::
  Integral r =>
  Int ->
  HomologicalDegree ->
  FiniteChainMap r ->
  HomologyPresentation ->
  HomologyPresentation ->
  Either ZigzagFailure RationalLinearMap
inducedHomologyMap arrowIndex degreeValue chainMap sourcePresentation targetPresentation = do
  let rationalColumns =
        sparseBoundaryColumns (finiteChainMapAt chainMap degreeValue)
  imageCoordinates <-
    traverse
      ( \sourceCycle -> do
          let mappedCycle = sparseLinearCombination rationalColumns sourceCycle
          coordinates <-
            maybe
              (Left (ZigzagHomologyCoordinatesMissing arrowIndex degreeValue))
              Right
              ( sparseCoordinatesInBasis
                  (homologyCoordinateBasis targetPresentation)
                  mappedCycle
              )
          pure
            ( IntMap.filterWithKey
                (\coordinateIndex _ -> coordinateIndex < homologyDimension targetPresentation)
                coordinates
            )
      )
      (homologyBasisVectors sourcePresentation)
  pure
    RationalLinearMap
      { rationalMapTargetDimension = homologyDimension targetPresentation,
        rationalMapColumns = imageCoordinates
      }

applyRationalLinearMap :: RationalLinearMap -> SparseRow -> SparseRow
applyRationalLinearMap = sparseLinearCombination . rationalMapColumns

advanceRightFiltration ::
  HomologicalDegree ->
  ([RightFiltrationLayer], IntervalMultiplicities) ->
  (Int, ZigzagArrow RationalLinearMap) ->
  Either ZigzagFailure ([RightFiltrationLayer], IntervalMultiplicities)
advanceRightFiltration degreeValue (currentFiltration, intervals) (arrowIndex, arrow) = do
  let nextBirthIndex = arrowIndex + 1
  (nextFiltration, survivingLayers) <-
    case arrow of
      ForwardArrow linearMap ->
        Right
          ( forwardRightFiltration
              nextBirthIndex
              linearMap
              currentFiltration
          )
      BackwardArrow linearMap ->
        backwardRightFiltration
          arrowIndex
          degreeValue
          nextBirthIndex
          linearMap
          currentFiltration
  updatedIntervals <-
    closeExpiredIntervals
      degreeValue
      arrowIndex
      currentFiltration
      survivingLayers
      intervals
  pure (nextFiltration, updatedIntervals)

forwardRightFiltration ::
  Int ->
  RationalLinearMap ->
  [RightFiltrationLayer] ->
  ([RightFiltrationLayer], [RightFiltrationLayer])
forwardRightFiltration nextBirthIndex linearMap currentFiltration =
  let targetDimension = rationalMapTargetDimension linearMap
      (reversedSurvivingLayers, imageBasis) =
        List.foldl'
          ( \(reversedLayers, accumulatedBasis) layer ->
              let mappedVectors = fmap (applyRationalLinearMap linearMap) (rightLayerBasis layer)
                  (independentImages, extendedBasis) =
                    sparseExtendEchelonBasis accumulatedBasis mappedVectors
               in ( layer {rightLayerBasis = independentImages} : reversedLayers,
                    extendedBasis
                  )
          )
          ([], sparseEchelonBasis [])
          currentFiltration
      survivingLayers = reverse reversedSurvivingLayers
      (newLayerBasis, _) =
        sparseExtendEchelonBasis imageBasis (standardSparseBasis targetDimension)
      nextFiltration =
        survivingLayers
          <> [ RightFiltrationLayer
                 { rightLayerBirthIndex = nextBirthIndex,
                   rightLayerBasis = newLayerBasis
                 }
             ]
   in (nextFiltration, survivingLayers)

backwardRightFiltration ::
  Int ->
  HomologicalDegree ->
  Int ->
  RationalLinearMap ->
  [RightFiltrationLayer] ->
  Either ZigzagFailure ([RightFiltrationLayer], [RightFiltrationLayer])
backwardRightFiltration arrowIndex degreeValue nextBirthIndex linearMap currentFiltration = do
  let targetDimension = rationalMapTargetDimension linearMap
      targetFiltrationBasis = currentFiltration >>= rightLayerBasis
      targetCoordinates = sparseCoordinateBasis targetFiltrationBasis
  imageCoordinateColumns <-
    traverse
      ( \imageColumn ->
          maybe
            (Left (ZigzagHomologyCoordinatesMissing arrowIndex degreeValue))
            Right
            (sparseCoordinatesInBasis targetCoordinates imageColumn)
      )
      (rationalMapColumns linearMap)
  let columnEchelon =
        sparseColumnEchelon
          (fmap (reverseSparseCoordinates targetDimension) imageCoordinateColumns)
      ascendingPivotPreimages =
        sparseColumnPivotPreimages columnEchelon
          & fmap
            (\(reversedPivot, preimage) -> (targetDimension - reversedPivot - 1, preimage))
          & reverse
      (_, survivingLayers) =
        List.mapAccumL
          pullbackLayer
          (0, ascendingPivotPreimages)
          currentFiltration
      kernelLayer =
        RightFiltrationLayer
          { rightLayerBirthIndex = nextBirthIndex,
            rightLayerBasis = sparseColumnKernelBasis columnEchelon
          }
  pure (kernelLayer : survivingLayers, survivingLayers)
 where
  pullbackLayer (lowerBound, remainingPivots) layer =
    let upperBound = lowerBound + length (rightLayerBasis layer)
        (layerPivots, laterPivots) =
          span ((< upperBound) . fst) remainingPivots
     in ( (upperBound, laterPivots),
          layer {rightLayerBasis = fmap snd layerPivots}
        )

reverseSparseCoordinates :: Int -> SparseRow -> SparseRow
reverseSparseCoordinates dimensionValue =
  IntMap.fromDistinctAscList
    . fmap (\(coordinateIndex, coefficient) -> (dimensionValue - coordinateIndex - 1, coefficient))
    . IntMap.toDescList

closeExpiredIntervals ::
  HomologicalDegree ->
  Int ->
  [RightFiltrationLayer] ->
  [RightFiltrationLayer] ->
  IntervalMultiplicities ->
  Either ZigzagFailure IntervalMultiplicities
closeExpiredIntervals degreeValue deathIndex currentLayers survivingLayers intervals =
  foldM
    ( \currentIntervals (currentLayer, survivingLayer) ->
        recordIntervalMultiplicity
          degreeValue
          (rightLayerBirthIndex currentLayer)
          deathIndex
          (length (rightLayerBasis currentLayer) - length (rightLayerBasis survivingLayer))
          currentIntervals
    )
    intervals
    (zip currentLayers survivingLayers)

closeTerminalIntervals ::
  HomologicalDegree ->
  Int ->
  [RightFiltrationLayer] ->
  IntervalMultiplicities ->
  Either ZigzagFailure IntervalMultiplicities
closeTerminalIntervals degreeValue deathIndex terminalFiltration intervals =
  foldM
    ( \currentIntervals layer ->
        recordIntervalMultiplicity
          degreeValue
          (rightLayerBirthIndex layer)
          deathIndex
          (length (rightLayerBasis layer))
          currentIntervals
    )
    intervals
    terminalFiltration

recordIntervalMultiplicity ::
  HomologicalDegree ->
  Int ->
  Int ->
  Int ->
  IntervalMultiplicities ->
  Either ZigzagFailure IntervalMultiplicities
recordIntervalMultiplicity degreeValue firstIndex lastIndex multiplicity intervals
  | multiplicity < 0 =
      Left (ZigzagNegativeIntervalMultiplicity degreeValue firstIndex lastIndex multiplicity)
  | multiplicity == 0 = Right intervals
  | otherwise = Right (Map.insertWith (+) (firstIndex, lastIndex) multiplicity intervals)

intervalsFromMultiplicities :: HomologicalDegree -> IntervalMultiplicities -> [ZigzagInterval Int]
intervalsFromMultiplicities degreeValue =
  fmap
    ( \((firstIndex, lastIndex), multiplicity) ->
        ZigzagInterval
          { zigzagIntervalDegree = degreeValue,
            zigzagIntervalFirst = firstIndex,
            zigzagIntervalLast = lastIndex,
            zigzagIntervalMultiplicity = multiplicity
          }
    )
    . Map.toAscList

standardSparseBasis :: Int -> [SparseRow]
standardSparseBasis dimensionValue =
  fmap (\coordinateIndex -> IntMap.singleton coordinateIndex 1) [0 .. dimensionValue - 1]

representativeSparseRow :: RepresentativeChain Rational Int -> SparseRow
representativeSparseRow representative =
  representativeTerms representative
    & fmap (\(coefficient, basisIndex) -> (basisIndex, coefficient))
    & IntMap.fromListWith (+)
    & compactSparseRow

finiteComplexesAgree :: Eq r => FiniteChainComplex r -> FiniteChainComplex r -> Bool
finiteComplexesAgree left right =
  let maximumDegree = max (maximumDegreeOf left) (maximumDegreeOf right)
   in all
        ( \degreeIndex ->
            finiteBoundaryAt left (HomologicalDegree degreeIndex)
              == finiteBoundaryAt right (HomologicalDegree degreeIndex)
        )
        [0 .. maximumDegree]

finiteBoundaryAt :: FiniteChainComplex r -> HomologicalDegree -> BoundaryIncidence r
finiteBoundaryAt finite degreeValue@(HomologicalDegree degreeIndex) =
  if degreeIndex >= 0 && degreeIndex <= maximumDegreeOf finite
    then incidenceMatrixAt finite degreeValue
    else
      emptyBoundaryIncidenceOf
        (naturalCardinality (degreeCardinality finite degreeValue))
        (naturalCardinality (degreeCardinality finite (HomologicalDegree (degreeIndex - 1))))

maximumDegreeOf :: FiniteChainComplex r -> Int
maximumDegreeOf = unHomologicalDegree . maxHomologicalDegree

naturalCardinality :: Int -> Natural
naturalCardinality = fromIntegral . max 0